1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-15 21:48:44 +00:00

Compare commits

..

207 Commits

Author SHA1 Message Date
Alex Xu
aa9ee43278 specify you should clone a fork, not the main KCC repo 2025-05-26 11:29:38 -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
Alex Xu
e637f37ef0 bump to 7.0.0 2024-11-11 15:52:29 -08:00
neyney10
6ba690659f Fix a missing line from previous commit. 2024-11-11 15:51:41 -08:00
neyney10
50eb48fb9b Fix crash when trying to crop blank images. 2024-11-11 15:51:41 -08:00
neyney10
4a661a1a17 [A new image cropping algorithm]
1. Replaced both crop margins and crop margins & page num with newer algorithm.
2. Crop max power level increased to 3.0
3. Adds NumPy as a new dependency.
2024-11-11 15:51:41 -08:00
Alex Xu
c26383c4b5 bump 6.3.1 2024-11-11 11:17:29 -08:00
Alex Xu
4e6ee8b59b fix bookmark drift when dedupecover is checked (#772)
* remove cover chapter as needed

* just skip cover chapter

* fix space
2024-11-11 08:19:04 -08:00
Alex Xu
6c6f591e45 bump 6.3.0 2024-11-07 21:54:37 -08:00
Alex Xu
78c014bf22 simplify cover upload on newer MTP based Kindles by replacing EBOK with PDOC (#767)
* simplify covers by replacing ebok with pdoc

* refactor

* fix order
2024-11-07 21:53:59 -08:00
Alex Xu
421e6bcb64 bump 6.2.2 2024-11-06 14:55:00 -08:00
Alex Xu
5168cd73c4 Make it easier to add new profiles (#764)
* refactor

* put pw12 under oasis profile

* remove kindle 12 tag

* separate ebok and pdoc

* put cs 12 on top
2024-11-04 20:24:12 -08:00
Alex Xu
f7f19b99da fix ComicInfo.xml chapters 2024-11-04 09:04:17 -08:00
Alex Xu
1131bab41f bump 6.2.1 2024-10-23 21:21:27 -07:00
Alex Xu
8d204668a7 add kindle 12th gen, including colorsoft (#762)
* add kindle colorsoft

* fix typo
2024-10-23 21:17:22 -07:00
Alex Xu
99d94ceaa7 fix _cffi_backend 2024-10-16 19:24:01 -07:00
Alex Xu
8ff401cc3a remove fastnumbers from armv7 2024-10-04 09:04:56 -07:00
Alex Xu
fb7d92d737 fix Docker linux arm (#746)
* fix linux arm64

* Update Dockerfile-base

* Update Dockerfile

* Update Dockerfile-base

* ENV PATH="/opt/venv/bin:$PATH"

* fix docker armv7

* Update Dockerfile-base

* Update Dockerfile-base
2024-09-29 10:21:35 -07:00
Alex Xu
1ca8b2c11b Update README.md 2024-09-15 11:49:49 -07:00
Alex Xu
40e0b4853b add flatpak note 2024-09-08 22:23:23 -07:00
Alex Xu
005313f978 flatpak mobi conversion stuck 2024-09-08 21:22:56 -07:00
Alex Xu
add2ef9faa fix image file is corrupt for real 2024-09-07 22:17:30 -07:00
Alex Xu
0c98acd606 bump to 6.2.0 2024-09-06 23:05:02 -07:00
Alex Xu
fe902ec213 fix corrupt 2024-09-06 23:04:32 -07:00
Alex Xu
4e9714e6f8 add packaging for Python 3.12 2024-08-30 10:02:44 -07:00
Alex Xu
1337ad7fe3 Python 3.12 supported 2024-08-28 14:32:23 -07:00
dependabot[bot]
511c7c1580 Bump python from 3.11-slim-bullseye to 3.12-slim-bullseye
Bumps python from 3.11-slim-bullseye to 3.12-slim-bullseye.

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-28 14:28:16 -07:00
Alex Xu
16dd034af4 add Python 3.12 support 2024-08-28 14:26:54 -07:00
Alex Xu
d22794555f bump to 6.1.1 2024-08-09 17:13:02 -07:00
Alex Xu
ab089adbca fix no such file or directory 7z (#728)
* refactor archive code

* simplify code

* Revert "simplify code"

This reverts commit 3e90dd66c3.

* add link to missing extraction software

* fix tar

* fix wording
2024-08-09 17:08:13 -07:00
Alex Xu
2c770f4562 search for kindlegen in %UserProfile% 2024-08-09 17:05:44 -07:00
Alex Xu
4c73006bd8 add APFS to external drive 2024-08-06 16:04:45 -07:00
Alex Xu
1093dbf65a Update README.md 2024-07-27 08:26:44 -07:00
Alex Xu
df9990c692 add python 3.12 unsupported note 2024-07-25 13:54:31 -07:00
Alex Xu
114f4c9e57 add macOS minimums 2024-07-21 10:19:49 -07:00
Alex Xu
c2c477475d combine files/chapters in readme 2024-07-20 12:37:20 -07:00
Alex Xu
a0f8d0b5cf add kcc 6.1 7z note 2024-07-01 08:55:20 -07:00
Alex Xu
2fc32bae58 Update __init__.py 2024-06-29 08:52:09 -07:00
Alex Xu
db25a0939c Build windows gui version normally. 2024-06-25 22:03:10 -07:00
Alex Xu
1bd7506140 remove unar macOS 2024-06-25 21:04:52 -07:00
Alex Xu
4fc5cc9dfb remove GUI windows docker 2024-06-25 20:45:32 -07:00
Alex Xu
89289412a3 Revert "add hiddenimports=['pkg_resources.extern']"
This reverts commit 193297c8fc.
2024-06-25 19:29:17 -07:00
Alex Xu
193297c8fc add hiddenimports=['pkg_resources.extern'] 2024-06-25 19:15:34 -07:00
Alex Xu
367d71e4ad fix typo 2024-06-25 12:52:41 -07:00
Alex Xu
cdde0f18cd update prereq readme (#714)
* update prereq readme

* Update README.md

* Update README.md

* Revert "Update README.md"

This reverts commit ceb35b03b6.

* Revert "Update README.md"

This reverts commit a24e818c0e.

* add other OS note
2024-06-25 12:42:38 -07:00
Alex Xu
9fe82a9dd4 improve kindlegen detection windows (#688)
* improve kindlegen detection windows

* add C:\Apps folders
2024-06-25 12:21:48 -07:00
detournemint
e958edd135 Update package-linux.yml to include libxcb-cursor0
Including libxcb-cursor0 for linux
2024-06-25 12:15:33 -07:00
Alex Xu
6738b70487 don't display upscale warning on kindle scribe 2024-06-09 08:00:55 -07:00
Dominik Gedon
44094bdb21 Add Kobo Libra/Clara colour (#703)
* fix: trailing whitespaces

* Add color toggle mode

* Add Kobo Libra Colour

Signed-off-by: Dominik Gedon <dominik@gedon.org>

* Add Kobo Clara Colour

Signed-off-by: Dominik Gedon <dominik@gedon.org>

* Address suggestions

* Address more suggestions

---------

Signed-off-by: Dominik Gedon <dominik@gedon.org>
2024-06-09 08:00:25 -07:00
Alex Xu
a9a2f47e30 fix 1860 width 2024-05-28 09:55:59 -07:00
Bruno Resende
c35dd137ea Add de-dupe cover option for landscape alignment (#561)
* Adds --author argument to CLI

* Add --prefercoverfile and --nocoveraspage arguments to CLI

* cover checks only run once

* Revert "cover checks only run once"

This reverts commit ad7b3cc106.

* split off author portion

* split off author

* remove prefercover

* whitespace fixes

* rename to duplicatecover

* whitespace fixes

* rename to dedupecover

* add dedupe to UI

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2024-05-17 09:56:22 -07:00
Alex Xu
1ea008c8f3 reduce dependency on 7z, unar, homebrew by using builtin tar (#693)
* reduce dependency on 7z, unar, homebrew by using builtin tar

* update readme

* fix typo

* add editor text

* remove unar

* Revert "remove unar"

This reverts commit 2c4b239d67.

* Revert "fix typo"

This reverts commit 79752c7f38.

* Revert "update readme"

This reverts commit 4f5c727a2d.
2024-05-17 09:56:04 -07:00
Alex Xu
cbc1ed5db4 Kindle Scribe 1860 max width (#691)
* Kindle Scribe 1860 max width

* create kindle_scribe_azw3 var

* clean up code
2024-05-17 09:55:43 -07:00
Alex Xu
6bf662bea3 add faq 2024-05-15 10:15:08 -07:00
Alex Xu
cfae306731 macos-latest is now macos-14 (m1) 2024-04-27 12:56:49 -07:00
Alex Xu
f6475f4c61 Close and re-open KCC note 2024-04-19 21:15:48 -07:00
Alex Xu
9646518575 shorten 7z/kindlegen notes 2024-04-18 20:58:12 -07:00
Alex Xu
937dd3984a kindlegen note 2024-04-18 18:07:06 -07:00
Alex Xu
4e6f6ec8e8 clarify 7z/kindlegen windows 2024-04-18 17:35:51 -07:00
Alex Xu
4b36fdbfa2 update C drive notes 2024-04-18 16:08:10 -07:00
Alex Xu
d8141af4eb update c drive notes 2024-04-18 15:45:34 -07:00
41 changed files with 2283 additions and 1243 deletions

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
@@ -34,7 +34,7 @@ jobs:
- name: Install python dependencies
run: |
sudo apt-get update
sudo apt-get install -y libpng-dev libjpeg-dev p7zip-full p7zip-rar python3-pip squashfs-tools libfuse2
sudo apt-get install -y libpng-dev libjpeg-dev p7zip-full p7zip-rar python3-pip squashfs-tools libfuse2 libxcb-cursor0
python -m pip install --upgrade pip setuptools wheel certifi pyinstaller --no-binary pyinstaller
python -m pip install -r requirements.txt
- name: build binary
@@ -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-latest, 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,47 +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.spec
- 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.exe dist/windows/KCC_${version_built}.exe
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/')
@@ -58,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-20230809
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'
@@ -16,4 +16,4 @@ COPY . /opt/kcc
RUN cat /opt/kcc/kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/'//g" > /IMAGE_VERSION
ENTRYPOINT ["/opt/kcc/kcc-c2e.py"]
CMD ["-h"]
CMD ["-h"]

View File

@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 python:3.11-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.11-slim-bullseye as compile-arm64
FROM --platform=linux/arm64 python:3.13-slim-bullseye as compile-arm64
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
@@ -28,6 +28,9 @@ ENV LC_ALL=C.UTF-8 \
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
COPY requirements.txt /opt/kcc/
ENV PATH="/opt/venv/bin:$PATH"
RUN set -x && \
TEMP_PACKAGES=() && \
KEPT_PACKAGES=() && \
@@ -64,14 +67,13 @@ RUN set -x && \
&& \
# Install required python modules
python -m pip install --upgrade pip && \
# python -m pip install -r /opt/kcc/requirements.txt && \
python -m venv /opt/venv && \
python -m pip install --upgrade pillow python-slugify psutil raven mozjpeg-lossless-optimization
python -m pip install -r /opt/kcc/requirements.txt
######################################################################################
FROM --platform=linux/arm/v7 python:3.11-slim-bullseye as compile-armv7
FROM --platform=linux/arm/v7 python:3.13-slim-bullseye as compile-armv7
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
@@ -83,6 +85,9 @@ ENV LC_ALL=C.UTF-8 \
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
COPY requirements.txt /opt/kcc/
ENV PATH="/opt/venv/bin:$PATH"
RUN set -x && \
TEMP_PACKAGES=() && \
KEPT_PACKAGES=() && \
@@ -120,19 +125,18 @@ RUN set -x && \
&& \
# Install required python modules
python -m pip install --upgrade pip && \
# python -m pip install -r /opt/kcc/requirements.txt && \
python -m venv /opt/venv && \
python -m pip install --upgrade pillow python-slugify psutil raven mozjpeg-lossless-optimization
python -m pip install --upgrade pillow psutil requests python-slugify raven packaging mozjpeg-lossless-optimization natsort distro numpy
######################################################################################
FROM --platform=linux/amd64 python:3.11-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.11-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.11-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
######################################################################################
@@ -156,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-20230809 > /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

145
README.md
View File

@@ -1,15 +1,24 @@
<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 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)
**Kindle Comic Converter** optimizes comics and manga for eink readers like Kindle, Kobo, ReMarkable, and more.
Pages display in fullscreen without margins, with proper fixed layout support.
Its main feature is various optional image processing steps to look good on eink screens,
which have different requirements than normal LCD screens.
It also does filesize optimization by downscaling to your specific device's screen resolution,
which can improve performance on underpowered ereaders.
Supported input formats include folders and archives of JPG/PNG files and more.
Supported output formats include virtual panel view MOBI/AZW3, EPUB, KEPUB, and CBZ.
**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.
![image](https://github.com/user-attachments/assets/36ad2131-6677-4559-bd6f-314a90c27218)
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,23 +31,30 @@ 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:
- [![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:
- [![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
- [![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)
## Sponsors
- Free code signing on Windows provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
## DOWNLOADS
- **https://github.com/ciromattia/kcc/releases**
Click on **Assets** of the latest release.
Click on **Assets** of the latest release.
You probably want either
You probably want either
- `KCC_*.*.*.exe` (Windows)
- `kcc_macos_arm_*.*.*.dmg` (recent Mac with Apple Silicon M1 chip or later)
- `kcc_macos_i386_*.*.*.dmg` (older Mac with Intel chip)
@@ -51,35 +67,38 @@ 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
- [Windows 7 support](https://github.com/ciromattia/kcc/issues/678)
- [Combine files/chapters](https://github.com/ciromattia/kcc/issues/612#issuecomment-2117985011)
- [Flatpak mobi conversion stuck](https://github.com/ciromattia/kcc/wiki/Installation#linux)
- Image too dark?
- The default gamma correction of 1.8 makes the image darker, and is useful for faded/gray artwork/text. Disable by setting gamma = 1.0
- [Better PDF support (Humble Bundle, Fanatical, etc)](https://github.com/ciromattia/kcc/issues/680)
- Cannot connect Kindle Scribe or 2024+ Kindle to macOS
- Use official MTP [Amazon USB File Transfer app](https://www.amazon.com/gp/help/customer/display.html/ref=hp_Connect_USB_MTP?nodeId=TCUBEdEkbIhK07ysFu)
(no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps.
- Huge margins / slow page turns?
- You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB.
## PREREQUISITES
You'll need to install various tools to access important but optional features.
### 7-Zip
#### Windows 7-Zip
First install 7z from https://www.7-zip.org/ or with command line:
```
winget install --id 7zip.7zip
```
On Windows, make sure to install the `64-bit x64` version. KCC expects it to be installed to `C:\Program Files\7-Zip` (on the `C:\` drive) which is default.
#### macOS 7-Zip/Unar
with [Homebrew](https://brew.sh/) installed
```
brew install p7zip
brew install unar
```
You'll need to install various tools to access important but optional features. Close and re-open KCC to get KCC to detect them.
### KindleGen
#### Windows / macOS KindleGen
On Windows and macOS, install [Kindle Previewer](https://www.amazon.com/Kindle-Previewer/b?ie=UTF8&node=21381691011) and `kindlegen` will be autodetected from it.
It's included in [Kindle Previewer](https://www.amazon.com/Kindle-Previewer/b?ie=UTF8&node=21381691011).
If you have issues detecting it, get stuck on the MOBI conversion step, or use Linux AppImage or Flatpak, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation#kindlegen
On Windows, KCC expects it to be installed on the `C:/` drive, which is by default.
### 7-Zip
This is optional but will make conversions much faster.
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:
@@ -114,7 +133,7 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
'KV': ("Kindle Paperwhite 3/4/Voyage/Oasis", (1072, 1448), Palette16, 1.8),
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
'KO': ("Kindle Oasis 2/3", (1264, 1680), 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),
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8),
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8),
@@ -125,10 +144,15 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.8),
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.8),
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.8),
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.8),
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.8),
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.8),
'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),
```
@@ -142,7 +166,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
@@ -162,8 +187,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
@@ -177,10 +205,16 @@ OUTPUT SETTINGS:
Output generated file to specified directory or file
-t TITLE, --title TITLE
Comic title [Default=filename or directory name]
-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.
--reducerainbow Reduce rainbow effect on color eink by slightly blurring images
CUSTOM PROFILE:
--customwidth CUSTOMWIDTH
@@ -216,15 +250,20 @@ 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
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.
### Windows install from source
@@ -243,6 +282,12 @@ 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
One time setup and running for the first time:
@@ -260,8 +305,14 @@ 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
**KCC** is made by
- [Ciro Mattia Gonano](http://github.com/ciromattia)
- [Paweł Jastrzębski](http://github.com/AcidWeb)
@@ -277,6 +328,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)
@@ -296,5 +353,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

@@ -9,7 +9,7 @@ dependencies:
- 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>

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>450</width>
<height>400</height>
<width>519</width>
<height>572</height>
</rect>
</property>
<property name="windowTitle">
@@ -22,188 +22,25 @@
<property name="bottomMargin">
<number>5</number>
</property>
<item row="5" column="0" colspan="2">
<widget class="QWidget" name="optionWidget" native="true">
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="1" column="1">
<widget class="QCheckBox" name="upscaleBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Nothing&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will not be resized.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Stretching&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will be resized. Aspect ratio will be not preserved.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Upscaling&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will be resized. Aspect ratio will be preserved.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Stretch/Upscale</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="rotateBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Split&lt;br/&gt;&lt;/span&gt;Double page spreads will be cut into two separate pages.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Rotate and split&lt;br/&gt;&lt;/span&gt;Double page spreads will be displayed twice. First rotated and then split. &lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Rotate&lt;br/&gt;&lt;/span&gt;Double page spreads will be rotated.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Spread splitter</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="outputSplit">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Automatic mode&lt;br/&gt;&lt;/span&gt;The output will be split automatically.&lt;/p&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Volume mode&lt;br/&gt;&lt;/span&gt;Every subdirectory will be considered as a separate volume.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Output split</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="webtoonBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Enable special parsing mode for Korean Webtoons.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Webtoon mode</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QCheckBox" name="colorBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Disable conversion to grayscale.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Color mode</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="gammaBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Disable automatic gamma correction.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Custom gamma</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="borderBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Autodetection&lt;br/&gt;&lt;/span&gt;The color of margins fill will be detected automatically.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - White&lt;br/&gt;&lt;/span&gt;Margins will be filled with white color.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Black&lt;br/&gt;&lt;/span&gt;Margins will be filled with black color.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>W/B margins</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="mangaBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Enable right-to-left reading.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Manga mode</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="qualityBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - 4 panels&lt;br/&gt;&lt;/span&gt;Zoom each corner separately.&lt;/p&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - 2 panels&lt;br/&gt;&lt;/span&gt;Zoom only the top and bottom of the page.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - 4 high-quality panels&lt;br/&gt;&lt;/span&gt;Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Panel View 4/2/HQ</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="mozJpegBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - JPEG&lt;br/&gt;&lt;/span&gt;Use JPEG files&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - force PNG&lt;br/&gt;&lt;/span&gt;Create PNG files instead JPEG&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - mozJpeg&lt;br/&gt;&lt;/span&gt;10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>JPEG/PNG/mozJpeg</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="maximizeStrips">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - 1x4&lt;br/&gt;&lt;/span&gt;Keep format 1x4 panels strips.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - 2x2&lt;br/&gt;&lt;/span&gt;Turn 1x4 strips to 2x2 to maximize screen usage.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>1x4 to 2x2 strips</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QCheckBox" name="croppingBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Disabled&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Disabled&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Margins&lt;br/&gt;&lt;/span&gt;Margins&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Margins + page numbers&lt;br/&gt;&lt;/span&gt;Margins +page numbers&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Cropping mode</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="deleteBox">
<property name="toolTip">
<string>Delete input file(s) or directory. It's not recoverable!</string>
</property>
<property name="text">
<string>Delete input</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QCheckBox" name="disableProcessingBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;pre style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Do not process any image, ignore profile and processing options&lt;/pre&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Disable processing</string>
</property>
</widget>
</item>
</layout>
<item row="2" column="0" colspan="2">
<widget class="QListWidget" name="jobList">
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::NoSelection</enum>
</property>
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
</property>
<property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<widget class="QWidget" name="gammaWidget" native="true">
<property name="visible">
<bool>false</bool>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item row="0" column="0" colspan="2">
<widget class="QWidget" name="toolWidget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
@@ -217,63 +54,39 @@
<number>0</number>
</property>
<item>
<widget class="QLabel" name="gammaLabel">
<widget class="QPushButton" name="editorButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Shift+Click to edit directory.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Gamma: Auto</string>
<string>Metadata Editor</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/editor.png</normaloff>:/Other/icons/editor.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="gammaSlider">
<property name="maximum">
<number>250</number>
<widget class="QPushButton" name="wikiButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="singleStep">
<number>5</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="8" column="0" colspan="2">
<widget class="QWidget" name="croppingWidget" native="true">
<property name="visible">
<bool>false</bool>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="croppingPowerLabel">
<property name="text">
<string>Cropping power:</string>
<string>Wiki</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="croppingPowerSlider">
<property name="maximum">
<number>200</number>
</property>
<property name="singleStep">
<number>1</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/wiki.png</normaloff>:/Other/icons/wiki.png</iconset>
</property>
</widget>
</item>
@@ -313,7 +126,7 @@
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Add directory containing JPG, PNG or GIF files to queue.&lt;br/&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;CBR, CBZ and CB7 files inside will not be processed!&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Add directory</string>
<string>Add image folder</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
@@ -333,7 +146,7 @@
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Add CBR, CBZ, CB7 or PDF file to queue.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Add file</string>
<string>Add file(s)</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
@@ -418,77 +231,6 @@
<zorder>formatBox</zorder>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QWidget" name="toolWidget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="editorButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Shift+Click to edit directory.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Editor</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/editor.png</normaloff>:/Other/icons/editor.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="wikiButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>Wiki</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/wiki.png</normaloff>:/Other/icons/wiki.png</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QListWidget" name="jobList">
<property name="styleSheet">
<string notr="true">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);}</string>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
<property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QProgressBar" name="progressBar">
<property name="minimumSize">
@@ -506,11 +248,11 @@
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignJustify|Qt::AlignVCenter</set>
<set>Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<item row="8" column="0" colspan="2">
<widget class="QWidget" name="customWidget" native="true">
<property name="visible">
<bool>false</bool>
@@ -583,6 +325,440 @@
</layout>
</widget>
</item>
<item row="9" column="0" colspan="2">
<widget class="QWidget" name="croppingWidget" native="true">
<property name="visible">
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="1" column="0">
<widget class="QLabel" name="preserveMarginLabel">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;After calculating the cropping boundaries, &amp;quot;back up&amp;quot; a specified percentage amount.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Preserve Margin %</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="croppingPowerLabel">
<property name="text">
<string>Cropping power:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSlider" name="croppingPowerSlider">
<property name="maximum">
<number>300</number>
</property>
<property name="singleStep">
<number>1</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="preserveMarginBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximum">
<number>99</number>
</property>
<property name="singleStep">
<number>5</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QWidget" name="optionWidget" native="true">
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="4" column="2">
<widget class="QCheckBox" name="croppingBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Disabled&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Disabled&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Margins&lt;br/&gt;&lt;/span&gt;Margins&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Margins + page numbers&lt;br/&gt;&lt;/span&gt;Margins +page numbers&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Cropping mode</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="mangaBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Enable right-to-left reading.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Manga mode</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="webtoonBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Enable special parsing mode for Korean Webtoons.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Webtoon mode</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="rotateBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Split&lt;br/&gt;&lt;/span&gt;Double page spreads will be cut into two separate pages.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Rotate and split&lt;br/&gt;&lt;/span&gt;Double page spreads will be displayed twice. First rotated and then split. &lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Rotate&lt;br/&gt;&lt;/span&gt;Double page spreads will be rotated.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Spread splitter</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="borderBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Autodetection&lt;br/&gt;&lt;/span&gt;The color of margins fill will be detected automatically.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - White&lt;br/&gt;&lt;/span&gt;Margins will be filled with white color.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Black&lt;br/&gt;&lt;/span&gt;Margins will be filled with black color.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>W/B margins</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QCheckBox" name="gammaBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Disable automatic gamma correction.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Custom gamma</string>
</property>
</widget>
</item>
<item row="6" column="2">
<widget class="QCheckBox" name="interPanelCropBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Disabled&lt;br/&gt;&lt;/span&gt;Disabled&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Horizontal&lt;br/&gt;&lt;/span&gt;Crop empty horizontal lines.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Both&lt;br/&gt;&lt;/span&gt;Crop empty horizontal and vertical lines.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Inter-panel crop</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QCheckBox" name="colorBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Disable conversion to grayscale.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Color mode</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="qualityBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - 4 panels&lt;br/&gt;&lt;/span&gt;Zoom each corner separately.&lt;/p&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - 2 panels&lt;br/&gt;&lt;/span&gt;Zoom only the top and bottom of the page.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - 4 high-quality panels&lt;br/&gt;&lt;/span&gt;Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Panel View 4/2/HQ</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="2">
<widget class="QCheckBox" name="disableProcessingBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Do not process any image, ignore profile and processing options.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Disable processing</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="maximizeStrips">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - 1x4&lt;br/&gt;&lt;/span&gt;Keep format 1x4 panels strips.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - 2x2&lt;br/&gt;&lt;/span&gt;Turn 1x4 strips to 2x2 to maximize screen usage.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>1x4 to 2x2 strips</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLineEdit" name="authorEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::FocusPolicy::ClickFocus</enum>
</property>
<property name="toolTip">
<string>Default Author is KCC</string>
</property>
<property name="placeholderText">
<string>Default Author</string>
</property>
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="deleteBox">
<property name="toolTip">
<string>Delete input file(s) or directory. It's not recoverable!</string>
</property>
<property name="text">
<string>Delete input</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="mozJpegBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - JPEG&lt;br/&gt;&lt;/span&gt;Use JPEG files&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - force PNG&lt;br/&gt;&lt;/span&gt;Create PNG files instead JPEG&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - mozJpeg&lt;br/&gt;&lt;/span&gt;10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>JPEG/PNG/mozJpeg</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="spreadShiftBox">
<property name="toolTip">
<string>Shift first page to opposite side in landscape for two page spread alignment</string>
</property>
<property name="text">
<string>Spread shift</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="upscaleBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Nothing&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will not be resized.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Stretching&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will be resized. Aspect ratio will be not preserved.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Upscaling&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will be resized. Aspect ratio will be preserved.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Stretch/Upscale</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="outputSplit">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Automatic mode&lt;br/&gt;&lt;/span&gt;The output will be split automatically.&lt;/p&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Volume mode&lt;br/&gt;&lt;/span&gt;Every subdirectory will be considered as a separate volume.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Output split</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="noRotateBox">
<property name="toolTip">
<string>Do not rotate double page spreads in spread splitter option.</string>
</property>
<property name="text">
<string>No rotate</string>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QCheckBox" name="reduceRainbowBox">
<property name="toolTip">
<string>Reduce rainbow effect on color eink by slightly blurring images</string>
</property>
<property name="text">
<string>Rainbow blur</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="chunkSizeCheckBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700; text-decoration: underline;&quot;&gt;Unchecked&lt;br/&gt;&lt;/span&gt;Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700; text-decoration: underline;&quot;&gt;Checked&lt;/span&gt;&lt;br/&gt;Output file size specified in &amp;quot;Chunk size MB&amp;quot; before split occurs.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Chunk size</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QWidget" name="gammaWidget" native="true">
<property name="visible">
<bool>false</bool>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="gammaLabel">
<property name="text">
<string>Gamma: Auto</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="gammaSlider">
<property name="maximum">
<number>250</number>
</property>
<property name="singleStep">
<number>5</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="6" column="0">
<widget class="QWidget" name="chunkSizeWidget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Warning: chunk size greater than default may cause&lt;br/&gt;performance/battery issues, especially on older devices.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="chunkSizeLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Chunk size MB:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="chunkSizeBox">
<property name="minimum">
<number>100</number>
</property>
<property name="maximum">
<number>600</number>
</property>
<property name="value">
<number>400</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="chunkSizeWarnLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Greater than default may cause performance issues on older ereaders.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QStatusBar" name="statusBar">
@@ -607,18 +783,23 @@
<tabstop>borderBox</tabstop>
<tabstop>outputSplit</tabstop>
<tabstop>colorBox</tabstop>
<tabstop>croppingBox</tabstop>
<tabstop>mozJpegBox</tabstop>
<tabstop>maximizeStrips</tabstop>
<tabstop>croppingBox</tabstop>
<tabstop>spreadShiftBox</tabstop>
<tabstop>deleteBox</tabstop>
<tabstop>disableProcessingBox</tabstop>
<tabstop>chunkSizeBox</tabstop>
<tabstop>noRotateBox</tabstop>
<tabstop>interPanelCropBox</tabstop>
<tabstop>reduceRainbowBox</tabstop>
<tabstop>heightBox</tabstop>
<tabstop>croppingPowerSlider</tabstop>
<tabstop>editorButton</tabstop>
<tabstop>wikiButton</tabstop>
<tabstop>jobList</tabstop>
<tabstop>gammaSlider</tabstop>
<tabstop>widthBox</tabstop>
<tabstop>heightBox</tabstop>
<tabstop>croppingPowerSlider</tabstop>
</tabstops>
<resources>
<include location="KCC.qrc"/>

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

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

@@ -8,7 +8,7 @@ a = Analysis(['kcc-c2e.py'],
pathex=['.'],
binaries=[],
datas=[],
hiddenimports=[],
hiddenimports=['_cffi_backend'],
hookspath=[],
runtime_hooks=[],
excludes=[],

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

@@ -8,7 +8,7 @@ a = Analysis(['kcc-c2p.py'],
pathex=['.'],
binaries=[],
datas=[],
hiddenimports=[],
hiddenimports=['_cffi_backend'],
hookspath=[],
runtime_hooks=[],
excludes=[],

101
kcc.py
View File

@@ -18,59 +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\\'),
'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()

View File

@@ -8,7 +8,7 @@ a = Analysis(['kcc.py'],
pathex=['.'],
binaries=[],
datas=[],
hiddenimports=[],
hiddenimports=['_cffi_backend'],
hookspath=[],
runtime_hooks=[],
excludes=[],

View File

@@ -16,6 +16,11 @@
# 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.
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
@@ -25,37 +30,35 @@ from shutil import move, rmtree
from subprocess import STDOUT, PIPE
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
from distutils.version import StrictVersion
from packaging.version import Version
from raven import Client
from tempfile import gettempdir
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run_silent
from .shared import HTMLStripper, available_archive_tools, sanitizeTrace, walkLevel, subprocess_run
from . import __version__
from . import comic2ebook
from . import image
from . import metadata
from . import kindle
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 +70,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 +85,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
@@ -146,9 +153,9 @@ class VersionThread(QtCore.QThread):
latest_version = json_parser["tag_name"]
latest_version = re.sub(r'^v', "", latest_version)
if ("b" not in __version__ and StrictVersion(latest_version) > StrictVersion(__version__)) \
if ("b" not in __version__ and Version(latest_version) > Version(__version__)) \
or ("b" in __version__
and StrictVersion(latest_version) >= StrictVersion(re.sub(r'b.*', '', __version__))):
and Version(latest_version) >= Version(re.sub(r'b.*', '', __version__))):
MW.addMessage.emit('<a href="' + html_url + '"><b>The new version is available!</b></a>', 'warning',
False)
except Exception:
@@ -158,9 +165,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 +189,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 +247,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,12 +257,18 @@ 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.deleteBox.isChecked():
options.delete = True
if GUI.spreadShiftBox.isChecked():
options.spreadshift = True
if GUI.noRotateBox.isChecked():
options.norotate = True
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
options.forcepng = True
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
@@ -263,6 +278,10 @@ 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
@@ -304,13 +323,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 '
@@ -378,7 +392,7 @@ class WorkerThread(QtCore.QThread):
except Exception:
pass
MW.addMessage.emit('Processing MOBI files... <b>Done!</b>', 'info', True)
k = kindle.Kindle()
k = kindle.Kindle(options.profile)
if k.path and k.coverSupport:
for item in outputPath:
comic2ebook.options.covers[outputPath.index(item)][0].saveToKindle(
@@ -410,6 +424,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):
@@ -427,7 +443,7 @@ class WorkerThread(QtCore.QThread):
MW.modeConvert.emit(1)
class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
class SystemTrayIcon(QSystemTrayIcon):
def __init__(self):
super().__init__()
if self.isSystemTrayAvailable():
@@ -440,7 +456,7 @@ 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)
@@ -450,7 +466,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if self.needClean:
self.needClean = False
GUI.jobList.clear()
dname = QtWidgets.QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
if dname != '':
if sys.platform.startswith('win'):
dname = dname.replace('/', '\\')
@@ -462,11 +478,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if self.needClean:
self.needClean = False
GUI.jobList.clear()
if self.sevenzip:
fnames = QtWidgets.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
if self.tar or self.sevenzip:
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 != '':
@@ -478,8 +494,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'):
@@ -487,11 +503,13 @@ 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 = ['']
self.showDialog("Editor is disabled due to a lack of 7z.", 'error')
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
' to enable metadata editing.', 'warning')
if fname[0] != '':
if sys.platform.startswith('win'):
sname = fname[0].replace('/', '\\')
@@ -514,7 +532,7 @@ 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 modeChange(self, mode):
if mode == 1:
@@ -549,16 +567,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)
@@ -593,6 +611,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']:
@@ -600,11 +620,12 @@ 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'] == 'KV' or profile['Label'] in image.ProfileData.ProfilesKindlePDOC.keys():
self.addMessage('This option is intended for older Kindle models.', 'warning')
self.addMessage('On this device, quality improvement will be negligible.', 'warning')
GUI.upscaleBox.setEnabled(False)
@@ -612,6 +633,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
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))
@@ -638,13 +662,17 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.modeChange(2)
else:
self.modeChange(1)
GUI.colorBox.setChecked(profile['ForceColor'])
self.changeFormat()
GUI.gammaSlider.setValue(0)
self.changeGamma(0)
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':
@@ -664,6 +692,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()
@@ -673,16 +707,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)
@@ -690,11 +723,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':
@@ -718,8 +751,8 @@ 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 QApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier:
dname = QFileDialog.getExistingDirectory(MW, 'Select output directory', self.lastPath)
if dname != '':
if sys.platform.startswith('win'):
dname = dname.replace('/', '\\')
@@ -752,7 +785,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def display_kindlegen_missing(self):
self.addMessage(
'<a href="https://github.com/ciromattia/kcc#kindlegen"><b>Install KindleGen (link)</b></a> to enable MOBI conversion for Kindles!',
'<a href="https://github.com/ciromattia/kcc#kindlegen"><b>Install KindleGen (link)</b></a> to enable MOBI conversion for Kindles!',
'error'
)
@@ -777,18 +810,25 @@ 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,
'mozJpegBox': GUI.mozJpegBox.checkState().value,
'widthBox': GUI.widthBox.value(),
'heightBox': GUI.heightBox.value(),
'deleteBox': GUI.deleteBox.checkState().value,
'spreadShiftBox': GUI.spreadShiftBox.checkState().value,
'noRotateBox': GUI.noRotateBox.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()
@@ -802,7 +842,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.needClean = False
GUI.jobList.clear()
formats = ['.pdf']
if self.sevenzip:
if self.tar or self.sevenzip:
formats.extend(['.cb7', '.7z', '.cbz', '.zip', '.cbr', '.rar'])
if os.path.isdir(message):
GUI.jobList.addItem(message)
@@ -840,12 +880,12 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
except Exception:
pass
try:
versionCheck = subprocess_run_silent(['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:
versionCheck = line.split('V')[1].split(' ')[0]
if StrictVersion(versionCheck) < StrictVersion('2.9'):
if Version(versionCheck) < Version('2.9'):
self.addMessage('Your <a href="https://www.amazon.com/b?node=23496309011">KindleGen</a>'
' is outdated! MOBI conversion might fail.', 'warning')
break
@@ -862,7 +902,7 @@ 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.lastDevice = self.settings.value('lastDevice', 0, type=int)
@@ -895,7 +935,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
elif sys.platform.startswith('darwin'):
for element in ['editorButton', 'wikiButton', 'directoryButton', '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)
@@ -906,89 +946,114 @@ 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, 'Label': 'KO'},
"Kindle Oasis 8": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'Label': 'KV'},
"Kindle Voyage": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'Label': 'KV'},
"Kindle Oasis 9/10": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO'},
"Kindle Oasis 8": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
"Kindle Voyage": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
"Kindle Scribe": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'Label': 'KS',
'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
},
"Kindle 11": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'Label': 'K11',
'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'K11',
},
"Kindle PW 11": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'Label': 'KPW5',
'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW5',
},
"Kindle PW 7/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'Label': 'KV'},
"Kindle PW 12": {
'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO',
},
"Kindle CS 12": {
'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': True, 'Label': 'KO',
},
"Kindle PW 7/10": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
"Kindle PW 5/6": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'Label': 'KPW'},
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KPW'},
"Kindle 4/5/7/8/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'Label': 'K578'},
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'K578'},
"Kindle DX": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 2,
'DefaultUpscale': False, 'Label': 'KDX'},
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KDX'},
"Kobo Mini/Touch": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1,
'DefaultUpscale': False, 'Label': 'KoMT'},
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KoMT'},
"Kobo Glo": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1,
'DefaultUpscale': False, 'Label': 'KoG'},
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KoG'},
"Kobo Glo HD": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1,
'DefaultUpscale': False, 'Label': 'KoGHD'},
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KoGHD'},
"Kobo Aura": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1,
'DefaultUpscale': False, 'Label': 'KoA'},
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KoA'},
"Kobo Aura HD": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1,
'DefaultUpscale': True, 'Label': 'KoAHD'},
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KoAHD'},
"Kobo Aura H2O": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1,
'DefaultUpscale': True, 'Label': 'KoAH2O'},
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KoAH2O'},
"Kobo Aura ONE": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1,
'DefaultUpscale': True, 'Label': 'KoAO'},
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KoAO'},
"Kobo Clara HD": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1,
'DefaultUpscale': True, 'Label': 'KoC'},
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KoC'},
"Kobo Libra H2O": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1,
'DefaultUpscale': True, 'Label': 'KoL'},
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KoL'},
"Kobo Forma": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1,
'DefaultUpscale': True, 'Label': 'KoF'},
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KoF'},
"Kindle 1": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'Label': 'K1'},
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'K1'},
"Kindle 2": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'Label': 'K2'},
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'K2'},
"Kindle Keyboard": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'Label': 'K34'},
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'K34'},
"Kindle Touch": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'Label': 'K34'},
"Kobo Nia": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True,
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'K34'},
"Kobo Nia": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'KoN'},
"Kobo Clara 2E": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True,
"Kobo Clara 2E": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'KoC'},
"Kobo Libra 2": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True,
"Kobo Clara Colour": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': True,
'Label': 'KoCC'},
"Kobo Libra 2": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'KoL'},
"Kobo Sage": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True,
"Kobo Libra Colour": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': True,
'Label': 'KoLC'},
"Kobo Sage": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'KoS'},
"Kobo Elipsa": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True,
"Kobo Elipsa": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'KoE'},
"Other": {'PVOptions': False, 'ForceExpert': True, 'DefaultFormat': 1, 'DefaultUpscale': False,
"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 Scribe",
"Kindle 11",
"Kindle PW 11",
"Kindle 11",
"Kindle Oasis 9/10",
"Separator",
"Kobo Clara 2E",
"Kobo Clara Colour",
"Kobo Sage",
"Kobo Libra 2",
"Kobo Libra Colour",
"Kobo Elipsa",
"Kobo Nia",
"Separator",
"reMarkable 1",
"reMarkable 2",
"reMarkable Paper Pro",
"Separator",
"Other",
"Separator",
"Kindle Oasis 8",
@@ -1014,11 +1079,11 @@ 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.'
statusBarLabel = QLabel('<b><a href="https://kcc.iosphe.re/">HOMEPAGE</a> - <a href="https://github.'
'com/ciromattia/kcc/blob/master/README.md#issues--new-features--donations">DO'
'NATE</a> - <a href="http://www.mobileread.com/forums/showthread.php?t=207461'
'">FORUM</a></b>')
statusBarLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
statusBarLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
statusBarLabel.setOpenExternalLinks(True)
GUI.statusBar.addPermanentWidget(statusBarLabel, 1)
@@ -1028,11 +1093,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
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_silent(['7z'], stdout=PIPE, stderr=STDOUT)
self.sevenzip = True
except FileNotFoundError:
self.sevenzip = False
self.tar = 'tar' in available_archive_tools()
self.sevenzip = '7z' 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)
@@ -1050,6 +1114,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)
@@ -1071,6 +1136,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:
@@ -1100,6 +1167,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():
@@ -1174,15 +1244,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.5.1
# Created by: The Resource Compiler for Qt version 6.8.2
# 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\
@@ -11397,6 +11529,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\
@@ -11463,11 +11599,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\x1c\
\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 +11611,52 @@ 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\x89\x0c\xe8\xc1\x86\
\x00\x00\x01\xea\x00\x00\x00\x00\x00\x01\x00\x02{q\
\x00\x00\x01\x89\x0c\xe8\xc1\x85\
\x00\x00\x01\xd6\x00\x00\x00\x00\x00\x01\x00\x02Qv\
\x00\x00\x01\x89\x0c\xe8\xc1\x84\
\x00\x00\x01\xc2\x00\x00\x00\x00\x00\x01\x00\x02F\x13\
\x00\x00\x01\x89\x0c\xe8\xc1\x85\
\x00\x00\x00X\x00\x02\x00\x00\x00\x03\x00\x00\x00\x0c\
\x00\x00\x01\xc0\x00\x00\x00\x00\x00\x01\x00\x02.\xed\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x01\xfe\x00\x00\x00\x00\x00\x01\x00\x02\x83\x87\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x01\xea\x00\x00\x00\x00\x00\x01\x00\x02Y\x8c\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x01\xd6\x00\x00\x00\x00\x00\x01\x00\x02N)\
\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\x89\x0c\xe8\xc1\x86\
\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x01\x00\x01\x1d\x90\
\x00\x00\x01\x89\x0c\xe8\xc1\x86\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x00\xbc\x00\x00\x00\x00\x00\x01\x00\x011\xef\
\x00\x00\x01\x89\x0c\xe8\xc1\x87\
\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\x89\x0c\xe8\xc1\x9a\
\x00\x00\x02\x00\x00\x00\x00\x00\x00\x01\x00\x02\x97\xc0\
\x00\x00\x01\x89\x0c\xe8\xc1\x98\
\x00\x00\x02\x16\x00\x00\x00\x00\x00\x01\x00\x02\xa1\x1d\
\x00\x00\x01\x89\x0c\xe8\xc1\x97\
\x00\x00\x00X\x00\x02\x00\x00\x00\x07\x00\x00\x00\x14\
\x00\x00\x02B\x00\x00\x00\x00\x00\x01\x00\x02\xb5\xd3\
\x00\x00\x01\x88;p\xbcJ\
\x00\x00\x02\x14\x00\x00\x00\x00\x00\x01\x00\x02\x9f\xd6\
\x00\x00\x01\x88;p\xbcI\
\x00\x00\x02*\x00\x00\x00\x00\x00\x01\x00\x02\xa93\
\x00\x00\x01\x88;p\xbcI\
\x00\x00\x00X\x00\x02\x00\x00\x00\x07\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\x89\x0c\xe8\xc1\x9b\
\x00\x00\x01\x1e\x00\x00\x00\x00\x00\x01\x00\x01qC\
\x00\x00\x01\x89\x0c\xe8\xc1\x97\
\x00\x00\x01\x80\x00\x00\x00\x00\x00\x01\x00\x01\xca\x17\
\x00\x00\x01\x89\x0c\xe8\xc1\x98\
\x00\x00\x01f\x00\x00\x00\x00\x00\x01\x00\x01\x84\xd0\
\x00\x00\x01\x89\x0c\xe8\xc1\x97\
\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x01\x00\x01D<\
\x00\x00\x01\x89\x0c\xe8\xc1\x8f\
\x00\x00\x00\xd4\x00\x00\x00\x00\x00\x01\x00\x017\xd3\
\x00\x00\x01\x89\x0c\xe8\xc1\x96\
\x00\x00\x01@\x00\x00\x00\x00\x00\x01\x00\x01z\x9a\
\x00\x00\x01\x89\x0c\xe8\xc1\x96\
\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\x88;p\xbcI\
\x00\x00\x01\x94\x00\x00\x00\x00\x00\x01\x00\x01\xd2-\
\x00\x00\x01\x94\xb4\xd4\xf0a\
\x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x01\x8c\xe6\
\x00\x00\x01\x88;p\xbcH\
\x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x01LR\
\x00\x00\x01\x88;p\xbcF\
\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x01?\xe9\
\x00\x00\x01\x88;p\xbcH\
\x00\x00\x01T\x00\x00\x00\x00\x00\x01\x00\x01\x82\xb0\
\x00\x00\x01\x88;p\xbcH\
\x00\x00\x00X\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1d\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00h\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x89\x0c\xe8\xc1\x96\
\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.5.1
## Created by: Qt User Interface Compiler version 6.8.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -16,156 +16,63 @@ 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(450, 400)
mainWindow.resize(519, 572)
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.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.upscaleBox = QCheckBox(self.optionWidget)
self.upscaleBox.setObjectName(u"upscaleBox")
self.upscaleBox.setTristate(True)
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_2.addWidget(self.upscaleBox, 1, 1, 1, 1)
self.gridLayout.addWidget(self.jobList, 2, 0, 1, 2)
self.rotateBox = QCheckBox(self.optionWidget)
self.rotateBox.setObjectName(u"rotateBox")
self.rotateBox.setTristate(True)
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.gridLayout_2.addWidget(self.rotateBox, 0, 1, 1, 1)
self.horizontalLayout.addWidget(self.editorButton)
self.outputSplit = QCheckBox(self.optionWidget)
self.outputSplit.setObjectName(u"outputSplit")
self.wikiButton = QPushButton(self.toolWidget)
self.wikiButton.setObjectName(u"wikiButton")
self.wikiButton.setMinimumSize(QSize(0, 30))
icon2 = QIcon()
icon2.addFile(u":/Other/icons/wiki.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.wikiButton.setIcon(icon2)
self.gridLayout_2.addWidget(self.outputSplit, 2, 1, 1, 1)
self.webtoonBox = QCheckBox(self.optionWidget)
self.webtoonBox.setObjectName(u"webtoonBox")
self.gridLayout_2.addWidget(self.webtoonBox, 1, 0, 1, 1)
self.colorBox = QCheckBox(self.optionWidget)
self.colorBox.setObjectName(u"colorBox")
self.gridLayout_2.addWidget(self.colorBox, 2, 2, 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.mangaBox = QCheckBox(self.optionWidget)
self.mangaBox.setObjectName(u"mangaBox")
self.gridLayout_2.addWidget(self.mangaBox, 0, 0, 1, 1)
self.qualityBox = QCheckBox(self.optionWidget)
self.qualityBox.setObjectName(u"qualityBox")
self.qualityBox.setTristate(True)
self.gridLayout_2.addWidget(self.qualityBox, 0, 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.maximizeStrips = QCheckBox(self.optionWidget)
self.maximizeStrips.setObjectName(u"maximizeStrips")
self.gridLayout_2.addWidget(self.maximizeStrips, 3, 1, 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.deleteBox = QCheckBox(self.optionWidget)
self.deleteBox.setObjectName(u"deleteBox")
self.gridLayout_2.addWidget(self.deleteBox, 4, 1, 1, 1)
self.disableProcessingBox = QCheckBox(self.optionWidget)
self.disableProcessingBox.setObjectName(u"disableProcessingBox")
self.gridLayout_2.addWidget(self.disableProcessingBox, 4, 2, 1, 1)
self.horizontalLayout.addWidget(self.wikiButton)
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
self.gammaWidget = QWidget(self.centralWidget)
self.gammaWidget.setObjectName(u"gammaWidget")
self.gammaWidget.setVisible(False)
self.horizontalLayout_2 = QHBoxLayout(self.gammaWidget)
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
self.gammaLabel = QLabel(self.gammaWidget)
self.gammaLabel.setObjectName(u"gammaLabel")
self.horizontalLayout_2.addWidget(self.gammaLabel)
self.gammaSlider = QSlider(self.gammaWidget)
self.gammaSlider.setObjectName(u"gammaSlider")
self.gammaSlider.setMaximum(250)
self.gammaSlider.setSingleStep(5)
self.gammaSlider.setOrientation(Qt.Horizontal)
self.horizontalLayout_2.addWidget(self.gammaSlider)
self.gridLayout.addWidget(self.gammaWidget, 6, 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.horizontalLayout_3.addWidget(self.croppingPowerLabel)
self.croppingPowerSlider = QSlider(self.croppingWidget)
self.croppingPowerSlider.setObjectName(u"croppingPowerSlider")
self.croppingPowerSlider.setMaximum(200)
self.croppingPowerSlider.setSingleStep(1)
self.croppingPowerSlider.setOrientation(Qt.Horizontal)
self.horizontalLayout_3.addWidget(self.croppingPowerSlider)
self.gridLayout.addWidget(self.croppingWidget, 8, 0, 1, 2)
self.gridLayout.addWidget(self.toolWidget, 0, 0, 1, 2)
self.buttonWidget = QWidget(self.centralWidget)
self.buttonWidget.setObjectName(u"buttonWidget")
sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.buttonWidget.sizePolicy().hasHeightForWidth())
@@ -176,18 +83,18 @@ class Ui_mainWindow(object):
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)
icon3 = QIcon()
icon3.addFile(u":/Other/icons/folder_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.directoryButton.setIcon(icon3)
self.gridLayout_4.addWidget(self.directoryButton, 0, 0, 1, 1)
self.fileButton = QPushButton(self.buttonWidget)
self.fileButton.setObjectName(u"fileButton")
self.fileButton.setMinimumSize(QSize(0, 30))
icon2 = QIcon()
icon2.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Normal, QIcon.Off)
self.fileButton.setIcon(icon2)
icon4 = QIcon()
icon4.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.fileButton.setIcon(icon4)
self.gridLayout_4.addWidget(self.fileButton, 0, 3, 1, 1)
@@ -209,18 +116,18 @@ class Ui_mainWindow(object):
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)
icon5 = QIcon()
icon5.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.convertButton.setIcon(icon5)
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)
icon6 = QIcon()
icon6.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.clearButton.setIcon(icon6)
self.gridLayout_4.addWidget(self.clearButton, 0, 2, 1, 1)
@@ -233,47 +140,12 @@ class Ui_mainWindow(object):
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.NoSelection)
self.jobList.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
self.jobList.setHorizontalScrollMode(QAbstractItemView.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.AlignJustify|Qt.AlignVCenter)
self.progressBar.setAlignment(Qt.AlignmentFlag.AlignJustify|Qt.AlignmentFlag.AlignVCenter)
self.gridLayout.addWidget(self.progressBar, 1, 0, 1, 2)
@@ -285,7 +157,7 @@ class Ui_mainWindow(object):
self.gridLayout_3.setContentsMargins(0, 0, 0, 0)
self.hLabel = QLabel(self.customWidget)
self.hLabel.setObjectName(u"hLabel")
sizePolicy1 = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
sizePolicy1.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.hLabel.sizePolicy().hasHeightForWidth())
@@ -313,7 +185,228 @@ class Ui_mainWindow(object):
self.gridLayout_3.addWidget(self.heightBox, 0, 3, 1, 1)
self.gridLayout.addWidget(self.customWidget, 7, 0, 1, 2)
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")
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
sizePolicy2.setHorizontalStretch(0)
sizePolicy2.setVerticalStretch(0)
sizePolicy2.setHeightForWidth(self.preserveMarginBox.sizePolicy().hasHeightForWidth())
self.preserveMarginBox.setSizePolicy(sizePolicy2)
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.croppingBox = QCheckBox(self.optionWidget)
self.croppingBox.setObjectName(u"croppingBox")
self.croppingBox.setTristate(True)
self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
self.mangaBox = QCheckBox(self.optionWidget)
self.mangaBox.setObjectName(u"mangaBox")
self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
self.webtoonBox = QCheckBox(self.optionWidget)
self.webtoonBox.setObjectName(u"webtoonBox")
self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
self.rotateBox = QCheckBox(self.optionWidget)
self.rotateBox.setObjectName(u"rotateBox")
self.rotateBox.setTristate(True)
self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1)
self.borderBox = QCheckBox(self.optionWidget)
self.borderBox.setObjectName(u"borderBox")
self.borderBox.setTristate(True)
self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
self.gammaBox = QCheckBox(self.optionWidget)
self.gammaBox.setObjectName(u"gammaBox")
self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
self.interPanelCropBox = QCheckBox(self.optionWidget)
self.interPanelCropBox.setObjectName(u"interPanelCropBox")
self.interPanelCropBox.setTristate(True)
self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1)
self.colorBox = QCheckBox(self.optionWidget)
self.colorBox.setObjectName(u"colorBox")
self.gridLayout_2.addWidget(self.colorBox, 3, 2, 1, 1)
self.qualityBox = QCheckBox(self.optionWidget)
self.qualityBox.setObjectName(u"qualityBox")
self.qualityBox.setTristate(True)
self.gridLayout_2.addWidget(self.qualityBox, 1, 2, 1, 1)
self.disableProcessingBox = QCheckBox(self.optionWidget)
self.disableProcessingBox.setObjectName(u"disableProcessingBox")
self.gridLayout_2.addWidget(self.disableProcessingBox, 5, 2, 1, 1)
self.maximizeStrips = QCheckBox(self.optionWidget)
self.maximizeStrips.setObjectName(u"maximizeStrips")
self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
self.authorEdit = QLineEdit(self.optionWidget)
self.authorEdit.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.authorEdit, 0, 0, 1, 1)
self.deleteBox = QCheckBox(self.optionWidget)
self.deleteBox.setObjectName(u"deleteBox")
self.gridLayout_2.addWidget(self.deleteBox, 5, 1, 1, 1)
self.mozJpegBox = QCheckBox(self.optionWidget)
self.mozJpegBox.setObjectName(u"mozJpegBox")
self.mozJpegBox.setTristate(True)
self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1)
self.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.noRotateBox = QCheckBox(self.optionWidget)
self.noRotateBox.setObjectName(u"noRotateBox")
self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1)
self.reduceRainbowBox = QCheckBox(self.optionWidget)
self.reduceRainbowBox.setObjectName(u"reduceRainbowBox")
self.gridLayout_2.addWidget(self.reduceRainbowBox, 7, 2, 1, 1)
self.chunkSizeCheckBox = QCheckBox(self.optionWidget)
self.chunkSizeCheckBox.setObjectName(u"chunkSizeCheckBox")
self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 1, 1)
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
self.gammaWidget = QWidget(self.centralWidget)
self.gammaWidget.setObjectName(u"gammaWidget")
self.gammaWidget.setVisible(False)
self.horizontalLayout_2 = QHBoxLayout(self.gammaWidget)
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
self.gammaLabel = QLabel(self.gammaWidget)
self.gammaLabel.setObjectName(u"gammaLabel")
self.horizontalLayout_2.addWidget(self.gammaLabel)
self.gammaSlider = QSlider(self.gammaWidget)
self.gammaSlider.setObjectName(u"gammaSlider")
self.gammaSlider.setMaximum(250)
self.gammaSlider.setSingleStep(5)
self.gammaSlider.setOrientation(Qt.Orientation.Horizontal)
self.horizontalLayout_2.addWidget(self.gammaSlider)
self.gridLayout.addWidget(self.gammaWidget, 7, 0, 1, 2)
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_4.addWidget(self.chunkSizeLabel)
self.chunkSizeBox = QSpinBox(self.chunkSizeWidget)
self.chunkSizeBox.setObjectName(u"chunkSizeBox")
self.chunkSizeBox.setMinimum(100)
self.chunkSizeBox.setMaximum(600)
self.chunkSizeBox.setValue(400)
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.chunkSizeWidget, 6, 0, 1, 1)
mainWindow.setCentralWidget(self.centralWidget)
self.statusBar = QStatusBar(mainWindow)
@@ -334,18 +427,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)
@@ -355,71 +453,18 @@ class Ui_mainWindow(object):
def retranslateUi(self, mainWindow):
mainWindow.setWindowTitle(QCoreApplication.translate("mainWindow", u"Kindle Comic Converter", None))
#if QT_CONFIG(tooltip)
self.upscaleBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Nothing<br/></span>Images smaller than device resolution will not be resized.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Stretching<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be not preserved.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Upscaling<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be preserved.</p></body></html>", None))
self.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.upscaleBox.setText(QCoreApplication.translate("mainWindow", u"Stretch/Upscale", 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.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.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.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.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.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.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.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.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.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.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))
self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None))
self.editorButton.setText(QCoreApplication.translate("mainWindow", u"Metadata Editor", None))
self.wikiButton.setText(QCoreApplication.translate("mainWindow", u"Wiki", None))
#if QT_CONFIG(tooltip)
self.directoryButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Add directory containing JPG, PNG or GIF files to queue.<br/><span style=\" font-weight:600;\">CBR, CBZ and CB7 files inside will not be processed!</span></p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.directoryButton.setText(QCoreApplication.translate("mainWindow", u"Add directory", None))
self.directoryButton.setText(QCoreApplication.translate("mainWindow", u"Add image folder", None))
#if QT_CONFIG(tooltip)
self.fileButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Add CBR, CBZ, CB7 or PDF file to queue.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.fileButton.setText(QCoreApplication.translate("mainWindow", u"Add file", None))
self.fileButton.setText(QCoreApplication.translate("mainWindow", u"Add file(s)", None))
#if QT_CONFIG(tooltip)
self.deviceBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Target device.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
@@ -431,11 +476,6 @@ class Ui_mainWindow(object):
#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))
#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)
@@ -450,5 +490,96 @@ 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.croppingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled</span></p><p>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Margins<br/></span>Margins</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Margins + page numbers<br/></span>Margins +page numbers</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
#if QT_CONFIG(tooltip)
self.mangaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Manga mode", None))
#if QT_CONFIG(tooltip)
self.webtoonBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", None))
#if QT_CONFIG(tooltip)
self.rotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Split<br/></span>Double page spreads will be cut into two separate pages.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Rotate and split<br/></span>Double page spreads will be displayed twice. First rotated and then split. </p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate<br/></span>Double page spreads will be rotated.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None))
#if QT_CONFIG(tooltip)
self.borderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - White<br/></span>Margins will be filled with white color.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
#if QT_CONFIG(tooltip)
self.gammaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable automatic gamma correction.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.gammaBox.setText(QCoreApplication.translate("mainWindow", u"Custom gamma", None))
#if QT_CONFIG(tooltip)
self.interPanelCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled<br/></span>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Horizontal<br/></span>Crop empty horizontal lines.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Both<br/></span>Crop empty horizontal and vertical lines.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.interPanelCropBox.setText(QCoreApplication.translate("mainWindow", u"Inter-panel crop", None))
#if QT_CONFIG(tooltip)
self.colorBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", None))
#if QT_CONFIG(tooltip)
self.qualityBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - 2 panels<br/></span>Zoom only the top and bottom of the page.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 4 high-quality panels<br/></span>Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
#if QT_CONFIG(tooltip)
self.disableProcessingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Do not process any image, ignore profile and processing options.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.disableProcessingBox.setText(QCoreApplication.translate("mainWindow", u"Disable processing", None))
#if QT_CONFIG(tooltip)
self.maximizeStrips.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 1x4<br/></span>Keep format 1x4 panels strips.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 2x2<br/></span>Turn 1x4 strips to 2x2 to maximize screen usage.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None))
#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.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.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.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.noRotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"Do not rotate double page spreads in spread splitter option.", None))
#endif // QT_CONFIG(tooltip)
self.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", None))
#if QT_CONFIG(tooltip)
self.reduceRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Reduce rainbow effect on color eink by slightly blurring images", None))
#endif // QT_CONFIG(tooltip)
self.reduceRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Rainbow blur", None))
#if QT_CONFIG(tooltip)
self.chunkSizeCheckBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:700; text-decoration: underline;\">Unchecked<br/></span>Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.</p><p><span style=\" font-weight:700; text-decoration: underline;\">Checked</span><br/>Output file size specified in &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))
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.5.1
## Created by: Qt User Interface Compiler version 6.8.2
##
## 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")
@@ -117,7 +117,7 @@ class Ui_editorDialog(object):
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.statusLabel = QLabel(self.optionWidget)
self.statusLabel.setObjectName(u"statusLabel")
sizePolicy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
sizePolicy = QSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.statusLabel.sizePolicy().hasHeightForWidth())
@@ -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__ = '6.0.0'
__version__ = '7.4.1'
__license__ = 'ISC'
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
__docformat__ = 'restructuredtext en'

