1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-15 13:38:46 +00:00

Compare commits

...

94 Commits

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

* add avif
2025-07-09 11:27:58 -07:00
Alex Xu
07ef11013a fix cbz metadata (#1018) 2025-07-09 11:14:25 -07:00
Alex Xu
551fe6edbf bump 8.0.2 2025-07-05 07:41:08 -07:00
Alex Xu
dbf8a3ddbd fix ._cover.jpg on macos 15 (#1016) 2025-07-05 07:40:09 -07:00
Alex Xu
8f9e230b62 rotate first (#1015) 2025-07-04 19:52:34 -07:00
Alex Xu
36d9a4151e Update README.md 2025-07-04 14:23:28 -07:00
Alex Xu
3e88dabd1a bump 8.0.1 2025-07-03 12:43:54 -07:00
Alex Xu
3b7d949128 only slugify cbz subfolders if sort matters (#1010)
* only slugify if sort matters

* add comments

* make conditions more granular

* fix

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

* use pure python

---------

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

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

* fix

* call it is

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

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

* media-type="image/gif"

---------

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

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

* Fixed merge error in gui

* Fixed bug

Missing () at the end of checkState on line 852

* small fixes

* fix checkbox not saving

* rename selectDir to selectOutputDirectory

---------

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

* Optimization for JPEG non-color pages in forcecolor mode
2025-06-18 15:12:12 -07:00
Adrian
2fa90c9f59 Fix png bit depth (#976) 2025-06-18 15:11:59 -07:00
Alex Xu
cb0520dcab bump 7.5.1 2025-06-17 11:34:25 -07:00
Alex Xu
623bce6ae3 fix scribe skinny images (#972) 2025-06-17 11:29:27 -07:00
Alex Xu
ad60894d19 fix button size 2025-06-17 10:52:59 -07:00
Alex Xu
4229b79c42 bump 7.5.0 2025-06-16 22:53:14 -07:00
Alex Xu
5fa6a59672 add links (#970) 2025-06-16 22:52:32 -07:00
Alex Xu
f171314a49 Update README.md 2025-06-16 22:46:46 -07:00
Alex Xu
6b28e313e6 Update README.md 2025-06-16 20:53:12 -07:00
Alex Xu
5a17435f7d split then rotate 2025-06-16 16:13:36 -07:00
Alex Xu
0d573eb0a1 Shift click convert button for custom output directory (#969)
* add shift click tip

* fix tooltip message
2025-06-16 16:08:54 -07:00
Alex Xu
30a3f90907 You can drag image folders or comic files/archives into this window to convert (#968) 2025-06-16 15:50:10 -07:00
Alex Xu
9a7de0f5d9 rename manga mode to right-to-left mode (#967) 2025-06-16 14:50:43 -07:00
Alex Xu
f1db31205b split then rotate (#966) 2025-06-16 14:47:55 -07:00
jorge-castellon-jr
eef5a85fa6 ComicInfo.xml Title checkbox (#944)
* Adds options to use metadata title as output name

* update .ui files

* write rest

* small fixes

* small fix

* fix small

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-06-16 11:58:26 -07:00
Alex Xu
271200d29f fix scribe order 2025-06-16 10:50:52 -07:00
Silver0006
ee375abfc5 Added ability to combine multiple CBZ into one files (#960)
* Added basic CBZ combine func

Need to add support for epub and maybe mobi.

* Removed irrelevant code for CBZ file fusion

* Fixed false description

* Removed irrelevant code

* Removed redundant code

Replaced page tracker and os.rename with os.renames. Removed unneeded reference to gui. Changed mkdir to mkdtemp.

* Made folder and cbz work together

You can select multiple folders of images, multiple cbz files, and folders with subfolders. Fusion will combine them all together at the same time. Mainly added this to idiot proof it.

* Updated gui

Removed redundant tooltip

* simplify code

* fix merging chapter folders with .

* uncheck fusion message

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-06-15 14:58:03 -07:00
Alex Xu
94257c396a Update README.md 2025-06-14 11:47:54 -07:00
Alex Xu
a05111b64a Scribe 2480 support (#962)
* add imgfile2

* add 2480

* use older mozjpeg

* fix above below

* fix imgsize2

* fix newline

* rename targetPath

* fix cover.jpg

* fix opf

* fix above

* fix splitting
2025-06-13 16:45:27 -07:00
Alex Xu
96e3ba7482 Update README.md 2025-06-13 16:36:10 -07:00
Alex Xu
eb0abb538c add tome text for splits (#963) 2025-06-13 12:22:07 -07:00
Alex Xu
87c2ef8033 Update README.md 2025-06-11 22:54:25 -07:00
Alex Xu
ae7f56c81b ignore kindlegen warnings (#961) 2025-06-11 16:01:25 -07:00
Alex Xu
70c73a82eb Update README.md 2025-06-11 13:57:50 -07:00
Alex Xu
dbf4e45d25 Update README.md 2025-06-11 13:36:14 -07:00
Alex Xu
1c6fe0cb22 Update README.md 2025-06-10 14:17:53 -07:00
Alex Xu
2f7f6ebf0a Update README.md 2025-06-10 12:28:28 -07:00
Alex Xu
21159c4328 add kodansha note 2025-06-10 11:51:30 -07:00
Alex Xu
4bb6ba55d3 cover has minimal processing and is shared across splits (#953)
* refactor cover handling

* skip cover processing

* rename cover to cover_path

* fix scribe mobi detection

* make things closer

* rename save to save_to_epub
2025-06-08 11:08:57 -07:00
Alex Xu
06ae4ec25f upgrade to numpy 2 (#954) 2025-06-07 21:59:32 -07:00
Alex Xu
3ac5709e73 Add Kindle PW 7/10, Kindle 8/10 to PDOC list for easier covers (#955)
* add more pdoc

* update readme
2025-06-07 21:59:06 -07:00
Alex Xu
fe7255a2d9 remove Kindle abbreviations (#957) 2025-06-07 21:56:12 -07:00
Alex Xu
4712eac3c2 fix ebok thumbnail aspect ratio (#956) 2025-06-07 21:45:42 -07:00
Alex Xu
8ef5bf14ac Update README.md 2025-06-05 14:47:42 -07:00
orpheus1120
c7e69f5bdb Do not slugify folder names when output format is CBZ (fixes #914) (#920) 2025-06-04 13:38:17 -07:00
Alex Xu
51d0be4379 Update README.md 2025-06-03 21:38:07 -07:00
Alex Xu
ddc0ca2ff5 Update README.md 2025-06-03 18:56:11 -07:00
Alex Xu
bd6dfa1e33 Update README.md 2025-06-03 18:53:51 -07:00
Alex Xu
a95dde4cba Delete bad_formatting_examples.md 2025-06-03 14:23:16 -07:00
Alex Xu
2882d0f707 Update README.md 2025-06-03 14:22:48 -07:00
Alex Xu
a1fa8e0ec3 Update README.md 2025-06-03 13:26:10 -07:00
Alex Xu
ae4e063e09 Update README.md 2025-06-03 13:25:39 -07:00
Alex Xu
8e0deff5ae expand introduction 2025-06-03 12:39:25 -07:00
Alex Xu
3bd752537d Update bad_formatting_examples.md 2025-06-02 12:54:09 -07:00
Alex Xu
4319f64815 Update README.md 2025-06-02 12:53:40 -07:00
Alex Xu
82d2f7f4bf Update bad_formatting_examples.md 2025-06-01 17:41:20 -07:00
Alex Xu
5a1e614a5d add link to bad formatting examples 2025-06-01 17:36:59 -07:00
Alex Xu
75e05a0ef0 Create bad_formatting_examples.md 2025-06-01 17:34:29 -07:00
Alex Xu
e0f5bff527 Update README.md 2025-06-01 15:52:42 -07:00
Alex Xu
a8316737be Update README.md 2025-06-01 15:39:11 -07:00
Alex Xu
04228d100b revamp readme introduction (#950) 2025-06-01 15:37:09 -07:00
Alex Xu
8cc44c99f7 Update README.md 2025-06-01 11:32:50 -07:00
Alex Xu
60f7902edd add download counter to readme (#948)
* add download counter to readme

* Update README.md

* Update README.md
2025-06-01 11:32:09 -07:00
Alex Xu
34bea98ca0 align preceding pages around pre-joined spreads in landscape (#942)
* refactor spread properties

* fix spread alighment backwards

* more consistent quotes
2025-05-30 10:33:19 -07:00
Alex Xu
734b179e8a add note about mobi/azw3 dual type 2025-05-30 08:15:25 -07:00
yaqinking
dcaa7401e7 Other profile max width change to 2400 to support 2400x3200 resolution. (#945) 2025-05-30 08:06:29 -07:00
Alex Xu
d4d71cdd05 restore 2 panel view option (#940)
* Revert "disable old panel view for new kindles"

This reverts commit c4bab13a3e.

* fix HQ panel view warnings

* restore half page portrait panel view

* fix imports

* remove unneeded
2025-05-26 17:40:37 -07:00
Alex Xu
ec613cce7b add note about macOS python 2025-05-26 11:32:44 -07:00
Alex Xu
ada001eb41 specify you should clone a fork, not the main KCC repo 2025-05-26 11:29:57 -07:00
20 changed files with 1624 additions and 663 deletions

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

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

View File

@@ -2,22 +2,46 @@
# KCC
[![GitHub release](https://img.shields.io/github/release/ciromattia/kcc.svg)](https://github.com/ciromattia/kcc/releases)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ciromattia/kcc/docker-publish.yml?label=docker%20build)](https://github.com/ciromattia/kcc/pkgs/container/kcc)
[![Github All Releases](https://img.shields.io/github/downloads/ciromattia/kcc/total.svg)](https://github.com/ciromattia/kcc/releases)
**Kindle Comic Converter** optimizes black & white comics and manga for E-ink ereaders
like Kindle, Kobo, ReMarkable, and more.
Pages display in fullscreen without margins,
with proper fixed layout support.
Supported input formats include JPG/PNG/GIF image files in folders, archives, or PDFs.
Supported output formats include MOBI/AZW3, EPUB, KEPUB, and CBZ.
If your source are super high resolution DRM-free PDFs from Kodansha/Humble Bundle/Fanatical,
you'll need to first [convert the PDFs to CBZ](https://github.com/ciromattia/kcc/issues/680) for use in KCC.
**Kindle Comic Converter** optimizes comics and manga for eink readers like Kindle, Kobo, ReMarkable, and more.
Pages display in fullscreen without margins, with proper fixed layout support.
Its main feature is various optional image processing steps to look good on eink screens,
which have different requirements than normal LCD screens.
It also does filesize optimization by downscaling to your specific device's screen resolution,
which can improve performance on underpowered ereaders.
Supported input formats include folders and archives of JPG/PNG files and more.
Supported output formats include virtual panel view MOBI/AZW3, EPUB, KEPUB, and CBZ.
Combining that with downscaling to your specific device's screen resolution
can result in filesize reductions of hundreds of MB per volume with no visible quality loss on eink.
This can also improve battery life, page turn speed, and general performance
on underpowered ereaders with small storage capacities.
KCC avoids many common formatting issues (some of which occur [even on the Kindle Store](https://github.com/ciromattia/kcc/wiki/Kindle-Store-bad-formatting)), such as:
1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain.
2) unneccessary margins at the bottom of the screen
3) Not utilizing the full 1860x2480 resolution of the 10" Kindle Scribe
4) incorrect page turn direction for manga that's read right to left
5) unaligned two page spreads in landscape, where pages are shifted over by 1
The GUI looks like this, built in Qt6, with my most commonly used settings:
![image](https://github.com/user-attachments/assets/36ad2131-6677-4559-bd6f-314a90c27218)
Simply drag and drop your files/folders into the KCC window,
adjust your settings (hover over each option to see details in a tooltip),
and hit convert to create ereader optimized files.
You can change the default output directory by holding `Shift` while clicking the convert button.
Then just drag and drop the generated output files onto your device's documents folder via USB.
If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac.
YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=IR2Fhcm9658
### A word of warning
@@ -44,6 +68,13 @@ If you find **KCC** valuable you can consider donating to the authors:
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Q5Q41BW8HS)
## Commissions
This section is subject to change:
Email (for commisions and inquiries): `kindle.comic.converter` gmail
## Sponsors
- Free code signing on Windows provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
@@ -68,15 +99,23 @@ On Mac, right click open to get past the security warning.
For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation
## FAQ
- All options have additional information in tooltips if you hover over the option.
- To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB
- Right to left mode not working?
- RTL mode only affects splitting order for CBZ output. Your cbz reader itself sets the page turn direction.
- Colors inverted?
- Disable Kindle dark mode
- Cannot connect Kindle Scribe or 2024+ Kindle to macOS
- Use official MTP [Amazon USB File Transfer app](https://www.amazon.com/gp/help/customer/display.html/ref=hp_Connect_USB_MTP?nodeId=TCUBEdEkbIhK07ysFu)
(no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps.
- How to make AZW3 instead of MOBI?
- The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons.
- [Windows 7 support](https://github.com/ciromattia/kcc/issues/678)
- [Combine files/chapters](https://github.com/ciromattia/kcc/issues/612#issuecomment-2117985011)
- [Flatpak mobi conversion stuck](https://github.com/ciromattia/kcc/wiki/Installation#linux)
- Image too dark?
- The default gamma correction of 1.8 makes the image darker, and is useful for faded/gray artwork/text. Disable by setting gamma = 1.0
- [Better PDF support (Humble Bundle, Fanatical, etc)](https://github.com/ciromattia/kcc/issues/680)
- Cannot connect Kindle Scribe or 2024+ Kindle to macOS
- Use official MTP [Amazon USB File Transfer app](https://www.amazon.com/gp/help/customer/display.html/ref=hp_Connect_USB_MTP?nodeId=TCUBEdEkbIhK07ysFu)
(no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps.
- Huge margins / slow page turns?
- You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB.
@@ -128,10 +167,12 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
'K578': ("Kindle", (600, 800), Palette16, 1.8),
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.8),
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.8),
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8),
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
'KV': ("Kindle Paperwhite 3/4/Voyage/Oasis", (1072, 1448), Palette16, 1.8),
'KV': ("Kindle Voyage, (1072, 1448), Palette16, 1.8),
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.8),
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8),
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
@@ -205,6 +246,7 @@ OUTPUT SETTINGS:
Output generated file to specified directory or file
-t TITLE, --title TITLE
Comic title [Default=filename or directory name]
--comicinfotitle Write title from ComicInfo.xml
-a AUTHOR, --author AUTHOR
Author name [Default=KCC]
-f FORMAT, --format FORMAT
@@ -214,6 +256,7 @@ OUTPUT SETTINGS:
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
--spreadshift Shift first page to opposite side in landscape for two page spread alignment
--norotate Do not rotate double page spreads in spread splitter option.
--rotatefirst Put rotated spread first in spread splitter option.
--reducerainbow Reduce rainbow effect on color eink by slightly blurring images
CUSTOM PROFILE:
@@ -250,7 +293,7 @@ OTHER:
This section is for developers who want to contribute to KCC or power users who want to run the latest code without waiting for an official release.
Easiest to use [GitHub Desktop](https://desktop.github.com) to clone the KCC repo. From GitHub Desktop, click on `Repository` in the toolbar, then `Command Prompt` (Windows)/`Terminal` (Mac) to open a window in the KCC repo.
Easiest to use [GitHub Desktop](https://desktop.github.com) to clone your fork of the KCC repo. From GitHub Desktop, click on `Repository` in the toolbar, then `Command Prompt` (Windows)/`Terminal` (Mac) to open a window in the KCC repo.
Depending on your system [Python](https://www.python.org) may be called either `python` or `python3`. We use virtual environments (venv) to manage dependencies.
@@ -261,10 +304,15 @@ 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
video of adding a new checkbox: https://youtu.be/g3I8DU74C7g
Do not use `git merge` to merge master from upstream,
use the "Sync fork" button on your fork on GitHub in your branch
to avoid weird looking merges in pull requests.
When making changes, be aware of how your change might affect file splitting/chunking
or chapter alignment.
### Windows install from source
One time setup and running for the first time:
@@ -290,6 +338,8 @@ python setup.py build_binary
### macOS install from source
If the system installed Python gives you issues, please install the latest Python from either brew or the official website.
One time setup and running for the first time:
```
python3 -m venv venv

View File

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

View File

@@ -27,5 +27,6 @@
<file>../icons/convert.png</file>
<file>../icons/document_new.png</file>
<file>../icons/folder_new.png</file>
<file>../icons/kofi_symbol.png</file>
</qresource>
</RCC>

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>519</width>
<height>572</height>
<width>566</width>
<height>573</height>
</rect>
</property>
<property name="windowTitle">
@@ -73,6 +73,29 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="kofiButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>Support me on Ko-fi</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/kofi_symbol.png</normaloff>:/Other/icons/kofi_symbol.png</iconset>
</property>
<property name="iconSize">
<size>
<width>19</width>
<height>16</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="wikiButton">
<property name="minimumSize">
@@ -114,27 +137,62 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QPushButton" name="directoryButton">
<item row="1" column="3">
<widget class="QPushButton" name="convertButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Add directory containing JPG, PNG or GIF files to queue.&lt;br/&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;CBR, CBZ and CB7 files inside will not be processed!&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Shift+Click to select the output directory for this list.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Add image folder</string>
<string>Convert</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/folder_new.png</normaloff>:/Other/icons/folder_new.png</iconset>
<normaloff>:/Other/icons/convert.png</normaloff>:/Other/icons/convert.png</iconset>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QPushButton" name="clearButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>Clear list</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/clear.png</normaloff>:/Other/icons/clear.png</iconset>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="deviceBox">
<property name="minimumSize">
<size>
<width>0</width>
<height>28</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Target device.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="fileButton">
<property name="minimumSize">
<size>
@@ -154,20 +212,46 @@
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QComboBox" name="deviceBox">
<item row="0" column="5">
<widget class="QPushButton" name="defaultOutputFolderButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>28</height>
<height>30</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Target device.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Use this to select the default output directory.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/folder_new.png</normaloff>:/Other/icons/folder_new.png</iconset>
</property>
</widget>
</item>
<item row="1" column="3">
<item row="0" column="4">
<widget class="QCheckBox" name="defaultOutputFolderBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - next to source&lt;br/&gt;&lt;/span&gt;Place output files next to source files&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - folder next to source&lt;br/&gt;&lt;/span&gt;Place output files in a folder next to source files&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Custom&lt;br/&gt;&lt;/span&gt;Place output files in custom directory specified by right button&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Output Folder</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="4" colspan="2">
<widget class="QComboBox" name="formatBox">
<property name="minimumSize">
<size>
@@ -180,55 +264,14 @@
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="convertButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Shift+Click to select the output directory.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Convert</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/convert.png</normaloff>:/Other/icons/convert.png</iconset>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="clearButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>Clear list</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/clear.png</normaloff>:/Other/icons/clear.png</iconset>
</property>
</widget>
</item>
</layout>
<zorder>directoryButton</zorder>
<zorder>clearButton</zorder>
<zorder>fileButton</zorder>
<zorder>deviceBox</zorder>
<zorder>convertButton</zorder>
<zorder>formatBox</zorder>
<zorder>defaultOutputFolderButton</zorder>
<zorder>fileButton</zorder>
<zorder>defaultOutputFolderBox</zorder>
</widget>
</item>
<item row="1" column="0" colspan="2">
@@ -292,7 +335,7 @@
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Resolution of the target device.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="maximum">
<number>2160</number>
<number>2400</number>
</property>
</widget>
</item>
@@ -410,75 +453,6 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item row="4" column="2">
<widget class="QCheckBox" name="croppingBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Disabled&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Disabled&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Margins&lt;br/&gt;&lt;/span&gt;Margins&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Margins + page numbers&lt;br/&gt;&lt;/span&gt;Margins +page numbers&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Cropping mode</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="mangaBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Enable right-to-left reading.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Manga mode</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="webtoonBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Enable special parsing mode for Korean Webtoons.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Webtoon mode</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="rotateBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Split&lt;br/&gt;&lt;/span&gt;Double page spreads will be cut into two separate pages.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Rotate and split&lt;br/&gt;&lt;/span&gt;Double page spreads will be displayed twice. First rotated and then split. &lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Rotate&lt;br/&gt;&lt;/span&gt;Double page spreads will be rotated.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Spread splitter</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="borderBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Autodetection&lt;br/&gt;&lt;/span&gt;The color of margins fill will be detected automatically.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - White&lt;br/&gt;&lt;/span&gt;Margins will be filled with white color.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Black&lt;br/&gt;&lt;/span&gt;Margins will be filled with black color.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>W/B margins</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QCheckBox" name="gammaBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Disable automatic gamma correction.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Custom gamma</string>
</property>
</widget>
</item>
<item row="6" column="2">
<widget class="QCheckBox" name="interPanelCropBox">
<property name="toolTip">
@@ -492,46 +466,13 @@
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QCheckBox" name="colorBox">
<item row="1" column="0">
<widget class="QCheckBox" name="mangaBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Disable conversion to grayscale.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Enable right-to-left reading.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Color mode</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="qualityBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - 4 panels&lt;br/&gt;&lt;/span&gt;Zoom each corner separately.&lt;/p&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - 2 panels&lt;br/&gt;&lt;/span&gt;Zoom only the top and bottom of the page.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - 4 high-quality panels&lt;br/&gt;&lt;/span&gt;Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Panel View 4/2/HQ</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="2">
<widget class="QCheckBox" name="disableProcessingBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Do not process any image, ignore profile and processing options.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Disable processing</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="maximizeStrips">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - 1x4&lt;br/&gt;&lt;/span&gt;Keep format 1x4 panels strips.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - 2x2&lt;br/&gt;&lt;/span&gt;Turn 1x4 strips to 2x2 to maximize screen usage.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>1x4 to 2x2 strips</string>
<string>Right-to-left mode</string>
</property>
</widget>
</item>
@@ -557,6 +498,39 @@
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QCheckBox" name="croppingBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Disabled&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Disabled&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Margins&lt;br/&gt;&lt;/span&gt;Margins&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Margins + page numbers&lt;br/&gt;&lt;/span&gt;Margins +page numbers&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Cropping mode</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="webtoonBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Enable special parsing mode for Korean Webtoons.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Webtoon mode</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QCheckBox" name="colorBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Disable conversion to grayscale.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Color mode</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="deleteBox">
<property name="toolTip">
@@ -567,6 +541,69 @@
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QCheckBox" name="comicinfoTitleBox">
<property name="toolTip">
<string>Write Title from ComicInfo.xml</string>
</property>
<property name="text">
<string>ComicInfo Title</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="qualityBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - 4 panels&lt;br/&gt;&lt;/span&gt;Zoom each corner separately.&lt;/p&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - 2 panels&lt;br/&gt;&lt;/span&gt;Zoom only the top and bottom of the page.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - 4 high-quality panels&lt;br/&gt;&lt;/span&gt;Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Panel View 4/2/HQ</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QCheckBox" name="reduceRainbowBox">
<property name="toolTip">
<string>Reduce rainbow effect on color eink by slightly blurring images</string>
</property>
<property name="text">
<string>Rainbow blur</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="noRotateBox">
<property name="toolTip">
<string>Do not rotate double page spreads in spread splitter option.</string>
</property>
<property name="text">
<string>No rotate</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="fileFusionBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Combines all selected files into a single file. (Helpful for combining chapters into volumes.)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>File Fusion</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QCheckBox" name="gammaBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Disable automatic gamma correction.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Custom gamma</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="mozJpegBox">
<property name="toolTip">
@@ -580,6 +617,26 @@
</property>
</widget>
</item>
<item row="5" column="2">
<widget class="QCheckBox" name="disableProcessingBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Do not process any image, ignore profile and processing options.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Disable processing</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="chunkSizeCheckBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700; text-decoration: underline;&quot;&gt;Unchecked&lt;br/&gt;&lt;/span&gt;Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700; text-decoration: underline;&quot;&gt;Checked&lt;/span&gt;&lt;br/&gt;Output file size specified in &amp;quot;Chunk size MB&amp;quot; before split occurs.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Chunk size</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="spreadShiftBox">
<property name="toolTip">
@@ -613,33 +670,49 @@
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="noRotateBox">
<item row="1" column="1">
<widget class="QCheckBox" name="rotateBox">
<property name="toolTip">
<string>Do not rotate double page spreads in spread splitter option.</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Split&lt;br/&gt;&lt;/span&gt;Double page spreads will be cut into two separate pages.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Split and rotate&lt;br/&gt;&lt;/span&gt;Double page spreads will be displayed twice. First split and then rotated. &lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Rotate&lt;br/&gt;&lt;/span&gt;Double page spreads will be rotated.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>No rotate</string>
<string>Spread splitter</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QCheckBox" name="reduceRainbowBox">
<item row="3" column="0">
<widget class="QCheckBox" name="borderBox">
<property name="toolTip">
<string>Reduce rainbow effect on color eink by slightly blurring images</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Autodetection&lt;br/&gt;&lt;/span&gt;The color of margins fill will be detected automatically.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - White&lt;br/&gt;&lt;/span&gt;Margins will be filled with white color.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Black&lt;br/&gt;&lt;/span&gt;Margins will be filled with black color.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Rainbow blur</string>
<string>W/B margins</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="chunkSizeCheckBox">
<item row="4" column="1">
<widget class="QCheckBox" name="maximizeStrips">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700; text-decoration: underline;&quot;&gt;Unchecked&lt;br/&gt;&lt;/span&gt;Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700; text-decoration: underline;&quot;&gt;Checked&lt;/span&gt;&lt;br/&gt;Output file size specified in &amp;quot;Chunk size MB&amp;quot; before split occurs.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - 1x4&lt;br/&gt;&lt;/span&gt;Keep format 1x4 panels strips.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - 2x2&lt;br/&gt;&lt;/span&gt;Turn 1x4 strips to 2x2 to maximize screen usage.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Chunk size</string>
<string>1x4 to 2x2 strips</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QCheckBox" name="rotateFirstBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;When the spread splitter option is partially checked,&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Rotate Last&lt;br/&gt;&lt;/span&gt;Put the rotated 2 page spread after the split spreads.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Rotate First&lt;br/&gt;&lt;/span&gt;Put the rotated 2 page spread before the split spreads.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Rotate First</string>
</property>
</widget>
</item>
@@ -770,8 +843,6 @@
<tabstops>
<tabstop>convertButton</tabstop>
<tabstop>clearButton</tabstop>
<tabstop>directoryButton</tabstop>
<tabstop>fileButton</tabstop>
<tabstop>deviceBox</tabstop>
<tabstop>formatBox</tabstop>
<tabstop>mangaBox</tabstop>

BIN
icons/kofi_symbol.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,9 @@
# 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 itertools
from pathlib import Path
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QApplication, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog)
@@ -27,7 +30,7 @@ import sys
from urllib.parse import unquote
from time import sleep
from shutil import move, rmtree
from subprocess import STDOUT, PIPE
from subprocess import STDOUT, PIPE, CalledProcessError
import requests
from xml.sax.saxutils import escape
@@ -37,10 +40,10 @@ from packaging.version import Version
from raven import Client
from tempfile import gettempdir
from .shared import HTMLStripper, available_archive_tools, sanitizeTrace, walkLevel, subprocess_run
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
from .comicarchive import SEVENZIP, available_archive_tools
from . import __version__
from . import comic2ebook
from . import image
from . import metadata
from . import kindle
from . import KCC_ui
@@ -263,12 +266,20 @@ class WorkerThread(QThread):
options.maximizestrips = True
if GUI.disableProcessingBox.isChecked():
options.noprocessing = True
if GUI.comicinfoTitleBox.isChecked():
options.comicinfotitle = True
if GUI.deleteBox.isChecked():
options.delete = True
if GUI.spreadShiftBox.isChecked():
options.spreadshift = True
if GUI.fileFusionBox.isChecked():
options.filefusion = True
else:
options.filefusion = False
if GUI.noRotateBox.isChecked():
options.norotate = True
if GUI.rotateFirstBox.isChecked():
options.rotatefirst = True
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
options.forcepng = True
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
@@ -288,6 +299,19 @@ class WorkerThread(QThread):
if GUI.jobList.item(i).icon().isNull():
currentJobs.append(str(GUI.jobList.item(i).text()))
GUI.jobList.clear()
if options.filefusion:
bookDir = []
MW.addMessage.emit('Attempting file fusion', 'info', False)
for job in currentJobs:
bookDir.append(job)
try:
comic2ebook.options = comic2ebook.checkOptions(copy(options))
currentJobs.clear()
currentJobs.append(comic2ebook.makeFusion(bookDir))
MW.addMessage.emit('Created fusion at ' + currentJobs[0], 'info', False)
except Exception as e:
print('Fusion Failed. ' + str(e))
MW.addMessage.emit('Fusion Failed. ' + str(e), 'error', True)
for job in currentJobs:
sleep(0.5)
if not self.conversionAlive:
@@ -433,6 +457,12 @@ class WorkerThread(QThread):
move(item, GUI.targetDirectory)
except Exception:
pass
if options.filefusion:
for path in currentJobs:
if os.path.isfile(path):
os.remove(path)
elif os.path.isdir(path):
rmtree(path)
GUI.progress.content = ''
GUI.progress.stop()
MW.hideProgressBar.emit()
@@ -462,17 +492,33 @@ class SystemTrayIcon(QSystemTrayIcon):
class KCCGUI(KCC_ui.Ui_mainWindow):
def selectDir(self):
if self.needClean:
self.needClean = False
GUI.jobList.clear()
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
def selectDefaultOutputFolder(self):
dname = QFileDialog.getExistingDirectory(MW, 'Select default output folder', self.defaultOutputFolder)
if self.is_directory_on_kindle(dname):
return
if dname != '':
if sys.platform.startswith('win'):
dname = dname.replace('/', '\\')
self.lastPath = os.path.abspath(os.path.join(dname, os.pardir))
GUI.jobList.addItem(dname)
GUI.jobList.scrollToBottom()
GUI.defaultOutputFolder = dname
def is_directory_on_kindle(self, dname):
path = Path(dname)
for parent in itertools.chain([path], path.parents):
if parent.name == 'documents' and parent.parent.joinpath('system').joinpath('thumbnails').is_dir():
self.addMessage("Cannot select Kindle as output directory", 'error')
return True
def selectOutputFolder(self):
dname = QFileDialog.getExistingDirectory(MW, 'Select output directory', self.lastPath)
if self.is_directory_on_kindle(dname):
return
if dname != '':
if sys.platform.startswith('win'):
dname = dname.replace('/', '\\')
GUI.targetDirectory = dname
else:
GUI.targetDirectory = ''
return GUI.targetDirectory
def selectFile(self):
if self.needClean:
@@ -534,6 +580,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
# noinspection PyCallByClass
QDesktopServices.openUrl(QUrl('https://github.com/ciromattia/kcc/wiki'))
def openKofi(self):
# noinspection PyCallByClass
QDesktopServices.openUrl(QUrl('https://ko-fi.com/eink_dude'))
def modeChange(self, mode):
if mode == 1:
self.currentMode = 1
@@ -556,7 +606,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.editorButton.setEnabled(status)
GUI.wikiButton.setEnabled(status)
GUI.deviceBox.setEnabled(status)
GUI.directoryButton.setEnabled(status)
GUI.defaultOutputFolderButton.setEnabled(status)
GUI.clearButton.setEnabled(status)
GUI.fileButton.setEnabled(status)
GUI.formatBox.setEnabled(status)
@@ -625,9 +675,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def togglequalityBox(self, value):
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
if value == 2:
if profile['Label'] == 'KV' or profile['Label'] in image.ProfileData.ProfilesKindlePDOC.keys():
if profile['Label'] not in ('K57', 'KPW', 'K810') :
self.addMessage('This option is intended for older Kindle models.', 'warning')
self.addMessage('On this device, quality improvement will be negligible.', 'warning')
self.addMessage('On this device, there will be conversion speed and quality issues.', 'warning')
self.addMessage('Use the Kindle Scribe profile if you want higher resolution when zooming.', 'warning')
GUI.upscaleBox.setEnabled(False)
GUI.upscaleBox.setChecked(True)
else:
@@ -752,13 +803,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.worker.sync()
else:
if QApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier:
dname = QFileDialog.getExistingDirectory(MW, 'Select output directory', self.lastPath)
if dname != '':
if sys.platform.startswith('win'):
dname = dname.replace('/', '\\')
GUI.targetDirectory = dname
else:
GUI.targetDirectory = ''
if not self.selectOutputFolder():
return
elif GUI.defaultOutputFolderBox.isChecked():
self.targetDirectory = self.defaultOutputFolder
else:
GUI.targetDirectory = ''
self.progress.start()
@@ -769,6 +817,12 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.addMessage('No files selected! Please choose files to convert.', 'error')
self.needClean = True
return
if GUI.defaultOutputFolderBox.checkState() == Qt.CheckState.PartiallyChecked:
parent = Path(self.jobList.item(0).text()).parent
target_path = parent.joinpath(f"{parent.name}")
if not target_path.exists():
target_path.mkdir()
self.targetDirectory = str(target_path)
if self.currentMode > 2 and (GUI.widthBox.value() == 0 or GUI.heightBox.value() == 0):
GUI.jobList.clear()
self.addMessage('Target resolution is not set!', 'error')
@@ -800,6 +854,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
event.ignore()
self.settings.setValue('settingsVersion', __version__)
self.settings.setValue('lastPath', self.lastPath)
self.settings.setValue('defaultOutputFolder', self.defaultOutputFolder)
self.settings.setValue('lastDevice', GUI.deviceBox.currentIndex())
self.settings.setValue('currentFormat', GUI.formatBox.currentIndex())
self.settings.setValue('startNumber', self.startNumber + 1)
@@ -819,12 +874,16 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'colorBox': GUI.colorBox.checkState().value,
'reduceRainbowBox': GUI.reduceRainbowBox.checkState().value,
'disableProcessingBox': GUI.disableProcessingBox.checkState().value,
'comicinfoTitleBox': GUI.comicinfoTitleBox.checkState().value,
'mozJpegBox': GUI.mozJpegBox.checkState().value,
'widthBox': GUI.widthBox.value(),
'heightBox': GUI.heightBox.value(),
'deleteBox': GUI.deleteBox.checkState().value,
'spreadShiftBox': GUI.spreadShiftBox.checkState().value,
'fileFusionBox': GUI.fileFusionBox.checkState().value,
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState().value,
'noRotateBox': GUI.noRotateBox.checkState().value,
'rotateFirstBox': GUI.rotateFirstBox.checkState().value,
'maximizeStrips': GUI.maximizeStrips.checkState().value,
'gammaSlider': float(self.gammaValue) * 100,
'chunkSizeCheckBox': GUI.chunkSizeCheckBox.checkState().value,
@@ -889,7 +948,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.addMessage('Your <a href="https://www.amazon.com/b?node=23496309011">KindleGen</a>'
' is outdated! MOBI conversion might fail.', 'warning')
break
except FileNotFoundError:
except (FileNotFoundError, CalledProcessError):
self.kindleGen = False
if startup:
self.display_kindlegen_missing()
@@ -905,6 +964,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.settings = QSettings('ciromattia', 'kcc')
self.settingsVersion = self.settings.value('settingsVersion', '', type=str)
self.lastPath = self.settings.value('lastPath', '', type=str)
self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str))
if not os.path.exists(self.defaultOutputFolder):
self.defaultOutputFolder = ''
self.lastDevice = self.settings.value('lastDevice', 0, type=int)
self.currentFormat = self.settings.value('currentFormat', 0, type=int)
self.startNumber = self.settings.value('startNumber', 0, type=int)
@@ -933,7 +995,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if self.windowSize == '0x0':
MW.resize(500, 500)
elif sys.platform.startswith('darwin'):
for element in ['editorButton', 'wikiButton', 'directoryButton', 'clearButton', 'fileButton', 'deviceBox',
for element in ['editorButton', 'wikiButton', 'defaultOutputFolderButton', 'clearButton', 'fileButton', 'deviceBox',
'convertButton', 'formatBox']:
getattr(GUI, element).setMinimumSize(QSize(0, 0))
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1)
@@ -954,33 +1016,35 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.profiles = {
"Kindle Oasis 9/10": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0,
"Kindle Oasis 9/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO'},
"Kindle Oasis 8": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
"Kindle Voyage": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0,
"Kindle 8/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'K810'},
"Kindle Oasis 8": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'},
"Kindle Voyage": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
"Kindle Scribe": {
'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
},
"Kindle 11": {
'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'K11',
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'K11',
},
"Kindle PW 11": {
'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW5',
"Kindle Paperwhite 11": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW5',
},
"Kindle PW 12": {
'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO',
"Kindle Paperwhite 12": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO',
},
"Kindle CS 12": {
'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': True, 'Label': 'KO',
"Kindle Colorsoft": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': True, 'Label': 'KO',
},
"Kindle PW 7/10": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
"Kindle PW 5/6": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
"Kindle Paperwhite 7/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'},
"Kindle Paperwhite 5/6": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KPW'},
"Kindle 4/5/7/8/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'K578'},
"Kindle 4/5/7": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'K57'},
"Kindle DX": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 2,
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KDX'},
"Kobo Mini/Touch": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1,
@@ -1035,10 +1099,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'Label': 'OTHER'},
}
profilesGUI = [
"Kindle CS 12",
"Kindle PW 12",
"Kindle Colorsoft",
"Kindle Paperwhite 12",
"Kindle Scribe",
"Kindle PW 11",
"Kindle Paperwhite 11",
"Kindle 11",
"Kindle Oasis 9/10",
"Separator",
@@ -1056,11 +1120,12 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Separator",
"Other",
"Separator",
"Kindle 8/10",
"Kindle Oasis 8",
"Kindle PW 7/10",
"Kindle Paperwhite 7/10",
"Kindle Voyage",
"Kindle PW 5/6",
"Kindle 4/5/7/8/10",
"Kindle Paperwhite 5/6",
"Kindle 4/5/7",
"Kindle Touch",
"Kindle Keyboard",
"Kindle DX",
@@ -1079,34 +1144,44 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Kobo Mini/Touch",
]
statusBarLabel = QLabel('<b><a href="https://kcc.iosphe.re/">HOMEPAGE</a> - <a href="https://github.'
'com/ciromattia/kcc/blob/master/README.md#issues--new-features--donations">DO'
'NATE</a> - <a href="http://www.mobileread.com/forums/showthread.php?t=207461'
'">FORUM</a></b>')
link_dict = {
'README': "https://github.com/ciromattia/kcc?tab=readme-ov-file#kcc",
'FAQ': "https://github.com/ciromattia/kcc/blob/master/README.md#faq",
'YOUTUBE': "https://youtu.be/IR2Fhcm9658?si=Z-2zzLaUFjmaEbrj",
'COMMISSIONS': "https://github.com/ciromattia/kcc?tab=readme-ov-file#commissions",
'DONATE': "https://github.com/ciromattia/kcc/blob/master/README.md#issues--new-features--donations",
'FORUM': "http://www.mobileread.com/forums/showthread.php?t=207461",
'DISCORD': "https://discord.com/invite/qj7wpnUHav",
}
link_html_list = [f'<a href="{v}">{k}</a>' for k, v in link_dict.items()]
statusBarLabel = QLabel(f'<b>{" - ".join(link_html_list)}</b>')
statusBarLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
statusBarLabel.setOpenExternalLinks(True)
GUI.statusBar.addPermanentWidget(statusBarLabel, 1)
self.addMessage('<b>Welcome!</b>', 'info')
self.addMessage('<b>Remember:</b> All options have additional information in tooltips.', 'info')
self.addMessage('<b>Tip:</b> Hover mouse over options to see additional information in tooltips.', 'info')
self.addMessage('<b>Tip:</b> You can drag and drop image folders or comic files/archives into this window to convert.', 'info')
if self.startNumber < 5:
self.addMessage('Since you are a new user of <b>KCC</b> please see few '
'<a href="https://github.com/ciromattia/kcc/wiki/Important-tips">important tips</a>.',
'info')
self.tar = 'tar' in available_archive_tools()
self.sevenzip = '7z' in available_archive_tools()
self.sevenzip = SEVENZIP in available_archive_tools()
if not any([self.tar, self.sevenzip]):
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
' to enable CBZ/CBR/ZIP/etc processing.', 'warning')
self.detectKindleGen(True)
APP.messageFromOtherInstance.connect(self.handleMessage)
GUI.directoryButton.clicked.connect(self.selectDir)
GUI.defaultOutputFolderButton.clicked.connect(self.selectDefaultOutputFolder)
GUI.clearButton.clicked.connect(self.clearJobs)
GUI.fileButton.clicked.connect(self.selectFile)
GUI.editorButton.clicked.connect(self.selectFileMetaEditor)
GUI.wikiButton.clicked.connect(self.openWiki)
GUI.kofiButton.clicked.connect(self.openKofi)
GUI.convertButton.clicked.connect(self.convertStart)
GUI.gammaSlider.valueChanged.connect(self.changeGamma)
GUI.gammaBox.stateChanged.connect(self.togglegammaBox)

View File

@@ -1,6 +1,6 @@
# Resource object code (Python 3)
# Created by: object code
# Created by: The Resource Compiler for Qt version 6.8.2
# Created by: The Resource Compiler for Qt version 6.9.1
# WARNING! All changes made in this file will be lost!
from PySide6 import QtCore
@@ -6062,6 +6062,588 @@ $\xb8I\x00B\xd9\xcb $]\xa6\x90qE\xb4{\
\x8a\xf6\x7f5\x09`\xd3%\x01\xf9'\xc1\xcd\xfa\x01\x0f\
\x02L\xdb\x8e|\xe3\xd9\x00\x00\x00\x00IEND\xae\
B`\x82\
\x00\x00$=\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x01A\x00\x00\x01\x02\x08\x06\x00\x00\x00`\xc2e\xf3\
\x00\x00\x00\x09pHYs\x00\x00,K\x00\x00,K\
\x01\xa5=\x96\xa9\x00\x00\x00\x01sRGB\x00\xae\xce\
\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfc\
a\x05\x00\x00#\xd2IDATx\x01\xed\x9dOl\
U\xe7\x99\xc6_P6\xd3\x11!\x95\x92\x15iq\x16\
\xb30\xa9\x94D\x8d\x94(\x15\x89S\xc9\xa8\x15\x8bB\
\x89\x94d\xd1\x09Fj\xe8f\x0a\x8c\xc2bfj\xd5\
\x94\xa8]\x18M\xec\xaeJ*\x81\x99\x8c\x14\x22AC\
\x16\xa8\x11V\x1b\x13\xd4\xaaH\xa9\xea\xa8-,s\x99\
\x94U*\x15\x075\xb3\xa4\xe7\xb9\xf7|\xf8\xf8\xe6\xfe\
\xbf\xdf\xfb~\x7f\xce\xf3\x93\x0e\xf7\xfa\xda\xd8\xbe\xbe\xf7\
<\xe7y\xff|\xef\xb7IH\x14\xdc\xb9sg\xa2\xf2\
\xa1\xbb\x7f_yH\x97\x8f\xb7w\xf8V\xed_\xd3\xef\
\xf1n\x0c\xfb\xf5!h\xc8\xf0\xdc*\x8fa\xbe\xe7Z\
\xdb\xffi\xff\x1e\x8d\x0e\xf7om\xda\xb4\xa9\xd7\xcf!\
\x91\xb0I\x88\x17\x0a\x11s\xa2\x81c\xa2r\xdf\x1d[\
\xdb>/\x92\x86\xd0\x90\xf1i\xc8F\xe1\xc4\xc7NX\
\xab\x9fk~L\xf1\xb4\x85\x22\xd8\x87\x8a\xb8=*\xeb\
\x02\xe6\x1c\x98{\x8cbF|\xd3\x90\x8a0\x16\xc7\x0d\
\xd9(\x9a\xab\x14K?P\x04K\x0a\xb1\x83\xa0\xe1\x98\
\x90\x96\xc8\xb9\xfb\x147\x12+N\x14q|X\x1c\xab\
\xd2r\x92\xabB\x06\xa6\x96\x22X\xe6\xdf\xa6\xa4%t\
\xcf\x94\xb7\x84\xe4\xc4jy@\x1cW(\x8c\xdd\xa9\x85\
\x08\x96!\xed\x1ei\x09\x1en\xe9\xeeH\xddh\x86\xd0\
\xc5qYZ\xa2\xb8\x22\xa4I\xb6\x22X\xba=\x08\xde\
\xb7\xa4\xe5\xfa\x08!\xeb8Q<#-QlHM\
\xc9J\x04K\xc7\xb7_(|\x84\x0cK\xa38\xde)\
\x8e\x0bus\x89Y\x88`!~S\xc5\xcd\x0f\x85\xc2\
G\x88\x0f\x1a\xc5\xb1R\x1c\x8bu\xc8%&+\x82\x15\
\xd7\xf7\x92\xb0\xb0A\x88\x16\x10\xc1E\xc98dNN\
\x04K\xf1;T\x1c\x87\x85\x05\x0eB,Y*\x8e3\
\xb9\x85\xcb\xc9\x88 \xc5\x8f\x90hh\xba\xc3B\x0c\x97\
$\x03\xa2\x17A\x8a\x1f!\xd1\xd2(\x8ec\xa9\x8ba\
\xd4\x22X\x16<N\xcb\xfaZ[BH|4$a\
1\x8cR\x04\xcb\x1e?\x88\xdf\x94\x10BR\xa1!\x09\
\x8a\xe1f\x89\x8cB\x00\x11\xf6\xfeA(\x80\x84\xa4\xc6\
Dq\x9c.\xce\xe1\x8f\x8ac\xbf$B4N\xb0t\
\x7fo\x0b\xdb]\x08\xc9\x85\x0b\xc5q$\xf6\xd6\x9a(\
\x9c`\xc5\xfdQ\x00\x09\xc9\x07,[\x85+\xfc\xa1D\
LP'XV~\x91\xfb\xdb#\x84\x90\x9ci\x14\xc7\
L\x8c=\x86\xc1\x9c`9\xbf\x0f\xee\x8f\x02HH\xfe\
L\x14\xc7{\xc5y\xffZi~\xa2!\x88\x13,\xfe\
\x08X\xea\xb6 \xec\xfb\x1b\x88\xbf\xfc\xe5/]?\xfe\
\xf4\xd3O\x9bG\xbf\xffS\xe5\xf6\xed\xdb\xb2\xb6\xb6&\
\xe3\x80\xef\xd1\xe9\xe7\xfa\xe0\xde{\xef\x95-[\xb6\x88\
/\x1e|\xf0\xc1\x91\xbe\x06\xbf\x07\x8en\x8fu\xfa<\
\x19\x88Fq<\x1bK\xae\xd0\x5c\x04\xcb\xfc\xc0\x9c\xd4\
\x00'D\xb8ub\xe5\x8e\xaa\x10\xdd\xbcys\xc3\xd7\
\xb7\xdf'\xf1S\x15D'\xa8N\xcc\xb7n\xdd\xda\xbc\
u_\xe3>\x8f\xdb\x9a\x0b\xe9\x5c!\x84\xc7$0\xa6\
\x22X\x08 \xf2\x7f\xfb%a `\x10('l\xee\
\xbesFU\xc1#dP \x88N\x1c\xb7m\xdbv\
W \xab\xb7\x838\xda\x04\x09^A6\x11\xc12\x07\
\x80\xf6\x97)I\x00\x08\xd9\xb5k\xd7\x9a\xb7pi\xee\
c\x8a\x1b\x09\x8d\x13\xc3\x1d;v4\xc5\x12\xb78\x12\
w\x93\x0d\x09\x18\x1e\xab\x8b`)\x80\xefI\xc4\xed/\
\xbf\xfb\xdd\xef\x9a\x22w\xf5\xea\xd5\xe6}\x0a\x1dI\x0d\
\x88 \xc4\xf0\x89'\x9eh\xde>\xf9\xe4\x93\xa9\x09#\
&]\xa3z|A\x8cQ\x15\xc1X\x05\x10\x22w\xee\
\xdc9Y^^\xbe\xeb\xf0\x08\xc9\x0d\xe7\x12\xa7\xa7\xa7\
S\x12E\xf3<\xa1\x9a\x08\xc6&\x80U\xe1\x83\xdb#\
\xa4n@\x08\x9d B\x1c#\xc6T\x08UD0&\
\x01\x84\xe0\x9d:u\xaa\x19\xea\xd2\xf1\x11\xd2\x02y\xc5\
]\xbbv\xc9\xcc\xccL\xac\x05\x97\x85B\x08\x8f\x88\x01\
Z\x22\x88\x22H\xd0&h\x88\xdf\xe2\xe2\x22]\x1f!\
}\x803\xdc\xb7o_S\x14#\x0b\x99\x97\x0a!\x9c\
\x11e\xbc\x8b`\xe8>@\x84\xbc\x10?\xf6\xd9\x112\
\x1c\x10@\x08\xe1\xa1C\x87br\x87\xeaB\xe8U\x04\
C\x0a \x9d\x1f!\xfe\x80;\x84\x18\xe26\x02T\x85\
\xd0\x9b\x08\x16\x02\x88\xf0\xf7m1\x06\x8e\xef\xe8\xd1\xa3\
\x14?B\x14p\xa1\xf2s\xcf='\x81Q\x13B/\
\x22X\xce\x02D!dB\x0cA\xc1\x03\xee\x8f\x05\x0f\
Bt\x81\x18\xce\xce\xce\x86\xae*\xabT\x8d}\x89\xe0\
Gb(\x80t\x7f\x84\x84\x01\x8e0p\xce\xd0\xbb\x10\
\x8e-\x82\xd6y@\x14>\x8e\x1f?N\xf7GH@\
\x0e\x1f>\xdc\x14\xc3@\xec/\x84\xf0\x8cxb,\x11\
,\xf7\x118-F@\xfc\x10\x02\x13B\xc2\x037\xf8\
\xe6\x9bo\x86p\x85Xb\x87\xb5\xc6\xab\xe2\x81\x91E\
\xd02\x0f\x88\xf0\xf7\xe0\xc1\x83\xcd%n\x84\x90\xb8\x08\
\xe4\x0a\x1b\xc5\xf1X!\x84\xb7dL\xc6\x11\xc1\xa5\xe2\
\xe6%Q\x06\x02\xf8\xe2\x8b/\xb2\xef\x8f\x90\x88\x09\xe4\
\x0aW\x0a\x11|V\xc6d\xa4\xf1\xfae\x18\xac.\x80\
p~\x14@B\xe2\x07\xe7\xe8\xee\xdd\xbb\x9b9{C\
\xa6|l\xe24\xb4\x13,\xd7\x05co\x90\x09Q\xc4\
\x09 \x0b \x84\xa4E\x80\xf0\xf8\xd9q6p\x1a\xc5\
\x09\xe2\xd9M\x88\x22\x14@B\xd2eaa\xa1\x99\xc3\
7<\x7fO\x8f\xb3y\xd3PN\xb0,\x86|$\x8a\
0\x07HH\x1e\x18\xe7\x09G\x9e:3\xac\x13\x9c\x13\
E(\x80\x84\xe4\x83\xf1\xf9|\xb80iS2\x02\x03\
;\xc1\xf2\x07\xbc'J\xc0:#\xb1J\x01\x5c\xdf\xad\
\xcc\xedPV}\x0c\xb4\xefP\xd6m\xc7\xb2^W`\
\x9fWg\x1f\xdfK\xebu\x1fd_\x98A\xb7-m\
\xff\xba\xf6\xadO\xddf[L\xe3l\xc4\xd0\x116d\
\x84\xb6\x99aD\x10\x028%J`\x19\x9cqeI\
\x1d'N\xd5\xdd\xc3\xbam\xb9\xc8=l\xf3\xa3\xba\xe5\
\xaa\xbb\xadn\xbbZ\xfd\x18\x02\x9a\xb3\x010\x14\xc2c\
\x85\x08\xce\x0d\xf3\x1f\x06\x12A\xed\x95!H\xa4b\x10\
Bj\xe0\x05\x85[\xc3\xa2r\xb7\x0b\x98;(jd\
\x14\xaa\xe2\xe8v<t\xfb\xe0\x5c\xbf~]R\xc6P\
\x08\x1f\x1af\xe7\xbaAE\x10-1*\xa3\xf2\xf1\x22\
\xef\xdc\xb9Sb\x06b699yw\xe3\x1a'z\
\x149b\x89\x13\xc6\xea\x91\x9a0\xe2\xbc\xb9x\xf1\xa2\
\xf6\xb93T\x13u_\x11\xd4\xcc\x05\xc6\x9a\x07\xc4\x0b\
\x84\xad\x0b1>\x08\x93v3\xdd\xf4\x9ad\x00\xce!\
LSrG\x0a\xa2\x88\xf3\x0a\x8eP\x99\x81{\x07\x07\
\x11A\xb5\x5c`L\x03\x11 |n\x9f\x85H\xa6\xe9\
\x12240\x14\x10C\xe4\xd7\xb1\xb9X\xac\x1c8p\
\xa09\x9fP\x91\x81\xdd`O\x11\xd4\xec\x0b\xc4\x0b\x85\
\xf2yh\xe0\xf8\xd0\xe1N\xe1#\xb9\xe1\x04\x11F#\
F\x878??\xaf=\xb1z\xef \x9b\xb9\xf7\x13\xc1\
%QZ#\x8c<`\xc80\xd8\x8d\x0c\xa7\xf8\x91:\
\x80s\x0d\x05\xc8\xf3\xe7\xcfK, \xfaB~P1\
\xdd\xd4(D\xf0\xa1~_\xd4U\x045]`\xc8j\
0\x9c\xdf\x89\x13'\x98\xe7#\xb5\x04b\x88P\x19b\
\x18C.\x1eEF\xe4\x07\x15\x0b%}s\x83\xbdV\
\x8cL\x89\x02\xf8\xc3\x87\xb8\x1a\xb9\xf2\xfc\xd9\xb3g)\
\x80\xa4\xb6\xe0\xbd\x8f\xf4\xcf\x95+W\xa2\xd8Z\x13\x15\
neC\xd4w\xcaL/'\xa8\xd2\x16\x13\xa2)\x1a\
IX\xbc\xe0li!d#\xb1\x84\xc90(\x8a\xa9\
\xa9\x9en\xb0\xa3\x08\x16\x02\x08\xf1\xfb\x83x\xc6\xba'\
\x10\xa2\x87\x0aT\x04\xdb\x05\x12\x125\xa1\xd7\xed#,\
F~P\x89\x9e\x95\xe2n\xe1\xf0aQ\xc02\x0f\xe8\
\xc2_\x0a !\xfd\xc1\xf9\xe2B\xe4\x10(\x87\xc5S\
e\x8d\xa3#\xdd\x9c\xa0\xf7-4-]`\xc0\x0d`\
\x08I\x9eP\xae\x10\x91\x1b\x84X)m\xd5uM\xf1\
\xe7\x9c`\xb9BdB<c\xe5\x02)\x80\x84\x8c\x87\
;\x87\xa6\xa7\xa7\xc5\x12\xac~Q\xd4\x89C\xdd\x06\xaf\
v\x0a\x87\xf7\x88\x02\x16\x1b\xa5\xe3\x0aB\x01$d|\
p\x0e\xbd\xfe\xfa\xeb\xe6\xe11\x1a\xbb\x95\x1c(\x04\xb0\
c\xa1\xb7\x93\x08>#\x9eA5\xd8\xc2Z\xa3\x03\x9d\
\x02H\x88?Bl\xa7\xa9\xe8\x06;\xb6\xcbl\x10\xc1\
2y\xe8\xbd-\xc6\xa2%\x06/\x16\xd6\xfd\x12B\xfc\
b-\x84\x8a\xa6\xe9\xd1N!q\xbb\x13\x9c\x12\xcf\xe0\
\xc9h/\xe4\x86\xfb\x0bU\xd5\x22\xa4\x0eX\x0b\xe1\xe9\
\xd3*\xe3K!\x80\xfb\xdb\x1fl\x17A\xef\xf9@\x0b\
\x17h0\x96\x87\x90\xda\x03!\xb4j9\x83n(m\
S\xf0\xad\xf6\x07\xdaE\xd0{>P\xbb\x13\x1d\x83\x10\
\x98\x07$\xc4\x06,>@c\xb36\x10@%7\xf8\
\xb9\x90\xf8\xae\x08\x96\xabDF\xde\xbb\xb3\x13n<\xb8\
&\xb8:\x11Bl@\x07\xc6\xc9\x93'M\x96\xa0*\
\xcd\x1a\xfd\x5c\x95\xb8\xea\x04\xa7\xc43\xdam1t\x81\
\x84\xd8c\x95\x83wS\xb3\x15\xd8\x90\xf6S\x15\xc1K\
\x97.\x89&t\x81\x84\x84\x01CI0\x96N\x9b\xe5\
\xe5eQ`C^\xb0*\x82\xdb\xc5#Pq\xcd\xaa\
0] !a\xc1\x5cN\xed\xb0X\xa9@2Q]\
K\xdc\x14\xc12Q\xe8\xb5?\x10\xf9@M8\x18\x81\
\x90\xb0\xc0\x84\xcc\xcc\xcc\x88&n\x87=\x05\xa6\xdc\x1d\
\xe7\x04\xbd7Hk\x86\xc2\xf8\xe3s,>\xc9\x0dw\
\xc2\xbb\x9d\xe3,\x0a\x8b\xe3\x82\xb0X\xdb\x0d*\x85\xc4\
w;a\xee)o\xbd\x8b\xa0\xa6\x13\xb4\xc8E\x10b\
\x81k\x05\xe9\xb7J\x02m)\xd5\xad`c1\x01\xf8\
\x9d\xe0\x065\x07\xa4\xe0o\xa3\xb03\xdd\x94\xbb\xd3\x1c\
\xa5U\x84\xc3\x0b\xc5\x8d\xd7r\xcf#\x8f<\xa2\xd5\xec\
\xa8=\x856N\xfe\xfa\xf1\xc6\x8f\xef\xff\x92d\xc1g\
\xc5{\xe4\xb3\xb5\xf5\x8fsy^\x03\x00\xa3p\xf0\xe0\
\xc1\x91\xdc\x1e\xc4\x07\xcbD\x91\x1b\x0f}.\xe0<\xc7\
\x98<\xad\xf3\x1d`\xe0\xaaB\x7f\xe2\x177m\xdat\
\xcb9\xc1G\xc4#xq5\xff \xd9\x0a \x84\xee\
\xdaoDn\xfcY\xe4\x93\xe2\xfe\x8d?\xb5\x04\xe2\xb3\
.\x7f\xcb/\x14a\xc8\x03_n\x09\xc7\xf6\x87\x0b\xbb\
\xf0\xb5\xe2\xf6+\xad\xc7c\x03\xcf\x09\xcf\xed\xff\xfe\xd4\
\xba\xff\xf7\xb5\xcf\x0b{\x15<\xa7\x07\xbe\xd4z>\xb8\
\xdf|n\x0fK.@\xf8F\x15@\x80\xf3\x0b\x0e\x09\
\x87kY\x09\x95'w{v+577Az@\
A\x04\x11\x01\xaf8'\xf87\xf1\xd8(\x8d| ^\
`\x0d\x8cv\xaf\xb7\xe3\xfaoE>\xf8e\xeb\xe8%\
\x0a\xc30\xf9\x94\xc8\xd3/\x14\xc2\xf1TXg\xe5\x9e\
\xdb\xfbg\xbb\x0b\xf90@\xdc'\x0b1|\xfc\x9b\xad\
#F\xb1\x1f\x10\x8d\xbdvpn\x84\x9a\xa4\xa4\xbd\x8f\
8f\x1bb\xb4\x97g\x8e\x14Np\xe1\x9e\xb22\xec\
u\xa5\x88f\x93t\x16\xf9@\x08\xc2\xbb\xc5\x0b\xfa\xcb\
\x93~\xc4\xa1\x1d\x88\x0f\x0e\xf0\xf4\xf3\x22\xfb\x8e\xda\x89\
\xa1\xe6s\xc3\xf7\xfb\xfd/[\xc7\x1b\xf7\xb6\x84\xd0\xf2\
\xb9yDcM=\xce\xbb\xdd\xbbw7]!\x0a\x16\
\x96@\x80qnj\xb5\xc5)}\xdff\x04\x8c\xea\xf0\
\x84x\xe6\xe6\xcd\x9b\xa2\x85\xc5\xbaE5\xe0\xf4N~\
_\xe4\xbb\xff\x22r~^G\x00\xdby\xff\xad\x22\xdb\
\xfbx\xeb\xe7\xfar\x9a\x9d\xc0s\xf9\xc5\x89\xe2g}\
\xd5\xe6\xb9\xe1\xfb\xbb\xe7\xf6\xea\xdeu\xd1O\x00\xcd\xa2\
!\xc2\xe4\xe3\xc7\x8f\x07\xd9\xd7[3M\x85\xe7\xa5P\
)o\x16\x84!\x82^] \xd0,\xeb')\x82N\
\xfe\xe3\xeb\xad\x137\x04N0\xf0{\xf8\x06\x02\xf4\
\x9f_\xb7\x13\xf6N?\x1fB\xa8-\xf4\x9e\xd0\xcc\x97\
;\xb0\x8d\xa6\xb5\x10j\xe7$\x15\x22\xcc\x09\xfc\xa3\xe2\
\x04\xb5\xaetH\xc0&\xb7J$\xb4@\xb4\x83\xdf\xe3\
\xf0\xe3~\xc4\x02\xcf\xe7\x8d\xd9\x96\x00}\x12\x81\xf8h\
\x0a}\x82@\x08\x95\x86\x10t\x04\xe7\xa6\xe6\xf9y\xfd\
\xfau\xf1\xcc}H\x07z\x17A\xcd\xab\x5cR.0\
6\x81\xa8\x82\xdf\x07b\xf1\xee\xcfed \xa2xn\
\xefzOV\x8f\x8fO\xa1O\x1c\x84\xc6\xda\xab\xb7\xaa\
h\xe6\xec?\xfeX\xe5\xf5\x9c\xf0\x1e\x0ek\xfe\xc1\xb7\
l\xd9\x22I\x10\xb3@Ty\xe3\x07\xa3\xb9&\xb4\xee\
\xb4\xc7m\xac|R\xbe\x06(\xa2\xd4\x1cT\xa2\xad\
\xd0\xcc\x0b*8A\xf0\xa8J8\xacE\x12N\xd0\x09\
`\xcc\x02Q\x05\xae\xe9\x8d!\xba\xf1\xaf\xbc\xd5z~\
1\x84\xf6\xfd\x80\x10\xfe\xf7\xfe\xda\x87\xc70&Va\
\xb1vqD\x81f8\xbcU<\xa2Y\x14\xb1\x18\xe4\
8\x16\xce!\xc5\x16\xfe\xf6\x03\x8eu\x10!\x84\x00\xfe\
\xec\xfbi\x08`\x15\x08}\xcd\x85\x10E\x12\x8b\x82\x0c\
r\x82Z\xe7\xa9R\x85xb\xb3xFS\x04\xa3.\
\x8a8\x07\x98\x9a@8 \x84\xbd\x84\xe2\xf7\xef\xb6\x04\
0Uj.\x84\x10\x10\xed\xf9\x9e\x0e\xcd\xf3TA\xc8\
\xb7B\x04\x1f\x92D\x88\xd6\x09\xa6.\x80\x0e\x08E\xa7\
b\x09\x9e\xdf\xcf\xfeM\x92\xa7\xdb\xf3\xab\x09\xda\xfb\xfd\
8&''E\x0b\x85\x9a\xc3\x17\x93r\x82Q\x8a \
\x84/\xc6\x0a\xf0\xa8\xa0X\x82\xb5\xbd\x8e\x5c\x04\xde\x81\
\xe7\x97Pc\xb5O\xd0gg\x15\x12'\x84\xff\x9c\xa0\
&Q\x8a N\xaa\x5c\x04\xd0\xf1\xdaK\xeb\xa2\x97\x93\
\xc0;P,\xa9i\xfb\x8c\xc5\x16\xb8\x9a\x22\xa8`\xb2\
\xb6\xab\xac\x18\xa9\x0d\xc8\xa3\x85Z\x01\xa2\x09D\xaf\x99\
C\x9b\xcfO\x00\x01&\xf3@\x08k\x88E\xcf\xa0\xa6\
Y\xb9}\xfb\xb6\xf8\xc6{8\xac\xf1KF\xc9_K\
\xa1\xc8\x15\x08\xfc\xf9\x8c\x0b\x09\xa8\xe4\xe7\xfc\xfc\xba\xa0\
\xb9\xef\x8fCS\x04\xd7\xd6\xd6\xc47\xdeEP\xe3\x97\
tD\x95k\x88e\x19\x1c\x19\x1d8\xdd\x9a\x85\xc5\x16\
\xe3\xfa\x13\xcb\x09\xfao\x91\xa9\x05\xcd\x19y\x19\x86\xc1\
u$\xe5\xb6\x9f\x11\x89}\xdf\x12k(\x82\xa3\xf0\xbf\
\xde\xf7; \xa1\xa8\xce^\xac\x09\x14\xc1\x8dP\x04\x87\
\x05\x0e0\xc7bA\x9d\xc99\xb7\xdb\x01\x8b6\x19-\
4f\x95&%\x82Q\x5c\xc1\xde=)$3j\xe6\
\x06S\x16A\x0d\xe8\x04\x87\xe1\xfao66\x12\x93|\
\xa8\x99\x1b$\xebP\x04\x87\x81\xc5\x90|\x81\x13\xe4\x05\
\xae\x96x\x17\xc1\xad[\x93Y\x802<\x1fp6]\
\xd6\xd4\xe4\xf5Mn:\xbb2\xdeE0\x99\xc1\xa7\xc3\
\x82\x13\x84}\x81y\x13\xfb\x10\x5c\xa2\x02\xc3\xe1A\xe1\
\x84\xe2\xfc\xc1r\xba\x1a\x14HRv\x82\xdb\xb6m\x13\
\xdfP\x04\x07\xa5\xa6\x93Gj\xc75\x8a`\xdd\xa0\x08\
\x0e\x02\xd6\x99\xb27\xb0\x1e\xa0\x03 c,\xb6\xa8H\
\xac\x05\xe7\x16D\xf0\x86\x90\xde\xb0jX\x1f2\x7f\xad\
5\x07\x9e:4EP\xa1\xf0z\x8bNp\x10n\xfc\
QHM@^0c\xd7\xbfk\xd7.\xd1FS\x04\
5\x0a\xaf\x14\xc1A\xa0\x13\xac\x17\x19\x87\xc4\x9a\xbb\xc1\
94g\x16*\xe43o$%\x82\xc1r\x0d\xcc\x07\
\xd6\x8bL_\xef\xe9\xe9i\x93\xe9\xecJ\xfb\x037\xd1\
\xf8\xfd)\x82\x83P\xd3Q\xec\xb5%\xd3\xd7\xfb\xb9\xe7\
\x9e\x13\x0b4g\x8a*\x88\xe0G\xdeE0\xbb\xf2\xfb\
gz/(\x89\x94\xbf\xe7\xd7\x14\x8f\xf3\xd2\x22\x1f\x08\
4\xa7W+T\xb7\xd3\x0a\x87\x83\x90\xe1\x09A\xfa\x90\
\xe1\x85\xef\xd0\xa1Cb\x01v\xb4\xd3\x02.P\xc1\x09\
\xb2:\xdc\x17:\xc1\xfa\xf1\xf7\xbc^\xf3}\xfb\xf6\x99\
\x85\xc2\x9aE\x11\xa5\x1e\xc7U\x88\xe0G\x92\x08Ar\
\x82\x14\xc1\xfa\xf1\xff\xf9\xb8\x7f\x84\xc1\x87\x0f\x1f\x16+\
.]\xba$Z(\x89`ZN0\x88\x08~!\xe3\
\xa98\xa43\xff\x14\xe1\xfe\xd6#\x80\xd0\xf1\xe4\xc9\x93\
fyz\x9c\x9f\x9a\xf9@\x8du\xc3\x9b6mZe\
a\xa4\x1f\x14\xc1\xfa\xf1\xcf\xba\xaf\xb9E\x9b\x0a~\xc6\
\xfc\xfc\xbc\xc929\x87\xa6\x0b\x04\x0a\xcfe\x15\xff\xdc\
#\x84\x10S\xb4E\x10\xdf\xff\xcd7\xdf4\x15@\xb0\
\xbc\xbc,\x9a(4z7s]I\xad\x1d\x0e\xb2\xc7\
\xc8\x03_\x12R3\x94\xdd?\xa2%\xad\x88\xe9\x89'\
\x9e\x90\x8b\x17/\x9a\x0b \xceMM'\xa8\xb4\xd2e\
\x05\xff\xb0:<\x08_\xc8#GD\x06\xc4\xe0\xc27\
33#>\x81\xfb\x9b\x9d\x9d\x95\xb3g\xcf\x06II\
i\xb6\xc6\x00\xad\xca0\xfe\xf1\x1e\x0eg9\xab\x0c\xce\
\x80S\xa5\xeb\x83\xc1E\xef\xc0\x81\x03\xcd\xe5e\xe7\xce\
\x9d\x93q\x80\xf8AP\xf1\xfd,r\x8d\xddX\x5c\x5c\
\x14M\xb0\xe4O\x81\x06\xfe\x81\x08\xde\x92D\x08\xb6\xe5\
\xe6\xc4W\xb8t\xaeNl\xff\x8aX\x80\xc2\x05*\x9e\
\xc3\x0a\x08\x8c\x06D\x01+@,\x06\x22\xf4\x03B\xae\
ynB\xdc\x15\x9e\xe7-T\x86q\xc7\xbb\x08\x86\xbc\
\x1a\xa9q?'\xf1\xd6\x8a\x07\xbe,V\xa0\x87\x0f\x8d\
\xcc\x08'\x91Sko\x03\x83\xe0\xe1\x9cryD\x88\
Al\xe7\x98\xb6\x0bD\x9eS\x81\xcb\xee\x8e\xf7pX\
\xf3\x05\x0a6@\xc1\xc8\x19\x90H\xd8\xfe\xb0X\x02q\
\x83\x10Z\xad\xea\xf0\x89\xb6\x0b\x04Jk\x9eW\xdc\x1d\
\x14F\x92\x09\x87o\xdf\xbe-A\x98\xfc\x9a\x90\x9a\xf0\
e[\x01L\x1dm\x17\x08\x94B\xfeUw\xc7\xbb\x08\
j\x16F\x829AT\x0bY!\xae\x07;x\xc1\x1b\
\x94\x85\x85\x05u\x17\x88\xdc\xa7\x82\xa6 \x1f\xb8\xe2>\
\xe0\xb2\xb9AaH\x5c\x0f&\x9f\x12\xd2\x1f\x88\x9f\x85\
\x0bT\x0a\x85/W?\x80\x086\xc43Zy\xc1\xa0\
\x22\xf8\xd5o\x0a\xa9\x01\xbc\xd8\x0d\xc4\x8b/\xbe(\xda\
\xb8\x5c\xa9\x02\x17\xaa\x1f\xa88\xc1,E\xf0q\x8a`\
\xf6 \x1f\xc8\x15B}\xb1\x08\x83\x81RU\x18\xacT\
?H\xca\x09\x82`\xbd\x8289\xee\xe7\x09\x925\xcf\
\xbc \xa47h\xe5\xb1\x08\x83\x81\xd2\x08\xb0\xcbE>\
\xb0Q}@\xc5\x09jl\x8b\x17\x05O?/$c\
\x98\xf2\xe8\x09\x0c\xc8\xd1\xa3G\xc5\x02\x0c\x82U*\xb2\
.\xb5?\xb0\xb9]\x15}\xa0Y!\x0e\xe6\x04\xc1\x0e\
&\xcd\xb3\x85\xa1pO\x90\x8aB\x1e\xd0\xea\xfcS\xec\
\x99\x5ci\x7f \xb9\x01\x0aAE\x10\xfd\x82\xac\x1e\xe6\
\xc97_\x16\xd2\x1dK\x01D_\xa0Ro\xe0\xe5N\
\xa6\xcf\x89\xa0\xd7qZY\xf6\x0a:\x182\xe5\x09\x1b\
\xe2\xbb\x82\x10Xs\xef\x90v\x147\x85Z\xea\xf4\xa0\
\x13\xc1\xbf\x89G\xb2\x5c:\xe7x\xe6y6N\xe7\xc6\
\xce\xe7\x19\x0aw\x01\x028\xee\xa4\x9ba@.P\xc9\
\x05bQ\xc8\x85N\x9f\xd8\x5c\xf9\x02odY\x1dv\
`\xac\xd67\x18:e\xc5>\x9bd\x7fJ\xb8\x1c\xa0\
\xa5\x00\x02\xc5M\xa1.\x14\xa1pG\x9dc8<\x0a\
O\xb3\x95\x22\x1b\x90\xe3\xa5\x0b\xdc\x00\x8c\x06\x04P{\
Pj;\x8a\x15a\xd0\xb5\xafG\xc5\x09j\x8a \x06\
Q\x06\x07'\xcdN\xb6\xcbd\xc1\xb7\xe9\x02\xab@\xf8\
\x80\x969@\xa0\xbc5\xe8e7;\xb0\x13\xc9\x85\
\xc3Q8A\xc0\x10*}\xe0\x02\xd9\xf6t\x97S\xa7\
N\x99V\x81\xab\xa0\x18\xa2h\x9e\x96z}\xd2\x89`\
C<\x02\x11\xd4\x5c:\x17\x85\x10\xd2\x0d\xa6\xcf\xc1\x9f\
\x0aY\x0f\x7f\x8f\x1f?.!P\x5c#\x0c\x1a\x85\x0b\
\x5c\xea\xf5\x05*N\x10d]\x1cq\xfc\xebqV\x8a\
S\x85\x15\xe1&p\x7f\xbbw\xef6\xcf\xff9\xdc\xf6\
\xa0\x8a\x1c\xeb\xf7\x05N\x04W\xc53\x9a[\xfeY\xe7\
+\xba\xc2Jq\xba\xd4<\x9d\x01\xd1\x83\xf8\xc1\xfd\x85\
\x8c\xac\x94\xc3\xe0\xbe.\x10\xb8\xf1\xfa\xde\x9d\xa0\xe6\xfa\
\xe1h\xf2\x82\x00+\x0d\xde\x7f\x8b\x1b1\xa5\x04\x8a!\
5u\x81n\x00B(\xe7W\x05\xd5`\xec\x92\xa7H\
_\x17\x08\x9aN\xb0\xec\x9fI\xa6B\x1c\x8d\x13\x04p\
\x83\xdf\xb3\x99\xaaA<\x80I@\xfb^\x91\xba\xe1\xaa\
\xbe!Z_:\xa1\x5c\x0d\x06\x03\xb9@P\xddhi\
\xad8\xee\x13Oh\x8a\xe0\xcd\x9b7%*\xb0\xe4\x0a\
9\xa6+o\x09\x89\x9c\x1f\xbc-u\x01\xb9\xf3\xf3\xe7\
\xcf7\xf3~QEO\x05\xc8\x03*\xefQ>\x90\x0b\
\x04U\x11D^p\xbbxB3'\x18Ma\xa4\x0a\
\x8a$\xd7\x7f\xcb\xb08fj\x10\x06\xc3\xe5]\xbdz\
\xb5y\x1b\x83\xe3\xeb\xc4\xec\xec\xac\xb6\x00\x0e\xec\x02A\
U\x04\x93Z:\x87+[T\xfb\xaf\xba\xb0\xf8\xd5o\
\x0b\x89\x90\x0c\xc3`\x9c\x07H\x0dA\xf4p\x1f\xa2\x17\
\x9b\xe3k\x07!\xb0r\x1e\x10\x1c\x19\xe6\x8b\xdb\x9d\xe0\
K\xe2\x09\xb7i\xb4\xd6\x8b\x82\x17]\xd3m\x8e\x04\xc2\
\xe2o|W\xe4\xdd\x9f\x0b\x89\x08\xb41E\x14\x06C\
\xb8\x10\xa2\x0e\x0b\xd2@\xaeO6\xcah\xa8\x0f\xe8\x05\
T\x9c\x10\xe3X*\x5c\xe0\x85a\xfe\x83\x9a\x13\x04\x10\
B\xad\x22\x06\xaez\xd1\x89 @\xeb\x05\xc2\xe2\x1b\x7f\
\x16\x12\x09\xfb\xe2\x0a\x83\xf1\xdeE\xae\xaeN`\xbf\x90\
\xf9\xf9yQ\x06\x1a6p.\xd0Q\x1d\xaa\xea\xbdW\
prrR\xb4\x88\xae8\xe2@X|\xe4\x0c\x9b\xa8\
c\x01}\x9c\x91\xf5r^\xbatI\xea\x04\xcc\xca\xeb\
\xaf\xbf.\x06,\x8e2)\xbf*\x82\x0d\xf1\x8c\xa6S\
\x8b5\xe9\xdb\x04\xae\x83K\xb2\xc2\x83\xed3\xbf\x13f\
)X/\xa2\x18\x02b\x044\x00\x95`\x83\xfc=\x8a\
!s2\x02wE\xb0\xec\x15Lf\xa4V\xf49\x11\
l\xd1\xf9\xed\xfa\xf5\xa3E\x03\x0a!G\x96$6\x90\
\x1e\x8a\xbdx\xe1\x0b\x84\xc0F\x02\x08\x9e\x95\x11i\xdf\
c\xc4kH\xac\xe9\x04\xf1F\x8a\xaai\xba\x13\xc8E\
q\x87:{\x5c!$\xc2v\x98\xa8#\x18\x8f\xa0\x08\
r\xf6\xecY+\x01<6\xce\x86q\xed\x22\xd8\x10\x8f\
\xb8\x0a\xb1\x16\xd1\x8b @8\xb6\xfda!\x86|\xef\
\xa7\xd1\xf6\x03\xa2\x9d%w\xd0\x06cP\x04q\xac\x8e\
\x1a\x06;TE\x10h\x86\xc4I\x5cUQ(\x81+\
\xe1\xc6\xed6\xe0\xa2\x13\xf1fX\xb9;A4B\x1b\
\xb4\xc18\x1a\xc5\xb1W\xc6D5\x1c\x06\xc8\x0bh\x91\
\xccU\x95Bh\x03V\x84D<\xd5'\xe7| \xcc\
\xce\xc5\x8b\x17-\x1a\xa1\xab\x8c\x15\x06;\xd4EP{\
\xf9\x5c2o*\x84g\x14B= \x80\x91\xaf\x08I\
\xb1\xc1y\x10\x5c\x01\xc4\xb8o\xf7\xd80K\xe3z\xb1\
A\x045*\xc4\xda\x7f\x98\xa4\xc2\x0b\x0a\xa1\x0e\x09\x08\
X^^\x96\x9c@\xbe\x1f\xe1/\x0a \xcak\x81\
\xdb\xb9<n\x1e\xb0\xca\xe6\x0e\x8fy\xaf\x10k\x16G\
\x92K4S\x08\xfd\x92\x88\x00\x82$\x0ay\x03\x02\xf7\
\x17 \xfc\x05\x8d\xe2\xd8/\x1eQ\x17A\xa0\xb9r$\
\xc9\xee{\x0a\xa1\x1f\x12\x12@7\xec u\x02\xba?\
\x80H\xf5Y\x1fy\xc0*&\x22\xc8\xbc`\x07(\x84\
\xe3\x91\x90\x00\x82\x1c\xaa\xc2\x98\x04\x1d\xc8\xfd9\xbc\x0b\
\xe8$\x82+\xe2\x99'\x9f|R4Iv-&\
\x85p4\xb0$1\xb1\xb1X)\xf7\x07\xba\xc2\xc7\x89\
\x13'B\xb8?\xc7L\xaf\xbd\x83\xc7\xe1s\x22\xa8Q\
\x1c\xa1\x08\xf6\xc0\x09!\x1b\xaa\xfb\x83\x95 \x10\xc0\x04\
W\xe1\xa4\xe8\x04\x9d\xf8!\xf4\xd5>\x87\xfb\xe0\xad\x12\
\xdc\x89\xcd]\x1e_\x11\x8f \x8f\xa0\x19\x12\xe3*\x9b\
t\xff\x95\x13B\xcc\x22$\x9d\xb9\xbf\xfc\x1b%*\x80\
)\xb5\xc7D$~\xe0\x98\xcfJp'\xba\x89`R\
M\xd3I\xac#\xee\x07\x1a\xaa\xbf\xf3*\x87.t\xc2\
\x09 \xa6\xc2$H\x0a\xb3\x03aT\xb0\xd2\xe3\xca\x95\
+\xb1\x88\x1fP\x17@`&\x82\xbbv\xed\x12M\xb2\
\xe9\xc1\xc2\xd0\x85\x83\x8b\x9cG\xe8\x98|J\xe4'\xbf\
Nzo\x90XCa\x08\x9fs}\x10?\xac\xf9\x0d\
\x98\xf3k\xc7D\x00\xc1\xa6n\x9f\xb8s\xe7\xce\xdf\xc4\
\xe3\xeespk;w\xeeT\x0b[\xf1\x82~\xf8\xe1\
\x87\x92\x0d\x9f|,\xf2\xea\xdezo\xdc\x94X\x05\xb8\
\x13n\xab\xcbX\xc0y2==\xddtz0&Q\
\xed\xd3\xb3\x8e\x99\x00\x82{z|\x0enpJ<\x81\
?6\xfa\x05\xb5\xaad\x10W\xbc\xe1\x22\xb1\xf1\xe3\x03\
\xe7\xf3\x93_\x89\xfc\xcfl\xfd\xb6\xf2\x84\x0b\xc6$\x98\
\x88\x07!\x0cJ\xe8P\xd8\x9dw\x10<\xe4\xe5\x138\
?f4\x8b \x9d\xe8%\x82\xef\x88G\x11\x04x!\
4[\x05\xce\x9d;\x97\x8f\x08\x82\xe6\x0ev\x85\x18L\
\x14\xb9\xb07f\xa5\x16 \xef\x87a\xa8\x19l\x8d\x89\
b\x08\xde\x93\x16@\xec\x9c\xe0!\xa4u\x82\x17Qx\
\xdb\x0ft\xa5\xec-\x04pE\x8c\xe9\x15\x0eO\x157\
\xef\x89G\xf0\xa6@H\xac\x05\xde\x04\xc8mDj\xf1\
\xc7\xa3\x0e\xe11&\xc0 '\x9aI>\x14\x02x\xf4\
\xe8Q\xd1\x02\x02\xe7&7'\xfe\x9eo\x88R#\xf4\
t+\x8cH\xa9\xc8^w\xa0\xc3\x8b\xa6]%\xb6\
\xba\xf2\x9a\xe3\xc2\xe3\x1c\xdbh z\xff\xbe\xd4\x9a\x05\
\x98QAhqqQ4\xc1\xb9\xa4=\xb8\xd8\x80\xcb\
\x12P\x00\xc1\xe6>\x9f\xbf,\x9e\xd1\x0eWs\x9b\xd4\
\xb1\x01\xd7F\x83\xeaq.\xabLP\xfd\xfd\xf1\xaf\xb3\
\xc8\xffU\xb1\xe8\x0d\xc4\x08\xfb\xc4\xc1\xeepS!\x05\
\x10\xf4\x13\xc1\x15\xf1\x8c\xb6\x08\xe2\xcd\x97\xfd>\x0eO\
\xbf\xd0\xea\x9b\xdb\x99\xf0\xfe%p|p~\x91\xee\x05\
2.\xda\x05\x118\xc0\x84\xf3\xdf\x880Q\x009,\
\x11\xd0O\x04\x97\xc43\x16\xc9\xda\xac\xdd\xa0\x03\xc2\x81\
\xa2I\x8a=\x85\xce\xfdE<\x05z\x1c,\x0a\x22\x9a\
i%e\x10]>f]\x01\xeeEO\x11,\xd7\x11\
{o\x9c\xc64\x0aM\xf0\x06\xac\xcb\xb6\x86MW\x08\
A\x81\xb0\xc4N\xe6\xee\xcf\xa1\x9d\x0b\x04\x89\x86\xc2\xc7\
b\x08\x7f\xdb\xe9\xe7\x04\xc1;\xe2\x19\xed\x17\x10\x02x\
\xfa\xf4i\xa9\x0dn\xedq\xcc\xb9\xc2\xcc\xdd\x9f\xc3\xc2\
\x05&\x18\x0a\xc3H=f\xd9\x00=\x0c\x83\x88\xe0\x8a\
xF\xbbJ\x0cN\x9d:%\xb5#\xc6\x5caM\xdc\
\x9f\x83.p\x03\x88$\xe1\xfe\x1e\xd3\x1a\x83\xe5\x83\xbe\
\x22\xa8\xd1*\x03\xb4\xd7\x12g\xdd.\xd3\x8bj\xae0\
\xb4+\xac\x89\xfbsX5Gk\xa7\x93<\xe1r\x7f\
s\x129\x838ApF<\x83\xab\x99v\x7fS\x0a\
\xd3;\xd4\x08\xe9\x0ak\xe6\xfe\x1c\x16.\x10\x02\x18\xf9\
*\x90\x86\xb4\xfa\xfe\xa2\xcb\xfducP\x11\xbc \x9e\
\x81\x00j_\xd1j\xd1.\xd3\x0b\xe7\x0a\x17>\xb0s\
\x85p}\x8b\xbf\xaf\x8d\xfbsX\xb9\xc0\x88CaD\
\x8bG\x0a\xe1{(\xc4\xd2\xb7q\x18H\x04S\x0d\x89\
\x81\xc5\xd59z \x86\x8b\x1f\xb4f\x15j\xb5\xd3`\
\xcd\xef\x7f\xfd\x22\xbbU\x1f\x83b\xf1>\x8b\xb4 \xd2\
\xcc\xfb\x15\x07\xc4oA\x12dP'\x08\xbc\xbf\xcax\
A\xb5\x0b$\xb5w\x83U\xb0.\x179:\x9f!2\
\x04\x0f\xe2\xfa\xe3_\x89\xec\xf8\x9a\xd4\x118@\x0b\x17\
\x88\xa1\xa7\x11Q\x15\xbf\xb9\xb2\x9d.I6\x0d\xfa\x85\
\x1a\x03\x15\x80\xc5\xbc5\x5cA1X\x81Tx\xffl\
\x914=1\xde@\x06\x88)\x84\xb5Fy\xbfN`\
(\x88\xf6\x12\xb9\x88\xde\xc3(x,\x14\xa2\xe7=E\
\x16\x8a\x81\x9d`\x19\x12\xaf\x88g,V\x90\xe0\x0dZ\
\xcb\x96\x99^\xa0p\x82\x10\x19!\xec0\xce\x10\xce\x0f\
\xf9>\xfc?\xe4\x1bk.\x80x_Y\xec\x1f\x12\xd8\
\x05\xc2\xe5!\x12t\x05\x8fl\x04\x10\x0c\xec\x04A\xe1\
\x06\xb1\xd6\xef5\xf1\xcc\xc2\xc2\x82zN%\xeb1[\
>\xf8\xec\xd3\xa2\xae\xf7G\x91\xeb\xbfm\xb9\xc3OJ\
\x87\x08\x91\xc3\xe0\x86\xfb\x1fl\x85\xbb\x89\xee\xf3\xa1\x01\
\xc4\x0fQ\x8c\x85\x0b\xc4\xc8\xac\x00Ua\xb8>\x08\xde\
R\xca\xe1n?\x86\x15A\x8c\xdb\xffH<\x8e\xdd\x07\
\xda\xa3\xf7\x1d\xd8C!\xb2\xbc\x0aI\x18\xcc\x0a\xb4\xc8\
\x05\x1a\xbfok!|U\x86)\x8c\xb8\xb5\xc4\xde\xc7\
k\xc1\x9d\xcd\xcc\xcc\x886p\x9c)m}H\xe2\xc5\
\xaa\x18\x02\xf7\xa7\xdcJ\x86s\x1a}\xc08\x01\xbfX\
\x86\xbb\x0bu\x11@0\x94\x08\x96\xa8\x94\xc1\x0f\x1c8\
`\x12\xaajN\xfa%\xf5\x00\x17R\xab\xd6+\xf4\x05\
*\x85\xc1\x0diUv!|\xfb1\xd5\xa5N\xc2W\
eh\x11\xd4*\x90X\xb9AT\xa3Y$!\xe3\x00\
\x01\xb4\x88( ~\x8aa\xf0\x99TVth3\x8a\
\x13\x04\xde'\xcb\x00+7h\xf5&&\xf9\x81\x94\x8a\
\xd5\x9at\xe5<\xe0\x92\x90&\xa3\x8a\xe0\x92(\xac \
\x81\x00Z$\x80Q\x80aXL\x86\xc52\x0c\x86\x0b\
T\x5c\x22\xb7D\x17\xb8\xceH\x22X\xe6\x0eT\xde\x0d\
p\x83\x16\xad\x00\x0c\x8b\xc90\xb8v\x18+\x94\xcd\xc0\
1!w\x19\xd5\x09\x02\x14HT\x12\xa9\xf3\xf3\xf3b\
\xc1\xf1\xe3\xc7\xe5\xda\xb5kBH?\xf0^\xb1J\xa1\
\xa0\x1aL\x17h\xc7\xc8\x22X\xbaA\xef#\xb6\x80\xc5\
\x9ab\xc7\xc1\x83\x07\x99\x1f$=A\x1e\xf0\xd2\xa5K\
b\x05\xfa\x02\x15\xa1\x0blc\x1c'\x08\xd4\xa6F\x9c\
8qB,\x80\x002?H\xba\x81\x94\x89\xe5$\x22\
\x08\xa0b:\x88.\xb0\x03c\x89`\xf9\x07Uq\x83\
\xca\xed\x01\x1b@~\x10\xe1\x0e!U\x90*\xb1|_\
\x18\xbc\xe7\xe9\x02;0\xae\x13\x04s\xa2\x84U\x91\x04\
X_\xf1I\xdcX\x17B\x80vK\x0c]`g\xc6\
\x16AM7\x88\x96\x19\xab\x22\x09@\xee\x87\x15c\xe2\
\x04\xd0r\xdbV\xe5b\x08\xa0\x0b\xec\x82\x0f'\x08\xe6\
D\x09\x14I\xa6\xa7\xa7\xc5\x0a\x84?\xb5\xdc\xa0\x894\
\xb1\x9a\x0cS\x05\xd1\x8er1\x84.\xb0\x07CM\x91\
\xe9\xc5\x9d;wP$Q\xf1\xf3VSf\xaa\xc0\x81\
&\xba\xc15\x19\x91\x10\x02\x080&Kyl\xfeC\
\x14\xc1\xee\xf8r\x82`N\x94\xfa\x06\x11\x16\xcf\xce\xce\
\x8a%Vc\x92H\x1c\x84\x12@8@e\x01<F\
\x01\xec\x8d7\x11\xd4\x5cE\x02\xe0\xca,\xc3b\x00!\
d\xb1$\x7fP\x05\xde\xbd{\xb7\xb9\x00\x1aT\x83\x1b\
\xc25\xc2}\xf1\xe9\x04\x81\xda*\x12\x80\xdeA\xeb\xe9\
\xba\x16S\xafI8\xe0\xf6\xad\x8b \x00\xd1\x0d\xc2`\
e\xe8\x02\x07\xc0\xab\x08\x96n\xf0\x88(a]-v\
@\x08\xb1\xb2\xc4\xfaD!\xba\xe0u\x85\xdb\x0f\xf1\xba\
\x22\xbd\xa3|Ao`F\xa0\x90\xbe\xf8v\x82R\xfe\
\xe1WD\x09\xe4OB\x8c\xc8\xc7\xb2\xa9\x10!\x13\xf1\
\x0fD\x0f\x17\xb5P\x0e\x1fy@\x83\xa2\xdb\xb3B\x06\
\xc2[u\xb8\x8a\xd6\xf6\x9cU^x\xe1\x05\xb9z\xf5\
\xaaX\xe3\x8a4\xac\x1c\xa7\x09\xf2\x7f!\xd7\x8b\xef\xd8\
\xb1C.^\xbc(\xca\xa0%F\x7fBq&\xa8\x88\
\xd0l\x99\x01\xb8\x9a\x87tfX\xcd\x02G\xca\xdd\
\xeb\xd2\xc1\xad\x0a\x0a\x95\xd60\xda5\xae!\xad\xad1\
\x1bB\x06BS\x04Uv\xa6\xab\x82\xabz\x88\xa4\xb6\
\x03of\xe4(\x95[\x1c\xc8\x98\xb8!\x19X#\x1e\
\x0a\x5c,\xe1\x00\x0d\x0a{3\xcc\x05\x0e\x87\xf7\x9c\xa0\
C\xbbH\x02\x10ZX\xf7\x0fVq\xbde8\xc1\x98\
+\x8c\x13\xb8?D\x0c!\x05\x10\xe0bi \x80K\
\x14\xc0\xe1Qs\x82\x8e\xc2\x11\x2278%\x8a\xc4\xd0\
\xc6\xe2z\xbe\x98+\x8c\x03\x88\x1e\xde\x13\xa1\xc5\x0f\x18\
\xed\x1b\xdc\x10\x86\xc1#a!\x82\x13\xc5\xcd\x1fD1\
,\x06?\xfa\xd1\x8f\xe4\xf4\xe9\xd3\x12\x1a\x88!\xdc\xe9\
\xae]\xbb\x84\xd8\xe3\xf6\x01\x89e\xb5\x8f\xe1\xc6\xe9\x0c\
\x83GD]\x04A!\x84X\x1d\xfe\x9a(\xf3\xf2\xcb\
/\xcb\xf2\xf2\xb2\xc4\x80k\xe5a\xbe\xd0\x06\xe4\x85q\
\x11D\xf8\x1bK?\xa7\xa1\x00\xb2\x1a<\x06&\x22\x08\
,\xc2b\xbc\xf9\x91\xa3\x8bi\xdf\x10\x88 \xf6S\xa6\
3\xd4!F\xf1\x03\xe8\x1e0\xcaW7\x84a\xf0X\
X\x8a\xe0\x84\x18\x84\xc51\x0a!p9C\x88!\xdb\
j\xc6\x07\xb9>\xb8~\x84\xbd\xb1\xad\xe4A^\xd8p\
e\x13'\xc4\x8c\x89\x99\x08\x82B\x08\xf7\x147o\x8b\
2\xa1&\x82\x0c\x02\x04\x10B\x08w\x88\xea6\x19\x1c\
\x88\x1dV\xee\x9c?\x7f>\x8a\x82G'\x8c\x05\x10k\
\x83\xe7\x84\x8c\x85\xa9\x08\x02\xed&jG\xccB\xe8\x80\
;t\xa1\xb2\xf5`\x88\x94\x88\xd9\xf5U1\x0c\x81\xc1\
\x85B\x00\xf7\x0a\x19\x9b\x10\x22\x88p\x18\xf9\xc1GE\
\x19\x08 \x96H\xa5\xb0\xb70\x5c\xa1\x9b\xa2]\xf7b\
\x0a\x84\x0e\xafY\x0a\xc2\xe70,\x82\x80\x860\x0f\xe8\
\x0ds\x11\x04V\xf9A\x10k\x8e\xb0\x17\x08\x99\xdd\xde\
\xcb\xb8\xadC\xd8\x8c\xd7\xc79>\xdcOib\x8f\xb1\
\x00b\x11\xc2c\x14@\x7f\x04\x11A`\x95\x1ft\xa4\
<)\x1a\xa2\x08!\xc4\x01aD\xe8\x9c\xb20\xc2\xa1\
C\xe80\x00\x03\xb7\xa9\x89^\x95\x00\xdb0\x1c)\x04\
Pm\xbf\xef:\x12L\x04A!\x84s\xc5\xcd\x0f\xc5\
\x88\xdc\x06\xa4B\x08!\x88\x93\x93\x93\xcd[\x1c\x10L\
w\x1b\x12\x08\x9d\x0bkq\x1f\xc7\xf5\xeb\xd7\xef>\x9e\
:\xf8\xfb\x9e<y\xd2:u\xc1B\x88\x02AE\x10\
\x14B\x087\xb8G\x8c\xa8\xd3\xa4h'\x8c\x00'\xed\
\x96-[\xee>>.\xb7o\xdf\x96\xb5\xb5\xb5\xbb\xf7\
!l\xae\x08\x95\xfb:j\xa3i0\xed\xb0\x10\xa2D\
\x0c\x22\x88\xbc \xf2\x83\x13bD\xe8\x99r$]P\
\xb8\xc26\x0f\xc6N{UZ\x85\x10\xb5\xad+\xeaL\
p\x11\x04e\xa1\x04\x15\xe3\x091\x22\x85\x16\x1a\x12\x17\
h\x7fA\x1b\x8c1\x0da%X\x15\xb5QZ\xc3P\
\xbe\xc0\xb0\xfafW:\x842W\xae\x5ci\xf6\xe9\x11\
\xd2\x0b\x17\xfeR\x00\xf3$\x0a'\xe8(\x1c\xe1\xfe\xe2\
\xc6|\x14\x0c\xaa\xc6\xc8\x13\xd2\x15\x92v\x02\x85\xbf\x80\
\xad0FD%\x82\xc0j\xe2L;\x10\xc0W^y\
%\xc8\xbe%$> z\xe8\xfd\x0b\xe0\xfe\x00\x04\x10\
\x0epU\x88:\xd1\x89 \xb0n\x9d\xa9\x12z\x1f\x0a\
\x12\x1e\xf4b\x86\xd8\xe3\xba\xc2\xdeB\x00/\x081!\
J\x11\x04!\x85\x10\xae\x10CZc\x99MHl\x08\
\xec\xfe\x1c\x1c\x8ejL\xb4\x22\x08B\x0a!`\xae\xb0\
>D\xb2{ \x050\x00Q\x8b \x08-\x84\x10@\
\x84\xc81\x8c\xee'\xfeA\xe8\x8b\xb5\xbf\x81\x87V4\
7%\xa3\x00\x86!z\x11\x04\xa1\x85\x10@\x0c\xb1\xda\
\x04\xb3\xecH\xfaD\xb4\x17\x0c\x8b \x81IB\x04A\
\x0cB\x08\xb0\xda\x04\xf9BV\x91\xd3$\x92\xbc\x9f\x83\
\x02\x18\x01\xc9\x88 \x88E\x08\x01\xc6>\xc1\x19R\x0c\
\xd3\x00\xe2\x87\xc6x\x88_$\xdb\x1b4\x84\x8d\xd0Q\
\x90\x94\x08\x82P\x0d\xd5\xdd\x80\x18\xa2\x80\xc209N\
0ig\xdf\xbe}\xcdqW\x11\xed\xed\xd2\x10\x0a`\
4$'\x82\xa0\x10BL\xa5\xc6\xf4\x99\x09\x89\x04\xe6\
\x0c\xe3\x22\x92\x82G'.\x17\xc7\x1e\x0eC\x88\x87$\
E\x10\x84\x18\xba0\x08\x10C\xb8C\xb6\xd6\xd8\xe3B\
^\x14;\x22\x1d:\xbbX\x88\xdfa!Q\x91\xac\x08\
\x82R\x08\xe1\x08\xd5\xf7+\x19\x05\x17*#oHA\
\xd4\xc3\xb9>\x08_\xc4\xdb\x99r\x22t\xa4$-\x82\
\x8e\x98\x0a&\xdd\xc0V\x918\xb0\x0a\x85K\xf2\xc6\x07\
\xc2\x07\xc7\x17Y\xae\xaf\x13\x0di-\x83c\x058R\
\xb2\x10AP\x0e^\x80\x10\xaao\xde4.t\x88\xc3\
\x03\xa1s\xc2\x97\xd0\x06\xf6\xc8\xff\xedg\x01$n\xb2\
\x11A\x10k\x9e\xb0\x17n\x975\x1c\x10E\xba\xc4u\
\xdcn{\xeeH\x0c\xee\x07\x92\x08Y\x89 (\xc7\xf5\
\xcf\x89\xc1\x06\xef\x1a\xb8\xdd\xd7\x10:\xbb\xcd\x89\xea\x80\
szn\x8b\xd1\xc8\xf3{\xbdhHk\x0d\xf0\x8a\x90\
$\xc8N\x04\x1de?!\xc2\xe3\x09I\x18\xb7c\x9b\
\xdb\xb5\x0d\xb7\x10\xc6T\x1d#\x84m\xdb\xb6mw\x85\
\xcem\x1f\x1apl\x95O0\xfej\x86\xed/i\x91\
\xad\x08\x822<Fc\xf5\x94d\x86\xdb\xdd\xad\xfd\xc0\
\xe38\xb0\x03\x9cu\xbe\x11\x02\xe7D\xce\xddw;\xde\
9g\x97\x89\xd8\xb5\x03\xd1;\xc6\xeao\x9ad-\x82\
\x8e\x94\x8a&\x1a81t\x02\xe9\x03'r\xed\xf7k\
\x08\x8b\x1f\x89S\x0b\x11\x04\xa5+\x9c+\x8e\x97\x84\x90\
\xf1\x81\xfb\x9b\xe1\x04\xe8\xf4\x89b\xb79\x0bp\xa5.\
\x8e\xfd\xc5]l/\xd7\x10BFg\xb18\x1e\xa2\x00\
\xe6Am\x9c`\x95\xd2\x15\x22DN\xb2\x82L\x82\x81\
\xd0w\x8e\x95\xdf\xbc\xa8\xa5\x08:\x18\x22\x93\x01\xe1\xe4\
\xe7\x8c\xa9\xb5\x08:ri\xa7!\xde\x81\xf8!\xf4]\
`\xdbK\xbeP\x04+P\x0cI\x09\xc5\xafFP\x04\
;@1\xac-\x14\xbf\x1aB\x11\xec\x01\xc5\xb06P\
\xfcj\x0cEp\x00J1\xc4\xf1\x8c\x90\x9c\xa0\xf8\x11\
\x8a\xe00\xb0\x9a\x9c\x0dhuY`\x9f\x1f\x01\x14\xc1\
\x11(\xc5pJ\x18*\xa7\x04\x9c\xde\x99\xe2\xb8\xc0>\
?R\x85\x228&\xe5\xa6Oh\xbcF\xa8<!$\
6\x9a\xae\xaf8V\x18\xf2\x92NP\x04=R\x08\xe2\
\x9e\xe2\x06\xc7\xb7\xa4\xa6\xc3\x1a\x22\x01\xc2\x87Pw\x89\
\xc2G\xfaA\x11T\xa2\x22\x88t\x886P\xf8\xc8H\
P\x04\x0d(\x04qJZ\x82\x88\xd0\x99\x15f?@\
\xe8\xde)\x8e\x15i\xe5\xf9(|d$(\x82\xc6\x94\
\xe3\xff\xa7*\xc7#B\x06\x01\x22\x07\xb7\xb7\x22-\xd1\
k\x08!\x1e\xa0\x08\x06\xa6\x14E8\xc4)Y\x17\xc5\
\xba\xe7\x13!x7\xa4%x\xab\xd2*j4\x84\x10\
\x05(\x82\x11R\x11F'\x8e\xf88Wq\x84\xd8A\
\xe8\x1a\xe5\xed*\xf7\xe8%\x96P\x04\x13\xa2\x22\x8e\x13\
m\x07\x1e\xdf.q\x8a$D\x0e\xcen\xb5\xbcm\xc8\
\xba\xe0\xddb.\x8f\x84\x86\x22\x98\x19e#\xf7}\xb2\
.\x88\x13m\xb7\xedT\xbf\xb6\x1f\xb7\xca\xa3\xdbc\x8d\
\xca\xc7\x148\x92\x04\xff\x000H\x87\xfd\xc2`\x8f\x83\
\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x09S\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -11549,6 +12131,10 @@ qt_resource_name = b"\
\x00\x1cX\x87\
\x00w\
\x00i\x00k\x00i\x00.\x00p\x00n\x00g\
\x00\x0f\
\x00\xb3\xceG\
\x00k\
\x00o\x00f\x00i\x00_\x00s\x00y\x00m\x00b\x00o\x00l\x00.\x00p\x00n\x00g\
\x00\x0e\
\x08\x9f\xcbG\
\x00f\
@@ -11599,7 +12185,7 @@ qt_resource_name = b"\
qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00J\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1c\
\x00\x00\x00J\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1d\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00&\x00\x02\x00\x00\x00\x01\x00\x00\x00\x14\
\x00\x00\x00\x00\x00\x00\x00\x00\
@@ -11611,13 +12197,13 @@ qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00X\x00\x02\x00\x00\x00\x04\x00\x00\x00\x07\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\xc0\x00\x00\x00\x00\x00\x01\x00\x02.\xed\
\x00\x00\x01\xe4\x00\x00\x00\x00\x00\x01\x00\x02S.\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x01\xfe\x00\x00\x00\x00\x00\x01\x00\x02\x83\x87\
\x00\x00\x02\x22\x00\x00\x00\x00\x00\x01\x00\x02\xa7\xc8\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x01\xea\x00\x00\x00\x00\x00\x01\x00\x02Y\x8c\
\x00\x00\x02\x0e\x00\x00\x00\x00\x00\x01\x00\x02}\xcd\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x01\xd6\x00\x00\x00\x00\x00\x01\x00\x02N)\
\x00\x00\x01\xfa\x00\x00\x00\x00\x00\x01\x00\x02rj\
\x00\x00\x01\x89\x89D9.\
\x00\x00\x00X\x00\x02\x00\x00\x00\x04\x00\x00\x00\x0c\
\x00\x00\x00\x00\x00\x00\x00\x00\
@@ -11631,29 +12217,31 @@ qt_resource_struct = b"\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x00X\x00\x02\x00\x00\x00\x03\x00\x00\x00\x11\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x02B\x00\x00\x00\x00\x00\x01\x00\x02\xb5\xd3\
\x00\x00\x02f\x00\x00\x00\x00\x00\x01\x00\x02\xda\x14\
\x00\x00\x01\x88;p\xbcJ\
\x00\x00\x02\x14\x00\x00\x00\x00\x00\x01\x00\x02\x9f\xd6\
\x00\x00\x028\x00\x00\x00\x00\x00\x01\x00\x02\xc4\x17\
\x00\x00\x01\x88;p\xbcI\
\x00\x00\x02*\x00\x00\x00\x00\x00\x01\x00\x02\xa93\
\x00\x00\x02N\x00\x00\x00\x00\x00\x01\x00\x02\xcdt\
\x00\x00\x01\x88;p\xbcI\
\x00\x00\x00X\x00\x02\x00\x00\x00\x07\x00\x00\x00\x15\
\x00\x00\x00X\x00\x02\x00\x00\x00\x08\x00\x00\x00\x15\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\x1c\x00\x00\x00\x00\x00\x01\x00\x01P\xb1\
\x00\x00\x01\x88;p\xbcJ\
\x00\x00\x012\x00\x00\x00\x00\x00\x01\x00\x01yY\
\x00\x00\x01\x97\xc9|\x88\xde\
\x00\x00\x01V\x00\x00\x00\x00\x00\x01\x00\x01\x9d\x9a\
\x00\x00\x01\x88;p\xbcI\
\x00\x00\x01\x94\x00\x00\x00\x00\x00\x01\x00\x01\xd2-\
\x00\x00\x01\xb8\x00\x00\x00\x00\x00\x01\x00\x01\xf6n\
\x00\x00\x01\x94\xb4\xd4\xf0a\
\x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x01\x8c\xe6\
\x00\x00\x01\x9e\x00\x00\x00\x00\x00\x01\x00\x01\xb1'\
\x00\x00\x01\x88;p\xbcH\
\x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x01LR\
\x00\x00\x01\x88;p\xbcF\
\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x01?\xe9\
\x00\x00\x01\x88;p\xbcH\
\x00\x00\x01T\x00\x00\x00\x00\x00\x01\x00\x01\x82\xb0\
\x00\x00\x01x\x00\x00\x00\x00\x00\x01\x00\x01\xa6\xf1\
\x00\x00\x01\x88;p\xbcH\
\x00\x00\x00X\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1d\
\x00\x00\x00X\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1e\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00h\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x88;p\xbcH\

View File

@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'KCC.ui'
##
## Created by: Qt User Interface Compiler version 6.8.2
## Created by: Qt User Interface Compiler version 6.9.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -26,7 +26,7 @@ class Ui_mainWindow(object):
def setupUi(self, mainWindow):
if not mainWindow.objectName():
mainWindow.setObjectName(u"mainWindow")
mainWindow.resize(519, 572)
mainWindow.resize(566, 573)
icon = QIcon()
icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
mainWindow.setWindowIcon(icon)
@@ -58,12 +58,22 @@ class Ui_mainWindow(object):
self.horizontalLayout.addWidget(self.editorButton)
self.kofiButton = QPushButton(self.toolWidget)
self.kofiButton.setObjectName(u"kofiButton")
self.kofiButton.setMinimumSize(QSize(0, 30))
icon2 = QIcon()
icon2.addFile(u":/Other/icons/kofi_symbol.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.kofiButton.setIcon(icon2)
self.kofiButton.setIconSize(QSize(19, 16))
self.horizontalLayout.addWidget(self.kofiButton)
self.wikiButton = QPushButton(self.toolWidget)
self.wikiButton.setObjectName(u"wikiButton")
self.wikiButton.setMinimumSize(QSize(0, 30))
icon2 = QIcon()
icon2.addFile(u":/Other/icons/wiki.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.wikiButton.setIcon(icon2)
icon3 = QIcon()
icon3.addFile(u":/Other/icons/wiki.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.wikiButton.setIcon(icon3)
self.horizontalLayout.addWidget(self.wikiButton)
@@ -80,63 +90,75 @@ class Ui_mainWindow(object):
self.gridLayout_4 = QGridLayout(self.buttonWidget)
self.gridLayout_4.setObjectName(u"gridLayout_4")
self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
self.directoryButton = QPushButton(self.buttonWidget)
self.directoryButton.setObjectName(u"directoryButton")
self.directoryButton.setMinimumSize(QSize(0, 30))
icon3 = QIcon()
icon3.addFile(u":/Other/icons/folder_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.directoryButton.setIcon(icon3)
self.gridLayout_4.addWidget(self.directoryButton, 0, 0, 1, 1)
self.fileButton = QPushButton(self.buttonWidget)
self.fileButton.setObjectName(u"fileButton")
self.fileButton.setMinimumSize(QSize(0, 30))
icon4 = QIcon()
icon4.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.fileButton.setIcon(icon4)
self.gridLayout_4.addWidget(self.fileButton, 0, 3, 1, 1)
self.deviceBox = QComboBox(self.buttonWidget)
self.deviceBox.setObjectName(u"deviceBox")
self.deviceBox.setMinimumSize(QSize(0, 28))
self.gridLayout_4.addWidget(self.deviceBox, 1, 0, 1, 1)
self.formatBox = QComboBox(self.buttonWidget)
self.formatBox.setObjectName(u"formatBox")
self.formatBox.setMinimumSize(QSize(0, 28))
self.gridLayout_4.addWidget(self.formatBox, 1, 3, 1, 1)
self.convertButton = QPushButton(self.buttonWidget)
self.convertButton.setObjectName(u"convertButton")
self.convertButton.setMinimumSize(QSize(0, 30))
font = QFont()
font.setBold(True)
self.convertButton.setFont(font)
icon5 = QIcon()
icon5.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.convertButton.setIcon(icon5)
icon4 = QIcon()
icon4.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.convertButton.setIcon(icon4)
self.gridLayout_4.addWidget(self.convertButton, 1, 2, 1, 1)
self.gridLayout_4.addWidget(self.convertButton, 1, 3, 1, 1)
self.clearButton = QPushButton(self.buttonWidget)
self.clearButton.setObjectName(u"clearButton")
self.clearButton.setMinimumSize(QSize(0, 30))
icon5 = QIcon()
icon5.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.clearButton.setIcon(icon5)
self.gridLayout_4.addWidget(self.clearButton, 0, 3, 1, 1)
self.deviceBox = QComboBox(self.buttonWidget)
self.deviceBox.setObjectName(u"deviceBox")
self.deviceBox.setMinimumSize(QSize(0, 28))
self.gridLayout_4.addWidget(self.deviceBox, 1, 1, 1, 1)
self.fileButton = QPushButton(self.buttonWidget)
self.fileButton.setObjectName(u"fileButton")
self.fileButton.setMinimumSize(QSize(0, 30))
icon6 = QIcon()
icon6.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.clearButton.setIcon(icon6)
icon6.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.fileButton.setIcon(icon6)
self.gridLayout_4.addWidget(self.clearButton, 0, 2, 1, 1)
self.gridLayout_4.addWidget(self.fileButton, 0, 1, 1, 1)
self.defaultOutputFolderButton = QPushButton(self.buttonWidget)
self.defaultOutputFolderButton.setObjectName(u"defaultOutputFolderButton")
self.defaultOutputFolderButton.setMinimumSize(QSize(0, 30))
icon7 = QIcon()
icon7.addFile(u":/Other/icons/folder_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.defaultOutputFolderButton.setIcon(icon7)
self.gridLayout_4.addWidget(self.defaultOutputFolderButton, 0, 5, 1, 1)
self.defaultOutputFolderBox = QCheckBox(self.buttonWidget)
self.defaultOutputFolderBox.setObjectName(u"defaultOutputFolderBox")
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
sizePolicy1.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.defaultOutputFolderBox.sizePolicy().hasHeightForWidth())
self.defaultOutputFolderBox.setSizePolicy(sizePolicy1)
self.defaultOutputFolderBox.setTristate(True)
self.gridLayout_4.addWidget(self.defaultOutputFolderBox, 0, 4, 1, 1)
self.formatBox = QComboBox(self.buttonWidget)
self.formatBox.setObjectName(u"formatBox")
self.formatBox.setMinimumSize(QSize(0, 28))
self.gridLayout_4.addWidget(self.formatBox, 1, 4, 1, 2)
self.directoryButton.raise_()
self.clearButton.raise_()
self.fileButton.raise_()
self.deviceBox.raise_()
self.convertButton.raise_()
self.formatBox.raise_()
self.defaultOutputFolderButton.raise_()
self.fileButton.raise_()
self.defaultOutputFolderBox.raise_()
self.gridLayout.addWidget(self.buttonWidget, 3, 0, 1, 2)
@@ -157,24 +179,24 @@ class Ui_mainWindow(object):
self.gridLayout_3.setContentsMargins(0, 0, 0, 0)
self.hLabel = QLabel(self.customWidget)
self.hLabel.setObjectName(u"hLabel")
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
sizePolicy1.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.hLabel.sizePolicy().hasHeightForWidth())
self.hLabel.setSizePolicy(sizePolicy1)
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
sizePolicy2.setHorizontalStretch(0)
sizePolicy2.setVerticalStretch(0)
sizePolicy2.setHeightForWidth(self.hLabel.sizePolicy().hasHeightForWidth())
self.hLabel.setSizePolicy(sizePolicy2)
self.gridLayout_3.addWidget(self.hLabel, 0, 2, 1, 1)
self.widthBox = QSpinBox(self.customWidget)
self.widthBox.setObjectName(u"widthBox")
self.widthBox.setMaximum(2160)
self.widthBox.setMaximum(2400)
self.gridLayout_3.addWidget(self.widthBox, 0, 1, 1, 1)
self.wLabel = QLabel(self.customWidget)
self.wLabel.setObjectName(u"wLabel")
sizePolicy1.setHeightForWidth(self.wLabel.sizePolicy().hasHeightForWidth())
self.wLabel.setSizePolicy(sizePolicy1)
sizePolicy2.setHeightForWidth(self.wLabel.sizePolicy().hasHeightForWidth())
self.wLabel.setSizePolicy(sizePolicy2)
self.gridLayout_3.addWidget(self.wLabel, 0, 0, 1, 1)
@@ -213,11 +235,8 @@ class Ui_mainWindow(object):
self.preserveMarginBox = QSpinBox(self.croppingWidget)
self.preserveMarginBox.setObjectName(u"preserveMarginBox")
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
sizePolicy2.setHorizontalStretch(0)
sizePolicy2.setVerticalStretch(0)
sizePolicy2.setHeightForWidth(self.preserveMarginBox.sizePolicy().hasHeightForWidth())
self.preserveMarginBox.setSizePolicy(sizePolicy2)
sizePolicy1.setHeightForWidth(self.preserveMarginBox.sizePolicy().hasHeightForWidth())
self.preserveMarginBox.setSizePolicy(sizePolicy1)
self.preserveMarginBox.setMaximum(99)
self.preserveMarginBox.setSingleStep(5)
self.preserveMarginBox.setValue(0)
@@ -232,65 +251,16 @@ class Ui_mainWindow(object):
self.gridLayout_2 = QGridLayout(self.optionWidget)
self.gridLayout_2.setObjectName(u"gridLayout_2")
self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
self.croppingBox = QCheckBox(self.optionWidget)
self.croppingBox.setObjectName(u"croppingBox")
self.croppingBox.setTristate(True)
self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
self.mangaBox = QCheckBox(self.optionWidget)
self.mangaBox.setObjectName(u"mangaBox")
self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
self.webtoonBox = QCheckBox(self.optionWidget)
self.webtoonBox.setObjectName(u"webtoonBox")
self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
self.rotateBox = QCheckBox(self.optionWidget)
self.rotateBox.setObjectName(u"rotateBox")
self.rotateBox.setTristate(True)
self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1)
self.borderBox = QCheckBox(self.optionWidget)
self.borderBox.setObjectName(u"borderBox")
self.borderBox.setTristate(True)
self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
self.gammaBox = QCheckBox(self.optionWidget)
self.gammaBox.setObjectName(u"gammaBox")
self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
self.interPanelCropBox = QCheckBox(self.optionWidget)
self.interPanelCropBox.setObjectName(u"interPanelCropBox")
self.interPanelCropBox.setTristate(True)
self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1)
self.colorBox = QCheckBox(self.optionWidget)
self.colorBox.setObjectName(u"colorBox")
self.mangaBox = QCheckBox(self.optionWidget)
self.mangaBox.setObjectName(u"mangaBox")
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.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
self.authorEdit = QLineEdit(self.optionWidget)
self.authorEdit.setObjectName(u"authorEdit")
@@ -304,17 +274,74 @@ class Ui_mainWindow(object):
self.gridLayout_2.addWidget(self.authorEdit, 0, 0, 1, 1)
self.croppingBox = QCheckBox(self.optionWidget)
self.croppingBox.setObjectName(u"croppingBox")
self.croppingBox.setTristate(True)
self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
self.webtoonBox = QCheckBox(self.optionWidget)
self.webtoonBox.setObjectName(u"webtoonBox")
self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
self.colorBox = QCheckBox(self.optionWidget)
self.colorBox.setObjectName(u"colorBox")
self.gridLayout_2.addWidget(self.colorBox, 3, 2, 1, 1)
self.deleteBox = QCheckBox(self.optionWidget)
self.deleteBox.setObjectName(u"deleteBox")
self.gridLayout_2.addWidget(self.deleteBox, 5, 1, 1, 1)
self.comicinfoTitleBox = QCheckBox(self.optionWidget)
self.comicinfoTitleBox.setObjectName(u"comicinfoTitleBox")
self.gridLayout_2.addWidget(self.comicinfoTitleBox, 7, 0, 1, 1)
self.qualityBox = QCheckBox(self.optionWidget)
self.qualityBox.setObjectName(u"qualityBox")
self.qualityBox.setTristate(True)
self.gridLayout_2.addWidget(self.qualityBox, 1, 2, 1, 1)
self.reduceRainbowBox = QCheckBox(self.optionWidget)
self.reduceRainbowBox.setObjectName(u"reduceRainbowBox")
self.gridLayout_2.addWidget(self.reduceRainbowBox, 7, 2, 1, 1)
self.noRotateBox = QCheckBox(self.optionWidget)
self.noRotateBox.setObjectName(u"noRotateBox")
self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1)
self.fileFusionBox = QCheckBox(self.optionWidget)
self.fileFusionBox.setObjectName(u"fileFusionBox")
self.gridLayout_2.addWidget(self.fileFusionBox, 6, 0, 1, 1)
self.gammaBox = QCheckBox(self.optionWidget)
self.gammaBox.setObjectName(u"gammaBox")
self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
self.mozJpegBox = QCheckBox(self.optionWidget)
self.mozJpegBox.setObjectName(u"mozJpegBox")
self.mozJpegBox.setTristate(True)
self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1)
self.disableProcessingBox = QCheckBox(self.optionWidget)
self.disableProcessingBox.setObjectName(u"disableProcessingBox")
self.gridLayout_2.addWidget(self.disableProcessingBox, 5, 2, 1, 1)
self.chunkSizeCheckBox = QCheckBox(self.optionWidget)
self.chunkSizeCheckBox.setObjectName(u"chunkSizeCheckBox")
self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 1, 1)
self.spreadShiftBox = QCheckBox(self.optionWidget)
self.spreadShiftBox.setObjectName(u"spreadShiftBox")
@@ -331,20 +358,27 @@ class Ui_mainWindow(object):
self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
self.noRotateBox = QCheckBox(self.optionWidget)
self.noRotateBox.setObjectName(u"noRotateBox")
self.rotateBox = QCheckBox(self.optionWidget)
self.rotateBox.setObjectName(u"rotateBox")
self.rotateBox.setTristate(True)
self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1)
self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1)
self.reduceRainbowBox = QCheckBox(self.optionWidget)
self.reduceRainbowBox.setObjectName(u"reduceRainbowBox")
self.borderBox = QCheckBox(self.optionWidget)
self.borderBox.setObjectName(u"borderBox")
self.borderBox.setTristate(True)
self.gridLayout_2.addWidget(self.reduceRainbowBox, 7, 2, 1, 1)
self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
self.chunkSizeCheckBox = QCheckBox(self.optionWidget)
self.chunkSizeCheckBox.setObjectName(u"chunkSizeCheckBox")
self.maximizeStrips = QCheckBox(self.optionWidget)
self.maximizeStrips.setObjectName(u"maximizeStrips")
self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 1, 1)
self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
self.rotateFirstBox = QCheckBox(self.optionWidget)
self.rotateFirstBox.setObjectName(u"rotateFirstBox")
self.gridLayout_2.addWidget(self.rotateFirstBox, 8, 1, 1, 1)
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
@@ -414,9 +448,7 @@ class Ui_mainWindow(object):
self.statusBar.setSizeGripEnabled(False)
mainWindow.setStatusBar(self.statusBar)
QWidget.setTabOrder(self.convertButton, self.clearButton)
QWidget.setTabOrder(self.clearButton, self.directoryButton)
QWidget.setTabOrder(self.directoryButton, self.fileButton)
QWidget.setTabOrder(self.fileButton, self.deviceBox)
QWidget.setTabOrder(self.clearButton, self.deviceBox)
QWidget.setTabOrder(self.deviceBox, self.formatBox)
QWidget.setTabOrder(self.formatBox, self.mangaBox)
QWidget.setTabOrder(self.mangaBox, self.rotateBox)
@@ -456,26 +488,31 @@ class Ui_mainWindow(object):
self.editorButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to edit directory.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.editorButton.setText(QCoreApplication.translate("mainWindow", u"Metadata Editor", None))
self.kofiButton.setText(QCoreApplication.translate("mainWindow", u"Support me on Ko-fi", None))
self.wikiButton.setText(QCoreApplication.translate("mainWindow", u"Wiki", None))
#if QT_CONFIG(tooltip)
self.directoryButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Add directory containing JPG, PNG or GIF files to queue.<br/><span style=\" font-weight:600;\">CBR, CBZ and CB7 files inside will not be processed!</span></p></body></html>", None))
self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory for this list.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.convertButton.setText(QCoreApplication.translate("mainWindow", u"Convert", None))
self.clearButton.setText(QCoreApplication.translate("mainWindow", u"Clear list", None))
#if QT_CONFIG(tooltip)
self.deviceBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Target device.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.directoryButton.setText(QCoreApplication.translate("mainWindow", u"Add image folder", None))
#if QT_CONFIG(tooltip)
self.fileButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Add CBR, CBZ, CB7 or PDF file to queue.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.fileButton.setText(QCoreApplication.translate("mainWindow", u"Add file(s)", None))
#if QT_CONFIG(tooltip)
self.deviceBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Target device.</p></body></html>", None))
self.defaultOutputFolderButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Use this to select the default output directory.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.defaultOutputFolderButton.setText("")
#if QT_CONFIG(tooltip)
self.defaultOutputFolderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - next to source<br/></span>Place output files next to source files</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - folder next to source<br/></span>Place output files in a folder next to source files</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Custom<br/></span>Place output files in custom directory specified by right button</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.defaultOutputFolderBox.setText(QCoreApplication.translate("mainWindow", u"Output Folder", None))
#if QT_CONFIG(tooltip)
self.formatBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Output format.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(tooltip)
self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.convertButton.setText(QCoreApplication.translate("mainWindow", u"Convert", None))
self.clearButton.setText(QCoreApplication.translate("mainWindow", u"Clear list", None))
#if QT_CONFIG(tooltip)
self.hLabel.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
@@ -495,62 +532,70 @@ class Ui_mainWindow(object):
#endif // QT_CONFIG(tooltip)
self.preserveMarginLabel.setText(QCoreApplication.translate("mainWindow", u"Preserve Margin %", None))
self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None))
#if QT_CONFIG(tooltip)
self.croppingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled</span></p><p>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Margins<br/></span>Margins</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Margins + page numbers<br/></span>Margins +page numbers</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
#if QT_CONFIG(tooltip)
self.mangaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Manga mode", None))
#if QT_CONFIG(tooltip)
self.webtoonBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", None))
#if QT_CONFIG(tooltip)
self.rotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Split<br/></span>Double page spreads will be cut into two separate pages.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Rotate and split<br/></span>Double page spreads will be displayed twice. First rotated and then split. </p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate<br/></span>Double page spreads will be rotated.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None))
#if QT_CONFIG(tooltip)
self.borderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - White<br/></span>Margins will be filled with white color.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
#if QT_CONFIG(tooltip)
self.gammaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable automatic gamma correction.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.gammaBox.setText(QCoreApplication.translate("mainWindow", u"Custom gamma", None))
#if QT_CONFIG(tooltip)
self.interPanelCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled<br/></span>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Horizontal<br/></span>Crop empty horizontal lines.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Both<br/></span>Crop empty horizontal and vertical lines.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.interPanelCropBox.setText(QCoreApplication.translate("mainWindow", u"Inter-panel crop", None))
#if QT_CONFIG(tooltip)
self.colorBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</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)
self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", None))
#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))
#endif // QT_CONFIG(tooltip)
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)
self.maximizeStrips.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 1x4<br/></span>Keep format 1x4 panels strips.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 2x2<br/></span>Turn 1x4 strips to 2x2 to maximize screen usage.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None))
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left mode", None))
#if QT_CONFIG(tooltip)
self.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None))
#endif // QT_CONFIG(tooltip)
self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", None))
#if QT_CONFIG(tooltip)
self.croppingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled</span></p><p>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Margins<br/></span>Margins</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Margins + page numbers<br/></span>Margins +page numbers</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
#if QT_CONFIG(tooltip)
self.webtoonBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", None))
#if QT_CONFIG(tooltip)
self.colorBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", None))
#if QT_CONFIG(tooltip)
self.deleteBox.setToolTip(QCoreApplication.translate("mainWindow", u"Delete input file(s) or directory. It's not recoverable!", None))
#endif // QT_CONFIG(tooltip)
self.deleteBox.setText(QCoreApplication.translate("mainWindow", u"Delete input", None))
#if QT_CONFIG(tooltip)
self.comicinfoTitleBox.setToolTip(QCoreApplication.translate("mainWindow", u"Write Title from ComicInfo.xml", None))
#endif // QT_CONFIG(tooltip)
self.comicinfoTitleBox.setText(QCoreApplication.translate("mainWindow", u"ComicInfo Title", None))
#if QT_CONFIG(tooltip)
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)
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
#if QT_CONFIG(tooltip)
self.reduceRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Reduce rainbow effect on color eink by slightly blurring images", None))
#endif // QT_CONFIG(tooltip)
self.reduceRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Rainbow blur", None))
#if QT_CONFIG(tooltip)
self.noRotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"Do not rotate double page spreads in spread splitter option.", None))
#endif // QT_CONFIG(tooltip)
self.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", None))
#if QT_CONFIG(tooltip)
self.fileFusionBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Combines all selected files into a single file. (Helpful for combining chapters into volumes.)</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.fileFusionBox.setText(QCoreApplication.translate("mainWindow", u"File Fusion", None))
#if QT_CONFIG(tooltip)
self.gammaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable automatic gamma correction.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.gammaBox.setText(QCoreApplication.translate("mainWindow", u"Custom gamma", None))
#if QT_CONFIG(tooltip)
self.mozJpegBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - JPEG<br/></span>Use JPEG files</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - force PNG<br/></span>Create PNG files instead JPEG</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - mozJpeg<br/></span>10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.mozJpegBox.setText(QCoreApplication.translate("mainWindow", u"JPEG/PNG/mozJpeg", None))
#if QT_CONFIG(tooltip)
self.disableProcessingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Do not process any image, ignore profile and processing options.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.disableProcessingBox.setText(QCoreApplication.translate("mainWindow", u"Disable processing", None))
#if QT_CONFIG(tooltip)
self.chunkSizeCheckBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:700; text-decoration: underline;\">Unchecked<br/></span>Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.</p><p><span style=\" font-weight:700; text-decoration: underline;\">Checked</span><br/>Output file size specified in &quot;Chunk size MB&quot; before split occurs.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.chunkSizeCheckBox.setText(QCoreApplication.translate("mainWindow", u"Chunk size", None))
#if QT_CONFIG(tooltip)
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)
@@ -564,17 +609,21 @@ class Ui_mainWindow(object):
#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))
self.rotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Split<br/></span>Double page spreads will be cut into two separate pages.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Split and rotate<br/></span>Double page spreads will be displayed twice. First split and then rotated. </p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate<br/></span>Double page spreads will be rotated.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", None))
self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None))
#if QT_CONFIG(tooltip)
self.reduceRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Reduce rainbow effect on color eink by slightly blurring images", None))
self.borderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - White<br/></span>Margins will be filled with white color.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.reduceRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Rainbow blur", None))
self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
#if QT_CONFIG(tooltip)
self.chunkSizeCheckBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:700; text-decoration: underline;\">Unchecked<br/></span>Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.</p><p><span style=\" font-weight:700; text-decoration: underline;\">Checked</span><br/>Output file size specified in &quot;Chunk size MB&quot; before split occurs.</p></body></html>", None))
self.maximizeStrips.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 1x4<br/></span>Keep format 1x4 panels strips.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 2x2<br/></span>Turn 1x4 strips to 2x2 to maximize screen usage.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.chunkSizeCheckBox.setText(QCoreApplication.translate("mainWindow", u"Chunk size", None))
self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None))
#if QT_CONFIG(tooltip)
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))
self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
#if QT_CONFIG(tooltip)
self.chunkSizeWidget.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Warning: chunk size greater than default may cause<br/>performance/battery issues, especially on older devices.</p></body></html>", None))

View File

@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'MetaEditor.ui'
##
## Created by: Qt User Interface Compiler version 6.8.2
## Created by: Qt User Interface Compiler version 6.9.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################

View File

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

View File

@@ -28,19 +28,22 @@ from copy import copy
from glob import glob, escape
from re import sub
from stat import S_IWRITE, S_IREAD, S_IEXEC
from typing import List
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
from tempfile import mkdtemp, gettempdir, TemporaryFile
from shutil import move, copytree, rmtree, copyfile
from multiprocessing import Pool
from uuid import uuid4
from natsort import os_sort_keygen
from natsort import os_sort_keygen, os_sorted
from slugify import slugify as slugify_ext
from PIL import Image, ImageFile
from pathlib import Path
from subprocess import STDOUT, PIPE, CalledProcessError
from psutil import virtual_memory, disk_usage
from html import escape as hescape
from .shared import available_archive_tools, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run
from .shared import getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run
from .comicarchive import SEVENZIP, available_archive_tools
from . import comic2panel
from . import image
from . import comicarchive
@@ -77,7 +80,7 @@ def main(argv=None):
return 0
def buildHTML(path, imgfile, imgfilepath):
def buildHTML(path, imgfile, imgfilepath, imgfile2=None):
key = pathlib.Path(imgfilepath).name
filename = getImageFileName(imgfile)
deviceres = options.profileData[1]
@@ -103,10 +106,13 @@ def buildHTML(path, imgfile, imgfilepath):
os.makedirs(htmlpath)
htmlfile = os.path.join(htmlpath, filename[0] + '.xhtml')
imgsize = Image.open(os.path.join(head, "Images", postfix, imgfile)).size
imgsizeframe = list(imgsize)
imgsize2 = (0, 0)
if imgfile2:
imgsize2 = Image.open(os.path.join(head, "Images", postfix, imgfile2)).size
imgsizeframe[1] += imgsize2[1]
if options.hq:
imgsizeframe = (int(imgsize[0] // 1.5), int(imgsize[1] // 1.5))
else:
imgsizeframe = imgsize
imgsizeframe = (int(imgsizeframe[0] // 1.5), int(imgsizeframe[1] // 1.5))
f = open(htmlfile, "w", encoding='UTF-8')
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
"<!DOCTYPE html>\n",
@@ -115,14 +121,17 @@ def buildHTML(path, imgfile, imgfilepath):
"<title>", hescape(filename[0]), "</title>\n",
"<link href=\"", "../" * (backref - 1), "style.css\" type=\"text/css\" rel=\"stylesheet\"/>\n",
"<meta name=\"viewport\" "
"content=\"width=" + str(imgsize[0]) + ", height=" + str(imgsize[1]) + "\"/>\n"
"content=\"width=" + str(imgsizeframe[0]) + ", height=" + str(imgsizeframe[1]) + "\"/>\n"
"</head>\n",
"<body style=\"" + additionalStyle + "\">\n",
"<div style=\"text-align:center;top:" + getTopMargin(deviceres, imgsizeframe) + "%;\">\n",
# this display none div fixes formatting issues with virtual panel mode, for some reason
'<div style="display:none;">.</div>\n',
"<img width=\"" + str(imgsizeframe[0]) + "\" height=\"" + str(imgsizeframe[1]) + "\" ",
"src=\"", "../" * backref, "Images/", postfix, imgfile, "\"/>\n</div>\n"])
])
f.write(f'<img width="{imgsize[0]}" height="{imgsize[1]}" src="{"../" * backref}Images/{postfix}{imgfile}"/>\n')
if imgfile2:
f.write(f'<img width="{imgsize2[0]}" height="{imgsize2[1]}" src="{"../" * backref}Images/{postfix}{imgfile2}"/>\n')
f.write("</div>\n")
if options.iskindle and options.panelview:
if options.autoscale:
size = (getPanelViewResolution(imgsize, deviceres))
@@ -314,12 +323,8 @@ def buildOPF(dstdir, title, filelist, cover=None):
"<item id=\"nav\" href=\"nav.xhtml\" ",
"properties=\"nav\" media-type=\"application/xhtml+xml\"/>\n"])
if cover is not None:
filename = getImageFileName(cover.replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\'))
if '.png' == filename[1]:
mt = 'image/png'
else:
mt = 'image/jpeg'
f.write("<item id=\"cover\" href=\"Images/cover" + filename[1] + "\" media-type=\"" + mt +
mt = 'image/jpeg'
f.write("<item id=\"cover\" href=\"Images/cover.jpg" + "\" media-type=\"" + mt +
"\" properties=\"cover-image\"/>\n")
reflist = []
for path in filelist:
@@ -332,10 +337,16 @@ def buildOPF(dstdir, title, filelist, cover=None):
".xhtml\" media-type=\"application/xhtml+xml\"/>\n")
if '.png' == filename[1]:
mt = 'image/png'
elif '.gif' == filename[1]:
mt = 'image/gif'
else:
mt = 'image/jpeg'
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" +
mt + "\"/>\n")
if 'above' in path[1]:
bottom = path[1].replace('above', 'below')
uniqueid = uniqueid.replace('above', 'below')
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + bottom + "\" media-type=\"" + mt + "\"/>\n")
f.write("<item id=\"css\" href=\"Text/style.css\" media-type=\"text/css\"/>\n")
@@ -358,63 +369,63 @@ def buildOPF(dstdir, title, filelist, cover=None):
pageside = "left"
else:
pageside = "right"
# initial spread order forwards
page_spread_property_list = []
for entry in reflist:
if options.righttoleft:
if entry.endswith("-kcc-a"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("center"))
)
if "-kcc-a" in entry or "-kcc-d" in entry:
page_spread_property_list.append("center")
pageside = "right"
elif entry.endswith("-kcc-b"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("right"))
)
elif "-kcc-b" in entry:
page_spread_property_list.append("right")
pageside = "right"
elif entry.endswith("-kcc-c"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("left"))
)
elif "-kcc-c" in entry:
page_spread_property_list.append("left")
pageside = "right"
else:
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty(pageside))
)
page_spread_property_list.append(pageside)
if pageside == "right":
pageside = "left"
else:
pageside = "right"
else:
if entry.endswith("-kcc-a"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("center"))
)
if "-kcc-a" in entry or "-kcc-d" in entry:
page_spread_property_list.append("center")
pageside = "left"
elif entry.endswith("-kcc-b"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("left"))
)
elif "-kcc-b" in entry:
page_spread_property_list.append("left")
pageside = "left"
elif entry.endswith("-kcc-c"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("right"))
)
elif "-kcc-c" in entry:
page_spread_property_list.append("right")
pageside = "left"
else:
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty(pageside))
)
page_spread_property_list.append(pageside)
if pageside == "right":
pageside = "left"
else:
pageside = "right"
# fix spread orders backward
spread_seen = False
for i in range(len(reflist) -1, -1, -1):
entry = reflist[i]
if "-kcc-x" not in entry:
spread_seen = True
if options.righttoleft:
pageside = "left"
else:
pageside = "right"
elif spread_seen:
page_spread_property_list[i] = pageside
if pageside == "right":
pageside = "left"
else:
pageside = "right"
for entry, prop in zip(reflist, page_spread_property_list):
f.write(f'<itemref idref="page_{entry}" {pageSpreadProperty(prop)}/>\n')
f.write("</spine>\n</package>\n")
f.close()
os.mkdir(os.path.join(dstdir, 'META-INF'))
@@ -427,10 +438,9 @@ def buildOPF(dstdir, title, filelist, cover=None):
"</container>"])
f.close()
def buildEPUB(path, chapternames, tomenumber, ischunked):
def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len_tomes=0):
filelist = []
chapterlist = []
cover = None
os.mkdir(os.path.join(path, 'OEBPS', 'Text'))
f = open(os.path.join(path, 'OEBPS', 'Text', 'style.css'), 'w', encoding='UTF-8')
f.writelines(["@page {\n",
@@ -440,7 +450,14 @@ def buildEPUB(path, chapternames, tomenumber, ischunked):
"display: block;\n",
"margin: 0;\n",
"padding: 0;\n",
"}\n"])
"}\n",
])
if options.kindle_scribe_azw3:
f.writelines([
"img {\n",
"display: block;\n",
"}\n",
])
if options.iskindle and options.panelview:
f.writelines(["#PV {\n",
"position: absolute;\n",
@@ -508,22 +525,24 @@ def buildEPUB(path, chapternames, tomenumber, ischunked):
"}\n"])
f.close()
build_html_start = perf_counter()
cover.save_to_epub(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes)
options.covers.append((cover, options.uuid))
for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')):
chapter = False
dirnames, filenames = walkSort(dirnames, filenames)
for afile in filenames:
if cover is None:
try:
cover = os.path.join(os.path.join(path, 'OEBPS', 'Images'),
'cover' + getImageFileName(afile)[1])
except Exception as e:
raise UserWarning(f"{afile}: {e}")
options.covers.append((image.Cover(os.path.join(dirpath, afile), cover, options,
tomenumber), options.uuid))
if afile == 'cover.jpg':
continue
if 'below' in afile:
continue
if not chapter:
chapterlist.append((dirpath.replace('Images', 'Text'), afile))
chapter = True
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile)))
if 'above' in afile:
bottom = afile.replace('above', 'below')
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile), bottom))
else:
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile)))
build_html_end = perf_counter()
print(f"buildHTML: {build_html_end - build_html_start} seconds")
# Overwrite chapternames if tree is flat and ComicInfo.xml has bookmarks
@@ -620,16 +639,21 @@ def imgFileProcessing(work):
workImg = image.ComicPageParser((dirpath, afile), opt)
for i in workImg.payload:
img = image.ComicPage(opt, *i)
is_not_color = not opt.forcecolor or not img.color
if is_not_color:
img.convertToGrayscale()
if opt.cropping == 2 and not opt.webtoon:
img.cropPageNumber(opt.croppingp, opt.croppingm)
if opt.cropping == 1 and not opt.webtoon:
img.cropMargin(opt.croppingp, opt.croppingm)
if opt.interpanelcrop > 0:
img.cropInterPanelEmptySections("horizontal" if opt.interpanelcrop == 1 else "both")
img.autocontrastImage()
img.gammaCorrectImage()
if is_not_color:
img.autocontrastImage()
img.resizeImage()
img.optimizeForDisplay(opt.reducerainbow)
if opt.forcepng and not opt.forcecolor:
if is_not_color and opt.forcepng:
img.quantizeImage()
output.append(img.saveToDir())
return output
@@ -668,16 +692,6 @@ def getWorkFolder(afile):
cbx = comicarchive.ComicArchive(afile)
path = cbx.extract(workdir)
sanitizePermissions(path)
tdir = os.listdir(workdir)
if len(tdir) == 2 and 'ComicInfo.xml' in tdir:
tdir.remove('ComicInfo.xml')
if os.path.isdir(os.path.join(workdir, tdir[0])):
os.replace(
os.path.join(workdir, 'ComicInfo.xml'),
os.path.join(workdir, tdir[0], 'ComicInfo.xml')
)
if len(tdir) == 1 and os.path.isdir(os.path.join(workdir, tdir[0])):
path = os.path.join(workdir, tdir[0])
except OSError as e:
rmtree(workdir, True)
raise UserWarning(e)
@@ -748,7 +762,9 @@ def getComicInfo(path, originalpath):
except Exception:
os.remove(xmlPath)
return
if defaultTitle:
if options.comicinfotitle:
options.title = xml.data['Title']
elif defaultTitle:
if xml.data['Series']:
options.title = xml.data['Series']
if xml.data['Volume']:
@@ -798,26 +814,48 @@ def getPanelViewSize(deviceres, size):
return str(int(x)), str(int(y))
def removeNonImages(filetree):
# clean dot from original file
dot_clean(filetree)
for root, dirs, files in os.walk(filetree):
for name in files:
_, ext = getImageFileName(name)
if ext not in ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.avif'):
if os.path.exists(os.path.join(root, name)):
os.remove(os.path.join(root, name))
# remove empty nested folders
for root, dirs, files in os.walk(filetree, False):
if not files and not dirs:
os.rmdir(root)
def sanitizeTree(filetree):
chapterNames = {}
page = 1
cover_path = None
for root, dirs, files in os.walk(filetree):
dirs.sort(key=OS_SORT_KEY)
files.sort(key=OS_SORT_KEY)
for name in files:
splitname = os.path.splitext(name)
_, ext = getImageFileName(name)
# 9999 page limit
slugified = f'kcc-{page:04}'
unique_name = f'kcc-{page:04}'
page += 1
newKey = os.path.join(root, slugified + splitname[1])
newKey = os.path.join(root, unique_name + ext)
key = os.path.join(root, name)
if key != newKey:
os.replace(key, newKey)
if not cover_path:
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):
tmpName = name
slugified = slugify(name)
slugified = slugify(name, is_natural_sorted)
while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper():
slugified += "A"
chapterNames[slugified] = tmpName
@@ -826,7 +864,7 @@ def sanitizeTree(filetree):
if key != newKey:
os.replace(key, newKey)
dirs[i] = newKey
return chapterNames
return chapterNames, cover_path
def flattenTree(filetree):
@@ -843,8 +881,7 @@ def sanitizePermissions(filetree):
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD)
for name in dirs:
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC)
# clean dot from original file
dot_clean(filetree)
def dot_clean(filetree):
for root, _, files in os.walk(filetree, topdown=False):
@@ -904,8 +941,12 @@ def chunk_process(path, mode, parent):
if mode < 3:
for root, dirs, files in walkLevel(path, 0):
for name in files if mode == 1 else dirs:
if mode == 1:
size = os.path.getsize(os.path.join(root, name))
size = 0
if mode == 1:
if 'below' not in name:
size = os.path.getsize(os.path.join(root, name))
if 'above' in name:
size += os.path.getsize(os.path.join(root, name.replace('above', 'below')))
else:
size = getDirectorySize(os.path.join(root, name))
if currentSize + size > targetSize:
@@ -935,7 +976,7 @@ def detectSuboptimalProcessing(tmppath, orgpath):
for root, _, files in os.walk(tmppath, False):
for name in files:
if getImageFileName(name) is not None:
if not alreadyProcessed and getImageFileName(name)[0].endswith('-kcc'):
if not alreadyProcessed and '-kcc' in getImageFileName(name)[0]:
alreadyProcessed = True
path = os.path.join(root, name)
pathOrg = orgpath + path.split('OEBPS' + os.path.sep + 'Images')[1]
@@ -959,10 +1000,6 @@ def detectSuboptimalProcessing(tmppath, orgpath):
os.remove(os.path.join(root, name))
except OSError as e:
raise RuntimeError(f"{name}: {e}")
# remove empty nested folders
for root, dirs, files in os.walk(tmppath, False):
if not files and not dirs:
os.rmdir(root)
if alreadyProcessed:
print("WARNING: Source files are probably created by KCC. The second conversion will decrease quality.")
if GUI:
@@ -985,21 +1022,27 @@ def createNewTome(parent):
return tomePath, tomePathRoot
def slugify(value):
value = slugify_ext(value, regex_pattern=r'[^-a-z0-9_\.]+').strip('.')
value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2))
def slugify(value, is_natural_sorted):
if options.format == 'CBZ' and is_natural_sorted:
return value
if options.format != 'CBZ':
# convert all unicode to ascii via slugify
value = slugify_ext(value, regex_pattern=r'[^-a-z0-9_\.]+').strip('.')
if not is_natural_sorted:
# pad zeros to numbers
value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2))
return value
def makeZIP(zipfilename, basedir, isepub=False):
start = perf_counter()
zipfilename = os.path.abspath(zipfilename) + '.zip'
if '7z' in available_archive_tools():
if SEVENZIP in available_archive_tools():
if isepub:
mimetypeFile = open(os.path.join(basedir, 'mimetype'), 'w')
mimetypeFile.write('application/epub+zip')
mimetypeFile.close()
subprocess_run(['7z', 'a', '-tzip', zipfilename, os.path.join(basedir, "*")], capture_output=True, check=True)
subprocess_run([SEVENZIP, 'a', '-tzip', zipfilename, os.path.join(basedir, "*")], capture_output=True, check=True)
else:
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
if isepub:
@@ -1048,6 +1091,8 @@ def makeParser():
help="Output generated file to specified directory or file")
output_options.add_argument("-t", "--title", action="store", dest="title", default="defaulttitle",
help="Comic title [Default=filename or directory name]")
output_options.add_argument("--comicinfotitle", action="store_true", dest="comicinfotitle", default=False,
help="Write Title from ComicInfo.xml")
output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor",
help="Author name [Default=KCC]")
output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto",
@@ -1062,6 +1107,8 @@ def makeParser():
help="Shift first page to opposite side in landscape for spread alignment")
output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False,
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,
help="Do not modify image and ignore any profil or processing option")
@@ -1154,9 +1201,8 @@ def checkOptions(options):
if options.profile == 'K1' or options.profile == 'K2' or options.profile == 'K34' or options.profile == 'KDX':
options.panelview = False
options.hq = False
if options.profile == 'KV' or options.profile in image.ProfileData.ProfilesKindlePDOC.keys():
if not options.hq and not options.autoscale:
options.panelview = False
options.hq = False
# Webtoon mode mandatory options
if options.webtoon:
options.panelview = False
@@ -1198,13 +1244,13 @@ def checkTools(source):
source = source.upper()
if source.endswith('.CB7') or source.endswith('.7Z') or source.endswith('.RAR') or source.endswith('.CBR') or \
source.endswith('.ZIP') or source.endswith('.CBZ'):
if '7z' not in available_archive_tools():
if SEVENZIP not in available_archive_tools():
print('ERROR: 7z is missing!')
sys.exit(1)
if options.format == 'MOBI':
try:
subprocess_run(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT, check=True)
except FileNotFoundError:
except (FileNotFoundError, CalledProcessError):
print('ERROR: KindleGen is missing!')
sys.exit(1)
@@ -1227,6 +1273,40 @@ def checkPre(source):
raise UserWarning("Target directory is not writable.")
def makeFusion(sources: List[str]):
if len(sources) < 2:
raise UserWarning('Fusion requires at least 2 sources. Did you forget to uncheck fusion?')
start = perf_counter()
first_path = Path(sources[0])
if first_path.is_file():
fusion_path = first_path.parent.joinpath(first_path.stem + ' [fused]')
else:
fusion_path = first_path.parent.joinpath(first_path.name + ' [fused]')
print("Running Fusion")
for source in sources:
print(f"Processing {source}...")
checkPre(source)
print("Checking images...")
path = getWorkFolder(source)
pathfinder = os.path.join(path, "OEBPS", "Images")
sanitizeTree(pathfinder)
# TODO: remove flattenTree when subchapters are supported
flattenTree(pathfinder)
source_path = Path(source)
if source_path.is_file():
os.renames(pathfinder, fusion_path.joinpath(source_path.stem))
else:
os.renames(pathfinder, fusion_path.joinpath(source_path.name))
end = perf_counter()
print(f"makefusion: {end - start} seconds")
print("Combined File: "+ str(fusion_path))
return str(fusion_path)
def makeBook(source, qtgui=None):
start = perf_counter()
global GUI
@@ -1235,13 +1315,17 @@ def makeBook(source, qtgui=None):
GUI.progressBarTick.emit('1')
else:
checkTools(source)
options.kindle_scribe_azw3 = options.profile == 'KS' and ('MOBI' in options.format or 'EPUB' in options.format)
checkPre(source)
print("Preparing source images...")
path = getWorkFolder(source)
print("Checking images...")
getComicInfo(os.path.join(path, "OEBPS", "Images"), source)
removeNonImages(os.path.join(path, "OEBPS", "Images"))
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
chapterNames = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
cover = image.Cover(cover_path, options)
if options.webtoon:
y = image.ProfileData.Profiles[options.profile][1][1]
comic2panel.main(['-y ' + str(y), '-i', '-m', path], qtgui)
@@ -1287,10 +1371,10 @@ def makeBook(source, qtgui=None):
else:
print("Creating EPUB file...")
if len(tomes) > 1:
buildEPUB(tome, chapterNames, tomeNumber, True)
buildEPUB(tome, chapterNames, tomeNumber, True, cover, len(tomes))
filepath.append(getOutputFilename(source, options.output, '.epub', ' ' + str(tomeNumber)))
else:
buildEPUB(tome, chapterNames, tomeNumber, False)
buildEPUB(tome, chapterNames, tomeNumber, False, cover)
filepath.append(getOutputFilename(source, options.output, '.epub', ''))
makeZIP(tome + '_comic', tome, True)
copyfile(tome + '_comic.zip', filepath[-1])
@@ -1383,7 +1467,9 @@ def makeMOBIWorker(item):
if kindlegenErrorCode > 0:
break
if ":I1036: Mobi file built successfully" in line:
break
return [0, '', item]
if ":I1037: Mobi file built with WARNINGS!" in line:
return [0, '', item]
# ERROR: KCC unknown generic error
if kindlegenErrorCode == 0:
kindlegenErrorCode = err.returncode

View File

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

View File

@@ -20,9 +20,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import io
import os
import numpy as np
from pathlib import Path
from functools import cached_property
import mozjpeg_lossless_optimization
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter, ImageDraw
from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin
from .inter_panel_crop_alg import crop_empty_inter_panel
@@ -85,12 +87,14 @@ class ProfileData:
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8),
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
'K578': ("Kindle", (600, 800), Palette16, 1.8),
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.8),
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
'KV': ("Kindle Paperwhite 3/4/Voyage/Oasis", (1072, 1448), Palette16, 1.8),
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.8),
}
ProfilesKindlePDOC = {
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.8),
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.8),
'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8),
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
@@ -144,11 +148,9 @@ class ComicPageParser:
# Detect corruption in source image, let caller catch any exceptions triggered.
srcImgPath = os.path.join(source[0], source[1])
Image.open(srcImgPath).verify()
self.image = Image.open(srcImgPath)
self.image.verify()
self.image = Image.open(srcImgPath).convert('RGB')
self.color = self.colorCheck()
self.fill = self.fillCheck()
# backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'):
@@ -179,13 +181,13 @@ class ComicPageParser:
new_image = Image.new("RGB", (int(width / 2), int(height*2)))
new_image.paste(pageone, (0, 0))
new_image.paste(pagetwo, (0, height))
self.payload.append(['N', self.source, new_image, self.color, self.fill])
self.payload.append(['N', self.source, new_image, self.fill])
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth \
and not self.opt.webtoon and self.opt.splitter == 1:
spread = self.image
if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.color, self.fill])
self.payload.append(['R', self.source, spread, self.fill])
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
if self.opt.splitter != 1:
if width > height:
@@ -200,38 +202,15 @@ class ComicPageParser:
else:
pageone = self.image.crop(leftbox)
pagetwo = self.image.crop(rightbox)
self.payload.append(['S1', self.source, pageone, self.color, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.color, self.fill])
self.payload.append(['S1', self.source, pageone, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.fill])
if self.opt.splitter > 0:
spread = self.image
if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread,
self.color, self.fill])
self.payload.append(['R', self.source, spread, self.fill])
else:
self.payload.append(['N', self.source, self.image, self.color, self.fill])
def colorCheck(self):
if self.opt.webtoon:
return True
else:
img = self.image.copy()
bands = img.getbands()
if bands == ('R', 'G', 'B') or bands == ('R', 'G', 'B', 'A'):
thumb = img.resize((40, 40))
SSE, bias = 0, [0, 0, 0]
bias = ImageStat.Stat(thumb).mean[:3]
bias = [b - sum(bias) / 3 for b in bias]
for pixel in thumb.getdata():
mu = sum(pixel) / 3
SSE += sum((pixel[i] - mu - bias[i]) * (pixel[i] - mu - bias[i]) for i in [0, 1, 2])
MSE = float(SSE) / (40 * 40)
if MSE > 22:
return True
else:
return False
else:
return False
self.payload.append(['N', self.source, self.image, self.fill])
def fillCheck(self):
if self.opt.bordersColor:
@@ -273,81 +252,121 @@ class ComicPageParser:
class ComicPage:
def __init__(self, options, mode, path, image, color, fill):
def __init__(self, options, mode, path, image, fill):
self.opt = options
_, self.size, self.palette, self.gamma = self.opt.profileData
if self.opt.hq:
self.size = (int(self.size[0] * 1.5), int(self.size[1] * 1.5))
self.kindle_scribe_azw3 = (options.profile == 'KS') and (options.format in ('MOBI', 'EPUB'))
self.image = image
self.color = color
self.original_color_mode = image.mode
self.image = image.convert("RGB")
self.fill = fill
self.rotated = False
self.orgPath = os.path.join(path[0], path[1])
self.targetPathStart = os.path.join(path[0], os.path.splitext(path[1])[0])
if 'N' in mode:
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc'
self.targetPathOrder = '-kcc-x'
elif 'R' in mode:
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc-a'
self.targetPathOrder = '-kcc-a' if options.rotatefirst else '-kcc-d'
if not options.norotate:
self.rotated = True
elif 'S1' in mode:
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc-b'
self.targetPathOrder = '-kcc-b'
elif 'S2' in mode:
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc-c'
self.targetPathOrder = '-kcc-c'
# backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'):
Image.Resampling = Image
@cached_property
def color(self):
if self.original_color_mode in ("L", "1"):
return False
img = self.image.convert("YCbCr")
_, cb, cr = img.split()
cb_hist = cb.histogram()
cr_hist = cr.histogram()
cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
cb_spread = cb_nonzero[-1] - cb_nonzero[0] if len(cb_nonzero) else 0
cr_spread = cr_nonzero[-1] - cr_nonzero[0] if len(cr_nonzero) else 0
SPREAD_THRESHOLD=20
if cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
return False
else:
return True
def saveToDir(self):
try:
flags = []
if not self.opt.forcecolor and not self.opt.forcepng:
self.image = self.image.convert('L')
if self.rotated:
flags.append('Rotated')
if self.fill != 'white':
flags.append('BlackBackground')
if self.opt.forcepng:
self.image.info["transparency"] = None
self.targetPath += '.png'
self.image.save(self.targetPath, 'PNG', optimize=1)
if self.opt.kindle_scribe_azw3 and self.image.size[1] > 1920:
w, h = self.image.size
targetPath = self.save_with_codec(self.image.crop((0, 0, w, 1920)), self.targetPathStart + self.targetPathOrder + '-above')
self.save_with_codec(self.image.crop((0, 1920, w, h)), self.targetPathStart + self.targetPathOrder + '-below')
elif self.opt.kindle_scribe_azw3:
targetPath = self.save_with_codec(self.image, self.targetPathStart + self.targetPathOrder + '-whole')
else:
self.targetPath += '.jpg'
if self.opt.mozjpeg:
with io.BytesIO() as output:
self.image.save(output, format="JPEG", optimize=1, quality=85)
input_jpeg_bytes = output.getvalue()
output_jpeg_bytes = mozjpeg_lossless_optimization.optimize(input_jpeg_bytes)
with open(self.targetPath, "wb") as output_jpeg_file:
output_jpeg_file.write(output_jpeg_bytes)
else:
self.image.save(self.targetPath, 'JPEG', optimize=1, quality=85)
targetPath = self.save_with_codec(self.image, self.targetPathStart + self.targetPathOrder)
if os.path.isfile(self.orgPath):
os.remove(self.orgPath)
return [Path(self.targetPath).name, flags]
return [Path(targetPath).name, flags]
except IOError as err:
raise RuntimeError('Cannot save image. ' + str(err))
def autocontrastImage(self):
def save_with_codec(self, image, targetPath):
if self.opt.forcepng:
image.info["transparency"] = None
if self.opt.iskindle and ('MOBI' in self.opt.format or 'EPUB' in self.opt.format):
targetPath += '.gif'
image.save(targetPath, 'GIF', optimize=1, interlace=False)
else:
targetPath += '.png'
image.save(targetPath, 'PNG', optimize=1)
else:
targetPath += '.jpg'
if self.opt.mozjpeg:
with io.BytesIO() as output:
image.save(output, format="JPEG", optimize=1, quality=85)
input_jpeg_bytes = output.getvalue()
output_jpeg_bytes = mozjpeg_lossless_optimization.optimize(input_jpeg_bytes)
with open(targetPath, "wb") as output_jpeg_file:
output_jpeg_file.write(output_jpeg_bytes)
else:
image.save(targetPath, 'JPEG', optimize=1, quality=85)
return targetPath
def gammaCorrectImage(self):
gamma = self.opt.gamma
if gamma < 0.1:
gamma = self.gamma
if self.gamma != 1.0 and self.color:
gamma = 1.0
if gamma == 1.0:
self.image = ImageOps.autocontrast(self.image)
pass
else:
self.image = ImageOps.autocontrast(Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma)))
self.image = Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma))
def autocontrastImage(self):
# autocontrast on non grayscale images has unexpected results
# since it autocontrasts each color channel separately
self.image = ImageOps.autocontrast(self.image)
def convertToGrayscale(self):
self.image = self.image.convert('L')
def quantizeImage(self):
colors = len(self.palette) // 3
if colors < 256:
self.palette += self.palette[:3] * (256 - colors)
# remove all color pixels from image, since colorCheck() has some tolerance
# quantize with a small number of color pixels in a mostly b/w image can have unexpected results
self.image = self.image.convert("RGB")
palImg = Image.new('P', (1, 1))
palImg.putpalette(self.palette)
self.image = self.image.convert('L')
self.image = self.image.convert('RGB')
# Quantize is deprecated but new function call it internally anyway...
self.image = self.image.quantize(palette=palImg)
def optimizeForDisplay(self, reducerainbow):
@@ -359,9 +378,6 @@ class ComicPage:
self.image = self.image.filter(unsharpFilter)
def resizeImage(self):
# kindle scribe conversion to mobi is limited in resolution by kindlegen, same with send to kindle and epub
if self.kindle_scribe_azw3:
self.size = (1440, 1920)
ratio_device = float(self.size[1]) / float(self.size[0])
ratio_image = float(self.image.size[1]) / float(self.image.size[0])
method = self.resize_method()
@@ -375,8 +391,6 @@ class ComicPage:
elif (self.opt.format == 'CBZ' or self.opt.kfx) and not self.opt.white_borders:
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
else:
if self.kindle_scribe_azw3:
self.size = (1860, 1920)
self.image = ImageOps.contain(self.image, self.size, method=method)
def resize_method(self):
@@ -412,14 +426,9 @@ class ComicPage:
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill)
class Cover:
def __init__(self, source, target, opt, tomeid):
def __init__(self, source, opt):
self.options = opt
self.source = source
self.target = target
if tomeid == 0:
self.tomeid = 1
else:
self.tomeid = tomeid
self.image = Image.open(source)
# backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'):
@@ -431,6 +440,14 @@ class Cover:
self.image = ImageOps.autocontrast(self.image)
if not self.options.forcecolor:
self.image = self.image.convert('L')
self.crop_main_cover()
size = list(self.options.profileData[1])
if self.options.kindle_scribe_azw3:
size[1] = min(size[1], 1920)
self.image.thumbnail(tuple(size), Image.Resampling.LANCZOS)
def crop_main_cover(self):
w, h = self.image.size
if w / h > 2:
if self.options.righttoleft:
@@ -442,17 +459,33 @@ class Cover:
self.image = self.image.crop((0, 0, w/2 - w * 0.03, h))
else:
self.image = self.image.crop((w/2 + w * 0.03, 0, w, h))
self.image.thumbnail(self.options.profileData[1], Image.Resampling.LANCZOS)
self.save()
def save(self):
def save_to_epub(self, target, tomeid, len_tomes=0):
try:
self.image.save(self.target, "JPEG", optimize=1, quality=85)
if tomeid == 0:
self.image.save(target, "JPEG", optimize=1, quality=85)
else:
copy = self.image.copy()
draw = ImageDraw.Draw(copy)
w, h = copy.size
draw.text(
xy=(w/2, h * .85),
text=f'{tomeid}/{len_tomes}',
anchor='ms',
font_size=h//7,
fill=255,
stroke_fill=0,
stroke_width=25
)
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:
raise RuntimeError('Failed to save cover.')
def saveToKindle(self, kindle, asin):
self.image = self.image.resize((300, 470), Image.Resampling.LANCZOS)
self.image = ImageOps.contain(self.image, (300, 470), Image.Resampling.LANCZOS)
try:
self.image.save(os.path.join(kindle.path.split('documents')[0], 'system', 'thumbnails',
'thumbnail_' + asin + '_EBOK_portrait.jpg'), 'JPEG', optimize=1, quality=85)

View File

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

View File

@@ -1,11 +1,11 @@
PySide6>=6.5.1
Pillow>=5.2.0
Pillow>=11.3.0
psutil>=5.9.5
requests>=2.31.0
python-slugify>=1.2.1
raven>=6.0.0
packaging>=23.2
mozjpeg-lossless-optimization>=1.1.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
numpy>=1.22.4,<2.0.0
numpy>=1.22.4

View File

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