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

Compare commits

...

241 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
Alex Xu
c4f845c221 dot_clean 2025-05-25 17:24:03 -07:00
Alex Xu
ebb59dbc2d make error message more clear for covers (#938)
* add error reporting to cover

* fix period
2025-05-25 17:02:14 -07:00
Alex Xu
8da2b4cb96 ignore ._ files for real 2025-05-25 13:30:04 -07:00
Alex Xu
7b8858678f ignore utf-8 decoding errors 2025-05-25 13:30:04 -07:00
Alex Xu
be07e0df6a add error check for filepath length check 2025-05-23 10:48:31 -07:00
Alex Xu
dc711e671d unescape ampersand (&) (#923) 2025-05-22 18:04:40 -07:00
Alex Xu
581ecd0ec2 flatten subfolders if over windows MAX_LENGTH=260 characters (#922) 2025-05-22 18:04:21 -07:00
Alex Xu
f3a32c6174 replace Qt Creator with pre-installed pyside6-designer 2025-05-22 18:02:49 -07:00
Alex Xu
0ce5f7f186 add slow page turn to FAQ 2025-05-17 13:27:38 -07:00
Alex Xu
c3d2f89471 don't add white padding if forced white background checked 2025-05-14 09:18:33 -07:00
Alex Xu
b1c4cd36f1 don't add borders around small images 2025-05-14 09:18:33 -07:00
Alex Xu
b7c6281b55 Update README.md 2025-05-12 23:38:12 -07:00
Alex Xu
559485184d refactor kindlegen errors 2025-05-11 15:11:56 -07:00
Alex Xu
75f5274449 throw kindlegen exceptions 2025-05-11 15:11:56 -07:00
Alex Xu
dfc149d893 add samples 2025-05-08 14:57:34 -07:00
dependabot[bot]
327b522080 Bump signpath/github-action-submit-signing-request from 1.1 to 1.2 (#918) 2025-05-04 20:21:05 -07:00
Alex Xu
7c029b4ba1 Update README.md 2025-05-04 19:37:12 -07:00
Alex Xu
2db8f5c8fd Add files via upload 2025-05-04 19:36:46 -07:00
Alex Xu
ce72921289 make header image smaller 2025-05-04 19:29:29 -07:00
Alex Xu
f1bbc47798 add header image 2025-05-04 19:26:36 -07:00
Alex Xu
5a39007db6 remove changelog 2025-04-24 15:47:03 -07:00
Alex Xu
0d7487f8d4 remove changelog 2025-04-24 15:46:41 -07:00
Alex Xu
8a78f82ff2 Update README.md 2025-04-23 15:54:34 -07:00
Alex Xu
149f7e5921 Add top/bottom margin note 2025-04-21 00:45:01 -07:00
Alex Xu
40d219de4d fix underscore in c2p/c2e 2025-04-19 15:31:39 -07:00
Alex Xu
370d1d7392 fix capitalization in c2e c2p files 2025-04-19 15:29:33 -07:00
Alex Xu
8295f163c2 Update package-windows-with-docker.yml (#908) 2025-04-19 13:53:29 -07:00
Alex Xu
9f9a97afaa bump 7.4.1 2025-04-18 22:13:26 -07:00
Alex Xu
d113cae154 fix ._ files (#907) 2025-04-18 21:52:26 -07:00
Alex Xu
bc4afeb69e remove video 2025-04-17 21:58:18 -07:00
Alex Xu
afb32f6287 add video to readme 2025-04-17 21:57:19 -07:00
Alex Xu
bc516634cf Revert "build on Python 3.12 instead of Python 3.11 (#885)" (#904)
This reverts commit 80b01d298f.
2025-04-17 09:53:40 -07:00
Alex Xu
36180f7904 Revert "fix Kindle Scribe panel view broken in 6.1.0 (#894)" (#903)
This reverts commit 1dd36e08eb.
2025-04-17 09:17:47 -07:00
Alex Xu
3517994d37 bump 7.4.0 2025-04-17 08:19:06 -07:00
Alex Xu
08acad10ea enable/disable panel view in Kindle Aa menu and landscape half page reading (#899)
* enable virtual panel view for all kindles

* only use old panel view method on older Kindles
2025-04-17 08:17:57 -07:00
Alex Xu
727df0c9d6 disable old panel view for new kindles (#902) 2025-04-17 07:57:14 -07:00
Alex Xu
13e71df172 remove nested empty folders (#900) 2025-04-16 23:07:41 -07:00
Alex Xu
3e7646bbad rename buttons (#898) 2025-04-15 15:50:32 -07:00
Alex Xu
1c81a9d5b3 fix panel view order for no rotate (#897) 2025-04-15 15:38:03 -07:00
Alex Xu
efa831341c disable Kindle Scribe upscale (#893) 2025-04-08 10:28:24 -07:00
Alex Xu
1dd36e08eb fix Kindle Scribe panel view broken in 6.1.0 (#894) 2025-04-08 10:27:35 -07:00
Alex Xu
d9d5ce2423 Delete MANIFEST.in 2025-04-05 19:54:11 -07:00
lalo101097
83c6b7b2d5 Update comic2panel.py (#892) 2025-04-05 12:33:33 -07:00
Alex Xu
55dfdd32f8 Update README.md 2025-04-05 08:54:11 -07:00
Alex Xu
3e3710dd76 Add preserve margin % toggle (#886)
* preserve margin

* set ratio to 0.5

* add preserve margin GUI

* increase step size to 5

* remove clear

* fix save

* save preserveMarginBox

* math

* max 99
2025-04-03 10:11:14 -07:00
Alex Xu
41f87273ca speed optimization: fix accidentally cropping pages twice (#889)
* don't crop each page twice

* Update comic2ebook.py
2025-03-30 08:58:35 -07:00
Alex Xu
b959739b53 For contributers, please "Sync fork" instead of "git merge" readme note (#888) 2025-03-30 07:33:48 -07:00
Alex Xu
d1b66d16fd rename reduce rainbow to rainbow blur (#887) 2025-03-30 07:27:52 -07:00
Alex Xu
80b01d298f build on Python 3.12 instead of Python 3.11 (#885)
* use python 3.12

* bump to python 3.12
2025-03-29 20:39:34 -07:00
Antonio Panariello
de2aad0b9c Add split size controls to GUI (#884)
* Add split size GUI widget

* Set split size and save GUI widget state in settings

* Set max split size in GUI to 600 MB

* Move chunk size control in middle column and widget on panel below.

Added warning label and tooltip

* Fix chunk size incorrectly enabled when webtoon active and format change

* add 'on older ereaders'

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-03-29 20:23:09 -07:00
Alex Xu
bc7ab0879c Update README.md 2025-03-29 15:37:17 -07:00
Alex Xu
2ab0135815 Update README.md 2025-03-27 21:46:31 -07:00
Alex Xu
3882b6ce0a Update README.md 2025-03-26 14:48:03 -07:00
Alex Xu
5e23e2cac1 add link to official macOS Amazon USB File Transfer MTP app (#883) 2025-03-26 14:45:37 -07:00
Alex Xu
d36933cb9a kfx is broken (#882) 2025-03-26 14:33:33 -07:00
Alex Xu
e40cf29aa3 don't attempt to sign on forks (#881)
* overwrite unsigned binary

* only sign on ciromattia

* Update package-windows.yml

* only sign on ciromattia
2025-03-26 13:47:16 -07:00
Alex Xu
9d1802453c Update package-windows-with-docker.yml 2025-03-26 13:46:02 -07:00
Alex Xu
4337d6c10d try to crop only main cover from wide cover scan (#875) 2025-03-21 10:17:23 -07:00
Alex Xu
9680ff24c2 split covers in half if too wide (#874) 2025-03-18 21:23:34 -07:00
Alex Xu
48b5b4f397 Update README.md 2025-03-18 16:50:08 -07:00
Alex Xu
c712dc12a2 improve faq 2025-03-17 08:23:26 -07:00
Alex Xu
d2be9138c4 Update README.md 2025-03-16 20:49:30 -07:00
Alex Xu
3cb40772a4 update summary 2025-03-16 20:35:03 -07:00
Alex Xu
ef3b756247 fix unsupported directory structure and large mobi conversion stuck with nested folders by flattening folders (#869) 2025-03-15 08:09:17 -07:00
Alex Xu
77af020abb Update README.md 2025-03-13 14:30:37 -07:00
Alex Xu
1c9c6c13b4 Update README.md 2025-03-13 11:00:39 -07:00
Alex Xu
85ce39b2c3 Update README.md 2025-03-13 10:48:18 -07:00
simesherbs
6a33aee241 New "MOBI + EPUB (200MB)" output option (#866)
* initial fix

* initial

* working, but mobi is a bit too large

* changed target size to 190mb to account for mobi conversion size increase

* changed target size back to 195

* removed debugging print statements

* add trailing comma

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-03-13 09:07:03 -07:00
Alex Xu
271a129537 bump 7.3.3 2025-03-12 19:10:29 -07:00
Alex Xu
23099cee81 fix landscape alignment (#865) 2025-03-12 19:09:58 -07:00
simesherbs
b957fcf3fe "Disable processing" tool tip font fix (#864)
* initial fix

* applied fix to appropriate file

* changed .ui file and recompiled

* final fixes
2025-03-12 18:46:30 -07:00
Alex Xu
187475a424 add SignPath to Windows GUI workflow (#862) 2025-03-11 15:41:12 -07:00
Alex Xu
88fd54e2ba bump 7.3.2 2025-03-11 15:30:02 -07:00
Alex Xu
b23b67bbbe revert build windows gui with docker (#861) 2025-03-11 15:29:33 -07:00
Alex Xu
9992d895cf fix comicrack large files again (#860) 2025-03-11 15:07:48 -07:00
Alex Xu
561951a349 restore imgMetadata dict (#858)
* Revert "memory optimization: store metadata in filenames, not a global dict (…"

This reverts commit 9a2a09eab9.

* only remove imgOld
2025-03-11 09:57:11 -07:00
Alex Xu
b8b7926366 use release-signing 2025-03-10 09:04:55 -07:00
Alex Xu
92f3308e1c bump to 7.3.1 2025-03-10 08:59:18 -07:00
Alex Xu
9e87ccef4e Free code signing on Windows provided by SignPath.io, certificate by SignPath Foundation 2025-03-10 08:58:37 -07:00
Alex Xu
9a2a09eab9 memory optimization: store metadata in filenames, not a global dict (#853)
* remove options.imgMetadata and store metadata in filename

* make kcc much more memory efficient by removing imgMetadata and imgOld dicts
2025-03-09 16:52:07 -07:00
Alex Xu
88cf2fd21f fix sanitize-first bug 2025-03-09 16:50:15 -07:00
Vinh Quang Tran
7a3ed262b1 fix: disabled overriding title (#857) 2025-03-09 16:22:34 -07:00
Alex Xu
9e204aad76 rename images before processing to fix splitting issues (#852)
* rename images before processing to fix splitting issues

* import pathlib

* 9999 page limit
2025-03-07 07:59:04 -08:00
Alex Xu
e1f9d12676 add test-signing 2025-03-06 14:08:48 -08:00
Ciro Mattia Gonano
24ab72fcbc Update LICENSE.txt to reflect Alex is current main maintainer 2025-03-06 12:06:55 +01:00
Ciro Mattia Gonano
4af6a75874 Update LICENSE.txt to reflect Alex is current main maintainer 2025-03-06 12:06:08 +01:00
Alex Xu
28b6188a3f Delete appveyor.yml 2025-03-05 19:03:14 -08:00
iwa
f000af1207 Delete .travis.yml (#849)
* Delete .travis.yml

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-03-05 13:13:59 -08:00
Alex Xu
299f916580 add screenshot 2025-03-04 19:59:54 -08:00
Alex Xu
c39e403595 add screenshot 2025-03-04 19:58:24 -08:00
Alex Xu
e787dd2897 bump 7.3.0 2025-03-04 11:29:44 -08:00
Alex Xu
01625904d1 huge speed optimization on HDD by removing md5 (#845)
* Eliminate unnecessary use of MD5 checksum

md5checksum() computes the actual checksum of a specified file, which is appropriately expensive, but the code seemed to be using the checksum result as a key into the imgMetadata dictionary to avoid handling image files being renamed during processing steps. This seems like a very expensive way to handle the rename so instead, I now update the imgMetadata keys with the new filename in the one place that the rename happens, and avoid MD5 checksums entirely.

* merge conflicts

* Add missing handling for image path renames due to nested chapter folder name

* merge conflicts

* merge conflicts

* add perf_counters

* imgFileProcessing perf_counter

* use startswith and removeprefix

---------

Co-authored-by: utopiafallen <utopiafallen@gmail.com>
2025-03-04 11:28:23 -08:00
Alex Xu
5f8526da44 all image files have unique ordered names (#848) 2025-03-03 07:50:21 -08:00
Alex Xu
1159e737a0 small correction to 7z (#847) 2025-03-02 19:55:45 -08:00
Alex Xu
5bbdb715e9 7z is faster note 2025-03-02 12:39:51 -08:00
Alex Xu
1a3cd6c916 add perf_counter to makeBook (#846) 2025-03-02 12:24:49 -08:00
Alex Xu
e1e6d587f4 makeZIP now prefers 7z for SPEED (#844) 2025-03-02 11:08:27 -08:00
Alex Xu
ca5c0bdd61 fix error messages that only say a single number (#843) 2025-03-01 19:40:55 -08:00
Alex Xu
c6f491d27e bump 7.2.3 2025-02-28 20:44:45 -08:00
Alex Xu
c9ed3feef1 deprioritize tar (#842) 2025-02-28 20:17:41 -08:00
Alex Xu
be147fe7e5 saves several seconds per file (#841) 2025-02-28 20:11:11 -08:00
utopiafallen
62ffa2bc80 Improve processing performance by partially undoing "Refactored detection of corrupted files"
Commit f952634971 moved image corruption detection out from the ComicPage constructor and into a standalone detectCorruption() function. This led to a performance regression because now corruption detection happens in a single thread when it used to be distributed across worker threads, and because a source image is now loaded twice in memory: once during corruption detection and once when actually going to process the image.

Image file corruption detection is now back inside the ComicPage constructor and the extra load() has been removed because the convert() call will automatically invoke load() and most likely throw the same exceptions.
2025-02-28 19:49:13 -08:00
Alex Xu
11186d07c0 fix file splitting/chunking for real in certain situations (#839)
* fix file splitting without ComicInfo.xml

* remove dead var
2025-02-28 19:19:11 -08:00
Alex Xu
4b3cd6882a Revert "Build windows GUI version normally without docker" (#835)
* Revert "remove GUI windows docker"

This reverts commit 4fc5cc9dfb.

* build windows gui version with docker

* cffi

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

* rename split to batch

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

* add comicinfo.xml handling

* sanitize

* add error handling

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

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

* remove unused imports
2025-01-23 09:36:03 -08:00
X5Games / Neyney10
70d10204ee Update UI to match Qt Creator 15.0. (#809) 2025-01-16 13:33:54 -08:00
Alex Xu
a9d0c57ba6 build appimage on ubuntu-22.04 instead of 24.04 (#803) 2025-01-06 18:05:47 -08:00
Alex Xu
5598596650 bump to 7.1.2 2025-01-05 08:59:37 -08:00
Alex Xu
4b7b6d1c58 Revert "qt6-wayland (#800)" (#801)
This reverts commit 075bc748d1.
2025-01-04 22:45:00 -08:00
Alex Xu
075bc748d1 qt6-wayland (#800) 2025-01-04 22:35:22 -08:00
Alex Xu
9e318ed33e add author gui (#799) 2025-01-04 21:49:51 -08:00
Alex Xu
6d21bfa6fa bump to 7.1.1 2025-01-03 20:08:13 -08:00
Alex Xu
132574d57d remove fastnumbers (#798) 2025-01-03 20:06:44 -08:00
Alex Xu
317fb33fd0 No Rotate option (#785)
* no rotate

* Revert "no rotate"

This reverts commit b6f1fe8882.

* implement norotate
2025-01-03 19:43:20 -08:00
Alex Xu
2189f4b1cb Revert "build appimage on ubuntu-20.04 instead of 22.04 (#795)" (#797)
This reverts commit 462a3cebde.
2024-12-31 18:41:10 -08:00
Alex Xu
462a3cebde build appimage on ubuntu-20.04 instead of 22.04 (#795) 2024-12-31 09:05:09 -08:00
Alex Xu
2a414ef936 bump to 7.1.0 2024-12-30 22:23:18 -08:00
Alex Xu
4adb998896 modify path with c2e and c2p (#790) 2024-12-30 22:19:23 -08:00
Alex Xu
315b6e150d enable synthetic spreads for all devices (#789) 2024-12-30 22:15:41 -08:00
Alex Xu
5875508597 fix dark mode (#794) 2024-12-30 20:16:11 -08:00
Alex Xu
0a4ef31daf Fix cropping power sliders (#793)
* undo gui changes

* fix gui
2024-12-30 19:15:02 -08:00
Alex Xu
c99df3308e macos-12 is deprecated on GitHub, switch to macos-13 intel (#791) 2024-12-25 12:49:46 -08:00
Alex Xu
e9482fbd6c linux search for kindlegen in ~/.local/bin for steam deck and more (#786)
* on steam deck, search for kindlegen in ~/.local/bin

* remove comment
2024-12-25 12:24:12 -08:00
Alex Xu
434fe90b00 don't delete/dedupe covers, just change initial alignment (#784)
* don't delete covers, just change initial alignment

* replace dedupecover with spreadshift
2024-12-19 09:06:59 -08:00
dependabot[bot]
73a91ec0ae Bump python from 3.12-slim-bullseye to 3.13-slim-bullseye
Bumps python from 3.12-slim-bullseye to 3.13-slim-bullseye.

---
updated-dependencies:
- dependency-name: python
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-13 15:43:51 -08:00
Bradley Newman
e0b1848e09 Update README to include the latest usage options for kcc-c2e.py 2024-12-13 15:42:19 -08:00
Bradley Newman
a9360e6bc3 Add --nokepub option
By setting this to true, '.epub' format will be output rather than
'.kepub.epub'
2024-12-13 15:42:19 -08:00
termdisc
ae475e709a Add additional check for differently named Fedora-based distros (#780)
Bazzite is Fedora-based but fails the `distro.id() == 'fedora'` check because `distro.id()` resolves to "bazzite." I added an additional check to include `distro.like() == 'fedora'` for situations like this.

This needs to be formatted as an or statement because plain Fedora resolves `distro.like()` as a null string.
2024-12-13 14:08:40 -08:00
Matthias Witte
4769f68265 Remarkable profiles (#781)
* Add profiles for remarkable 1, 2 and Paper Pro

* Add remarkable icons based on Other.png

* Mention remarkable profile in README.md
2024-12-13 14:08:08 -08:00
Alex Xu
dbe6043542 add center spread property 2024-12-13 10:13:18 -08:00
Alex Xu
e1a318145d fix left to right comic spread alignment 2024-12-13 09:59:33 -08:00
Alex Xu
a71523b2d4 Remove scribe cover guide 2024-12-03 16:52:43 -08:00
Alex Xu
7a5473f530 Update 7z readme 2024-12-03 09:42:11 -08:00
Alex Xu
f5a5624112 add numpy to armv7 2024-11-16 20:09:36 -08:00
Alex Xu
3b7e8dc9a5 Docker numpy (#775)
* docker-base-20241116

* docker-base-20241116
2024-11-16 20:01:50 -08:00
41 changed files with 3191 additions and 1454 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

@@ -23,7 +23,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python
@@ -70,6 +70,5 @@ jobs:
prerelease: true
generate_release_notes: true
files: |
CHANGELOG.md
LICENSE.txt
*.AppImage*

View File

@@ -25,7 +25,7 @@ jobs:
build:
strategy:
matrix:
os: [ macos-12, macos-14 ]
os: [ macos-13, macos-14 ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
@@ -89,7 +89,6 @@ jobs:
prerelease: true
generate_release_notes: true
files: |
CHANGELOG.md
LICENSE.txt
dist/*.dmg
- name: Clean up keychain and provisioning profile

View File

@@ -10,41 +10,47 @@ on:
jobs:
build:
strategy:
matrix:
entry: [ kcc-c2e, kcc-c2p ]
include:
- entry: kcc-c2e
capital: KCC_c2e
- entry: kcc-c2p
capital: KCC_c2p
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# - name: Set up Python
# uses: actions/setup-python@v4
# with:
# python-version: 3.11
# cache: 'pip'
# - name: Install python dependencies
# run: |
# python -m pip install --upgrade pip setuptools wheel pyinstaller
# pip install -r requirements.txt
# - name: build binary
# run: |
# pyi-makespec -F -i icons\\comic2ebook.ico -n KCC_test -w --noupx kcc.py
- name: Package Application
uses: JackMcKew/pyinstaller-action-windows@main
with:
path: .
spec: ./kcc-c2e.spec
- name: Package Application
uses: JackMcKew/pyinstaller-action-windows@main
with:
path: .
spec: ./kcc-c2p.spec
spec: ./${{ matrix.entry }}.spec
- name: rename binaries
run: |
version_built=$(cat kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/[^.0-9b]//g")
mv dist/windows/kcc-c2e.exe dist/windows/KCC_c2e_${version_built}.exe
mv dist/windows/kcc-c2p.exe dist/windows/KCC_c2p_${version_built}.exe
- name: upload build
mv dist/windows/${{ matrix.entry }}.exe dist/windows/${{ matrix.capital }}_${version_built}.exe
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v4
with:
name: windows-build
name: windows-build-${{ matrix.entry }}
path: dist/windows/*.exe
- id: optional_step_id
uses: signpath/github-action-submit-signing-request@v1.2
if: ${{ github.repository == 'ciromattia/kcc' }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '1dc1bad6-4a8c-4f85-af30-5c5d3d392ea6'
project-slug: 'kcc'
signing-policy-slug: 'release-signing'
github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: 'dist/windows/'
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
@@ -52,6 +58,5 @@ jobs:
prerelease: true
generate_release_notes: true
files: |
CHANGELOG.md
LICENSE.txt
dist/windows/*.exe

View File

@@ -41,11 +41,23 @@ jobs:
- name: build binary
run: |
python setup.py build_binary
- name: upload build
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v4
with:
name: windows-build
path: dist/*.exe
- id: optional_step_id
uses: signpath/github-action-submit-signing-request@v1.2
if: ${{ github.repository == 'ciromattia/kcc' }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '1dc1bad6-4a8c-4f85-af30-5c5d3d392ea6'
project-slug: 'kcc'
signing-policy-slug: 'release-signing'
github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: 'dist/'
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
@@ -53,6 +65,5 @@ jobs:
prerelease: true
generate_release_notes: true
files: |
CHANGELOG.md
LICENSE.txt
dist/*.exe
dist/*.exe

View File

@@ -1,30 +0,0 @@
matrix:
include:
- os: osx
language: generic
osx_image: xcode11.1
before_install:
- pip3 install --upgrade pip setuptools wheel
install:
- pip3 install -r requirements.txt
- pip3 install certifi https://github.com/pyinstaller/pyinstaller/archive/develop.zip
- npm install -g appdmg
script: python3 setup.py build_binary
before_deploy:
- shopt -s extglob
- rm -r dist/!(*.deb|*.dmg)
deploy:
provider: gcs
access_key_id: GOOG1EC62457RKUYFR2TIZUWV4EFSV2EP5LVLPPFXUAKADWJFDYPFW63BQSLA
secret_access_key:
secure: sxYjeho7U3im0Ezf6cz6TjYDiLvf0kAM2ETQHYoFNbD1VVvhJJyymDCnPH80zpFKmhc1MWTB6ndwsrPfcyZDLR2meSdWGPjZfFPY3RcrfImndKi7ln+mYQDBQ7W1lGit4YcH3Ju7LHceaTbRA7fVTX8pWKOcbXL2oM+lQxTJHH32+crVma+ChhbjzTWsSLRoakt3Nhiveec5p/qSW7AFe4Zq+b3C85IgwjSJI/xVwzaWrs6p915h1zZi7KL7YCMIxfQFrvRPFR2KTbh/DoLCCrqfbD4qh0PVy1li51Ac3hd/u3foiNnTNchzgE3Nv/nbKmtFU6huuLNgzkQGuLA+yn7mKYzBwA3ZmFgoimdH9+yRCMkZ8B5VHpvfN1hgpJcyEl1T98Kv4cdtRYNB4w9iAMy1qSVxhjeI+2rjuWGoXro0lU6L4LIRCOruY3AuLCAKG8Qw5Ak9ksmIKBhZ9soxpoIwu/TYDUQkFj29IrUQucg9TEp7uAoxu8/7EHxB7hWnBRaBAAQbMuIRg7yysT3FT0Os6SB0t9+RBsVMSPuIti9JJZ2Lu0uRI1+Se+g7ItzYtJoPhBJAzAa+J9OONj0RNj2z8Vq2oIBhH4z6b6zTRMVroos3cdfYl5qIKs9SQ7rmeHoPRROcqpCznsUZ/ESa4f2MewFU/7AYcEnCesZV4xg=
bucket: kcc-deploy
local-dir: dist
skip_cleanup: true
on:
repo: AcidWeb/KCC

View File

@@ -1,5 +1,5 @@
# Select final stage based on TARGETARCH ARG
FROM ghcr.io/ciromattia/kcc:docker-base-20240928
FROM ghcr.io/ciromattia/kcc:docker-base-20241116
LABEL com.kcc.name="Kindle Comic Converter"
LABEL com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi"
LABEL org.opencontainers.image.description='Kindle Comic Converter'

View File

@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 python:3.12-slim-bullseye as compile-amd64
FROM --platform=linux/amd64 python:3.13-slim-bullseye as compile-amd64
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
@@ -16,7 +16,7 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
######################################################################################
FROM --platform=linux/arm64 python:3.12-slim-bullseye as compile-arm64
FROM --platform=linux/arm64 python:3.13-slim-bullseye as compile-arm64
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
@@ -73,7 +73,7 @@ RUN set -x && \
######################################################################################
FROM --platform=linux/arm/v7 python:3.12-slim-bullseye as compile-armv7
FROM --platform=linux/arm/v7 python:3.13-slim-bullseye as compile-armv7
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
@@ -126,17 +126,17 @@ RUN set -x && \
# Install required python modules
python -m pip install --upgrade pip && \
python -m venv /opt/venv && \
python -m pip install --upgrade pillow psutil requests python-slugify raven packaging mozjpeg-lossless-optimization natsort distro
python -m pip install --upgrade pillow psutil requests python-slugify raven packaging mozjpeg-lossless-optimization natsort distro numpy
######################################################################################
FROM --platform=linux/amd64 python:3.12-slim-bullseye as build-amd64
FROM --platform=linux/amd64 python:3.13-slim-bullseye as build-amd64
COPY --from=compile-amd64 /opt/venv /opt/venv
FROM --platform=linux/arm64 python:3.12-slim-bullseye as build-arm64
FROM --platform=linux/arm64 python:3.13-slim-bullseye as build-arm64
COPY --from=compile-arm64 /opt/venv /opt/venv
FROM --platform=linux/arm/v7 python:3.12-slim-bullseye as build-armv7
FROM --platform=linux/arm/v7 python:3.13-slim-bullseye as build-armv7
COPY --from=compile-armv7 /opt/venv /opt/venv
######################################################################################
@@ -160,5 +160,5 @@ WORKDIR /app
RUN DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
apt-get install -y p7zip-full unrar-free && \
ln -s /app/kindlegen /bin/kindlegen && \
echo docker-base-20240928 > /IMAGE_VERSION
echo docker-base-20241116 > /IMAGE_VERSION

View File

@@ -1,8 +1,9 @@
ISC LICENSE
Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
Copyright (c) 2012-2025 Ciro Mattia Gonano <ciromattia@gmail.com>
Copyright (c) 2013-2019 Paweł Jastrzębski <pawelj@iosphe.re>
Copyright (c) 2021-2023 Darodi
Copyright (c) 2021-2023 Darodi (https://github.com/darodi)
Copyright (c) 2023-2025 Alex Xu (https://github.com/axu2)
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted, provided that the

View File

@@ -1 +0,0 @@
exclude kindlecomicconverter/sentry.py

160
README.md
View File

@@ -1,15 +1,48 @@
<img src="header.jpg" alt="Header Image" width="400">
# 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** is a Python app to convert comic/manga files or folders to EPUB, Panel View MOBI or E-Ink optimized CBZ.
It was initially developed for Kindle but since version 4.6 it outputs valid EPUB 3.0 so _**despite its name, KCC is
actually a comic/manga to EPUB converter that every e-reader owner can happily use**_.
It can also optionally optimize images by applying a number of transformations.
**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.
Its main feature is various optional image processing steps to look good on eink screens,
which have different requirements than normal LCD screens.
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
**KCC** _is not_ [Amazon's Kindle Comic Creator](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1001103761) nor is in any way endorsed by Amazon.
@@ -22,15 +55,29 @@ If you have some **technical** problems using KCC please [file an issue here](ht
If you can fix an open issue, fork & make a pull request.
If you find **KCC** valuable you can consider donating to the authors:
- Ciro Mattia Gonano (founder, active 2013-2014):
- [![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=D8WNYNPBGDAS2)
- [![Donate Flattr](https://img.shields.io/badge/Donate-Flattr-green.svg)](http://flattr.com/thing/2260449/ciromattiakcc-on-GitHub)
- Paweł Jastrzębski (active 2013-2019):
- [![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YTTJ4LK2JDHPS)
- [![Donate Bitcoin](https://img.shields.io/badge/Donate-Bitcoin-green.svg)](https://jastrzeb.ski/donate/)
- Alex Xu (active 2023-Present)
- [![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/donate/?business=QFJVE7A6LCP6U&no_recurring=0&item_name=Kindle+Comic+Converter&currency_code=USD)
- Ciro Mattia Gonano (founder, active 2012-2014):
[![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=D8WNYNPBGDAS2)
- Paweł Jastrzębski (active 2013-2019):
[![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YTTJ4LK2JDHPS)
[![Donate Bitcoin](https://img.shields.io/badge/Donate-Bitcoin-green.svg)](https://jastrzeb.ski/donate/)
- Alex Xu (active 2023-Present)
[![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/)
## DOWNLOADS
@@ -52,11 +99,25 @@ 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
- [Kindle Scribe cover guide](https://github.com/ciromattia/kcc/issues/508) (also works for older Kindles)
- 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)
- 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.
## PREREQUISITES
@@ -70,9 +131,13 @@ If you have issues detecting it, get stuck on the MOBI conversion step, or use L
### 7-Zip
This is no longer required as of KCC 6.1.
This is optional but will make conversions much faster.
If you still need it, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation#7-zip
This is required for certain files and advanced features.
KCC will ask you to install if needed.
Refer to the wiki to install: https://github.com/ciromattia/kcc/wiki/Installation#7-zip
## INPUT FORMATS
**KCC** can understand and convert, at the moment, the following input types:
@@ -102,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),
@@ -124,6 +191,9 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.8),
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.8),
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.8),
'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.8),
'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.8),
'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.8),
'OTHER': ("Other", (0, 0), Palette16, 1.8),
```
@@ -137,7 +207,8 @@ MANDATORY:
MAIN:
-p PROFILE, --profile PROFILE
Device profile (Available options: K1, K2, K34, K578, KDX, KPW, KPW5, KV, KO, K11, KS, KoMT, KoG, KoGHD, KoA, KoAHD, KoAH2O, KoAO, KoN, KoC, KoL, KoF, KoS, KoE) [Default=KV]
Device profile (Available options: K1, K2, K34, K578, KDX, KPW, KPW5, KV, KO, K11, KS, KoMT, KoG, KoGHD, KoA, KoAHD, KoAH2O, KoAO, KoN, KoC, KoCC, KoL, KoLC, KoF, KoS, KoE)
[Default=KV]
-m, --manga-style Manga style (right-to-left reading and splitting)
-q, --hq Try to increase the quality of magnification
-2, --two-panel Display two not four panels in Panel View mode
@@ -157,8 +228,11 @@ PROCESSING:
Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]
--cp CROPPINGP, --croppingpower CROPPINGP
Set cropping power [Default=1.0]
--preservemargin After calculating crop, "back up" a specified percentage amount [Default=0]
--cm CROPPINGM, --croppingminimum CROPPINGM
Set cropping minimum area ratio [Default=0.0]
--ipc INTERPANELCROP, --interpanelcrop INTERPANELCROP
Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]
--blackborders Disable autodetection and force black borders
--whiteborders Disable autodetection and force white borders
--forcecolor Don't convert images to grayscale
@@ -172,10 +246,18 @@ 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
Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) [Default=Auto]
--nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'
-b BATCHSPLIT, --batchsplit BATCHSPLIT
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:
--customwidth CUSTOMWIDTH
@@ -211,15 +293,25 @@ 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.
If you want to edit the code, a good code editor is [VS Code](https://code.visualstudio.com).
If you want to edit the `.ui` files, use [Qt Creator](https://www.qt.io/download-qt-installer-oss), included in **Qt for desktop development**.
If you want to edit the `.ui` files, use `pyside6-designer` which is included in the `pip install pyside6`.
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
@@ -238,8 +330,16 @@ venv\Scripts\activate.bat
python kcc.py
```
You can build a `.exe` of KCC like the downloads we offer with
```
python setup.py build_binary
```
### macOS install from source
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
@@ -255,6 +355,12 @@ source venv/bin/activate
python kcc.py
```
You can build a `.app` of KCC like the downloads we offer with
```
python setup.py build_binary
```
## CREDITS
**KCC** is made by
@@ -272,6 +378,12 @@ The app relies and includes the following scripts:
- Icon is by **Nikolay Verin** ([http://ncrow.deviantart.com/](http://ncrow.deviantart.com/)) and released under [CC BY-NC-SA 3.0](http://creativecommons.org/licenses/by-nc-sa/3.0/) License.
## SAMPLE FILES CREATED BY KCC
https://www.mediafire.com/folder/ixh40veo6hrc5/kcc_samples
Older links (dead):
* [Kindle Oasis 2 / 3](http://kcc.iosphe.re/Samples/Ubunchu!-KO.mobi)
* [Kindle Paperwhite 3 / 4 / Voyage / Oasis](http://kcc.iosphe.re/Samples/Ubunchu!-KV.mobi)
* [Kindle Paperwhite 1 / 2](http://kcc.iosphe.re/Samples/Ubunchu!-KPW.mobi)
@@ -291,5 +403,5 @@ The app relies and includes the following scripts:
Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).
## COPYRIGHT
Copyright (c) 2012-2023 Ciro Mattia Gonano, Paweł Jastrzębski and Darodi.
Copyright (c) 2012-2025 Ciro Mattia Gonano, Paweł Jastrzębski, Darodi and Alex Xu.
**KCC** is released under ISC LICENSE; see [LICENSE.txt](./LICENSE.txt) for further details.

View File

@@ -1,14 +0,0 @@
environment:
PYTHON: "C:\\Python37-x64"
install:
- set PATH="%PYTHON%\\Scripts";%PATH%
- "%PYTHON%\\python.exe -m pip install --upgrade pip setuptools wheel"
- "%PYTHON%\\python.exe -m pip install -r requirements.txt"
- "%PYTHON%\\python.exe -m pip install certifi https://github.com/pyinstaller/pyinstaller/archive/develop.zip"
build_script:
- "%PYTHON%\\python.exe setup.py build_binary"
artifacts:
- path: dist\KCC*

View File

@@ -4,12 +4,12 @@ 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
- distro
- natsort[fast]>=8.4.0
- natsort>=8.4.0
- pip
- pip:
- mozjpeg-lossless-optimization>=1.1.2

View File

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

View File

@@ -6,6 +6,7 @@
<file>../icons/Kobo.png</file>
<file>../icons/Other.png</file>
<file>../icons/Kindle.png</file>
<file>../icons/Rmk.png</file>
</qresource>
<qresource prefix="Formats">
<file>../icons/CBZ.png</file>
@@ -26,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>

1060
gui/KCC.ui

File diff suppressed because it is too large Load Diff

BIN
header.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 KiB

BIN
icons/Rmk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
icons/kofi_symbol.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -20,6 +20,8 @@
import sys
from kcc import modify_path
if sys.version_info < (3, 8, 0):
print('ERROR: This is a Python 3.8+ script!')
sys.exit(1)
@@ -28,6 +30,7 @@ from multiprocessing import freeze_support, set_start_method
from kindlecomicconverter.startup import startC2E
if __name__ == "__main__":
modify_path()
set_start_method('spawn')
freeze_support()
startC2E()

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

@@ -20,6 +20,8 @@
import sys
from kcc import modify_path
if sys.version_info < (3, 8, 0):
print('ERROR: This is a Python 3.8+ script!')
sys.exit(1)
@@ -28,6 +30,7 @@ from multiprocessing import freeze_support, set_start_method
from kindlecomicconverter.startup import startC2P
if __name__ == "__main__":
modify_path()
set_start_method('spawn')
freeze_support()
startC2P()

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,

105
kcc.py
View File

@@ -18,63 +18,76 @@
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
import os
import sys
import platform
from pathlib import Path
if sys.version_info < (3, 8, 0):
print('ERROR: This is a Python 3.8+ script!')
sys.exit(1)
# OS specific workarounds
import os
if sys.platform.startswith('darwin'):
# prioritize KC2 since it optionally also installs KP3
mac_paths = [
'/Applications/Kindle Comic Creator/Kindle Comic Creator.app/Contents/MacOS',
'/Applications/Kindle Previewer 3.app/Contents/lib/fc/bin/',
]
if getattr(sys, 'frozen', False):
os.environ['PATH'] += os.pathsep + os.pathsep.join(mac_paths +
[
'/opt/homebrew/bin',
'/usr/local/bin',
'/usr/bin',
'/bin',
]
)
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
else:
os.environ['PATH'] += os.pathsep + os.pathsep.join(mac_paths)
os.chdir(os.path.dirname(os.path.abspath(__file__)))
elif sys.platform.startswith('win'):
# prioritize KC2 since it optionally also installs KP3
win_paths = [
os.path.expandvars('%LOCALAPPDATA%\\Amazon\\KC2'),
os.path.expandvars('%LOCALAPPDATA%\\Amazon\\Kindle Previewer 3\\lib\\fc\\bin\\'),
os.path.expandvars('%UserProfile%\\Kindle Previewer 3\\lib\\fc\\bin\\'),
'C:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin',
'D:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin',
'E:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin',
'C:\\Program Files\\7-Zip',
'D:\\Program Files\\7-Zip',
'E:\\Program Files\\7-Zip',
]
if getattr(sys, 'frozen', False):
os.environ['PATH'] += os.pathsep + os.pathsep.join(win_paths)
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
else:
os.environ['PATH'] += os.pathsep + os.pathsep.join(win_paths)
os.chdir(os.path.dirname(os.path.abspath(__file__)))
# Load additional Sentry configuration
# if getattr(sys, 'frozen', False):
# try:
# import kindlecomicconverter.sentry
# except ImportError:
# pass
def modify_path():
if platform.system() == 'Darwin':
mac_paths = [
'/Applications/Kindle Comic Creator/Kindle Comic Creator.app/Contents/MacOS',
'/Applications/Kindle Previewer 3.app/Contents/lib/fc/bin/',
]
if getattr(sys, 'frozen', False):
os.environ['PATH'] += os.pathsep + os.pathsep.join(mac_paths +
[
'/opt/homebrew/bin',
'/usr/local/bin',
'/usr/bin',
'/bin',
]
)
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
else:
os.environ['PATH'] += os.pathsep + os.pathsep.join(mac_paths)
os.chdir(os.path.dirname(os.path.abspath(__file__)))
elif platform.system() == 'Linux':
if getattr(sys, 'frozen', False):
os.environ['PATH'] += os.pathsep + os.pathsep.join(
[
str(Path.home() / ".local" / "bin"),
'/opt/homebrew/bin',
'/usr/local/bin',
'/usr/bin',
'/bin',
]
)
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
else:
os.chdir(os.path.dirname(os.path.abspath(__file__)))
elif platform.system() == 'Windows':
win_paths = [
os.path.expandvars('%LOCALAPPDATA%\\Amazon\\KC2'),
os.path.expandvars('%LOCALAPPDATA%\\Amazon\\Kindle Previewer 3\\lib\\fc\\bin\\'),
os.path.expandvars('%UserProfile%\\Kindle Previewer 3\\lib\\fc\\bin\\'),
'C:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin',
'D:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin',
'E:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin',
'C:\\Program Files\\7-Zip',
'D:\\Program Files\\7-Zip',
'E:\\Program Files\\7-Zip',
]
if getattr(sys, 'frozen', False):
os.environ['PATH'] += os.pathsep + os.pathsep.join(win_paths)
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
else:
os.environ['PATH'] += os.pathsep + os.pathsep.join(win_paths)
os.chdir(os.path.dirname(os.path.abspath(__file__)))
from multiprocessing import freeze_support, set_start_method
from kindlecomicconverter.startup import start
if __name__ == "__main__":
modify_path()
set_start_method('spawn')
freeze_support()
start()

39
kcc.spec Normal file
View File

@@ -0,0 +1,39 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(['kcc.py'],
pathex=['.'],
binaries=[],
datas=[],
hiddenimports=['_cffi_backend'],
hookspath=[],
runtime_hooks=[],
excludes=['pkg_resources'],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='kcc',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None , icon='icons\\comic2ebook.ico')

View File

@@ -16,18 +16,23 @@
# 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)
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
import os
import re
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
# noinspection PyUnresolvedReferences
from PySide6 import QtGui, QtCore, QtWidgets, QtNetwork
from PySide6.QtCore import Qt
from xml.sax.saxutils import escape
from psutil import Process
from copy import copy
@@ -36,6 +41,7 @@ from raven import Client
from tempfile import gettempdir
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
from .comicarchive import SEVENZIP, available_archive_tools
from . import __version__
from . import comic2ebook
from . import metadata
@@ -44,18 +50,18 @@ from . import KCC_ui
from . import KCC_ui_editor
class QApplicationMessaging(QtWidgets.QApplication):
messageFromOtherInstance = QtCore.Signal(bytes)
class QApplicationMessaging(QApplication):
messageFromOtherInstance = Signal(bytes)
def __init__(self, argv):
QtWidgets.QApplication.__init__(self, argv)
QApplication.__init__(self, argv)
self._key = 'KCC'
self._timeout = 1000
self._locked = False
socket = QtNetwork.QLocalSocket(self)
socket.connectToServer(self._key, QtCore.QIODeviceBase.OpenModeFlag.WriteOnly)
socket = QLocalSocket(self)
socket.connectToServer(self._key, QIODeviceBase.OpenModeFlag.WriteOnly)
if not socket.waitForConnected(self._timeout):
self._server = QtNetwork.QLocalServer(self)
self._server = QLocalServer(self)
self._server.newConnection.connect(self.handleMessage)
self._server.listen(self._key)
else:
@@ -67,11 +73,11 @@ class QApplicationMessaging(QtWidgets.QApplication):
self._server.close()
def event(self, e):
if e.type() == QtCore.QEvent.Type.FileOpen:
if e.type() == QEvent.Type.FileOpen:
self.messageFromOtherInstance.emit(bytes(e.file(), 'UTF-8'))
return True
else:
return QtWidgets.QApplication.event(self, e)
return QApplication.event(self, e)
def isRunning(self):
return self._locked
@@ -82,54 +88,58 @@ class QApplicationMessaging(QtWidgets.QApplication):
self.messageFromOtherInstance.emit(socket.readAll().data())
def sendMessage(self, message):
socket = QtNetwork.QLocalSocket(self)
socket.connectToServer(self._key, QtCore.QIODeviceBase.OpenModeFlag.WriteOnly)
socket = QLocalSocket(self)
socket.connectToServer(self._key, QIODeviceBase.OpenModeFlag.WriteOnly)
socket.waitForConnected(self._timeout)
socket.write(bytes(message, 'UTF-8'))
socket.waitForBytesWritten(self._timeout)
socket.disconnectFromServer()
class QMainWindowKCC(QtWidgets.QMainWindow):
progressBarTick = QtCore.Signal(str)
modeConvert = QtCore.Signal(int)
addMessage = QtCore.Signal(str, str, bool)
addTrayMessage = QtCore.Signal(str, str)
showDialog = QtCore.Signal(str, str)
hideProgressBar = QtCore.Signal()
forceShutdown = QtCore.Signal()
class QMainWindowKCC(QMainWindow):
progressBarTick = Signal(str)
modeConvert = Signal(int)
addMessage = Signal(str, str, bool)
addTrayMessage = Signal(str, str)
showDialog = Signal(str, str)
hideProgressBar = Signal()
forceShutdown = Signal()
class Icons:
def __init__(self):
self.deviceKindle = QtGui.QIcon()
self.deviceKindle.addPixmap(QtGui.QPixmap(":/Devices/icons/Kindle.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.deviceKobo = QtGui.QIcon()
self.deviceKobo.addPixmap(QtGui.QPixmap(":/Devices/icons/Kobo.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.deviceOther = QtGui.QIcon()
self.deviceOther.addPixmap(QtGui.QPixmap(":/Devices/icons/Other.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.deviceKindle = QIcon()
self.deviceKindle.addPixmap(QPixmap(":/Devices/icons/Kindle.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.deviceKobo = QIcon()
self.deviceKobo.addPixmap(QPixmap(":/Devices/icons/Kobo.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.deviceRmk = QIcon()
self.deviceRmk.addPixmap(QPixmap(":/Devices/icons/Rmk.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.deviceOther = QIcon()
self.deviceOther.addPixmap(QPixmap(":/Devices/icons/Other.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.MOBIFormat = QtGui.QIcon()
self.MOBIFormat.addPixmap(QtGui.QPixmap(":/Formats/icons/MOBI.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.CBZFormat = QtGui.QIcon()
self.CBZFormat.addPixmap(QtGui.QPixmap(":/Formats/icons/CBZ.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.EPUBFormat = QtGui.QIcon()
self.EPUBFormat.addPixmap(QtGui.QPixmap(":/Formats/icons/EPUB.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.MOBIFormat = QIcon()
self.MOBIFormat.addPixmap(QPixmap(":/Formats/icons/MOBI.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.CBZFormat = QIcon()
self.CBZFormat.addPixmap(QPixmap(":/Formats/icons/CBZ.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.EPUBFormat = QIcon()
self.EPUBFormat.addPixmap(QPixmap(":/Formats/icons/EPUB.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.KFXFormat = QIcon()
self.KFXFormat.addPixmap(QPixmap(":/Formats/icons/KFX.png"), QIcon.Normal, QIcon.Off)
self.info = QtGui.QIcon()
self.info.addPixmap(QtGui.QPixmap(":/Status/icons/info.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.warning = QtGui.QIcon()
self.warning.addPixmap(QtGui.QPixmap(":/Status/icons/warning.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.error = QtGui.QIcon()
self.error.addPixmap(QtGui.QPixmap(":/Status/icons/error.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.info = QIcon()
self.info.addPixmap(QPixmap(":/Status/icons/info.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.warning = QIcon()
self.warning.addPixmap(QPixmap(":/Status/icons/warning.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.error = QIcon()
self.error.addPixmap(QPixmap(":/Status/icons/error.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.programIcon = QtGui.QIcon()
self.programIcon.addPixmap(QtGui.QPixmap(":/Icon/icons/comic2ebook.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.programIcon = QIcon()
self.programIcon.addPixmap(QPixmap(":/Icon/icons/comic2ebook.png"), QIcon.Mode.Normal, QIcon.State.Off)
class VersionThread(QtCore.QThread):
class VersionThread(QThread):
def __init__(self):
QtCore.QThread.__init__(self)
QThread.__init__(self)
self.newVersion = ''
self.md5 = ''
self.barProgress = 0
@@ -158,9 +168,9 @@ class VersionThread(QtCore.QThread):
self.answer = dialoganswer
class ProgressThread(QtCore.QThread):
class ProgressThread(QThread):
def __init__(self):
QtCore.QThread.__init__(self)
QThread.__init__(self)
self.running = False
self.content = None
self.progress = 0
@@ -182,9 +192,9 @@ class ProgressThread(QtCore.QThread):
self.running = False
class WorkerThread(QtCore.QThread):
class WorkerThread(QThread):
def __init__(self):
QtCore.QThread.__init__(self)
QThread.__init__(self)
self.conversionAlive = False
self.errors = False
self.kindlegenErrorCode = [0]
@@ -240,6 +250,8 @@ class WorkerThread(QtCore.QThread):
options.cropping = GUI.croppingBox.checkState().value
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
options.croppingp = float(GUI.croppingPowerValue)
options.preservemargin = GUI.preserveMarginBox.value()
options.interpanelcrop = GUI.interPanelCropBox.checkState().value
if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked:
options.white_borders = True
elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
@@ -248,14 +260,26 @@ class WorkerThread(QtCore.QThread):
options.batchsplit = 2
if GUI.colorBox.isChecked():
options.forcecolor = True
if GUI.reduceRainbowBox.isChecked():
options.reducerainbow = True
if GUI.maximizeStrips.isChecked():
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.dedupeCoverBox.isChecked():
options.dedupecover = 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:
@@ -265,12 +289,29 @@ class WorkerThread(QtCore.QThread):
options.customheight = str(GUI.heightBox.value())
if GUI.targetDirectory != '':
options.output = GUI.targetDirectory
if GUI.authorEdit.text():
options.author = str(GUI.authorEdit.text())
if GUI.chunkSizeCheckBox.isChecked():
options.targetsize = int(GUI.chunkSizeBox.value())
for i in range(GUI.jobList.count()):
# Make sure that we don't consider any system message as job to do
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:
@@ -306,13 +347,8 @@ class WorkerThread(QtCore.QThread):
GUI.progress.content = ''
self.errors = True
_, _, traceback = sys.exc_info()
if len(err.args) == 1:
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
% (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error')
else:
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
% (jobargv[-1], str(err.args[0]), err.args[1]), 'error')
GUI.sentry.extra_context({'realTraceback': err.args[1]})
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
% (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error')
if ' is corrupted.' not in str(err):
GUI.sentry.captureException()
MW.addMessage.emit('Error during conversion! Please consult '
@@ -412,6 +448,8 @@ class WorkerThread(QtCore.QThread):
MW.addMessage.emit('Created EPUB file was too big.', 'error', False)
MW.addMessage.emit('EPUB file: ' + str(epubSize) + 'MB. Supported size: ~350MB.', 'error',
False)
if self.kindlegenErrorCode[0] == 3221226505:
MW.addMessage.emit('Unknown Windows error. Possibly filepath too long?', 'error', False)
else:
for item in outputPath:
if GUI.targetDirectory and GUI.targetDirectory != os.path.dirname(item):
@@ -419,6 +457,12 @@ class WorkerThread(QtCore.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()
@@ -429,7 +473,7 @@ class WorkerThread(QtCore.QThread):
MW.modeConvert.emit(1)
class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
class SystemTrayIcon(QSystemTrayIcon):
def __init__(self):
super().__init__()
if self.isSystemTrayAvailable():
@@ -442,33 +486,49 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
MW.activateWindow()
def addTrayMessage(self, message, icon):
icon = getattr(QtWidgets.QSystemTrayIcon.MessageIcon, icon)
icon = getattr(QSystemTrayIcon.MessageIcon, icon)
if self.supportsMessages() and not MW.isActiveWindow():
self.showMessage('Kindle Comic Converter', message, icon)
class KCCGUI(KCC_ui.Ui_mainWindow):
def selectDir(self):
if self.needClean:
self.needClean = False
GUI.jobList.clear()
dname = QtWidgets.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:
self.needClean = False
GUI.jobList.clear()
if self.tar or self.sevenzip:
fnames = QtWidgets.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.pdf);;All (*.*)')
else:
fnames = QtWidgets.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
'Comic (*.pdf);;All (*.*)')
for fname in fnames[0]:
if fname != '':
@@ -480,8 +540,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def selectFileMetaEditor(self):
sname = ''
if QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.ShiftModifier:
dname = QtWidgets.QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
if dname != '':
sname = os.path.join(dname, 'ComicInfo.xml')
if sys.platform.startswith('win'):
@@ -489,7 +549,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.lastPath = os.path.abspath(sname)
else:
if self.sevenzip:
fname = QtWidgets.QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath,
fname = QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath,
'Comic (*.cbz *.cbr *.cb7)')
else:
fname = ['']
@@ -518,7 +578,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def openWiki(self):
# noinspection PyCallByClass
QtGui.QDesktopServices.openUrl(QtCore.QUrl('https://github.com/ciromattia/kcc/wiki'))
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:
@@ -542,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)
@@ -553,16 +617,16 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if enable == 1:
self.conversionAlive = False
self.worker.sync()
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(":/Other/icons/convert.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon = QIcon()
icon.addPixmap(QPixmap(":/Other/icons/convert.png"), QIcon.Mode.Normal, QIcon.State.Off)
GUI.convertButton.setIcon(icon)
GUI.convertButton.setText('Convert')
GUI.centralWidget.setAcceptDrops(True)
elif enable == 0:
self.conversionAlive = True
self.worker.sync()
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(":/Other/icons/clear.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon = QIcon()
icon.addPixmap(QPixmap(":/Other/icons/clear.png"), QIcon.Mode.Normal, QIcon.State.Off)
GUI.convertButton.setIcon(icon)
GUI.convertButton.setText('Abort')
GUI.centralWidget.setAcceptDrops(False)
@@ -597,6 +661,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.rotateBox.setChecked(False)
GUI.upscaleBox.setEnabled(False)
GUI.upscaleBox.setChecked(True)
GUI.chunkSizeCheckBox.setEnabled(False)
GUI.chunkSizeCheckBox.setChecked(False)
else:
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
if profile['PVOptions']:
@@ -604,18 +670,23 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.mangaBox.setEnabled(True)
GUI.rotateBox.setEnabled(True)
GUI.upscaleBox.setEnabled(True)
GUI.chunkSizeCheckBox.setEnabled(True)
def togglequalityBox(self, value):
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
if value == 2:
if profile['Label'] in ['KV', 'KO']:
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:
GUI.upscaleBox.setEnabled(True)
GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
def togglechunkSizeCheckBox(self, value):
GUI.chunkSizeWidget.setVisible(value)
def changeGamma(self, value):
valueRaw = int(5 * round(float(value) / 5))
@@ -649,7 +720,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if not GUI.webtoonBox.isChecked():
GUI.qualityBox.setEnabled(profile['PVOptions'])
GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
GUI.mangaBox.setChecked(True)
if profile['Label'] == 'KS':
GUI.upscaleBox.setDisabled(True)
else:
GUI.upscaleBox.setEnabled(True)
if not profile['PVOptions']:
GUI.qualityBox.setChecked(False)
if str(GUI.deviceBox.currentText()) == 'Other':
@@ -669,6 +743,12 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
else:
GUI.outputSplit.setEnabled(False)
GUI.outputSplit.setChecked(False)
if (GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'EPUB-200MB' or
GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'MOBI+EPUB-200MB'):
GUI.chunkSizeCheckBox.setEnabled(False)
GUI.chunkSizeCheckBox.setChecked(False)
elif not GUI.webtoonBox.isChecked():
GUI.chunkSizeCheckBox.setEnabled(True)
def stripTags(self, html):
s = HTMLStripper()
@@ -678,16 +758,15 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def addMessage(self, message, icon, replace=False):
if icon != '':
icon = getattr(self.icons, icon)
item = QtWidgets.QListWidgetItem(icon, ' ' + self.stripTags(message))
item = QListWidgetItem(icon, ' ' + self.stripTags(message))
else:
item = QtWidgets.QListWidgetItem(' ' + self.stripTags(message))
item = QListWidgetItem(' ' + self.stripTags(message))
if replace:
GUI.jobList.takeItem(GUI.jobList.count() - 1)
# Due to lack of HTML support in QListWidgetItem we overlay text field with QLabel
# We still fill original text field with transparent content to trigger creation of horizontal scrollbar
item.setForeground(QtGui.QColor('transparent'))
label = QtWidgets.QLabel(message)
label.setStyleSheet('background-image:url('');background-color:rgba(0,0,0,0);color:rgb(0,0,0);')
item.setForeground(QColor('transparent'))
label = QLabel(message)
label.setOpenExternalLinks(True)
GUI.jobList.addItem(item)
GUI.jobList.setItemWidget(item, label)
@@ -695,11 +774,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def showDialog(self, message, kind):
if kind == 'error':
QtWidgets.QMessageBox.critical(MW, 'KCC - Error', message, QtWidgets.QMessageBox.StandardButton.Ok)
QMessageBox.critical(MW, 'KCC - Error', message, QMessageBox.StandardButton.Ok)
elif kind == 'question':
GUI.versionCheck.setAnswer(QtWidgets.QMessageBox.question(MW, 'KCC - Question', message,
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No))
GUI.versionCheck.setAnswer(QMessageBox.question(MW, 'KCC - Question', message,
QMessageBox.Yes,
QMessageBox.No))
def updateProgressbar(self, command):
if command == 'tick':
@@ -723,14 +802,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.conversionAlive = False
self.worker.sync()
else:
if QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.KeyboardModifier.ShiftModifier:
dname = QtWidgets.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 QApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier:
if not self.selectOutputFolder():
return
elif GUI.defaultOutputFolderBox.isChecked():
self.targetDirectory = self.defaultOutputFolder
else:
GUI.targetDirectory = ''
self.progress.start()
@@ -741,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')
@@ -772,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)
@@ -782,19 +865,29 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'gammaBox': GUI.gammaBox.checkState().value,
'croppingBox': GUI.croppingBox.checkState().value,
'croppingPowerSlider': float(self.croppingPowerValue) * 100,
'preserveMarginBox': self.preserveMarginBox.value(),
'interPanelCropBox': GUI.interPanelCropBox.checkState().value,
'upscaleBox': GUI.upscaleBox.checkState().value,
'borderBox': GUI.borderBox.checkState().value,
'webtoonBox': GUI.webtoonBox.checkState().value,
'outputSplit': GUI.outputSplit.checkState().value,
'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,
'dedupeCoverBox': GUI.dedupeCoverBox.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})
'gammaSlider': float(self.gammaValue) * 100,
'chunkSizeCheckBox': GUI.chunkSizeCheckBox.checkState().value,
'chunkSizeBox': GUI.chunkSizeBox.value()})
self.settings.sync()
self.tray.hide()
@@ -846,7 +939,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
except Exception:
pass
try:
versionCheck = subprocess_run(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT, encoding='UTF-8')
versionCheck = subprocess_run(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT, encoding='UTF-8', errors='ignore', check=True)
self.kindleGen = True
for line in versionCheck.stdout.splitlines():
if 'Amazon kindlegen' in line:
@@ -855,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()
@@ -868,9 +961,12 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.setupUi(MW)
self.editor = KCCGUI_MetaEditor()
self.icons = Icons()
self.settings = QtCore.QSettings('ciromattia', 'kcc')
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)
@@ -899,9 +995,9 @@ 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(QtCore.QSize(0, 0))
getattr(GUI, element).setMinimumSize(QSize(0, 0))
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1)
for element in ['gridLayout_2', 'gridLayout_3', 'gridLayout_4', 'horizontalLayout', 'horizontalLayout_2']:
getattr(GUI, element).setContentsMargins(-1, 0, -1, 0)
@@ -912,17 +1008,20 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"MOBI/AZW3": {'icon': 'MOBI', 'format': 'MOBI'},
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
"EPUB (Calibre KFX)": {'icon': 'EPUB', 'format': 'KFX'},
"KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'},
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'}
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
"MOBI + EPUB (200MB limit)": {'icon': 'MOBI', 'format': 'MOBI+EPUB-200MB'},
}
self.profiles = {
"Kindle Oasis 9/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO'},
"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': 'KV'},
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'},
"Kindle Voyage": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
"Kindle Scribe": {
@@ -931,21 +1030,21 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Kindle 11": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'K11',
},
"Kindle PW 11": {
"Kindle Paperwhite 11": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW5',
},
"Kindle PW 12": {
"Kindle Paperwhite 12": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO',
},
"Kindle CS 12": {
"Kindle Colorsoft": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': True, 'Label': 'KO',
},
"Kindle PW 7/10": {'PVOptions': True, '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,
@@ -990,14 +1089,20 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'Label': 'KoS'},
"Kobo Elipsa": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'KoE'},
"reMarkable 1": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'Rmk1'},
"reMarkable 2": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'Rmk2'},
"reMarkable Paper Pro": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': True,
'Label': 'RmkPP'},
"Other": {'PVOptions': False, 'ForceExpert': True, 'DefaultFormat': 1, 'DefaultUpscale': False, 'ForceColor': False,
'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",
@@ -1009,13 +1114,18 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Kobo Elipsa",
"Kobo Nia",
"Separator",
"reMarkable 1",
"reMarkable 2",
"reMarkable Paper Pro",
"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",
@@ -1034,41 +1144,44 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Kobo Mini/Touch",
]
statusBarLabel = QtWidgets.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>')
statusBarLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
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')
try:
subprocess_run(['tar'], stdout=PIPE, stderr=STDOUT)
self.tar = True
except FileNotFoundError:
self.tar = False
try:
subprocess_run(['7z'], stdout=PIPE, stderr=STDOUT)
self.sevenzip = True
except FileNotFoundError:
self.sevenzip = False
if not self.tar:
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
' to enable CBZ/CBR/ZIP/etc processing.', 'warning')
self.tar = 'tar' 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)
@@ -1076,6 +1189,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.croppingPowerSlider.valueChanged.connect(self.changeCroppingPower)
GUI.webtoonBox.stateChanged.connect(self.togglewebtoonBox)
GUI.qualityBox.stateChanged.connect(self.togglequalityBox)
GUI.chunkSizeCheckBox.stateChanged.connect(self.togglechunkSizeCheckBox)
GUI.deviceBox.activated.connect(self.changeDevice)
GUI.formatBox.activated.connect(self.changeFormat)
MW.progressBarTick.connect(self.updateProgressbar)
@@ -1097,6 +1211,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.deviceBox.addItem(self.icons.deviceOther, profile)
elif profile == "Separator":
GUI.deviceBox.insertSeparator(GUI.deviceBox.count() + 1)
elif 'reM' in profile:
GUI.deviceBox.addItem(self.icons.deviceRmk, profile)
elif 'Ko' in profile:
GUI.deviceBox.addItem(self.icons.deviceKobo, profile)
else:
@@ -1126,6 +1242,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if GUI.croppingPowerSlider.isEnabled():
GUI.croppingPowerSlider.setValue(int(self.options[option]))
self.changeCroppingPower(int(self.options[option]))
GUI.preserveMarginBox.setValue(self.options.get('preserveMarginBox', 0))
elif str(option) == "chunkSizeBox":
GUI.chunkSizeBox.setValue(int(self.options[option]))
else:
try:
if getattr(GUI, option).isEnabled():
@@ -1200,15 +1319,15 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
return escape(s.strip())
def __init__(self):
self.ui = QtWidgets.QDialog()
self.ui = QDialog()
self.parser = None
self.setupUi(self.ui)
self.ui.setWindowFlags(self.ui.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint)
self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
self.okButton.clicked.connect(self.saveData)
self.cancelButton.clicked.connect(self.ui.close)
if sys.platform.startswith('linux'):
self.ui.resize(450, 260)
self.ui.setMinimumSize(QtCore.QSize(450, 260))
self.ui.setMinimumSize(QSize(450, 260))
elif sys.platform.startswith('darwin'):
self.ui.resize(450, 310)
self.ui.setMinimumSize(QtCore.QSize(450, 310))
self.ui.setMinimumSize(QSize(450, 310))

View File

@@ -1,6 +1,6 @@
# Resource object code (Python 3)
# Created by: object code
# Created by: The Resource Compiler for Qt version 6.6.3
# 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
@@ -4908,6 +4908,138 @@ D-\xbea6\x9bu\xd3\xe9\xf4+@\x03\xb0\xa2V\
$\x12\x89D\x22\x91H$\x12\x89D\x15\xd1\xff\x00V\
\x1c\x01\xcd\xc9\x01\xf3\xd5\x00\x00\x00\x00IEND\xae\
B`\x82\
\x00\x00\x08\x12\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x80\x00\x00\x00\x80\x08\x03\x00\x00\x00\xf4\xe0\x91\xf9\
\x00\x00\x00 cHRM\x00\x00z&\x00\x00\x80\x84\
\x00\x00\xfa\x00\x00\x00\x80\xe8\x00\x00u0\x00\x00\xea`\
\x00\x00:\x98\x00\x00\x17p\x9c\xbaQ<\x00\x00\x027\
PLTE\x00\x00\x00333333333\
3333333333333333\
3333333333333333\
3333333333333333\
3333333333333333\
3333333333333333\
3333333333333333\
3333333333333333\
3333333333333333\
3333333333333333\
3333333333333333\
33333333333333<<\
<[[[\x5c\x5c\x5c^^^___]]]\
RRRaaa\xf9\xf9\xf9\xca\xca\xca\xfa\xfa\xfa\xfb\
\xfb\xfb\xfc\xfc\xfc\xfd\xfd\xfd\xfe\xfe\xfe\xff\xff\xff\xfe\xfc\
\xfbbbbdddeeeggg444\
iiiccc\xff\xfe\xfe\xf6\xf6\xf6\x00\x00\x00\xaa\
\xaa\xaa222\xbd\xbd\xbdKKK\xd2\xd2\xd2\xf3\xf3\
\xf3\xb6\xb6\xb6\xd9\xd9\xd9\x95\x95\x95qqq\xe9\xe9\xe9\
\xf5\xf5\xf5\xf8\xf8\xf8&&&\x9d\x9d\x9d***\xbe\
\xbe\xbe\x96\x96\x96555\xcf\xcf\xcf\x8f\x8f\x8f\xc8\xc8\
\xc8\xe5\xe5\xe5\xb2\xb2\xb2fff@@@sss\
\xc1\xc1\xc1\xc0\xc0\xc0hhh\xd0\xd0\xd0\x91\x91\x91\x94\
\x94\x94\x9e\x9e\x9eppp\x80\x80\x80\xc2\xc2\xc2\xbb\xbb\
\xbb\x8e\x8e\x8e\x22\x22\x22\x0c\x0c\x0c\x0d\x0d\x0d\xaf\xaf\xaf\
\x90\x90\x90\xee\xee\xee\x1c\x1c\x1c\xf4\xf4\xf4\xcc\xcc\xcc}\
}}\x98\x98\x98\xa7\xa7\xa7\x7f\x7f\x7f\xc5\xc5\xc5\x8c\x8c\
\x8c\x9b\x9b\x9b\xc7\xc7\xc7\xb4\xb4\xb4\xf0\xf0\xf0\xd7\xd7\xd7\
\xda\xda\xda\xad\xad\xad\xcd\xcd\xcd\xc9\xc9\xc9\x87\x87\x87\xa3\
\xa3\xa3\xd4\xd4\xd4\x16\x16\x16\xdb\xdb\xdb\xd8\xd8\xd8\xc6\xc6\
\xc6MMM\x92\x92\x92\xa4\xa4\xa4\x82\x82\x82\xde\xde\xde\
EEE\xe0\xe0\xe0XXX\x93\x93\x93vvvu\
uu\xeb\xeb\xeb\xba\xba\xba\xdf\xdf\xdf\xb9\xb9\xb9SS\
S\xdc\xdc\xdc\xf2\xf2\xf2\xe8\xe8\xe8\x97\x97\x97\xbc\xbc\xbc\
\xd3\xd3\xd3\x8a\x8a\x8ammm\xfe\xfe\xfd\xfe\xfd\xfb\xfe\
\xfd\xfc\xfe\xfc\xfaVVV\xa5\xa5\xa5\xecOs\x00\x00\
\x00\x00=tRNS\x00\x1ba\x80\x8f\x85i)\xb1\
\xfd\xc9?S\xf4xA\xfak\x09\xe3\xf8\x22n\x9b\xc2\
\xec\x03\xf3\x0b9\x10>\x0f=\x02\xfb-\xdd\xfc\x94+\
V\x90\xbd\x01\x05\xb7\xd6\x14\x04\x8c\xfe\xaa%\x87\xcd\xed\
\xf2\xd5\x968\x82\x10\xbfn\x00\x00\x00\x01bKGD\
M\x80h e\x00\x00\x00\x09pHYs\x00\x00\x0b\
\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tI\
ME\x07\xe8\x0c\x0a\x0c\x07#x\xb2 \xfb\x00\x00\x04\
\xecIDATx\xda\xed\x9b\x87w\x14E\x1c\xc7\xcf\
\x8a\xb1w\xb1\x12\x1b\xf6\xde\xcbI\x88zn\x99!\xa0\
\x06F\x10PS,(\xc1\x18\x90\xa8\x04\x05BDP\
\xac\xa0\xa0X\xc0\x02\xb6(\x88\xf5\x8fs\xca\xce\x96\xbb\
\xcb\xee\xec\xdcws\x8f\xf7\xf6\x9b\x97w7sy\xf7\
\xfd\xbc\xdf\xcc\xfcfvfR\xa9\x94*U\xaaT\xa9\
,\x1du\xf41\xc7\xda\xeb\xb8\xe3g\xb4f?\xe3\x84\
\x8ej\x8b:\xf1\xa4\x16\xfcO>\xa5U{\xae\x8eS\
\xad\xfdO;\x1d\xe0\xcfu\x86\xa5\xff\x99ga\xfc\xab\
g\x9fc\x07p.\xc8\xbfZ=\xcf\x0e`&\x0c\xe0\
\xfc\x0b\xac\x00.\x84\x01T\xad\xda\xe0\x22\x9c\x7f\xf5b\
\x1b\x80K\x80\x00\xb3\x8eh\x80\x07\xe6\x84\xea\xea\x9a\xdb\
=\xf7\xc1\xae9\xa6z\x08\x02\xf0p\xcdZ\x8f\xc0\x01\
\x1c\xa9\xb6\x018\x8e\xeb\xba\x1e\xff5E\x00\x03p{\
\xcf\x17\xf2L\x11\xb0\x00\x8e\xb0'B\x02\xc1\x88\x00\x0c\
\xe0){\x85`D\x00\x05p\x5c\xe9O\xb9$\x82g\
@\x80\x04\xe0\x0d\xa0\xfd5Av\x10\x80\x00\xa2\x03\x84\
\xfe\x0a\xc1\x80\x00\x07\x10\xf8G\x00\x94\xcc\x93\x04\xce\xf4\
\x004\xfaG\xcd\xe0\x14\x0e\xd0\x13v\xc0F\x82\x18B\
\xb3$\x89\x01\x98\xdf4\x00!\x81/S\xa3\x1b(\xc9\
\x80\x01X\x10\x04\x806H\xe7\x04\xdf\x93\x92/\x89$\
\x89\x01x4HA\xb4\x99\xc2\xcc\xa4a\xbc\xf8\xd8\x80\
\x00<\xf6xM\xb6\x00\x9dB\xa4N\xf1\xd1\x09\x02\xa8\
\xcf\x01\x19\x081\x02L\x13\x04\x00\xda\x8d\xa6\xa9\x8e\x00\
\xd5\x07\xe2\x11 \xe9\x04*=`\x01\x16$:a\x16\
@\x22Ic\x00z\x13\xc3\x90\x90\x85&\x04H\x80\x9e\
8\x00\xc9\x8c@\x10\x02\xec\x5c\x10u\x82l\xffx\x08\
p\x93Q\x94\x8a\xcd\x00\x88\x0a\x01r6\xcc\x01\x10\x85\
\x00\xb7\x1e\x88\xb5\x81\x01\x00\xd5!@.H|\x83\xee\
\x97\x08\x81\x18\x89\xf0\x15Q\xdcd\x11\xd3z\x22\xacs\
\x16\xeb\xba%\xb2\x0d\x90k\xc2\x14\x80'=]\xb74\
\xac[B\xd0\x00\x8dk\x92E\xcb\x96\x07nO\xe9\xaa\
\xa7U\xf9\x99\xbe~\x07\x0d\xd0tM0\xc0\xd8\xa00\
|V\x97\x9fS\xe5\xe7i0\x0c\xf0\xcf\x05u\x9d\x8d\
\xb1\x17\x84\xe1\x8a\x9a*\xbe\xc8\x06_\x12\xe5\x95E\x00\
\x0c\xe9\x0eW[\xf5\xf2\xe0\xf0+#D\x01\xac^#\
j_U\x00k\xd9\xe8k\x12\x00?\x0aB\x80\xd7\xbd\
7\xd4\x9bu\x01\xc0\x98(\xacW\x9d\xe2M\xb6\xb48\
\x80\x81\xbe\x91\xb7\xf8wo\xd8\x18t\xbcM\x01\xc0\xb8\
(l\x9e\x10\xa5\xb7\xd9\x0a'\x04\x00'\x22\xd9\x0b\xb6\
H\xe7w\xfa\xc7\xbb\xf9\xcb\xd6\x00\x80n\x13\x95\xef\x8a\
\xd2{l;\x95\x00\xef\xe3S\xb1\xd4Z\xf1\xe5\xc3\xbc\
\xc3}\xc0X\xf7\x87\x1a`\xa5\xa8\xdd\xc2\x0b\xfd\x9b\xd9\
GTG\xa08\x80\xed\xdc\xea\xe3Ov\xb8T\x03\xf0\
\x96\xe7\x1a\xa7t'\x1bv\xa3\x08\xb8E\x01\xacK\x0e\
\xc3\xd5\x94\x8e\x8a\xea1J?e\x9f\xd1i\x00\xd8\xd5\
\x00\xb0KT\xaf\xa1\xcbv\xb3\xcf\x8b\x05p$\xc0\x17\
\x0d\x00\xee\xb0\xa8\x1f\xda\xc3\xbe\xf4\x0b\x06\xf8\xaa9\x00\
\xfdZ\xd4o\xfcF\xce\x8am\x01\xd8+\xea\xf7m`\
\xdf\x16\x0b\xe0\xb8S\x01\x90.\x99 \xbe#\xc5\x02\xb8\
\xde\xf7S\x00\x88\xbc \x1a\x81\x16\x0a\xc0\xa7\xe3\xa9\x22\
@\xfb$\xc0P\xa1\x00bQ\xf8\x83\xf8\xf2\x1d1\xff\
\x1ac\xab\xe4\x9b\xfd\xfc\x83^\xf9nT-\x87\x88\x07\
\x06p\xdc\xda\x81\x1f\xd5t\xf8\xd3DM\xaf\x88~f\
\xec\x97_'y\xd3\x8f\xf0\x0f\x96\xf3\xaa\x89\xbd\xfb\xe4\
\xdf\xfc61\x80\x06\xf0\xa2\xe5\x1e\xfb=\xb9&\x9c\xa4\
tr7c}r\x85\xa4u\x10\xfcd\xe4z\x87\xd2\
\x00\xe8\x1f\xec0M\x00\x8c\xa1\x01\xf2<\x16\x10\xdd\x05\
\xd0\xcf\x86\xe6\x00\xd8\xc7\xf3\xc4\xd3q;\x00z\x9a\xaf\
\x88\xd3Z\x00\x1d\x81\x5c\x00\xf1m\xaa\xb6E@oS\
\x01#\x90\xa3\x0f\x90h\x8f\xa6m\x9d\x10\x9e\x8ac\xe7\
E\xc6!\x00\x03\xe4\x0d\x81_\xc8\x0eI\x9e\x10x\x85\
\xec\x90\xe4 @o\xd1\xc4N-\xb3vJc\x03\x11\
\x1f\x01\x09`DP(\x80\x10\x99\xbe\x08\xf4\xc4\xfd\xc5\
\xf7g\x87\x00\x0b\xd0\x9b\xd8\xaa5\x1d\x05@\x80\xf9y\
\xd3\x00\xa5\x7f\x06\xd3\x11\xee\xd8\xceh2\x0a[f\xe1\
_\xd0Dd\x0c\x10\xb6\xc0\xdf\xff@\x9f\x0b\xf26\x01\
\xf1kzA0m\x9d\x90D?\xc1Y*vA\x12\
\x1f\x86D\xf9\xa5\xca\xc7\xae\x88\xea\x13Q\xa6\xa2\xe3[\
\xf0f\xb5o\xc2 o\xf98\xd0sC\xbdW\xecz\
\xea\x16Q\xaa\xd4\xf99\xf6\xe8VoP8\xe1%\x81\
T\xc5o\x10\xe0\xef\x92\x05\xf7$\xd2\x84\xbfA\xd1\xf6\
\xdbt\xff\xee\xb4\xd6\x7f\x10\x00\x84J\x80\x12\xa0\x048\
2\x01:\x81\x00\x97\xda\x00\x5cv9\x0e\xe0\x0a\x1b\x80\
\xca\x950\xff\xd9\x9dV\x00W\xc1\x00fZ\xf9W\xae\
\x9e\x8d\x02\xb8\xc6\x0e\xa0r-\xc8\xff\xba\xeb-\x01n\
\xb8\x11\xe2\x7f\xd3\xcd\x96\xfe\x95\xca-\xb7\xb6\xfe\x9fF\
\xb7\xdd\xdei\xed\xcfu\xc7\x9dw\xdd\xdda\xaf{\xee\
\xbd\xef\xfeV\xecK\x95*U\xaax\xfd\x0f\xf4\x94\xdc\
\x07\xeb\xfb\xc9m\x00\x00\x00\x00IEND\xaeB`\
\x82\
\x00\x00\x05\xe0\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -5930,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\
@@ -11397,6 +12111,10 @@ qt_resource_name = b"\
\x05\x92]\x07\
\x00K\
\x00o\x00b\x00o\x00.\x00p\x00n\x00g\
\x00\x07\
\x09>W\xe7\
\x00R\
\x00m\x00k\x00.\x00p\x00n\x00g\
\x00\x09\
\x0e\xc5\xfa\x07\
\x00O\
@@ -11413,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\
@@ -11463,11 +12185,11 @@ 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\x1b\
\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\x13\
\x00\x00\x00&\x00\x02\x00\x00\x00\x01\x00\x00\x00\x14\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x0f\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x10\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x006\x00\x02\x00\x00\x00\x01\x00\x00\x00\x0b\
\x00\x00\x00\x00\x00\x00\x00\x00\
@@ -11475,50 +12197,54 @@ 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\xac\x00\x00\x00\x00\x00\x01\x00\x02&\xd7\
\x00\x00\x01\x90(\xef\xc4\x03\
\x00\x00\x01\xea\x00\x00\x00\x00\x00\x01\x00\x02{q\
\x00\x00\x01\x90(\xef\xc4\x00\
\x00\x00\x01\xd6\x00\x00\x00\x00\x00\x01\x00\x02Qv\
\x00\x00\x01\x90(\xef\xc3\xff\
\x00\x00\x01\xc2\x00\x00\x00\x00\x00\x01\x00\x02F\x13\
\x00\x00\x01\x90(\xef\xc4\x01\
\x00\x00\x00X\x00\x02\x00\x00\x00\x03\x00\x00\x00\x0c\
\x00\x00\x01\xe4\x00\x00\x00\x00\x00\x01\x00\x02S.\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x02\x22\x00\x00\x00\x00\x00\x01\x00\x02\xa7\xc8\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x02\x0e\x00\x00\x00\x00\x00\x01\x00\x02}\xcd\
\x00\x00\x01\x88;p\xbcB\
\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\
\x00\x00\x00\xa6\x00\x00\x00\x00\x00\x01\x00\x01(\x97\
\x00\x00\x01\x90(\xef\xc4\x03\
\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x01\x00\x01\x1d\x90\
\x00\x00\x01\x90(\xef\xc4\x02\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x00\xbc\x00\x00\x00\x00\x00\x01\x00\x011\xef\
\x00\x00\x01\x90(\xef\xc4\x04\
\x00\x00\x00X\x00\x02\x00\x00\x00\x03\x00\x00\x00\x10\
\x00\x00\x01\x96\x16b\x1f\x99\
\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x01\x00\x01\x1d\x90\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x01:\x05\
\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\x02.\x00\x00\x00\x00\x00\x01\x00\x02\xad\xbd\
\x00\x00\x01\x90(\xef\xc4!\
\x00\x00\x02\x00\x00\x00\x00\x00\x00\x01\x00\x02\x97\xc0\
\x00\x00\x01\x90(\xef\xc4\x1d\
\x00\x00\x02\x16\x00\x00\x00\x00\x00\x01\x00\x02\xa1\x1d\
\x00\x00\x01\x90(\xef\xc4\x19\
\x00\x00\x00X\x00\x02\x00\x00\x00\x07\x00\x00\x00\x14\
\x00\x00\x02f\x00\x00\x00\x00\x00\x01\x00\x02\xda\x14\
\x00\x00\x01\x88;p\xbcJ\
\x00\x00\x028\x00\x00\x00\x00\x00\x01\x00\x02\xc4\x17\
\x00\x00\x01\x88;p\xbcI\
\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\x08\x00\x00\x00\x15\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\x08\x00\x00\x00\x00\x00\x01\x00\x01H\x9b\
\x00\x00\x01\x90(\xef\xc4\x22\
\x00\x00\x01\x1e\x00\x00\x00\x00\x00\x01\x00\x01qC\
\x00\x00\x01\x90(\xef\xc4\x1c\
\x00\x00\x01\x80\x00\x00\x00\x00\x00\x01\x00\x01\xca\x17\
\x00\x00\x01\x90(\xef\xc4\x1e\
\x00\x00\x01f\x00\x00\x00\x00\x00\x01\x00\x01\x84\xd0\
\x00\x00\x01\x90(\xef\xc4\x18\
\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x01\x00\x01D<\
\x00\x00\x01\x90(\xef\xc4\x0e\
\x00\x00\x00\xd4\x00\x00\x00\x00\x00\x01\x00\x017\xd3\
\x00\x00\x01\x90(\xef\xc4\x17\
\x00\x00\x01@\x00\x00\x00\x00\x00\x01\x00\x01z\x9a\
\x00\x00\x01\x90(\xef\xc4\x18\
\x00\x00\x00X\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1c\
\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\xb8\x00\x00\x00\x00\x00\x01\x00\x01\xf6n\
\x00\x00\x01\x94\xb4\xd4\xf0a\
\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\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\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\x90(\xef\xc4\x16\
\x00\x00\x01\x88;p\xbcH\
"
def qInitResources():

View File

@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'KCC.ui'
##
## Created by: Qt User Interface Compiler version 6.6.3
## Created by: Qt User Interface Compiler version 6.9.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -16,110 +16,369 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractItemView, QApplication, QCheckBox, QComboBox,
QGridLayout, QHBoxLayout, QLabel, QListWidget,
QListWidgetItem, QMainWindow, QProgressBar, QPushButton,
QSizePolicy, QSlider, QSpinBox, QStatusBar,
QWidget)
QGridLayout, QHBoxLayout, QLabel, QLineEdit,
QListWidget, QListWidgetItem, QMainWindow, QProgressBar,
QPushButton, QSizePolicy, QSlider, QSpinBox,
QStatusBar, QWidget)
from . import KCC_rc
class Ui_mainWindow(object):
def setupUi(self, mainWindow):
if not mainWindow.objectName():
mainWindow.setObjectName(u"mainWindow")
mainWindow.resize(481, 400)
mainWindow.resize(566, 573)
icon = QIcon()
icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Normal, QIcon.Off)
icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
mainWindow.setWindowIcon(icon)
self.centralWidget = QWidget(mainWindow)
self.centralWidget.setObjectName(u"centralWidget")
self.gridLayout = QGridLayout(self.centralWidget)
self.gridLayout.setObjectName(u"gridLayout")
self.gridLayout.setContentsMargins(-1, -1, -1, 5)
self.jobList = QListWidget(self.centralWidget)
self.jobList.setObjectName(u"jobList")
self.jobList.setStyleSheet(u"")
self.jobList.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.jobList.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.jobList.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.gridLayout.addWidget(self.jobList, 2, 0, 1, 2)
self.toolWidget = QWidget(self.centralWidget)
self.toolWidget.setObjectName(u"toolWidget")
self.horizontalLayout = QHBoxLayout(self.toolWidget)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.editorButton = QPushButton(self.toolWidget)
self.editorButton.setObjectName(u"editorButton")
self.editorButton.setMinimumSize(QSize(0, 30))
icon1 = QIcon()
icon1.addFile(u":/Other/icons/editor.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.editorButton.setIcon(icon1)
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))
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)
self.gridLayout.addWidget(self.toolWidget, 0, 0, 1, 2)
self.buttonWidget = QWidget(self.centralWidget)
self.buttonWidget.setObjectName(u"buttonWidget")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.buttonWidget.sizePolicy().hasHeightForWidth())
self.buttonWidget.setSizePolicy(sizePolicy)
self.gridLayout_4 = QGridLayout(self.buttonWidget)
self.gridLayout_4.setObjectName(u"gridLayout_4")
self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
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)
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, 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/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.fileButton.setIcon(icon6)
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.clearButton.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)
self.progressBar = QProgressBar(self.centralWidget)
self.progressBar.setObjectName(u"progressBar")
self.progressBar.setMinimumSize(QSize(0, 30))
self.progressBar.setFont(font)
self.progressBar.setVisible(False)
self.progressBar.setAlignment(Qt.AlignmentFlag.AlignJustify|Qt.AlignmentFlag.AlignVCenter)
self.gridLayout.addWidget(self.progressBar, 1, 0, 1, 2)
self.customWidget = QWidget(self.centralWidget)
self.customWidget.setObjectName(u"customWidget")
self.customWidget.setVisible(False)
self.gridLayout_3 = QGridLayout(self.customWidget)
self.gridLayout_3.setObjectName(u"gridLayout_3")
self.gridLayout_3.setContentsMargins(0, 0, 0, 0)
self.hLabel = QLabel(self.customWidget)
self.hLabel.setObjectName(u"hLabel")
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(2400)
self.gridLayout_3.addWidget(self.widthBox, 0, 1, 1, 1)
self.wLabel = QLabel(self.customWidget)
self.wLabel.setObjectName(u"wLabel")
sizePolicy2.setHeightForWidth(self.wLabel.sizePolicy().hasHeightForWidth())
self.wLabel.setSizePolicy(sizePolicy2)
self.gridLayout_3.addWidget(self.wLabel, 0, 0, 1, 1)
self.heightBox = QSpinBox(self.customWidget)
self.heightBox.setObjectName(u"heightBox")
self.heightBox.setMaximum(3840)
self.gridLayout_3.addWidget(self.heightBox, 0, 3, 1, 1)
self.gridLayout.addWidget(self.customWidget, 8, 0, 1, 2)
self.croppingWidget = QWidget(self.centralWidget)
self.croppingWidget.setObjectName(u"croppingWidget")
self.croppingWidget.setVisible(False)
self.gridLayout_5 = QGridLayout(self.croppingWidget)
self.gridLayout_5.setObjectName(u"gridLayout_5")
self.gridLayout_5.setContentsMargins(0, 0, 0, 0)
self.preserveMarginLabel = QLabel(self.croppingWidget)
self.preserveMarginLabel.setObjectName(u"preserveMarginLabel")
self.gridLayout_5.addWidget(self.preserveMarginLabel, 1, 0, 1, 1)
self.croppingPowerLabel = QLabel(self.croppingWidget)
self.croppingPowerLabel.setObjectName(u"croppingPowerLabel")
self.gridLayout_5.addWidget(self.croppingPowerLabel, 0, 0, 1, 1)
self.croppingPowerSlider = QSlider(self.croppingWidget)
self.croppingPowerSlider.setObjectName(u"croppingPowerSlider")
self.croppingPowerSlider.setMaximum(300)
self.croppingPowerSlider.setSingleStep(1)
self.croppingPowerSlider.setOrientation(Qt.Orientation.Horizontal)
self.gridLayout_5.addWidget(self.croppingPowerSlider, 0, 1, 1, 1)
self.preserveMarginBox = QSpinBox(self.croppingWidget)
self.preserveMarginBox.setObjectName(u"preserveMarginBox")
sizePolicy1.setHeightForWidth(self.preserveMarginBox.sizePolicy().hasHeightForWidth())
self.preserveMarginBox.setSizePolicy(sizePolicy1)
self.preserveMarginBox.setMaximum(99)
self.preserveMarginBox.setSingleStep(5)
self.preserveMarginBox.setValue(0)
self.gridLayout_5.addWidget(self.preserveMarginBox, 1, 1, 1, 1)
self.gridLayout.addWidget(self.croppingWidget, 9, 0, 1, 2)
self.optionWidget = QWidget(self.centralWidget)
self.optionWidget.setObjectName(u"optionWidget")
self.gridLayout_2 = QGridLayout(self.optionWidget)
self.gridLayout_2.setObjectName(u"gridLayout_2")
self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
self.qualityBox = QCheckBox(self.optionWidget)
self.qualityBox.setObjectName(u"qualityBox")
self.qualityBox.setTristate(True)
self.interPanelCropBox = QCheckBox(self.optionWidget)
self.interPanelCropBox.setObjectName(u"interPanelCropBox")
self.interPanelCropBox.setTristate(True)
self.gridLayout_2.addWidget(self.qualityBox, 0, 2, 1, 1)
self.deleteBox = QCheckBox(self.optionWidget)
self.deleteBox.setObjectName(u"deleteBox")
self.gridLayout_2.addWidget(self.deleteBox, 4, 1, 1, 1)
self.maximizeStrips = QCheckBox(self.optionWidget)
self.maximizeStrips.setObjectName(u"maximizeStrips")
self.gridLayout_2.addWidget(self.maximizeStrips, 3, 1, 1, 1)
self.gammaBox = QCheckBox(self.optionWidget)
self.gammaBox.setObjectName(u"gammaBox")
self.gridLayout_2.addWidget(self.gammaBox, 1, 2, 1, 1)
self.borderBox = QCheckBox(self.optionWidget)
self.borderBox.setObjectName(u"borderBox")
self.borderBox.setTristate(True)
self.gridLayout_2.addWidget(self.borderBox, 2, 0, 1, 1)
self.webtoonBox = QCheckBox(self.optionWidget)
self.webtoonBox.setObjectName(u"webtoonBox")
self.gridLayout_2.addWidget(self.webtoonBox, 1, 0, 1, 1)
self.upscaleBox = QCheckBox(self.optionWidget)
self.upscaleBox.setObjectName(u"upscaleBox")
self.upscaleBox.setTristate(True)
self.gridLayout_2.addWidget(self.upscaleBox, 1, 1, 1, 1)
self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1)
self.mangaBox = QCheckBox(self.optionWidget)
self.mangaBox.setObjectName(u"mangaBox")
self.gridLayout_2.addWidget(self.mangaBox, 0, 0, 1, 1)
self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
self.rotateBox = QCheckBox(self.optionWidget)
self.rotateBox.setObjectName(u"rotateBox")
self.rotateBox.setTristate(True)
self.authorEdit = QLineEdit(self.optionWidget)
self.authorEdit.setObjectName(u"authorEdit")
sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
sizePolicy3.setHorizontalStretch(0)
sizePolicy3.setVerticalStretch(0)
sizePolicy3.setHeightForWidth(self.authorEdit.sizePolicy().hasHeightForWidth())
self.authorEdit.setSizePolicy(sizePolicy3)
self.authorEdit.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
self.authorEdit.setClearButtonEnabled(False)
self.gridLayout_2.addWidget(self.rotateBox, 0, 1, 1, 1)
self.gridLayout_2.addWidget(self.authorEdit, 0, 0, 1, 1)
self.croppingBox = QCheckBox(self.optionWidget)
self.croppingBox.setObjectName(u"croppingBox")
self.croppingBox.setTristate(True)
self.gridLayout_2.addWidget(self.croppingBox, 3, 2, 1, 1)
self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
self.outputSplit = QCheckBox(self.optionWidget)
self.outputSplit.setObjectName(u"outputSplit")
self.webtoonBox = QCheckBox(self.optionWidget)
self.webtoonBox.setObjectName(u"webtoonBox")
self.gridLayout_2.addWidget(self.outputSplit, 2, 1, 1, 1)
self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
self.colorBox = QCheckBox(self.optionWidget)
self.colorBox.setObjectName(u"colorBox")
self.gridLayout_2.addWidget(self.colorBox, 3, 2, 1, 1)
self.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, 3, 0, 1, 1)
self.colorBox = QCheckBox(self.optionWidget)
self.colorBox.setObjectName(u"colorBox")
self.gridLayout_2.addWidget(self.colorBox, 2, 2, 1, 1)
self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1)
self.disableProcessingBox = QCheckBox(self.optionWidget)
self.disableProcessingBox.setObjectName(u"disableProcessingBox")
self.gridLayout_2.addWidget(self.disableProcessingBox, 4, 2, 1, 1)
self.gridLayout_2.addWidget(self.disableProcessingBox, 5, 2, 1, 1)
self.dedupeCoverBox = QCheckBox(self.optionWidget)
self.dedupeCoverBox.setObjectName(u"dedupeCoverBox")
self.chunkSizeCheckBox = QCheckBox(self.optionWidget)
self.chunkSizeCheckBox.setObjectName(u"chunkSizeCheckBox")
self.gridLayout_2.addWidget(self.dedupeCoverBox, 4, 0, 1, 1)
self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 1, 1)
self.spreadShiftBox = QCheckBox(self.optionWidget)
self.spreadShiftBox.setObjectName(u"spreadShiftBox")
self.gridLayout_2.addWidget(self.spreadShiftBox, 5, 0, 1, 1)
self.upscaleBox = QCheckBox(self.optionWidget)
self.upscaleBox.setObjectName(u"upscaleBox")
self.upscaleBox.setTristate(True)
self.gridLayout_2.addWidget(self.upscaleBox, 2, 1, 1, 1)
self.outputSplit = QCheckBox(self.optionWidget)
self.outputSplit.setObjectName(u"outputSplit")
self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
self.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.maximizeStrips = QCheckBox(self.optionWidget)
self.maximizeStrips.setObjectName(u"maximizeStrips")
self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
self.rotateFirstBox = QCheckBox(self.optionWidget)
self.rotateFirstBox.setObjectName(u"rotateFirstBox")
self.gridLayout_2.addWidget(self.rotateFirstBox, 8, 1, 1, 1)
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
@@ -144,181 +403,44 @@ class Ui_mainWindow(object):
self.horizontalLayout_2.addWidget(self.gammaSlider)
self.gridLayout.addWidget(self.gammaWidget, 6, 0, 1, 2)
self.gridLayout.addWidget(self.gammaWidget, 7, 0, 1, 2)
self.croppingWidget = QWidget(self.centralWidget)
self.croppingWidget.setObjectName(u"croppingWidget")
self.croppingWidget.setVisible(False)
self.horizontalLayout_3 = QHBoxLayout(self.croppingWidget)
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0)
self.croppingPowerLabel = QLabel(self.croppingWidget)
self.croppingPowerLabel.setObjectName(u"croppingPowerLabel")
self.chunkSizeWidget = QWidget(self.centralWidget)
self.chunkSizeWidget.setObjectName(u"chunkSizeWidget")
sizePolicy3.setHeightForWidth(self.chunkSizeWidget.sizePolicy().hasHeightForWidth())
self.chunkSizeWidget.setSizePolicy(sizePolicy3)
self.chunkSizeWidget.setVisible(False)
self.horizontalLayout_4 = QHBoxLayout(self.chunkSizeWidget)
self.horizontalLayout_4.setSpacing(0)
self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
self.chunkSizeLabel = QLabel(self.chunkSizeWidget)
self.chunkSizeLabel.setObjectName(u"chunkSizeLabel")
sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
sizePolicy4.setHorizontalStretch(0)
sizePolicy4.setVerticalStretch(0)
sizePolicy4.setHeightForWidth(self.chunkSizeLabel.sizePolicy().hasHeightForWidth())
self.chunkSizeLabel.setSizePolicy(sizePolicy4)
self.horizontalLayout_3.addWidget(self.croppingPowerLabel)
self.horizontalLayout_4.addWidget(self.chunkSizeLabel)
self.croppingPowerSlider = QSlider(self.croppingWidget)
self.croppingPowerSlider.setObjectName(u"croppingPowerSlider")
self.croppingPowerSlider.setMaximum(300)
self.croppingPowerSlider.setSingleStep(1)
self.croppingPowerSlider.setOrientation(Qt.Orientation.Horizontal)
self.chunkSizeBox = QSpinBox(self.chunkSizeWidget)
self.chunkSizeBox.setObjectName(u"chunkSizeBox")
self.chunkSizeBox.setMinimum(100)
self.chunkSizeBox.setMaximum(600)
self.chunkSizeBox.setValue(400)
self.horizontalLayout_3.addWidget(self.croppingPowerSlider)
self.horizontalLayout_4.addWidget(self.chunkSizeBox)
self.chunkSizeWarnLabel = QLabel(self.chunkSizeWidget)
self.chunkSizeWarnLabel.setObjectName(u"chunkSizeWarnLabel")
sizePolicy4.setHeightForWidth(self.chunkSizeWarnLabel.sizePolicy().hasHeightForWidth())
self.chunkSizeWarnLabel.setSizePolicy(sizePolicy4)
self.horizontalLayout_4.addWidget(self.chunkSizeWarnLabel)
self.gridLayout.addWidget(self.croppingWidget, 8, 0, 1, 2)
self.buttonWidget = QWidget(self.centralWidget)
self.buttonWidget.setObjectName(u"buttonWidget")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.buttonWidget.sizePolicy().hasHeightForWidth())
self.buttonWidget.setSizePolicy(sizePolicy)
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))
icon1 = QIcon()
icon1.addFile(u":/Other/icons/folder_new.png", QSize(), QIcon.Normal, QIcon.Off)
self.directoryButton.setIcon(icon1)
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))
icon2 = QIcon()
icon2.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Normal, QIcon.Off)
self.fileButton.setIcon(icon2)
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)
icon3 = QIcon()
icon3.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Normal, QIcon.Off)
self.convertButton.setIcon(icon3)
self.gridLayout_4.addWidget(self.convertButton, 1, 2, 1, 1)
self.clearButton = QPushButton(self.buttonWidget)
self.clearButton.setObjectName(u"clearButton")
self.clearButton.setMinimumSize(QSize(0, 30))
icon4 = QIcon()
icon4.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Normal, QIcon.Off)
self.clearButton.setIcon(icon4)
self.gridLayout_4.addWidget(self.clearButton, 0, 2, 1, 1)
self.directoryButton.raise_()
self.clearButton.raise_()
self.fileButton.raise_()
self.deviceBox.raise_()
self.convertButton.raise_()
self.formatBox.raise_()
self.gridLayout.addWidget(self.buttonWidget, 3, 0, 1, 2)
self.toolWidget = QWidget(self.centralWidget)
self.toolWidget.setObjectName(u"toolWidget")
self.horizontalLayout = QHBoxLayout(self.toolWidget)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.editorButton = QPushButton(self.toolWidget)
self.editorButton.setObjectName(u"editorButton")
self.editorButton.setMinimumSize(QSize(0, 30))
icon5 = QIcon()
icon5.addFile(u":/Other/icons/editor.png", QSize(), QIcon.Normal, QIcon.Off)
self.editorButton.setIcon(icon5)
self.horizontalLayout.addWidget(self.editorButton)
self.wikiButton = QPushButton(self.toolWidget)
self.wikiButton.setObjectName(u"wikiButton")
self.wikiButton.setMinimumSize(QSize(0, 30))
icon6 = QIcon()
icon6.addFile(u":/Other/icons/wiki.png", QSize(), QIcon.Normal, QIcon.Off)
self.wikiButton.setIcon(icon6)
self.horizontalLayout.addWidget(self.wikiButton)
self.gridLayout.addWidget(self.toolWidget, 0, 0, 1, 2)
self.jobList = QListWidget(self.centralWidget)
self.jobList.setObjectName(u"jobList")
self.jobList.setStyleSheet(u"QListWidget#jobList {background:#ffffff;background-image:url(:/Other/icons/list_background.png);background-position:center center;background-repeat:no-repeat;color:rgb(0,0,0);}")
self.jobList.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.jobList.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.jobList.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.gridLayout.addWidget(self.jobList, 2, 0, 1, 2)
self.progressBar = QProgressBar(self.centralWidget)
self.progressBar.setObjectName(u"progressBar")
self.progressBar.setMinimumSize(QSize(0, 30))
self.progressBar.setFont(font)
self.progressBar.setVisible(False)
self.progressBar.setAlignment(Qt.AlignmentFlag.AlignJustify|Qt.AlignmentFlag.AlignVCenter)
self.gridLayout.addWidget(self.progressBar, 1, 0, 1, 2)
self.customWidget = QWidget(self.centralWidget)
self.customWidget.setObjectName(u"customWidget")
self.customWidget.setVisible(False)
self.gridLayout_3 = QGridLayout(self.customWidget)
self.gridLayout_3.setObjectName(u"gridLayout_3")
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)
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.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)
self.gridLayout_3.addWidget(self.wLabel, 0, 0, 1, 1)
self.heightBox = QSpinBox(self.customWidget)
self.heightBox.setObjectName(u"heightBox")
self.heightBox.setMaximum(3840)
self.gridLayout_3.addWidget(self.heightBox, 0, 3, 1, 1)
self.gridLayout.addWidget(self.customWidget, 7, 0, 1, 2)
self.gridLayout.addWidget(self.chunkSizeWidget, 6, 0, 1, 1)
mainWindow.setCentralWidget(self.centralWidget)
self.statusBar = QStatusBar(mainWindow)
@@ -326,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)
@@ -339,18 +459,23 @@ class Ui_mainWindow(object):
QWidget.setTabOrder(self.gammaBox, self.borderBox)
QWidget.setTabOrder(self.borderBox, self.outputSplit)
QWidget.setTabOrder(self.outputSplit, self.colorBox)
QWidget.setTabOrder(self.colorBox, self.croppingBox)
QWidget.setTabOrder(self.croppingBox, self.mozJpegBox)
QWidget.setTabOrder(self.colorBox, self.mozJpegBox)
QWidget.setTabOrder(self.mozJpegBox, self.maximizeStrips)
QWidget.setTabOrder(self.maximizeStrips, self.deleteBox)
QWidget.setTabOrder(self.maximizeStrips, self.croppingBox)
QWidget.setTabOrder(self.croppingBox, self.spreadShiftBox)
QWidget.setTabOrder(self.spreadShiftBox, self.deleteBox)
QWidget.setTabOrder(self.deleteBox, self.disableProcessingBox)
QWidget.setTabOrder(self.disableProcessingBox, self.editorButton)
QWidget.setTabOrder(self.disableProcessingBox, self.chunkSizeBox)
QWidget.setTabOrder(self.chunkSizeBox, self.noRotateBox)
QWidget.setTabOrder(self.noRotateBox, self.interPanelCropBox)
QWidget.setTabOrder(self.interPanelCropBox, self.reduceRainbowBox)
QWidget.setTabOrder(self.reduceRainbowBox, self.heightBox)
QWidget.setTabOrder(self.heightBox, self.croppingPowerSlider)
QWidget.setTabOrder(self.croppingPowerSlider, self.editorButton)
QWidget.setTabOrder(self.editorButton, self.wikiButton)
QWidget.setTabOrder(self.wikiButton, self.jobList)
QWidget.setTabOrder(self.jobList, self.gammaSlider)
QWidget.setTabOrder(self.gammaSlider, self.widthBox)
QWidget.setTabOrder(self.widthBox, self.heightBox)
QWidget.setTabOrder(self.heightBox, self.croppingPowerSlider)
self.retranslateUi(mainWindow)
@@ -360,91 +485,34 @@ class Ui_mainWindow(object):
def retranslateUi(self, mainWindow):
mainWindow.setWindowTitle(QCoreApplication.translate("mainWindow", u"Kindle Comic Converter", 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))
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.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
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.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.maximizeStrips.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 1x4<br/></span>Keep format 1x4 panels strips.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 2x2<br/></span>Turn 1x4 strips to 2x2 to maximize screen usage.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None))
#if QT_CONFIG(tooltip)
self.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.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.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.upscaleBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Nothing<br/></span>Images smaller than device resolution will not be resized.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Stretching<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be not preserved.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Upscaling<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be preserved.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.upscaleBox.setText(QCoreApplication.translate("mainWindow", u"Stretch/Upscale", None))
#if QT_CONFIG(tooltip)
self.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.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.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.outputSplit.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Automatic mode<br/></span>The output will be split automatically.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Checked - Volume mode<br/></span>Every subdirectory will be considered as a separate volume.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", None))
#if QT_CONFIG(tooltip)
self.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.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.disableProcessingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><pre style=\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Do not process any image, ignore profile and processing options</pre></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.disableProcessingBox.setText(QCoreApplication.translate("mainWindow", u"Disable processing", None))
#if QT_CONFIG(tooltip)
self.dedupeCoverBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Don't duplicate the first page as the cover. Useful for 2 page spread alignment.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.dedupeCoverBox.setText(QCoreApplication.translate("mainWindow", u"De-dupe cover", None))
self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", 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))
#endif // QT_CONFIG(tooltip)
self.directoryButton.setText(QCoreApplication.translate("mainWindow", u"Add directory", 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", 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)
#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))
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.editorButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to edit directory.</p></body></html>", None))
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)
#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.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)
self.editorButton.setText(QCoreApplication.translate("mainWindow", u"Editor", None))
self.wikiButton.setText(QCoreApplication.translate("mainWindow", u"Wiki", 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)
@@ -459,5 +527,108 @@ class Ui_mainWindow(object):
#if QT_CONFIG(tooltip)
self.heightBox.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)
#if QT_CONFIG(tooltip)
self.preserveMarginLabel.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>After calculating the cropping boundaries, &quot;back up&quot; a specified percentage amount.</p></body></html>", None))
#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.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.mangaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left mode", None))
#if QT_CONFIG(tooltip)
self.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)
self.spreadShiftBox.setText(QCoreApplication.translate("mainWindow", u"Spread shift", None))
#if QT_CONFIG(tooltip)
self.upscaleBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Nothing<br/></span>Images smaller than device resolution will not be resized.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Stretching<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be not preserved.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Upscaling<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be preserved.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.upscaleBox.setText(QCoreApplication.translate("mainWindow", u"Stretch/Upscale", None))
#if QT_CONFIG(tooltip)
self.outputSplit.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Automatic mode<br/></span>The output will be split automatically.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Checked - Volume mode<br/></span>Every subdirectory will be considered as a separate volume.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", None))
#if QT_CONFIG(tooltip)
self.rotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Split<br/></span>Double page spreads will be cut into two separate pages.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Split and rotate<br/></span>Double page spreads will be displayed twice. First split and then rotated. </p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate<br/></span>Double page spreads will be rotated.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None))
#if QT_CONFIG(tooltip)
self.borderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - White<br/></span>Margins will be filled with white color.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
#if QT_CONFIG(tooltip)
self.maximizeStrips.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 1x4<br/></span>Keep format 1x4 panels strips.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 2x2<br/></span>Turn 1x4 strips to 2x2 to maximize screen usage.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None))
#if QT_CONFIG(tooltip)
self.rotateFirstBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>When the spread splitter option is partially checked,</p><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Rotate Last<br/></span>Put the rotated 2 page spread after the split spreads.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate First<br/></span>Put the rotated 2 page spread before the split spreads.</p></body></html>", None))
#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))
#endif // QT_CONFIG(tooltip)
self.chunkSizeLabel.setText(QCoreApplication.translate("mainWindow", u"Chunk size MB:", None))
self.chunkSizeWarnLabel.setText(QCoreApplication.translate("mainWindow", u"Greater than default may cause performance issues on older ereaders.", None))
# retranslateUi

View File

@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'MetaEditor.ui'
##
## Created by: Qt User Interface Compiler version 6.6.3
## Created by: Qt User Interface Compiler version 6.9.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -27,7 +27,7 @@ class Ui_editorDialog(object):
editorDialog.resize(400, 260)
editorDialog.setMinimumSize(QSize(400, 260))
icon = QIcon()
icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Normal, QIcon.Off)
icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
editorDialog.setWindowIcon(icon)
self.verticalLayout = QVBoxLayout(editorDialog)
self.verticalLayout.setObjectName(u"verticalLayout")
@@ -129,7 +129,7 @@ class Ui_editorDialog(object):
self.okButton.setObjectName(u"okButton")
self.okButton.setMinimumSize(QSize(0, 30))
icon1 = QIcon()
icon1.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Normal, QIcon.Off)
icon1.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.okButton.setIcon(icon1)
self.horizontalLayout.addWidget(self.okButton)
@@ -138,7 +138,7 @@ class Ui_editorDialog(object):
self.cancelButton.setObjectName(u"cancelButton")
self.cancelButton.setMinimumSize(QSize(0, 30))
icon2 = QIcon()
icon2.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Normal, QIcon.Off)
icon2.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.cancelButton.setIcon(icon2)
self.horizontalLayout.addWidget(self.cancelButton)

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -181,7 +181,7 @@ def splitImage(work):
panelImg = imgOrg.crop((0, panelsProcessed[panel][0], widthImg, panelsProcessed[panel][1]))
newPage.paste(panelImg, (0, targetHeight))
targetHeight += panelsProcessed[panel][2]
newPage.save(os.path.join(path, os.path.splitext(name)[0] + '-' + str(pageNumber) + '.png'), 'PNG')
newPage.save(os.path.join(path, os.path.splitext(name)[0] + '-' + str(pageNumber).zfill(4) + '.png'), 'PNG')
pageNumber += 1
os.remove(filePath)
except Exception:

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,10 +40,10 @@ 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':
if distro.id() == 'fedora' or distro.like() == 'fedora':
extraction_commands.append(
['unrar', 'l', '-y', '-p1', self.filepath],
)
@@ -68,15 +69,17 @@ 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]
)
if distro.id() == 'fedora':
extraction_commands.reverse()
if distro.id() == 'fedora' or distro.like() == 'fedora':
extraction_commands.append(
['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.filepath, targetdir]
)
@@ -84,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:
@@ -98,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)
@@ -112,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

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

View File

@@ -20,10 +20,13 @@
# 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 .shared import md5Checksum
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
AUTO_CROP_THRESHOLD = 0.015
@@ -84,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),
@@ -119,9 +124,16 @@ class ProfileData:
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.8),
}
ProfilesRemarkable = {
'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.8),
'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.8),
'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.8),
}
Profiles = {
**ProfilesKindle,
**ProfilesKobo,
**ProfilesRemarkable,
'OTHER': ("Other", (0, 0), Palette16, 1.8),
}
@@ -133,8 +145,12 @@ class ComicPageParser:
self.source = source
self.size = self.opt.profileData[1]
self.payload = []
self.image = Image.open(os.path.join(source[0], source[1])).convert('RGB')
self.color = self.colorCheck()
# 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.fill = self.fillCheck()
# backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'):
@@ -165,10 +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:
self.payload.append(['R', self.source, self.image.rotate(90, Image.Resampling.BICUBIC, True), self.color, self.fill])
spread = self.image
if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
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:
@@ -183,35 +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:
self.payload.append(['R', self.source, self.image.rotate(90, Image.Resampling.BICUBIC, True),
self.color, self.fill])
spread = self.image
if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
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:
@@ -253,113 +252,159 @@ 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.rotated = True
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)
return [md5Checksum(self.targetPath), flags, self.orgPath]
targetPath = self.save_with_codec(self.image, self.targetPathStart + self.targetPathOrder)
if os.path.isfile(self.orgPath):
os.remove(self.orgPath)
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):
# Reduce rainbow artifacts for grayscale images by breaking up dither patterns that cause Moire interference with color filter array
if reducerainbow and not self.color:
unsharpFilter = ImageFilter.UnsharpMask(radius=1, percent=100)
self.image = self.image.filter(unsharpFilter)
self.image = self.image.filter(ImageFilter.BoxBlur(1.0))
self.image = self.image.filter(unsharpFilter)
def resizeImage(self):
# 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()
if self.opt.stretch:
self.image = self.image.resize(self.size, method)
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
if self.opt.format == 'CBZ' or self.opt.kfx:
borderw = int((self.size[0] - self.image.size[0]) / 2)
borderh = int((self.size[1] - self.image.size[1]) / 2)
self.image = ImageOps.expand(self.image, border=(borderw, borderh), fill=self.fill)
if self.image.size[0] != self.size[0] or self.image.size[1] != self.size[1]:
self.image = ImageOps.fit(self.image, self.size, method=method)
pass
else: # if image bigger than device resolution or smaller with upscaling
if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
self.image = ImageOps.fit(self.image, self.size, method=method)
elif self.opt.format == 'CBZ' or self.opt.kfx:
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):
if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
if self.image.size[0] < self.size[0] and self.image.size[1] < self.size[1]:
return Image.Resampling.BICUBIC
else:
return Image.Resampling.LANCZOS
def maybeCrop(self, box, minimum):
w, h = self.image.size
left, upper, right, lower = box
if self.opt.preservemargin:
ratio = 1 - self.opt.preservemargin / 100
box = left * ratio, upper * ratio, right + (w - right) * (1 - ratio), lower + (h - lower) * (1 - ratio)
box_area = (box[2] - box[0]) * (box[3] - box[1])
image_area = self.image.size[0] * self.image.size[1]
if (box_area / image_area) >= minimum:
@@ -377,16 +422,13 @@ class ComicPage:
if bbox:
self.maybeCrop(bbox, minimum)
def cropInterPanelEmptySections(self, direction):
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'):
@@ -398,17 +440,52 @@ class Cover:
self.image = ImageOps.autocontrast(self.image)
if not self.options.forcecolor:
self.image = self.image.convert('L')
self.image.thumbnail(self.options.profileData[1], Image.Resampling.LANCZOS)
self.save()
self.crop_main_cover()
def save(self):
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:
self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h))
else:
self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h))
elif w / h > 1.3:
if self.options.righttoleft:
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))
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

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

View File

@@ -20,6 +20,7 @@ import os
from xml.dom.minidom import parse, Document
from tempfile import mkdtemp
from shutil import rmtree
from xml.sax.saxutils import unescape
from . import comicarchive
@@ -52,19 +53,19 @@ class MetadataParser:
def parseXML(self):
if len(self.rawdata.getElementsByTagName('Series')) != 0:
self.data['Series'] = self.rawdata.getElementsByTagName('Series')[0].firstChild.nodeValue
self.data['Series'] = unescape(self.rawdata.getElementsByTagName('Series')[0].firstChild.nodeValue)
if len(self.rawdata.getElementsByTagName('Volume')) != 0:
self.data['Volume'] = self.rawdata.getElementsByTagName('Volume')[0].firstChild.nodeValue
if len(self.rawdata.getElementsByTagName('Number')) != 0:
self.data['Number'] = self.rawdata.getElementsByTagName('Number')[0].firstChild.nodeValue
if len(self.rawdata.getElementsByTagName('Summary')) != 0:
self.data['Summary'] = self.rawdata.getElementsByTagName('Summary')[0].firstChild.nodeValue
self.data['Summary'] = unescape(self.rawdata.getElementsByTagName('Summary')[0].firstChild.nodeValue)
if len(self.rawdata.getElementsByTagName('Title')) != 0:
self.data['Title'] = self.rawdata.getElementsByTagName('Title')[0].firstChild.nodeValue
self.data['Title'] = unescape(self.rawdata.getElementsByTagName('Title')[0].firstChild.nodeValue)
for field in ['Writer', 'Penciller', 'Inker', 'Colorist']:
if len(self.rawdata.getElementsByTagName(field)) != 0:
for person in self.rawdata.getElementsByTagName(field)[0].firstChild.nodeValue.split(', '):
self.data[field + 's'].append(person)
self.data[field + 's'].append(unescape(person))
self.data[field + 's'] = list(set(self.data[field + 's']))
self.data[field + 's'].sort()
if len(self.rawdata.getElementsByTagName('Page')) != 0:

View File

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

View File

@@ -19,7 +19,6 @@
#
import os
from hashlib import md5
from html.parser import HTMLParser
import subprocess
from packaging.version import Version
@@ -49,8 +48,6 @@ class HTMLStripper(HTMLParser):
def getImageFileName(imgfile):
name, ext = os.path.splitext(imgfile)
ext = ext.lower()
if (name.startswith('.') and len(name) == 1) or ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp']:
return None
return [name, ext]
@@ -74,16 +71,6 @@ def walkLevel(some_dir, level=1):
del dirs[:]
def md5Checksum(fpath):
with open(fpath, 'rb') as fh:
m = md5()
while True:
data = fh.read(8192)
if not data:
break
m.update(data)
return m.hexdigest()
def sanitizeTrace(traceback):
return ''.join(format_tb(traceback))\
@@ -129,10 +116,10 @@ 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)

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
natsort[fast]>=8.4.0
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,15 +75,15 @@ 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',
'requests>=2.31.0',
'mozjpeg-lossless-optimization>=1.1.2',
'natsort[fast]>=8.4.0',
'natsort>=8.4.0',
'distro',
'numpy>=1.22.4,<2.0.0'
'numpy>=1.22.4'
],
classifiers=[],
zip_safe=False,