View File

@@ -23,7 +23,7 @@ import pathlib
import re
import sys
from argparse import ArgumentParser
from time import strftime, gmtime
from time import perf_counter, strftime, gmtime
from copy import copy
from glob import glob, escape
from re import sub
@@ -33,14 +33,14 @@ from tempfile import mkdtemp, gettempdir, TemporaryFile
from shutil import move, copytree, rmtree, copyfile
from multiprocessing import Pool
from uuid import uuid4
from natsort import os_sorted
from natsort import os_sort_keygen
from slugify import slugify as slugify_ext
from PIL import Image, ImageFile
from subprocess import STDOUT, PIPE
from subprocess import STDOUT, PIPE, CalledProcessError
from psutil import virtual_memory, disk_usage
from html import escape as hescape
from .shared import md5Checksum, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run_silent
from .shared import available_archive_tools, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run
from . import comic2panel
from . import image
from . import comicarchive
@@ -51,7 +51,7 @@ from . import kindle
from . import __version__
ImageFile.LOAD_TRUNCATED_IMAGES = True
OS_SORT_KEY = os_sort_keygen()
def main(argv=None):
global options
@@ -78,14 +78,14 @@ def main(argv=None):
def buildHTML(path, imgfile, imgfilepath):
imgfilepath = md5Checksum(imgfilepath)
key = pathlib.Path(imgfilepath).name
filename = getImageFileName(imgfile)
deviceres = options.profileData[1]
if not options.noprocessing and "Rotated" in options.imgMetadata[imgfilepath]:
if not options.noprocessing and "Rotated" in options.imgMetadata[key]:
rotatedPage = True
else:
rotatedPage = False
if not options.noprocessing and "BlackBackground" in options.imgMetadata[imgfilepath]:
if not options.noprocessing and "BlackBackground" in options.imgMetadata[key]:
additionalStyle = 'background-color:#000000;'
else:
additionalStyle = ''
@@ -119,6 +119,8 @@ def buildHTML(path, imgfile, imgfilepath):
"</head>\n",
"<body style=\"" + additionalStyle + "\">\n",
"<div style=\"text-align:center;top:" + getTopMargin(deviceres, imgsizeframe) + "%;\">\n",
# this display none div fixes formatting issues with virtual panel mode, for some reason
'<div style="display:none;">.</div>\n',
"<img width=\"" + str(imgsizeframe[0]) + "\" height=\"" + str(imgsizeframe[1]) + "\" ",
"src=\"", "../" * backref, "Images/", postfix, imgfile, "\"/>\n</div>\n"])
if options.iskindle and options.panelview:
@@ -216,7 +218,7 @@ def buildNCX(dstdir, title, chapters, chapternames):
folder = chapter[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\')
filename = getImageFileName(os.path.join(folder, chapter[1]))
navID = folder.replace('/', '_').replace('\\', '_')
if options.chapters:
if options.comicinfo_chapters:
title = chapternames[chapter[1]]
navID = filename[0].replace('/', '_').replace('\\', '_')
elif os.path.basename(folder) != "Text":
@@ -244,7 +246,7 @@ def buildNAV(dstdir, title, chapters, chapternames):
for chapter in chapters:
folder = chapter[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\')
filename = getImageFileName(os.path.join(folder, chapter[1]))
if options.chapters:
if options.comicinfo_chapters:
title = chapternames[chapter[1]]
elif os.path.basename(folder) != "Text":
title = chapternames[os.path.basename(folder)]
@@ -256,7 +258,7 @@ def buildNAV(dstdir, title, chapters, chapternames):
for chapter in chapters:
folder = chapter[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\')
filename = getImageFileName(os.path.join(folder, chapter[1]))
if options.chapters:
if options.comicinfo_chapters:
title = chapternames[chapter[1]]
elif os.path.basename(folder) != "Text":
title = chapternames[os.path.basename(folder)]
@@ -283,9 +285,9 @@ def buildOPF(dstdir, title, filelist, cover=None):
"<dc:identifier id=\"BookID\">urn:uuid:", options.uuid, "</dc:identifier>\n",
"<dc:contributor id=\"contributor\">KindleComicConverter-" + __version__ + "</dc:contributor>\n"])
if len(options.summary) > 0:
f.writelines(["<dc:description>", options.summary, "</dc:description>\n"])
f.writelines(["<dc:description>", hescape(options.summary), "</dc:description>\n"])
for author in options.authors:
f.writelines(["<dc:creator>", author, "</dc:creator>\n"])
f.writelines(["<dc:creator>", hescape(author), "</dc:creator>\n"])
f.writelines(["<meta property=\"dcterms:modified\">" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "</meta>\n",
"<meta name=\"cover\" content=\"cover\"/>\n"])
if options.iskindle and options.profile != 'Custom':
@@ -298,22 +300,15 @@ def buildOPF(dstdir, title, filelist, cover=None):
"<meta name=\"zero-margin\" content=\"true\"/>\n",
"<meta name=\"ke-border-color\" content=\"#FFFFFF\"/>\n",
"<meta name=\"ke-border-width\" content=\"0\"/>\n",
"<meta property=\"rendition:spread\">landscape</meta>\n",
"<meta property=\"rendition:layout\">pre-paginated</meta>\n",
"<meta name=\"orientation-lock\" content=\"none\"/>\n"])
if options.kfx:
f.writelines(["<meta name=\"region-mag\" content=\"false\"/>\n"])
else:
f.writelines(["<meta name=\"region-mag\" content=\"true\"/>\n"])
elif options.supportSyntheticSpread:
f.writelines([
"<meta property=\"rendition:spread\">landscape</meta>\n",
"<meta property=\"rendition:layout\">pre-paginated</meta>\n"
])
else:
f.writelines(["<meta property=\"rendition:orientation\">portrait</meta>\n",
"<meta property=\"rendition:spread\">portrait</meta>\n",
"<meta property=\"rendition:layout\">pre-paginated</meta>\n"])
f.writelines([
"<meta property=\"rendition:spread\">landscape</meta>\n",
"<meta property=\"rendition:layout\">pre-paginated</meta>\n"
])
f.writelines(["</metadata>\n<manifest>\n<item id=\"ncx\" href=\"toc.ncx\" ",
"media-type=\"application/x-dtbncx+xml\"/>\n",
"<item id=\"nav\" href=\"nav.xhtml\" ",
@@ -358,55 +353,68 @@ def buildOPF(dstdir, title, filelist, cover=None):
else:
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
pageside = "left"
if options.iskindle or options.supportSyntheticSpread:
for entry in reflist:
if options.righttoleft:
if entry.endswith("-b"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("right"))
)
pageside = "right"
elif entry.endswith("-c"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("left"))
)
pageside = "right"
else:
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty(pageside))
)
if pageside == "right":
pageside = "left"
else:
pageside = "right"
if options.spreadshift:
if pageside == "right":
pageside = "left"
else:
pageside = "right"
for entry in reflist:
if options.righttoleft:
if entry.endswith("-kcc-a"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("center"))
)
pageside = "right"
elif entry.endswith("-kcc-b"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("right"))
)
pageside = "right"
elif entry.endswith("-kcc-c"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("left"))
)
pageside = "right"
else:
if entry.endswith("-b"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("left"))
)
pageside = "left"
elif entry.endswith("-c"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("right"))
)
pageside = "left"
else:
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty(pageside))
)
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty(pageside))
)
if pageside == "right":
pageside = "left"
else:
pageside = "right"
else:
if entry.endswith("-kcc-a"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("center"))
)
pageside = "left"
elif entry.endswith("-kcc-b"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("left"))
)
pageside = "left"
elif entry.endswith("-kcc-c"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("right"))
)
pageside = "left"
else:
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty(pageside))
)
if pageside == "right":
pageside = "left"
else:
pageside = "right"
else:
for entry in reflist:
f.write("<itemref idref=\"page_" + entry + "\"/>\n")
f.write("</spine>\n</package>\n")
f.close()
os.mkdir(os.path.join(dstdir, 'META-INF'))
@@ -419,8 +427,7 @@ def buildOPF(dstdir, title, filelist, cover=None):
"</container>"])
f.close()
def buildEPUB(path, chapternames, tomenumber):
def buildEPUB(path, chapternames, tomenumber, ischunked):
filelist = []
chapterlist = []
cover = None
@@ -500,21 +507,30 @@ def buildEPUB(path, chapternames, tomenumber):
"display: none;\n",
"}\n"])
f.close()
build_html_start = perf_counter()
for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')):
chapter = False
dirnames, filenames = walkSort(dirnames, filenames)
for afile in filenames:
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile)))
if not chapter:
chapterlist.append((dirpath.replace('Images', 'Text'), filelist[-1][1]))
chapter = True
if cover is None:
cover = os.path.join(os.path.join(path, 'OEBPS', 'Images'),
'cover' + getImageFileName(filelist[-1][1])[1])
options.covers.append((image.Cover(os.path.join(filelist[-1][0], filelist[-1][1]), cover, options,
try:
cover = os.path.join(os.path.join(path, 'OEBPS', 'Images'),
'cover' + getImageFileName(afile)[1])
except Exception as e:
raise UserWarning(f"{afile}: {e}")
options.covers.append((image.Cover(os.path.join(dirpath, afile), cover, options,
tomenumber), options.uuid))
if not chapter:
chapterlist.append((dirpath.replace('Images', 'Text'), afile))
chapter = True
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile)))
build_html_end = perf_counter()
print(f"buildHTML: {build_html_end - build_html_start} seconds")
# Overwrite chapternames if tree is flat and ComicInfo.xml has bookmarks
if not chapternames and options.chapters:
if ischunked:
options.comicinfo_chapters = []
if not chapternames and options.comicinfo_chapters:
chapterlist = []
global_diff = 0
@@ -527,7 +543,7 @@ def buildEPUB(path, chapternames, tomenumber):
elif options.splitter == 2:
diff_delta = 2
for aChapter in options.chapters:
for aChapter in options.comicinfo_chapters:
pageid = aChapter[0]
cur_diff = global_diff
global_diff = 0
@@ -550,7 +566,6 @@ def imgDirectoryProcessing(path):
workerPool = Pool(maxtasksperchild=100)
workerOutput = []
options.imgMetadata = {}
options.imgOld = []
work = []
pagenumber = 0
for dirpath, _, filenames in os.walk(path):
@@ -560,19 +575,23 @@ def imgDirectoryProcessing(path):
if GUI:
GUI.progressBarTick.emit(str(pagenumber))
if len(work) > 0:
img_processing_start = perf_counter()
for i in work:
workerPool.apply_async(func=imgFileProcessing, args=(i,), callback=imgFileProcessingTick)
workerPool.close()
workerPool.join()
img_processing_end = perf_counter()
print(f"imgFileProcessing: {img_processing_end - img_processing_start} seconds")
# macOS 15 likes to add ._ files after multiprocessing
dot_clean(path)
if GUI and not GUI.conversionAlive:
rmtree(os.path.join(path, '..', '..'), True)
raise UserWarning("Conversion interrupted.")
if len(workerOutput) > 0:
rmtree(os.path.join(path, '..', '..'), True)
raise RuntimeError("One of workers crashed. Cause: " + workerOutput[0][0], workerOutput[0][1])
for file in options.imgOld:
if os.path.isfile(file):
os.remove(file)
else:
rmtree(os.path.join(path, '..', '..'), True)
raise UserWarning("Source directory is empty.")
@@ -586,7 +605,6 @@ def imgFileProcessingTick(output):
for page in output:
if page is not None:
options.imgMetadata[page[0]] = page[1]
options.imgOld.append(page[2])
if GUI:
GUI.progressBarTick.emit('tick')
if not GUI.conversionAlive:
@@ -604,10 +622,13 @@ def imgFileProcessing(work):
img = image.ComicPage(opt, *i)
if opt.cropping == 2 and not opt.webtoon:
img.cropPageNumber(opt.croppingp, opt.croppingm)
if opt.cropping > 0 and not opt.webtoon:
if opt.cropping == 1 and not opt.webtoon:
img.cropMargin(opt.croppingp, opt.croppingm)
if opt.interpanelcrop > 0:
img.cropInterPanelEmptySections("horizontal" if opt.interpanelcrop == 1 else "both")
img.autocontrastImage()
img.resizeImage()
img.optimizeForDisplay(opt.reducerainbow)
if opt.forcepng and not opt.forcecolor:
img.quantizeImage()
output.append(img.saveToDir())
@@ -620,7 +641,7 @@ def getWorkFolder(afile):
if os.path.isdir(afile):
if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5:
raise UserWarning("Not enough disk space to perform conversion.")
workdir = mkdtemp('', 'KCC-')
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
try:
os.rmdir(workdir)
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
@@ -636,23 +657,34 @@ def getWorkFolder(afile):
if afile.lower().endswith('.pdf'):
pdf = pdfjpgextract.PdfJpgExtract(afile)
path, njpg = pdf.extract()
workdir = path
sanitizePermissions(path)
if njpg == 0:
rmtree(path, True)
raise UserWarning("Failed to extract images from PDF file.")
else:
workdir = mkdtemp('', 'KCC-')
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
try:
cbx = comicarchive.ComicArchive(afile)
path = cbx.extract(workdir)
sanitizePermissions(path)
tdir = os.listdir(workdir)
if len(tdir) == 2 and 'ComicInfo.xml' in tdir:
tdir.remove('ComicInfo.xml')
if os.path.isdir(os.path.join(workdir, tdir[0])):
os.replace(
os.path.join(workdir, 'ComicInfo.xml'),
os.path.join(workdir, tdir[0], 'ComicInfo.xml')
)
if len(tdir) == 1 and os.path.isdir(os.path.join(workdir, tdir[0])):
path = os.path.join(workdir, tdir[0])
except OSError as e:
rmtree(workdir, True)
raise UserWarning(e)
else:
raise UserWarning("Failed to open source file/directory.")
sanitizePermissions(path)
newpath = mkdtemp('', 'KCC-')
copytree(path, os.path.join(newpath, 'OEBPS', 'Images'))
rmtree(path, True)
newpath = mkdtemp('', 'KCC-', os.path.dirname(afile))
os.renames(path, os.path.join(newpath, 'OEBPS', 'Images'))
return newpath
@@ -660,7 +692,11 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
if srcpath[-1] == os.path.sep:
srcpath = srcpath[:-1]
if 'Ko' in options.profile and options.format == 'EPUB':
ext = '.kepub.epub'
if options.noKepub:
# Just use normal epub extension if no_kepub option is true
ext = '.epub'
else:
ext = '.kepub.epub'
if wantedname is not None:
if wantedname.endswith(ext):
filename = os.path.abspath(wantedname)
@@ -689,7 +725,7 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
def getComicInfo(path, originalpath):
xmlPath = os.path.join(path, 'ComicInfo.xml')
options.chapters = []
options.comicinfo_chapters = []
options.summary = ''
titleSuffix = ''
if options.title == 'defaulttitle':
@@ -712,11 +748,9 @@ def getComicInfo(path, originalpath):
except Exception:
os.remove(xmlPath)
return
if xml.data['Title']:
options.title = hescape(xml.data['Title'])
elif defaultTitle:
if defaultTitle:
if xml.data['Series']:
options.title = hescape(xml.data['Series'])
options.title = xml.data['Series']
if xml.data['Volume']:
titleSuffix += ' V' + xml.data['Volume'].zfill(2)
if xml.data['Number']:
@@ -726,16 +760,16 @@ def getComicInfo(path, originalpath):
options.authors = []
for field in ['Writers', 'Pencillers', 'Inkers', 'Colorists']:
for person in xml.data[field]:
options.authors.append(hescape(person))
options.authors.append(person)
if len(options.authors) > 0:
options.authors = list(set(options.authors))
options.authors.sort()
else:
options.authors = ['KCC']
if xml.data['Bookmarks'] and options.batchsplit == 0:
options.chapters = xml.data['Bookmarks']
if xml.data['Bookmarks']:
options.comicinfo_chapters = xml.data['Bookmarks']
if xml.data['Summary']:
options.summary = hescape(xml.data['Summary'])
options.summary = xml.data['Summary']
os.remove(xmlPath)
@@ -766,22 +800,22 @@ def getPanelViewSize(deviceres, size):
def sanitizeTree(filetree):
chapterNames = {}
for root, dirs, files in os.walk(filetree, False):
for i, name in enumerate(os_sorted(files)):
page = 1
for root, dirs, files in os.walk(filetree):
dirs.sort(key=OS_SORT_KEY)
files.sort(key=OS_SORT_KEY)
for name in files:
splitname = os.path.splitext(name)
# file needs kcc at front AND back to avoid renaming issues
slugified = f'kcc-{i:04}'
for suffix in '-KCC', '-KCC-A', '-KCC-B', '-KCC-C':
if splitname[0].endswith(suffix):
slugified += suffix.lower()
break
# 9999 page limit
slugified = f'kcc-{page:04}'
page += 1
newKey = os.path.join(root, slugified + splitname[1])
key = os.path.join(root, name)
if key != newKey:
os.replace(key, newKey)
for name in dirs:
for i, name in enumerate(dirs):
tmpName = name
slugified = slugify(name)
while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper():
@@ -791,40 +825,63 @@ def sanitizeTree(filetree):
key = os.path.join(root, name)
if key != newKey:
os.replace(key, newKey)
dirs[i] = newKey
return chapterNames
def flattenTree(filetree):
for root, dirs, files in os.walk(filetree, topdown=False):
for name in files:
os.rename(os.path.join(root, name), os.path.join(filetree, name))
for name in dirs:
os.rmdir(os.path.join(root, name))
def sanitizePermissions(filetree):
for root, dirs, files in os.walk(filetree, False):
for name in files:
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD)
for name in dirs:
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC)
# clean dot from original file
dot_clean(filetree)
def dot_clean(filetree):
for root, _, files in os.walk(filetree, topdown=False):
for name in files:
if name.startswith('._'):
os.remove(os.path.join(root, name))
def splitDirectory(path):
def chunk_directory(path):
level = -1
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
for f in files:
if f.endswith('.jpg') or f.endswith('.jpeg') or f.endswith('.png') or f.endswith('.gif') or \
f.endswith('.webp'):
# Windows MAX_LENGTH = 260 plus some buffer
if len(os.path.join(root, f)) > 180:
flattenTree(os.path.join(path, 'OEBPS', 'Images'))
level = 1
break
if getImageFileName(f):
newLevel = os.path.join(root, f).replace(os.path.join(path, 'OEBPS', 'Images'), '').count(os.sep)
if level != -1 and level != newLevel:
level = 0
flattenTree(os.path.join(path, 'OEBPS', 'Images'))
level = 1
break
else:
level = newLevel
if level > 0:
splitter = splitProcess(os.path.join(path, 'OEBPS', 'Images'), level)
parent = pathlib.Path(path).parent
chunker = chunk_process(os.path.join(path, 'OEBPS', 'Images'), level, parent)
path = [path]
for tome in splitter:
for tome in chunker:
path.append(tome)
return path
else:
raise UserWarning('Unsupported directory structure.')
def splitProcess(path, mode):
def chunk_process(path, mode, parent):
output = []
currentSize = 0
currentTarget = path
@@ -836,6 +893,14 @@ def splitProcess(path, mode):
targetSize = 419430400
if options.batchsplit == 2 and mode == 2:
mode = 3
if options.batchsplit == 1 and mode == 2:
with os.scandir(path) as it:
for entry in it:
if not entry.name.startswith('.') and entry.is_dir():
if getDirectorySize(os.path.join(path, entry)) > targetSize:
flattenTree(path)
mode = 1
break
if mode < 3:
for root, dirs, files in walkLevel(path, 0):
for name in files if mode == 1 else dirs:
@@ -844,7 +909,7 @@ def splitProcess(path, mode):
else:
size = getDirectorySize(os.path.join(root, name))
if currentSize + size > targetSize:
currentTarget, pathRoot = createNewTome()
currentTarget, pathRoot = createNewTome(parent)
output.append(pathRoot)
currentSize = size
else:
@@ -856,15 +921,14 @@ def splitProcess(path, mode):
for root, dirs, _ in walkLevel(path, 0):
for name in dirs:
if not firstTome:
currentTarget, pathRoot = createNewTome()
currentTarget, pathRoot = createNewTome(parent)
output.append(pathRoot)
move(os.path.join(root, name), os.path.join(currentTarget, name))
else:
firstTome = False
return output
def detectCorruption(tmppath, orgpath):
def detectSuboptimalProcessing(tmppath, orgpath):
imageNumber = 0
imageSmaller = 0
alreadyProcessed = False
@@ -880,9 +944,6 @@ def detectCorruption(tmppath, orgpath):
raise RuntimeError('Image file %s is corrupted.' % pathOrg)
try:
img = Image.open(path)
img.verify()
img = Image.open(path)
img.load()
imageNumber += 1
if options.profileData[1][0] > img.size[0] and options.profileData[1][1] > img.size[1]:
imageSmaller += 1
@@ -893,14 +954,22 @@ def detectCorruption(tmppath, orgpath):
else:
raise RuntimeError('Image file %s is corrupted. Error: %s' % (pathOrg, str(err)))
else:
os.remove(os.path.join(root, name))
try:
if os.path.exists(os.path.join(root, name)):
os.remove(os.path.join(root, name))
except OSError as e:
raise RuntimeError(f"{name}: {e}")
# remove empty nested folders
for root, dirs, files in os.walk(tmppath, False):
if not files and not dirs:
os.rmdir(root)
if alreadyProcessed:
print("WARNING: Source files are probably created by KCC. The second conversion will decrease quality.")
if GUI:
GUI.addMessage.emit('Source files are probably created by KCC. The second conversion will decrease quality.'
, 'warning', False)
GUI.addMessage.emit('', '', False)
if imageSmaller > imageNumber * 0.25 and not options.upscale and not options.stretch:
if imageSmaller > imageNumber * 0.25 and not options.upscale and not options.stretch and options.profile != 'KS':
print("WARNING: More than 25% of images are smaller than target device resolution. "
"Consider enabling stretching or upscaling to improve readability.")
if GUI:
@@ -909,8 +978,8 @@ def detectCorruption(tmppath, orgpath):
GUI.addMessage.emit('', '', False)
def createNewTome():
tomePathRoot = mkdtemp('', 'KCC-')
def createNewTome(parent):
tomePathRoot = mkdtemp('', 'KCC-', parent)
tomePath = os.path.join(tomePathRoot, 'OEBPS', 'Images')
os.makedirs(tomePath)
return tomePath, tomePathRoot
@@ -923,17 +992,27 @@ def slugify(value):
def makeZIP(zipfilename, basedir, isepub=False):
start = perf_counter()
zipfilename = os.path.abspath(zipfilename) + '.zip'
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
if isepub:
zipOutput.writestr('mimetype', 'application/epub+zip', ZIP_STORED)
for dirpath, _, filenames in os.walk(basedir):
for name in filenames:
path = os.path.normpath(os.path.join(dirpath, name))
aPath = os.path.normpath(os.path.join(dirpath.replace(basedir, ''), name))
if os.path.isfile(path):
zipOutput.write(path, aPath)
zipOutput.close()
if '7z' in available_archive_tools():
if isepub:
mimetypeFile = open(os.path.join(basedir, 'mimetype'), 'w')
mimetypeFile.write('application/epub+zip')
mimetypeFile.close()
subprocess_run(['7z', 'a', '-tzip', zipfilename, os.path.join(basedir, "*")], capture_output=True, check=True)
else:
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
if isepub:
zipOutput.writestr('mimetype', 'application/epub+zip', ZIP_STORED)
for dirpath, _, filenames in os.walk(basedir):
for name in filenames:
path = os.path.normpath(os.path.join(dirpath, name))
aPath = os.path.normpath(os.path.join(dirpath.replace(basedir, ''), name))
if os.path.isfile(path):
zipOutput.write(path, aPath)
zipOutput.close()
end = perf_counter()
print(f"makeZIP time: {end - start} seconds")
return zipfilename
@@ -951,8 +1030,7 @@ def makeParser():
help="Full path to comic folder or file(s) to be processed.")
main_options.add_argument("-p", "--profile", action="store", dest="profile", default="KV",
help="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)"
help=f"Device profile (Available options: {', '.join(image.ProfileData.Profiles.keys())})"
" [Default=KV]")
main_options.add_argument("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
help="Manga style (right-to-left reading and splitting)")
@@ -975,9 +1053,15 @@ def makeParser():
output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto",
help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) "
"[Default=Auto]")
output_options.add_argument("--nokepub", action="store_true", dest="noKepub", default=False,
help="If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'")
output_options.add_argument("-b", "--batchsplit", type=int, dest="batchsplit", default="0",
help="Split output into multiple files. 0: Don't split 1: Automatic mode "
"2: Consider every subdirectory as separate volume [Default=0]")
output_options.add_argument("--spreadshift", action="store_true", dest="spreadshift", default=False,
help="Shift first page to opposite side in landscape for spread alignment")
output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False,
help="Do not rotate double page spreads in spread splitter option.")
processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False,
help="Do not modify image and ignore any profil or processing option")
@@ -993,14 +1077,20 @@ def makeParser():
help="Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]")
processing_options.add_argument("--cp", "--croppingpower", type=float, dest="croppingp", default="1.0",
help="Set cropping power [Default=1.0]")
processing_options.add_argument("--preservemargin", type=int, dest="preservemargin", default="0",
help="After calculating crop, back up specified percentage amount. [Default=0]")
processing_options.add_argument("--cm", "--croppingminimum", type=float, dest="croppingm", default="0.0",
help="Set cropping minimum area ratio [Default=0.0]")
processing_options.add_argument("--ipc", "--interpanelcrop", type=int, dest="interpanelcrop", default="0",
help="Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]")
processing_options.add_argument("--blackborders", action="store_true", dest="black_borders", default=False,
help="Disable autodetection and force black borders")
processing_options.add_argument("--whiteborders", action="store_true", dest="white_borders", default=False,
help="Disable autodetection and force white borders")
processing_options.add_argument("--forcecolor", action="store_true", dest="forcecolor", default=False,
help="Don't convert images to grayscale")
output_options.add_argument("--reducerainbow", action="store_true", dest="reducerainbow", default=False,
help="Reduce rainbow effect on color eink by slightly blurring images.")
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
help="Create PNG files instead JPEG")
processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False,
@@ -1032,27 +1122,27 @@ def checkOptions(options):
options.format = 'EPUB'
if options.batchsplit != 2:
options.batchsplit = 1
if options.format == 'MOBI+EPUB-200MB':
options.keep_epub = True
options.targetsize = 195
options.format = 'MOBI'
if options.batchsplit != 2:
options.batchsplit = 1
if options.format == 'MOBI+EPUB':
options.keep_epub = True
options.format = 'MOBI'
options.kfx = False
options.supportSyntheticSpread = False
if options.format == 'Auto':
if options.profile in ['K1', 'K2', 'K34', 'K578', 'KPW', 'KPW5', 'KV', 'KO', 'K11', 'KS']:
options.format = 'MOBI'
elif options.profile in ['OTHER', 'KoMT', 'KoG', 'KoGHD', 'KoA', 'KoAHD', 'KoAH2O', 'KoAO',
'KoN', 'KoC', 'KoL', 'KoF', 'KoS', 'KoE']:
options.format = 'EPUB'
elif options.profile in ['KDX']:
if options.profile in ['KDX']:
options.format = 'CBZ'
if options.profile in ['K1', 'K2', 'K34', 'K578', 'KPW', 'KPW5', 'KV', 'KO', 'K11', 'KS']:
elif options.profile in image.ProfileData.ProfilesKindle.keys():
options.format = 'MOBI'
else:
options.format = 'EPUB'
if options.profile in image.ProfileData.ProfilesKindle.keys():
options.iskindle = True
elif options.profile in ['OTHER', 'KoMT', 'KoG', 'KoGHD', 'KoA', 'KoAHD', 'KoAH2O', 'KoAO', 'KoN', 'KoC', 'KoL', 'KoF', 'KoS', 'KoE']:
else:
options.isKobo = True
# Other Kobo devices probably support synthetic spreads as well, but
# they haven't been tested.
if options.profile in ['KoF']:
options.supportSyntheticSpread = True
if options.white_borders:
options.bordersColor = 'white'
if options.black_borders:
@@ -1064,6 +1154,9 @@ def checkOptions(options):
if options.profile == 'K1' or options.profile == 'K2' or options.profile == 'K34' or options.profile == 'KDX':
options.panelview = False
options.hq = False
if options.profile == 'KV' or options.profile in image.ProfileData.ProfilesKindlePDOC.keys():
options.panelview = False
options.hq = False
# Webtoon mode mandatory options
if options.webtoon:
options.panelview = False
@@ -1098,10 +1191,6 @@ def checkOptions(options):
image.ProfileData.Profiles["Custom"] = newProfile
options.profile = "Custom"
options.profileData = image.ProfileData.Profiles[options.profile]
# kindle scribe conversion to mobi is limited in resolution by kindlegen, same with send to kindle and epub
if options.profile == 'KS' and (options.format == 'MOBI' or options.format == 'EPUB'):
options.profileData = list(options.profileData)
options.profileData[1] = (1440, 1920)
return options
@@ -1109,14 +1198,12 @@ def checkTools(source):
source = source.upper()
if source.endswith('.CB7') or source.endswith('.7Z') or source.endswith('.RAR') or source.endswith('.CBR') or \
source.endswith('.ZIP') or source.endswith('.CBZ'):
try:
subprocess_run_silent(['7z'], stdout=PIPE, stderr=STDOUT)
except FileNotFoundError:
if '7z' not in available_archive_tools():
print('ERROR: 7z is missing!')
sys.exit(1)
if options.format == 'MOBI':
try:
subprocess_run_silent(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT)
subprocess_run(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT, check=True)
except FileNotFoundError:
print('ERROR: KindleGen is missing!')
sys.exit(1)
@@ -1141,6 +1228,7 @@ def checkPre(source):
def makeBook(source, qtgui=None):
start = perf_counter()
global GUI
GUI = qtgui
if GUI:
@@ -1152,7 +1240,8 @@ def makeBook(source, qtgui=None):
path = getWorkFolder(source)
print("Checking images...")
getComicInfo(os.path.join(path, "OEBPS", "Images"), source)
detectCorruption(os.path.join(path, "OEBPS", "Images"), source)
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
chapterNames = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
if options.webtoon:
y = image.ProfileData.Profiles[options.profile][1][1]
comic2panel.main(['-y ' + str(y), '-i', '-m', path], qtgui)
@@ -1165,9 +1254,8 @@ def makeBook(source, qtgui=None):
imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"))
if GUI:
GUI.progressBarTick.emit('1')
chapterNames = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
if options.batchsplit > 0:
tomes = splitDirectory(path)
tomes = chunk_directory(path)
else:
tomes = [path]
filepath = []
@@ -1198,10 +1286,11 @@ def makeBook(source, qtgui=None):
makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"))
else:
print("Creating EPUB file...")
buildEPUB(tome, chapterNames, tomeNumber)
if len(tomes) > 1:
buildEPUB(tome, chapterNames, tomeNumber, True)
filepath.append(getOutputFilename(source, options.output, '.epub', ' ' + str(tomeNumber)))
else:
buildEPUB(tome, chapterNames, tomeNumber, False)
filepath.append(getOutputFilename(source, options.output, '.epub', ''))
makeZIP(tome + '_comic', tome, True)
copyfile(tome + '_comic.zip', filepath[-1])
@@ -1224,7 +1313,7 @@ def makeBook(source, qtgui=None):
print('Error: KindleGen failed to create MOBI!')
print(errors)
return filepath
k = kindle.Kindle()
k = kindle.Kindle(options.profile)
if k.path and k.coverSupport:
print("Kindle detected. Uploading covers...")
for i in filepath:
@@ -1242,16 +1331,19 @@ def makeBook(source, qtgui=None):
elif os.path.isdir(source):
rmtree(source)
end = perf_counter()
print(f"makeBook: {end - start} seconds")
return filepath
def makeMOBIFix(item, uuid):
is_pdoc = options.profile in image.ProfileData.ProfilesKindlePDOC.keys()
if not options.keep_epub:
os.remove(item)
mobiPath = item.replace('.epub', '.mobi')
move(mobiPath, mobiPath + '_toclean')
try:
dualmetafix.DualMobiMetaFix(mobiPath + '_toclean', mobiPath, bytes(uuid, 'UTF-8'))
dualmetafix.DualMobiMetaFix(mobiPath + '_toclean', mobiPath, bytes(uuid, 'UTF-8'), is_pdoc)
return [True]
except Exception as err:
return [False, format(err)]
@@ -1273,28 +1365,29 @@ def makeMOBIWorker(item):
kindlegenError = ''
try:
if os.path.getsize(item) < 629145600:
output = subprocess_run_silent(['kindlegen', '-dont_append_source', '-locale', 'en', item],
stdout=PIPE, stderr=STDOUT, encoding='UTF-8')
for line in output.stdout.splitlines():
# ERROR: Generic error
if "Error(" in line:
kindlegenErrorCode = 1
kindlegenError = line
# ERROR: EPUB too big
if ":E23026:" in line:
kindlegenErrorCode = 23026
if kindlegenErrorCode > 0:
break
if ":I1036: Mobi file built successfully" in line:
break
output = subprocess_run(['kindlegen', '-dont_append_source', '-locale', 'en', item],
stdout=PIPE, stderr=STDOUT, encoding='UTF-8', errors='ignore', check=True)
else:
# ERROR: EPUB too big
kindlegenErrorCode = 23026
return [kindlegenErrorCode, kindlegenError, item]
except Exception as err:
except CalledProcessError as err:
for line in err.stdout.splitlines():
# ERROR: Generic error
if "Error(" in line:
kindlegenErrorCode = 1
kindlegenError = line
# ERROR: EPUB too big
if ":E23026:" in line:
kindlegenErrorCode = 23026
if kindlegenErrorCode > 0:
break
if ":I1036: Mobi file built successfully" in line:
break
# ERROR: KCC unknown generic error
kindlegenErrorCode = 1
kindlegenError = format(err)
if kindlegenErrorCode == 0:
kindlegenErrorCode = err.returncode
kindlegenError = err.stdout
return [kindlegenErrorCode, kindlegenError, item]

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,15 +18,14 @@
# PERFORMANCE OF THIS SOFTWARE.
#
from functools import cached_property
import os
import platform
import subprocess
import distro
from shutil import move
from subprocess import STDOUT, PIPE
from subprocess import STDOUT, PIPE, CalledProcessError
from xml.dom.minidom import parseString
from xml.parsers.expat import ExpatError
from .shared import subprocess_run_silent
from .shared import subprocess_run
EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.'
@@ -34,53 +33,80 @@ EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KC
class ComicArchive:
def __init__(self, filepath):
self.filepath = filepath
self.type = None
if not os.path.isfile(self.filepath):
raise OSError('File not found.')
process = subprocess_run_silent(['7z', 'l', '-y', '-p1', self.filepath], stderr=STDOUT, stdout=PIPE)
for line in process.stdout.splitlines():
if b'Type =' in line:
self.type = line.rstrip().decode().split(' = ')[1].upper()
break
if process.returncode != 0 and distro.id() == 'fedora':
process = subprocess_run_silent(['unrar', 'l', '-y', '-p1', self.filepath], stderr=STDOUT, stdout=PIPE)
for line in process.stdout.splitlines():
if b'Details: ' in line:
self.type = line.rstrip().decode().split(' ')[1].upper()
break
if process.returncode != 0:
raise OSError(EXTRACTION_ERROR)
@cached_property
def type(self):
extraction_commands = [
['7z', 'l', '-y', '-p1', self.filepath],
]
if distro.id() == 'fedora' or distro.like() == 'fedora':
extraction_commands.append(
['unrar', 'l', '-y', '-p1', self.filepath],
)
for cmd in extraction_commands:
try:
process = subprocess_run(cmd, capture_output=True, check=True)
for line in process.stdout.splitlines():
if b'Type =' in line:
return line.rstrip().decode().split(' = ')[1].upper()
except FileNotFoundError:
pass
except CalledProcessError:
pass
raise OSError(EXTRACTION_ERROR)
def extract(self, targetdir):
if not os.path.isdir(targetdir):
raise OSError('Target directory doesn\'t exist.')
process = subprocess_run_silent(['7z', 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.filepath],
stdout=PIPE, stderr=STDOUT)
if process.returncode != 0 and distro.id() == 'fedora':
process = subprocess_run_silent(['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.filepath, targetdir]
, stdout=PIPE, stderr=STDOUT)
if process.returncode != 0:
raise OSError(EXTRACTION_ERROR)
elif process.returncode != 0 and platform.system() == 'Darwin':
process = subprocess_run_silent(['unar', self.filepath, '-f', '-o', targetdir],
stdout=PIPE, stderr=STDOUT)
elif process.returncode != 0:
missing = []
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],
]
if platform.system() == 'Darwin':
extraction_commands.append(
['unar', self.filepath, '-f', '-o', targetdir]
)
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]
)
for cmd in extraction_commands:
try:
subprocess_run(cmd, capture_output=True, check=True)
return targetdir
except FileNotFoundError:
missing.append(cmd[0])
except CalledProcessError:
pass
if missing:
raise OSError(f'Extraction failed, install <a href="https://github.com/ciromattia/kcc#7-zip">specialized extraction software.</a> ')
else:
raise OSError(EXTRACTION_ERROR)
tdir = os.listdir(targetdir)
if 'ComicInfo.xml' in tdir:
tdir.remove('ComicInfo.xml')
return targetdir
def addFile(self, sourcefile):
if self.type in ['RAR', 'RAR5']:
raise NotImplementedError
process = subprocess_run_silent(['7z', 'a', '-y', self.filepath, sourcefile],
process = subprocess_run(['7z', '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_silent(['7z', 'x', '-y', '-so', self.filepath, 'ComicInfo.xml'],
process = subprocess_run(['7z', 'x', '-y', '-so', self.filepath, 'ComicInfo.xml'],
stdout=PIPE, stderr=STDOUT)
if process.returncode != 0:
raise OSError(EXTRACTION_ERROR)

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

@@ -136,7 +136,11 @@ def del_exth(rec0, exth_num):
class DualMobiMetaFix:
def __init__(self, infile, outfile, asin):
def __init__(self, infile, outfile, asin, is_pdoc):
cdetype = b'EBOK'
if is_pdoc:
cdetype = b'PDOC'
shutil.copyfile(infile, outfile)
f = open(outfile, "r+b")
self.datain = mmap.mmap(f.fileno(), 0)
@@ -147,7 +151,7 @@ class DualMobiMetaFix:
rec0 = self.datain_rec0
rec0 = del_exth(rec0, 501)
rec0 = del_exth(rec0, 113)
rec0 = add_exth(rec0, 501, b'EBOK')
rec0 = add_exth(rec0, 501, cdetype)
rec0 = add_exth(rec0, 113, asin)
replacesection(self.datain, 0, rec0)
@@ -174,7 +178,7 @@ class DualMobiMetaFix:
rec0 = self.datain_kfrec0
rec0 = del_exth(rec0, 501)
rec0 = del_exth(rec0, 113)
rec0 = add_exth(rec0, 501, b'EBOK')
rec0 = add_exth(rec0, 501, cdetype)
rec0 = add_exth(rec0, 113, asin)
replacesection(self.datain, datain_kf8, rec0)

View File

@@ -20,9 +20,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import io
import os
from pathlib import Path
import mozjpeg_lossless_optimization
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter
from .shared import md5Checksum
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
@@ -78,18 +80,29 @@ class ProfileData:
PalleteNull = [
]
Profiles = {
ProfilesKindleEBOK = {
'K1': ("Kindle 1", (600, 670), Palette4, 1.8),
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
'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),
'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),
}
ProfilesKindlePDOC = {
'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),
'KO': ("Kindle Oasis 2/3", (1264, 1680), Palette16, 1.8),
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
}
ProfilesKindle = {
**ProfilesKindleEBOK,
**ProfilesKindlePDOC
}
ProfilesKobo = {
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8),
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8),
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.8),
@@ -99,10 +112,24 @@ class ProfileData:
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.8),
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.8),
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.8),
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.8),
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.8),
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.8),
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.8),
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.8),
'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),
}
@@ -114,7 +141,13 @@ 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')
# Detect corruption in source image, let caller catch any exceptions triggered.
srcImgPath = os.path.join(source[0], source[1])
self.image = Image.open(srcImgPath)
self.image.verify()
self.image = Image.open(srcImgPath).convert('RGB')
self.color = self.colorCheck()
self.fill = self.fillCheck()
# backwards compatibility for Pillow >9.1.0
@@ -149,7 +182,10 @@ class ComicPageParser:
self.payload.append(['N', self.source, new_image, self.color, 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.color, self.fill])
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
if self.opt.splitter != 1:
if width > height:
@@ -167,7 +203,10 @@ class ComicPageParser:
self.payload.append(['S1', self.source, pageone, self.color, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.color, self.fill])
if self.opt.splitter > 0:
self.payload.append(['R', self.source, self.image.rotate(90, Image.Resampling.BICUBIC, True),
spread = self.image
if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread,
self.color, self.fill])
else:
self.payload.append(['N', self.source, self.image, self.color, self.fill])
@@ -239,20 +278,22 @@ class ComicPage:
_, 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.fill = fill
self.rotated = False
self.orgPath = os.path.join(path[0], path[1])
if 'N' in mode:
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC'
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc'
elif 'R' in mode:
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC-A'
self.rotated = True
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc-a'
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.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc-b'
elif 'S2' in mode:
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC-C'
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc-c'
# backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'):
Image.Resampling = Image
@@ -281,7 +322,9 @@ class ComicPage:
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]
if os.path.isfile(self.orgPath):
os.remove(self.orgPath)
return [Path(self.targetPath).name, flags]
except IOError as err:
raise RuntimeError('Cannot save image. ' + str(err))
@@ -307,76 +350,66 @@ class ComicPage:
# 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]:
method = Image.Resampling.BICUBIC
if self.image.size[0] < self.size[0] and self.image.size[1] < self.size[1]:
return Image.Resampling.BICUBIC
else:
method = Image.Resampling.LANCZOS
return method
def getBoundingBox(self, tmptmg):
min_margin = [int(0.005 * i + 0.5) for i in tmptmg.size]
max_margin = [int(0.1 * i + 0.5) for i in tmptmg.size]
bbox = tmptmg.getbbox()
bbox = (
max(0, min(max_margin[0], bbox[0] - min_margin[0])),
max(0, min(max_margin[1], bbox[1] - min_margin[1])),
min(tmptmg.size[0],
max(tmptmg.size[0] - max_margin[0], bbox[2] + min_margin[0])),
min(tmptmg.size[1],
max(tmptmg.size[1] - max_margin[1], bbox[3] + min_margin[1])),
)
return bbox
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:
self.image = self.image.crop(box)
def cropPageNumber(self, power, minimum):
if self.fill != 'white':
tmptmg = self.image.convert(mode='L')
else:
tmptmg = ImageOps.invert(self.image.convert(mode='L'))
tmptmg = tmptmg.point(lambda x: x and 255)
tmptmg = tmptmg.filter(ImageFilter.MinFilter(size=3))
tmptmg = tmptmg.filter(ImageFilter.GaussianBlur(radius=5))
tmptmg = tmptmg.point(lambda x: (x >= 16 * power) and x)
if tmptmg.getbbox():
self.maybeCrop(tmptmg.getbbox(), minimum)
bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill)
if bbox:
self.maybeCrop(bbox, minimum)
def cropMargin(self, power, minimum):
if self.fill != 'white':
tmptmg = self.image.convert(mode='L')
else:
tmptmg = ImageOps.invert(self.image.convert(mode='L'))
tmptmg = tmptmg.filter(ImageFilter.GaussianBlur(radius=3))
tmptmg = tmptmg.point(lambda x: (x >= 16 * power) and x)
if tmptmg.getbbox():
self.maybeCrop(self.getBoundingBox(tmptmg), minimum)
bbox = get_bbox_crop_margin(self.image, power, self.fill)
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):
@@ -398,6 +431,17 @@ class Cover:
self.image = ImageOps.autocontrast(self.image)
if not self.options.forcecolor:
self.image = self.image.convert('L')
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))
self.image.thumbnail(self.options.profileData[1], Image.Resampling.LANCZOS)
self.save()

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

@@ -19,9 +19,12 @@
import os.path
import psutil
from . import image
class Kindle:
def __init__(self):
def __init__(self, profile):
self.profile = profile
self.path = self.findDevice()
if self.path:
self.coverSupport = self.checkThumbnails()
@@ -29,9 +32,11 @@ class Kindle:
self.coverSupport = False
def findDevice(self):
if self.profile in image.ProfileData.ProfilesKindlePDOC.keys():
return False
for drive in reversed(psutil.disk_partitions(False)):
if (drive[2] == 'FAT32' and drive[3] == 'rw,removable') or \
(drive[2] in ('vfat', 'msdos', 'FAT') and 'rw' in drive[3]):
(drive[2] in ('vfat', 'msdos', 'FAT', 'apfs') and 'rw' in drive[3]):
if os.path.isdir(os.path.join(drive[1], 'system')) and \
os.path.isdir(os.path.join(drive[1], 'documents')):
return drive[1]

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

@@ -0,0 +1,184 @@
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
We assume that the size of the number (including all digits) is between
'min_shape_size_tolerated_size' and 'max_shape_size_tolerated_size' relative to the image size.
We assume the distance between the digit is no more than 'max_dist_size' (x,y), and no more than 3 digits.
'''
max_shape_size_tolerated_size = (0.015*3, 0.02) # percent
min_shape_size_tolerated_size = (0.003, 0.006) # percent
window_h_size = max_shape_size_tolerated_size[1]*1.25 # percent
max_dist_size = (0.01, 0.002) # percent
'''
E-reader screen real-estate is an important resource.
More available screensize means more details can be better seen, especially text.
Text is one of the most important elements that need to be clearly readable on e-readers,
which mostly are smaller devices where the need to zoom is unwanted.
By cropping the page number on the bottom of the page, 2%-5% of the page height can be regained
that allows us to upscale the image even more.
- Most of the times the screen height is the limiting factor in upscaling, rather than its width.
Parameters:
img (PIL image): A PIL image.
power (float): The power to 'chop' through pixels matching the background. Values in range[0,3].
background_color (string): 'white' for white background, anything else for black.
Returns:
bbox (4-tuple, left|top|right|bot): The tightest bounding box calculated after trying to remove the bottom page number. Returns None if couldnt find anything satisfactory
'''
def get_bbox_crop_margin_page_number(img, power=1, background_color='white'):
if img.mode != 'L':
img = ImageOps.grayscale(img)
if background_color != 'white':
img = ImageOps.invert(img)
'''
Autocontrast: due to some threshold values, it's important that the blacks will be blacks and white will be whites.
Box/MeanFilter: Allows us to reduce noise like bad a page scan or compression artifacts.
Note: MedianFilter works better in my experience, but takes 2x-3x longer to perform.
'''
img = ImageOps.autocontrast(img, 1).filter(ImageFilter.BoxBlur(1))
'''
The 'power' parameters determines the threshold. The higher the power, the more "force" it can crop through black pixels (in case of white background)
and the lower the power, more sensitive to black pixels.
'''
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
is between 'min_shape_size_tolerated_size' and 'max_shape_size_tolerated_size'.
'''
window_h = int(img.size[1] * window_h_size)
img_part = img.crop((left,bot_y_pos-window_h, right, bot_y_pos))
'''
We detect related-pixels by proximity, with max distance defined in 'max_dist_size'.
Related pixels (in x axis) for each image-row are then merged to boxes with adjacent rows (in y axis)
to form bounding boxes of the detected objects (which one of them could be the page number).
'''
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_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)
boxes = merge_boxes(window_groups, (img.size[0]*max_dist_size[0], img.size[1]*max_dist_size[1]))
'''
We assume that the lowest part of the image that has black pixels on is the page number.
In case that there are more than one detected object in the loewst part, we assume that one of them is probably
manga-content and shouldn't be cropped.
'''
# filter all small objects
boxes = list(filter(lambda box: box[1]-box[0] >= img.size[0]*min_shape_size_tolerated_size[0]
and box[3]-box[2] >= img.size[1]*min_shape_size_tolerated_size[1], boxes))
lowest_boxes = list(filter(lambda box: box[3] == window_h-1, boxes))
min_y_of_lowest_boxes = 0
if len(lowest_boxes) > 0:
min_y_of_lowest_boxes = np.min(np.array(lowest_boxes)[:,2])
boxes_in_same_y_range = list(filter(lambda box: box[3] >= min_y_of_lowest_boxes, boxes))
max_shape_size_tolerated = (img.size[0] * max_shape_size_tolerated_size[0],
max(img.size[1] *max_shape_size_tolerated_size[1], 3))
should_force_crop = (
len(boxes_in_same_y_range) == 1
and (boxes_in_same_y_range[0][1] - boxes_in_same_y_range[0][0] <= max_shape_size_tolerated[0])
and (boxes_in_same_y_range[0][3] - boxes_in_same_y_range[0][2] <= max_shape_size_tolerated[1])
)
cropped_bbox = (0, 0, img.size[0], img.size[1])
if should_force_crop:
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
'''
Parameters:
img (PIL image): A PIL image.
power (float): The power to 'chop' through pixels matching the background. Values in range[0,3].
background_color (string): 'white' for white background, anything else for black.
Returns:
bbox (4-tuple, left|top|right|bot): The tightest bounding box calculated after trying to remove the bottom page number. Returns None if couldnt find anything satisfactory
'''
def get_bbox_crop_margin(img, power=1, background_color='white'):
if img.mode != 'L':
img = ImageOps.grayscale(img)
if background_color != 'white':
img = ImageOps.invert(img)
'''
Autocontrast: due to some threshold values, it's important that the blacks will be blacks and white will be whites.
Box/MeanFilter: Allows us to reduce noise like bad a page scan or compression artifacts.
Note: MedianFilter works better in my experience, but takes 2x-3x longer to perform.
'''
img = ImageOps.autocontrast(img, 1).filter(ImageFilter.BoxBlur(1))
'''
The 'power' parameters determines the threshold. The higher the power, the more "force" it can crop through black pixels (in case of white background)
and the lower the power, more sensitive to black pixels.
'''
threshold = threshold_from_power(power)
bw_img = img.point(lambda p: 255 if p <= threshold else 0)
return bw_img.getbbox()
def box_intersect(box1, box2, max_dist):
return not (box2[0]-max_dist[0] > box1[1]
or box2[1]+max_dist[0] < box1[0]
or box2[2]-max_dist[1] > box1[3]
or box2[3]+max_dist[1] < box1[2])
'''
Merge close bounding boxes (left,right, top,bot) (x axis) with distance threshold defined in
'max_dist_tolerated'. Boxes with less 'max_dist_tolerated' distance (Chebyshev distance).
'''
def merge_boxes(boxes, max_dist_tolerated):
j = 0
while j < len(boxes)-1:
g1 = boxes[j]
intersecting_boxes = []
other_boxes = []
for i in range(j+1,len(boxes)):
g2 = boxes[i]
if box_intersect(g1,g2, max_dist_tolerated):
intersecting_boxes.append(g2)
else:
other_boxes.append(g2)
if len(intersecting_boxes) > 0:
intersecting_boxes = np.array([g1, *intersecting_boxes])
merged_box = np.array([
np.min(intersecting_boxes[:,0]),
np.max(intersecting_boxes[:,1]),
np.min(intersecting_boxes[:,2]),
np.max(intersecting_boxes[:,3])
])
other_boxes.append(merged_box)
boxes = np.concatenate([boxes[:j], other_boxes])
j = 0
else:
j += 1
return boxes

View File

@@ -18,11 +18,12 @@
# PERFORMANCE OF THIS SOFTWARE.
#
from functools import lru_cache
import os
from hashlib import md5
from html.parser import HTMLParser
import subprocess
from distutils.version import StrictVersion
from packaging.version import Version
from re import split
import sys
from traceback import format_tb
@@ -49,7 +50,11 @@ 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']:
if (name.startswith('.') and len(name) == 1):
return None
if name.startswith('._'):
return None
if ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.j2k', '.jpx']:
return None
return [name, ext]
@@ -74,16 +79,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))\
@@ -103,7 +98,7 @@ def dependencyCheck(level):
if level > 2:
try:
from PySide6.QtCore import qVersion as qtVersion
if StrictVersion('6.5.1') > StrictVersion(qtVersion()):
if Version('6.5.1') > Version(qtVersion()):
missing.append('PySide 6.5.1+')
except ImportError:
missing.append('PySide 6.5.1+')
@@ -114,7 +109,7 @@ def dependencyCheck(level):
if level > 1:
try:
from psutil import __version__ as psutilVersion
if StrictVersion('5.0.0') > StrictVersion(psutilVersion):
if Version('5.0.0') > Version(psutilVersion):
missing.append('psutil 5.0.0+')
except ImportError:
missing.append('psutil 5.0.0+')
@@ -123,13 +118,13 @@ def dependencyCheck(level):
from slugify import __version__ as slugifyVersion
if isinstance(slugifyVersion, ModuleType):
slugifyVersion = slugifyVersion.__version__
if StrictVersion('1.2.1') > StrictVersion(slugifyVersion):
if Version('1.2.1') > Version(slugifyVersion):
missing.append('python-slugify 1.2.1+')
except ImportError:
missing.append('python-slugify 1.2.1+')
try:
from PIL import __version__ as pillowVersion
if StrictVersion('5.2.0') > StrictVersion(pillowVersion):
if Version('5.2.0') > Version(pillowVersion):
missing.append('Pillow 5.2.0+')
except ImportError:
missing.append('Pillow 5.2.0+')
@@ -137,7 +132,20 @@ def dependencyCheck(level):
print('ERROR: ' + ', '.join(missing) + ' is not installed!')
sys.exit(1)
def subprocess_run_silent(command, **kwargs):
@lru_cache
def available_archive_tools():
available = []
for tool in ['tar', '7z', 'unar', 'unrar']:
try:
subprocess_run([tool], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
available.append(tool)
except FileNotFoundError:
pass
return available
def subprocess_run(command, **kwargs):
if (os.name == 'nt'):
kwargs.setdefault('creationflags', subprocess.CREATE_NO_WINDOW)
return subprocess.run(command, **kwargs)

View File

@@ -4,6 +4,8 @@ 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
natsort>=8.4.0
distro>=1.8.0
numpy>=1.22.4,<2.0.0

View File

@@ -14,7 +14,6 @@ import os
import platform
import sys
import setuptools
import distutils.cmd
from kindlecomicconverter import __version__
NAME = 'KindleComicConverter'
@@ -23,7 +22,7 @@ VERSION = __version__
# noinspection PyUnresolvedReferences
class BuildBinaryCommand(distutils.cmd.Command):
class BuildBinaryCommand(setuptools.Command):
description = 'build binary release'
user_options = []
@@ -37,16 +36,16 @@ class BuildBinaryCommand(distutils.cmd.Command):
def run(self):
VERSION = __version__
if sys.platform == 'darwin':
os.system('pyinstaller -y -D -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py')
os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py')
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
os.system(f'appdmg kcc.json dist/kcc_macos_{platform.processor()}_{VERSION}.dmg')
sys.exit(0)
elif sys.platform == 'win32':
os.system('pyinstaller -y -F -i icons\\comic2ebook.ico -n KCC_' + VERSION + ' -w --noupx kcc.py')
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n KCC_' + VERSION + ' -w --noupx kcc.py')
sys.exit(0)
elif sys.platform == 'linux':
os.system(
'pyinstaller --hidden-import=queue -y -F -i icons/comic2ebook.ico -n kcc_linux_' + VERSION + ' kcc.py')
'pyinstaller --hidden-import=_cffi_backend --hidden-import=queue -y -F -i icons/comic2ebook.ico -n kcc_linux_' + VERSION + ' kcc.py')
sys.exit(0)
else:
sys.exit(0)
@@ -82,8 +81,9 @@ setuptools.setup(
'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'
],
classifiers=[],
zip_safe=False,