mirror of
https://github.com/ciromattia/kcc
synced 2026-04-15 13:38:46 +00:00
Compare commits
492 Commits
v6.2.2
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83564f0468 | ||
|
|
b42f05686e | ||
|
|
adf48d24f9 | ||
|
|
723fa4c0b8 | ||
|
|
2632d18e2c | ||
|
|
94e4937566 | ||
|
|
f7ce1cf271 | ||
|
|
ab93c03838 | ||
|
|
541b1d876b | ||
|
|
d189f9909d | ||
|
|
1dce4f8d2c | ||
|
|
58aab0cb65 | ||
|
|
d2dc089c62 | ||
|
|
3660f2370f | ||
|
|
87c6e3a35e | ||
|
|
981c556550 | ||
|
|
123d603cbd | ||
|
|
a344dd73bf | ||
|
|
095694e9cf | ||
|
|
4b4860b976 | ||
|
|
56e8e24176 | ||
|
|
b0f8f1c633 | ||
|
|
38acc3bf05 | ||
|
|
fbd5980b9b | ||
|
|
667d702b8a | ||
|
|
9a4143ce62 | ||
|
|
f63387cae4 | ||
|
|
f5fd2bb7fe | ||
|
|
4baca03214 | ||
|
|
7de212dca3 | ||
|
|
c99444b96a | ||
|
|
6d7a635c3d | ||
|
|
be86bcbf6a | ||
|
|
5cbc07e65d | ||
|
|
42d94d8202 | ||
|
|
7897627c43 | ||
|
|
8e42fc1162 | ||
|
|
d6b0e43d70 | ||
|
|
af189ed265 | ||
|
|
aa5f4991dd | ||
|
|
f9064ef0e4 | ||
|
|
e14abe1787 | ||
|
|
c58387f4f4 | ||
|
|
9b63b7af2c | ||
|
|
f74e108a3e | ||
|
|
f088ad732e | ||
|
|
8e5d57364d | ||
|
|
b767d5dc2c | ||
|
|
7228055bca | ||
|
|
8c57fbf318 | ||
|
|
7e94861fa1 | ||
|
|
9992ca4d26 | ||
|
|
f47d1427f0 | ||
|
|
ce8998375c | ||
|
|
8870898a87 | ||
|
|
a017cfd00d | ||
|
|
3f4ef3e21e | ||
|
|
4733c6348b | ||
|
|
5ad23d9629 | ||
|
|
db4eb78963 | ||
|
|
988fc93dc5 | ||
|
|
74fee9346c | ||
|
|
9fcacd7ae6 | ||
|
|
8ac58e361f | ||
|
|
61d6972e22 | ||
|
|
c7c1557e72 | ||
|
|
cb93704e08 | ||
|
|
62c5183609 | ||
|
|
a629f267a1 | ||
|
|
aeec4dd294 | ||
|
|
0d3076465b | ||
|
|
984d44b371 | ||
|
|
1111263893 | ||
|
|
5035c7403e | ||
|
|
067aa68162 | ||
|
|
72d07d53ea | ||
|
|
8c242d45d7 | ||
|
|
c655922a57 | ||
|
|
77e8952f12 | ||
|
|
5b069322a4 | ||
|
|
2444a28127 | ||
|
|
3aad79fc30 | ||
|
|
2dbc13303f | ||
|
|
4c36c7c586 | ||
|
|
65007aec07 | ||
|
|
9429bed91c | ||
|
|
3a3ee15cba | ||
|
|
2394aa3747 | ||
|
|
b57992a754 | ||
|
|
c7a62fdcd6 | ||
|
|
8861299d24 | ||
|
|
636447bb62 | ||
|
|
b23c7744cb | ||
|
|
2398a5b1ac | ||
|
|
2b2ac8ff55 | ||
|
|
5209d9a7b8 | ||
|
|
5336870097 | ||
|
|
4371d14391 | ||
|
|
f96b7cb22b | ||
|
|
4dfd2ea942 | ||
|
|
ba7f4336a5 | ||
|
|
9561b04bec | ||
|
|
2a8f8e9ab4 | ||
|
|
b9cef59912 | ||
|
|
f2ab730691 | ||
|
|
44401583e4 | ||
|
|
28faf524c4 | ||
|
|
2d288f72ea | ||
|
|
fb9b3c676b | ||
|
|
cff1de4fa5 | ||
|
|
0e56cde791 | ||
|
|
9a021ad5d4 | ||
|
|
d648081086 | ||
|
|
b2a8b364d9 | ||
|
|
1f935a0635 | ||
|
|
58b9651ff3 | ||
|
|
11a56dd892 | ||
|
|
c23af709cf | ||
|
|
87a7197c16 | ||
|
|
5813f914fc | ||
|
|
530ae410d4 | ||
|
|
a7428f18b6 | ||
|
|
06194b33ad | ||
|
|
80ea17ff02 | ||
|
|
e0466ad040 | ||
|
|
df924824aa | ||
|
|
82f1c1bb0a | ||
|
|
6f801a3334 | ||
|
|
ee8fc77ca9 | ||
|
|
1da06b43e2 | ||
|
|
d28740491e | ||
|
|
e2b98db1a2 | ||
|
|
4c5ec95a9b | ||
|
|
e56612228c | ||
|
|
3470d01367 | ||
|
|
a795a84899 | ||
|
|
ef44f6f285 | ||
|
|
6acdebae3c | ||
|
|
dc8136f6fd | ||
|
|
28ec2d23fc | ||
|
|
108363ce2f | ||
|
|
543fb81027 | ||
|
|
e6c6c05d40 | ||
|
|
18cd55f439 | ||
|
|
7ce4438886 | ||
|
|
841ec517ac | ||
|
|
4614fdd07a | ||
|
|
d0b72e1c83 | ||
|
|
e11755b118 | ||
|
|
a9bfc152bf | ||
|
|
f228ed261e | ||
|
|
b8020758a6 | ||
|
|
6540638e03 | ||
|
|
565e4aec45 | ||
|
|
0453fcfc90 | ||
|
|
a6ee867da2 | ||
|
|
1e4434fc7c | ||
|
|
7122945fa2 | ||
|
|
788163f3df | ||
|
|
3fa90735d5 | ||
|
|
e6cd26c773 | ||
|
|
e92b5c74de | ||
|
|
a031e4622e | ||
|
|
ae1989335e | ||
|
|
a7cb3565aa | ||
|
|
ec8098291f | ||
|
|
297e8757f0 | ||
|
|
2cfd6d9728 | ||
|
|
5733786dc6 | ||
|
|
8a862f11ac | ||
|
|
c32620cfeb | ||
|
|
512cac7f8a | ||
|
|
5e86acc740 | ||
|
|
20388304e8 | ||
|
|
420bed995b | ||
|
|
bc92c2dd85 | ||
|
|
a1f4e040ba | ||
|
|
137d53672a | ||
|
|
0cc75ab1e7 | ||
|
|
4cecf6fc4d | ||
|
|
2f0c9ae95d | ||
|
|
b856a176b0 | ||
|
|
e50163fb59 | ||
|
|
0ab20a5ce3 | ||
|
|
9c69a6fdcc | ||
|
|
26f0bf9989 | ||
|
|
b3db3256d6 | ||
|
|
96a92fb9bb | ||
|
|
18f02df3a1 | ||
|
|
235db43fa6 | ||
|
|
cab7cae714 | ||
|
|
a41f947030 | ||
|
|
d2828877af | ||
|
|
704dcd6dbe | ||
|
|
6a7500441d | ||
|
|
753eeb64eb | ||
|
|
a63d9e3ad0 | ||
|
|
cc01dc611a | ||
|
|
9a605c2d8a | ||
|
|
a7005748c7 | ||
|
|
55193119fb | ||
|
|
741cab68f3 | ||
|
|
7f8f3e67b7 | ||
|
|
be12661f38 | ||
|
|
06e2ee2968 | ||
|
|
27296565a3 | ||
|
|
bb70337a35 | ||
|
|
cf0586ae70 | ||
|
|
4100141b46 | ||
|
|
8eb81b7d67 | ||
|
|
e2dbc05a83 | ||
|
|
50b82786a1 | ||
|
|
88d1643f64 | ||
|
|
ddf2fa360f | ||
|
|
43e974f20d | ||
|
|
e3a3e9b3c2 | ||
|
|
380dc5c42c | ||
|
|
8ab7520754 | ||
|
|
3cd6e09bcb | ||
|
|
cb5f4db5c4 | ||
|
|
f1ffb2c4e8 | ||
|
|
26327728d0 | ||
|
|
61bfb0a51f | ||
|
|
2f03119926 | ||
|
|
8f08c63f4e | ||
|
|
eb24a400b4 | ||
|
|
cc2eb9dcf3 | ||
|
|
d1a443b3d8 | ||
|
|
f8c35ce634 | ||
|
|
580a800d25 | ||
|
|
023797f012 | ||
|
|
63a18627d3 | ||
|
|
0d967084a0 | ||
|
|
2da5b11858 | ||
|
|
f6d10337d8 | ||
|
|
cf047ecf6f | ||
|
|
e7ee8bed9d | ||
|
|
9c8a1759cf | ||
|
|
6299754964 | ||
|
|
a3db86a29b | ||
|
|
67714a9b06 | ||
|
|
95f9a3cda9 | ||
|
|
0e12fc30c6 | ||
|
|
90c9ba7539 | ||
|
|
84da718167 | ||
|
|
fe7559e6a9 | ||
|
|
a79c740387 | ||
|
|
bc98eecae9 | ||
|
|
e7a07377ef | ||
|
|
07ef11013a | ||
|
|
551fe6edbf | ||
|
|
dbf8a3ddbd | ||
|
|
8f9e230b62 | ||
|
|
36d9a4151e | ||
|
|
3e88dabd1a | ||
|
|
3b7d949128 | ||
|
|
68186285bd | ||
|
|
0abf620698 | ||
|
|
69d3bf3278 | ||
|
|
793992f408 | ||
|
|
f41d5327e0 | ||
|
|
6f960aa1d0 | ||
|
|
17c0a73f9f | ||
|
|
1fa5a5b19b | ||
|
|
e8d05c16aa | ||
|
|
74187b0d77 | ||
|
|
f39e0caad0 | ||
|
|
6299c45790 | ||
|
|
c7ebb230c2 | ||
|
|
3e4b729a30 | ||
|
|
16a1d9b45f | ||
|
|
b7aef324aa | ||
|
|
1a42730ea0 | ||
|
|
217a18b7b5 | ||
|
|
2ecbf7d2e9 | ||
|
|
a1cf9c5c7d | ||
|
|
32020d6b07 | ||
|
|
221f964f14 | ||
|
|
e9f0310b94 | ||
|
|
2fa90c9f59 | ||
|
|
cb0520dcab | ||
|
|
623bce6ae3 | ||
|
|
ad60894d19 | ||
|
|
4229b79c42 | ||
|
|
5fa6a59672 | ||
|
|
f171314a49 | ||
|
|
6b28e313e6 | ||
|
|
5a17435f7d | ||
|
|
0d573eb0a1 | ||
|
|
30a3f90907 | ||
|
|
9a7de0f5d9 | ||
|
|
f1db31205b | ||
|
|
eef5a85fa6 | ||
|
|
271200d29f | ||
|
|
ee375abfc5 | ||
|
|
94257c396a | ||
|
|
a05111b64a | ||
|
|
96e3ba7482 | ||
|
|
eb0abb538c | ||
|
|
87c2ef8033 | ||
|
|
ae7f56c81b | ||
|
|
70c73a82eb | ||
|
|
dbf4e45d25 | ||
|
|
1c6fe0cb22 | ||
|
|
2f7f6ebf0a | ||
|
|
21159c4328 | ||
|
|
4bb6ba55d3 | ||
|
|
06ae4ec25f | ||
|
|
3ac5709e73 | ||
|
|
fe7255a2d9 | ||
|
|
4712eac3c2 | ||
|
|
8ef5bf14ac | ||
|
|
c7e69f5bdb | ||
|
|
51d0be4379 | ||
|
|
ddc0ca2ff5 | ||
|
|
bd6dfa1e33 | ||
|
|
a95dde4cba | ||
|
|
2882d0f707 | ||
|
|
a1fa8e0ec3 | ||
|
|
ae4e063e09 | ||
|
|
8e0deff5ae | ||
|
|
3bd752537d | ||
|
|
4319f64815 | ||
|
|
82d2f7f4bf | ||
|
|
5a1e614a5d | ||
|
|
75e05a0ef0 | ||
|
|
e0f5bff527 | ||
|
|
a8316737be | ||
|
|
04228d100b | ||
|
|
8cc44c99f7 | ||
|
|
60f7902edd | ||
|
|
34bea98ca0 | ||
|
|
734b179e8a | ||
|
|
dcaa7401e7 | ||
|
|
d4d71cdd05 | ||
|
|
ec613cce7b | ||
|
|
ada001eb41 | ||
|
|
c4f845c221 | ||
|
|
ebb59dbc2d | ||
|
|
8da2b4cb96 | ||
|
|
7b8858678f | ||
|
|
be07e0df6a | ||
|
|
dc711e671d | ||
|
|
581ecd0ec2 | ||
|
|
f3a32c6174 | ||
|
|
0ce5f7f186 | ||
|
|
c3d2f89471 | ||
|
|
b1c4cd36f1 | ||
|
|
b7c6281b55 | ||
|
|
559485184d | ||
|
|
75f5274449 | ||
|
|
dfc149d893 | ||
|
|
327b522080 | ||
|
|
7c029b4ba1 | ||
|
|
2db8f5c8fd | ||
|
|
ce72921289 | ||
|
|
f1bbc47798 | ||
|
|
5a39007db6 | ||
|
|
0d7487f8d4 | ||
|
|
8a78f82ff2 | ||
|
|
149f7e5921 | ||
|
|
40d219de4d | ||
|
|
370d1d7392 | ||
|
|
8295f163c2 | ||
|
|
9f9a97afaa | ||
|
|
d113cae154 | ||
|
|
bc4afeb69e | ||
|
|
afb32f6287 | ||
|
|
bc516634cf | ||
|
|
36180f7904 | ||
|
|
3517994d37 | ||
|
|
08acad10ea | ||
|
|
727df0c9d6 | ||
|
|
13e71df172 | ||
|
|
3e7646bbad | ||
|
|
1c81a9d5b3 | ||
|
|
efa831341c | ||
|
|
1dd36e08eb | ||
|
|
d9d5ce2423 | ||
|
|
83c6b7b2d5 | ||
|
|
55dfdd32f8 | ||
|
|
3e3710dd76 | ||
|
|
41f87273ca | ||
|
|
b959739b53 | ||
|
|
d1b66d16fd | ||
|
|
80b01d298f | ||
|
|
de2aad0b9c | ||
|
|
bc7ab0879c | ||
|
|
2ab0135815 | ||
|
|
3882b6ce0a | ||
|
|
5e23e2cac1 | ||
|
|
d36933cb9a | ||
|
|
e40cf29aa3 | ||
|
|
9d1802453c | ||
|
|
4337d6c10d | ||
|
|
9680ff24c2 | ||
|
|
48b5b4f397 | ||
|
|
c712dc12a2 | ||
|
|
d2be9138c4 | ||
|
|
3cb40772a4 | ||
|
|
ef3b756247 | ||
|
|
77af020abb | ||
|
|
1c9c6c13b4 | ||
|
|
85ce39b2c3 | ||
|
|
6a33aee241 | ||
|
|
271a129537 | ||
|
|
23099cee81 | ||
|
|
b957fcf3fe | ||
|
|
187475a424 | ||
|
|
88fd54e2ba | ||
|
|
b23b67bbbe | ||
|
|
9992d895cf | ||
|
|
561951a349 | ||
|
|
b8b7926366 | ||
|
|
92f3308e1c | ||
|
|
9e87ccef4e | ||
|
|
9a2a09eab9 | ||
|
|
88cf2fd21f | ||
|
|
7a3ed262b1 | ||
|
|
9e204aad76 | ||
|
|
e1f9d12676 | ||
|
|
24ab72fcbc | ||
|
|
4af6a75874 | ||
|
|
28b6188a3f | ||
|
|
f000af1207 | ||
|
|
299f916580 | ||
|
|
c39e403595 | ||
|
|
e787dd2897 | ||
|
|
01625904d1 | ||
|
|
5f8526da44 | ||
|
|
1159e737a0 | ||
|
|
5bbdb715e9 | ||
|
|
1a3cd6c916 | ||
|
|
e1e6d587f4 | ||
|
|
ca5c0bdd61 | ||
|
|
c6f491d27e | ||
|
|
c9ed3feef1 | ||
|
|
be147fe7e5 | ||
|
|
62ffa2bc80 | ||
|
|
11186d07c0 | ||
|
|
4b3cd6882a | ||
|
|
b35a2baf05 | ||
|
|
11a395e983 | ||
|
|
2e39a8c227 | ||
|
|
02535421a0 | ||
|
|
3d4fae62d8 | ||
|
|
2b550b8b98 | ||
|
|
ecee7cf6f5 | ||
|
|
b0a5558da1 | ||
|
|
1b487c18d6 | ||
|
|
a3546d19c3 | ||
|
|
2f703ef92c | ||
|
|
4fb993b38b | ||
|
|
1401f94c1f | ||
|
|
70d10204ee | ||
|
|
a9d0c57ba6 | ||
|
|
5598596650 | ||
|
|
4b7b6d1c58 | ||
|
|
075bc748d1 | ||
|
|
9e318ed33e | ||
|
|
6d21bfa6fa | ||
|
|
132574d57d | ||
|
|
317fb33fd0 | ||
|
|
2189f4b1cb | ||
|
|
462a3cebde | ||
|
|
2a414ef936 | ||
|
|
4adb998896 | ||
|
|
315b6e150d | ||
|
|
5875508597 | ||
|
|
0a4ef31daf | ||
|
|
c99df3308e | ||
|
|
e9482fbd6c | ||
|
|
434fe90b00 | ||
|
|
73a91ec0ae | ||
|
|
e0b1848e09 | ||
|
|
a9360e6bc3 | ||
|
|
ae475e709a | ||
|
|
4769f68265 | ||
|
|
dbe6043542 | ||
|
|
e1a318145d | ||
|
|
a71523b2d4 | ||
|
|
7a5473f530 | ||
|
|
f5a5624112 | ||
|
|
3b7e8dc9a5 | ||
|
|
e637f37ef0 | ||
|
|
6ba690659f | ||
|
|
50eb48fb9b | ||
|
|
4a661a1a17 | ||
|
|
c26383c4b5 | ||
|
|
4e6ee8b59b | ||
|
|
6c6f591e45 | ||
|
|
78c014bf22 |
@@ -1,13 +1,40 @@
|
||||
.git
|
||||
.github
|
||||
|
||||
build
|
||||
dist
|
||||
KindleComicConverter.egg-info
|
||||
|
||||
.dockerignore
|
||||
.gitignore
|
||||
.travis.yml
|
||||
|
||||
Dockerfile
|
||||
venv
|
||||
.venv
|
||||
__pycache__/
|
||||
*/__pycache__/
|
||||
*.pyc
|
||||
|
||||
*.md
|
||||
LICENSE.txt
|
||||
*.txt
|
||||
!requirements-docker.txt
|
||||
MANIFEST.in
|
||||
|
||||
*.yml
|
||||
*.spec
|
||||
|
||||
*.svg
|
||||
*.jpg
|
||||
*.json
|
||||
|
||||
gen_ui_files.bat
|
||||
gen_ui_files.sh
|
||||
|
||||
gui/
|
||||
icons/
|
||||
|
||||
kindlecomicconverter/KCC_gui.py
|
||||
kindlecomicconverter/KCC_rc.py
|
||||
kindlecomicconverter/KCC_ui_editor.py
|
||||
kindlecomicconverter/KCC_ui.py
|
||||
|
||||
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: eink_dude
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -38,11 +38,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -69,6 +69,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
34
.github/workflows/docker-base-publish.yml
vendored
34
.github/workflows/docker-base-publish.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Docker base
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags: [ 'docker-base-*' ]
|
||||
|
||||
# Don't trigger if it's just a documentation update
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '**.MD'
|
||||
- '**.yml'
|
||||
- 'docs/**'
|
||||
- 'LICENSE'
|
||||
- '.gitattributes'
|
||||
- '.gitignore'
|
||||
- '.dockerignore'
|
||||
|
||||
|
||||
jobs:
|
||||
build_and_push:
|
||||
uses: sdr-enthusiasts/common-github-workflows/.github/workflows/build_and_push_image.yml@main
|
||||
with:
|
||||
docker_build_file: ./Dockerfile-base
|
||||
platform_linux_arm32v7_enabled: true
|
||||
platform_linux_arm64v8_enabled: true
|
||||
platform_linux_amd64_enabled: true
|
||||
push_enabled: true
|
||||
build_nohealthcheck: false
|
||||
ghcr_repo_owner: ${{ github.repository_owner }}
|
||||
ghcr_repo: ${{ github.repository }}
|
||||
build_latest: false
|
||||
secrets:
|
||||
ghcr_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
68
.github/workflows/docker-publish.yml
vendored
68
.github/workflows/docker-publish.yml
vendored
@@ -1,10 +1,10 @@
|
||||
name: Docker
|
||||
name: Build and Publish Docker Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
# Publish semver tags as releases.
|
||||
tags: [ 'v*.*.*' ]
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
# Don't trigger if it's just a documentation update
|
||||
paths-ignore:
|
||||
@@ -15,19 +15,53 @@ on:
|
||||
- 'LICENSE'
|
||||
- '.gitattributes'
|
||||
- '.gitignore'
|
||||
- '.dockerignore'
|
||||
|
||||
|
||||
jobs:
|
||||
build_and_push:
|
||||
uses: sdr-enthusiasts/common-github-workflows/.github/workflows/build_and_push_image.yml@main
|
||||
with:
|
||||
platform_linux_arm32v7_enabled: true
|
||||
platform_linux_arm64v8_enabled: true
|
||||
platform_linux_amd64_enabled: true
|
||||
push_enabled: true
|
||||
build_nohealthcheck: false
|
||||
ghcr_repo_owner: ${{ github.repository_owner }}
|
||||
ghcr_repo: ${{ github.repository }}
|
||||
secrets:
|
||||
ghcr_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
build_and_publish_base_image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Set Release Date
|
||||
id: release_date
|
||||
run: |
|
||||
echo "release_date=$(date --rfc-3339=date)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/kcc
|
||||
# Always creates the "latest" tag
|
||||
flavor: |
|
||||
latest=true
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=raw,value=${{ steps.release_date.outputs.release_date }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.meta.outputs.tags }}
|
||||
cache-from: |
|
||||
type=registry,ref=ghcr.io/ciromattia/kcc:cache
|
||||
type=registry,ref=ghcr.io/${{ github.repository_owner }}/kcc:cache
|
||||
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/kcc:cache,mode=max
|
||||
|
||||
11
.github/workflows/package-linux.yml
vendored
11
.github/workflows/package-linux.yml
vendored
@@ -23,11 +23,11 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.11
|
||||
cache: 'pip'
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
env:
|
||||
UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: AppImage
|
||||
path: './*.AppImage*'
|
||||
@@ -68,8 +68,7 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
files: |
|
||||
CHANGELOG.md
|
||||
LICENSE.txt
|
||||
*.AppImage*
|
||||
|
||||
16
.github/workflows/package-macos.yml
vendored
16
.github/workflows/package-macos.yml
vendored
@@ -25,12 +25,14 @@ jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos-12, macos-14 ]
|
||||
os: [ macos-15-intel, macos-14 ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: '14.0'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.11
|
||||
cache: 'pip'
|
||||
@@ -69,7 +71,7 @@ jobs:
|
||||
# apply provisioning profile
|
||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm install -g appdmg
|
||||
@@ -78,7 +80,7 @@ jobs:
|
||||
run: |
|
||||
python setup.py build_binary
|
||||
- name: upload build
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: mac-os-build-${{ runner.arch }}
|
||||
path: dist/*.dmg
|
||||
@@ -87,10 +89,8 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
files: |
|
||||
CHANGELOG.md
|
||||
LICENSE.txt
|
||||
dist/*.dmg
|
||||
- name: Clean up keychain and provisioning profile
|
||||
# TODO signing
|
||||
|
||||
66
.github/workflows/package-osx-legacy.yml
vendored
Normal file
66
.github/workflows/package-osx-legacy.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: build KCC for osx legacy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
# Don't trigger if it's just a documentation update
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '**.MD'
|
||||
- '**.yml'
|
||||
- '**.sh'
|
||||
- 'docs/**'
|
||||
- 'Dockerfile'
|
||||
- 'LICENSE'
|
||||
- '.gitattributes'
|
||||
- '.gitignore'
|
||||
- '.dockerignore'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos-15-intel ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
# We need the official Python, because the GA ones only support newer macOS versions
|
||||
# The deployment target is picked up by the Python build tools automatically
|
||||
PYTHON_VERSION: 3.11.9
|
||||
MACOSX_DEPLOYMENT_TARGET: '10.14'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Get Python
|
||||
run: curl https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg -o "python.pkg"
|
||||
- name: Install Python
|
||||
run: |
|
||||
sudo installer -pkg python.pkg -target /
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python3 --version
|
||||
pip3 install --upgrade pip setuptools wheel pyinstaller certifi
|
||||
pip3 install --upgrade -r requirements-osx-legacy.txt
|
||||
./gen_ui_files.sh
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm install -g appdmg
|
||||
- name: build binary
|
||||
run: |
|
||||
python3 setup.py build_binary
|
||||
- name: upload build
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: osx-build-${{ runner.arch }}
|
||||
path: dist/*.dmg
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
generate_release_notes: false
|
||||
files: |
|
||||
LICENSE.txt
|
||||
dist/*.dmg
|
||||
@@ -1,57 +0,0 @@
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
|
||||
name: build KCC for windows with docker
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# - name: Set up Python
|
||||
# uses: actions/setup-python@v4
|
||||
# with:
|
||||
# python-version: 3.11
|
||||
# cache: 'pip'
|
||||
# - name: Install python dependencies
|
||||
# run: |
|
||||
# python -m pip install --upgrade pip setuptools wheel pyinstaller
|
||||
# pip install -r requirements.txt
|
||||
# - name: build binary
|
||||
# run: |
|
||||
# pyi-makespec -F -i icons\\comic2ebook.ico -n KCC_test -w --noupx kcc.py
|
||||
- name: Package Application
|
||||
uses: JackMcKew/pyinstaller-action-windows@main
|
||||
with:
|
||||
path: .
|
||||
spec: ./kcc-c2e.spec
|
||||
- name: Package Application
|
||||
uses: JackMcKew/pyinstaller-action-windows@main
|
||||
with:
|
||||
path: .
|
||||
spec: ./kcc-c2p.spec
|
||||
- name: rename binaries
|
||||
run: |
|
||||
version_built=$(cat kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/[^.0-9b]//g")
|
||||
mv dist/windows/kcc-c2e.exe dist/windows/KCC_c2e_${version_built}.exe
|
||||
mv dist/windows/kcc-c2p.exe dist/windows/KCC_c2p_${version_built}.exe
|
||||
- name: upload build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-build
|
||||
path: dist/windows/*.exe
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
CHANGELOG.md
|
||||
LICENSE.txt
|
||||
dist/windows/*.exe
|
||||
40
.github/workflows/package-windows.yml
vendored
40
.github/workflows/package-windows.yml
vendored
@@ -23,11 +23,21 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
entry: [ kcc, kcc-c2e, kcc-c2p ]
|
||||
include:
|
||||
- entry: kcc
|
||||
command: build_binary
|
||||
- entry: kcc-c2e
|
||||
command: build_c2e
|
||||
- entry: kcc-c2p
|
||||
command: build_c2p
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.11
|
||||
cache: 'pip'
|
||||
@@ -40,19 +50,29 @@ jobs:
|
||||
pip install certifi pyinstaller --no-binary pyinstaller
|
||||
- name: build binary
|
||||
run: |
|
||||
python setup.py build_binary
|
||||
- name: upload build
|
||||
uses: actions/upload-artifact@v4
|
||||
python setup.py ${{ matrix.command }}
|
||||
- name: upload-unsigned-artifact
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: windows-build
|
||||
name: windows-build-${{ matrix.entry }}
|
||||
path: dist/*.exe
|
||||
- id: optional_step_id
|
||||
uses: signpath/github-action-submit-signing-request@v2.0
|
||||
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/')
|
||||
with:
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
files: |
|
||||
CHANGELOG.md
|
||||
LICENSE.txt
|
||||
dist/*.exe
|
||||
dist/*.exe
|
||||
|
||||
60
.github/workflows/package-windows7.yml
vendored
Normal file
60
.github/workflows/package-windows7.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
|
||||
name: build KCC for windows 7
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
# Don't trigger if it's just a documentation update
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '**.MD'
|
||||
- '**.yml'
|
||||
- '**.sh'
|
||||
- 'docs/**'
|
||||
- 'Dockerfile'
|
||||
- 'LICENSE'
|
||||
- '.gitattributes'
|
||||
- '.gitignore'
|
||||
- '.dockerignore'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-2022
|
||||
env:
|
||||
WINDOWS_7: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.8
|
||||
cache: 'pip'
|
||||
- name: Install dependencies
|
||||
env:
|
||||
PYINSTALLER_COMPILE_BOOTLOADER: 1
|
||||
run: |
|
||||
python -m pip install --upgrade pip setuptools wheel
|
||||
pip install -r requirements-win7.txt
|
||||
pip install certifi pyinstaller --no-binary pyinstaller
|
||||
.\gen_ui_files.bat
|
||||
- name: build binary
|
||||
run: |
|
||||
python setup.py build_binary
|
||||
- name: upload-unsigned-artifact
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: windows7-build
|
||||
path: dist/*.exe
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
generate_release_notes: false
|
||||
files: |
|
||||
dist/*.exe
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,6 +8,9 @@ dist/
|
||||
build/
|
||||
KindleComicConverter*.egg-info/
|
||||
.idea/
|
||||
.vscode/
|
||||
win7
|
||||
osx10.11
|
||||
/venv/
|
||||
/kindlegen*
|
||||
/kcc.bat
|
||||
|
||||
30
.travis.yml
30
.travis.yml
@@ -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
|
||||
90
Dockerfile
90
Dockerfile
@@ -1,19 +1,77 @@
|
||||
# Select final stage based on TARGETARCH ARG
|
||||
FROM ghcr.io/ciromattia/kcc:docker-base-20240928
|
||||
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'
|
||||
LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc'
|
||||
LABEL org.opencontainers.image.source='https://github.com/ciromattia/kcc'
|
||||
LABEL org.opencontainers.image.authors='darodi'
|
||||
LABEL org.opencontainers.image.url='https://github.com/ciromattia/kcc'
|
||||
LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc'
|
||||
LABEL org.opencontainers.image.vendor='ciromattia'
|
||||
LABEL org.opencontainers.image.licenses='ISC'
|
||||
LABEL org.opencontainers.image.title="Kindle Comic Converter"
|
||||
# STAGE 1: BUILDER
|
||||
# Contains all build tools and dev dependencies, will be discarded
|
||||
FROM python:3.13-slim-bullseye AS builder
|
||||
|
||||
COPY . /opt/kcc
|
||||
RUN cat /opt/kcc/kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/'//g" > /IMAGE_VERSION
|
||||
# Install system dependencies
|
||||
RUN set -x && \
|
||||
BUILD_DEPS="build-essential cmake libffi-dev libfreetype6-dev libfontconfig1-dev libpng-dev libjpeg-dev libssl-dev libxft-dev make python3-dev python3-setuptools python3-wheel" && \
|
||||
RUNTIME_DEPS="bash ca-certificates chrpath locales locales-all libfreetype6 libfontconfig1 p7zip-full python3 python3-pip libgl1" && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -y && \
|
||||
apt-get install -y --no-install-recommends ${BUILD_DEPS} ${RUNTIME_DEPS}
|
||||
|
||||
ENTRYPOINT ["/opt/kcc/kcc-c2e.py"]
|
||||
RUN \
|
||||
set -x && \
|
||||
python -m venv /opt/venv && \
|
||||
. /opt/venv/bin/activate && \
|
||||
pip install --upgrade pip
|
||||
|
||||
# Install numpy first, as it is unlikely to change and takes too long to compile
|
||||
RUN \
|
||||
set -x && \
|
||||
. /opt/venv/bin/activate && \
|
||||
pip install --no-cache-dir numpy==2.3.4
|
||||
|
||||
# Install PyMuPDF separately, as it is likely to change but still takes too long to compile
|
||||
RUN \
|
||||
set -x && \
|
||||
. /opt/venv/bin/activate && \
|
||||
pip install --no-cache-dir PyMuPDF==1.26.6
|
||||
|
||||
# Install Python dependencies using virtual environment
|
||||
COPY requirements-docker.txt .
|
||||
|
||||
RUN \
|
||||
set -x && \
|
||||
. /opt/venv/bin/activate && \
|
||||
pip install --no-cache-dir -r requirements-docker.txt
|
||||
|
||||
# STAGE 2: FINAL
|
||||
# Clean, small and secure image with only runtime dependencies
|
||||
FROM python:3.13-slim-bullseye
|
||||
|
||||
# Install runtime dependencies only
|
||||
RUN \
|
||||
set -x && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -y && \
|
||||
apt-get install -y --no-install-recommends p7zip-full && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy artifacts from builder
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
COPY . /opt/kcc/
|
||||
|
||||
WORKDIR /opt/kcc
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
# Setup executable and version file
|
||||
RUN \
|
||||
chmod +x /opt/kcc/entrypoint.sh && \
|
||||
ln -s /opt/kcc/kcc-c2e.py /usr/local/bin/c2e && \
|
||||
ln -s /opt/kcc/kcc-c2p.py /usr/local/bin/c2p && \
|
||||
ln -s /opt/kcc/entrypoint.sh /usr/local/bin/entrypoint && \
|
||||
ln -s /opt/kcc/kindlegen/kindlegen /usr/local/bin/kindlegen && \
|
||||
cat /opt/kcc/kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/'//g" > /IMAGE_VERSION
|
||||
|
||||
LABEL com.kcc.name="Kindle Comic Converter" \
|
||||
com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi" \
|
||||
org.opencontainers.image.title="Kindle Comic Converter" \
|
||||
org.opencontainers.image.description='Kindle Comic Converter' \
|
||||
org.opencontainers.image.documentation='https://github.com/ciromattia/kcc' \
|
||||
org.opencontainers.image.source='https://github.com/ciromattia/kcc' \
|
||||
org.opencontainers.image.authors='Darodi and José Cerezo' \
|
||||
org.opencontainers.image.url='https://github.com/ciromattia/kcc' \
|
||||
org.opencontainers.image.vendor='ciromattia' \
|
||||
org.opencontainers.image.licenses='ISC'
|
||||
|
||||
ENTRYPOINT ["entrypoint"]
|
||||
CMD ["-h"]
|
||||
|
||||
164
Dockerfile-base
164
Dockerfile-base
@@ -1,164 +0,0 @@
|
||||
FROM --platform=linux/amd64 python:3.12-slim-bullseye as compile-amd64
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
|
||||
|
||||
|
||||
COPY requirements.txt /opt/kcc/
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
RUN DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
|
||||
apt-get install -y libpng-dev libjpeg-dev p7zip-full unrar-free libgl1 && \
|
||||
python -m pip install --upgrade pip && \
|
||||
python -m venv /opt/venv && \
|
||||
python -m pip install -r /opt/kcc/requirements.txt
|
||||
|
||||
|
||||
######################################################################################
|
||||
|
||||
FROM --platform=linux/arm64 python:3.12-slim-bullseye as compile-arm64
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
|
||||
|
||||
ENV LC_ALL=C.UTF-8 \
|
||||
LANG=C.UTF-8 \
|
||||
LANGUAGE=en_US:en
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
COPY requirements.txt /opt/kcc/
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
RUN set -x && \
|
||||
TEMP_PACKAGES=() && \
|
||||
KEPT_PACKAGES=() && \
|
||||
# Packages only required during build
|
||||
TEMP_PACKAGES+=(build-essential) && \
|
||||
TEMP_PACKAGES+=(cmake) && \
|
||||
TEMP_PACKAGES+=(libfreetype6-dev) && \
|
||||
TEMP_PACKAGES+=(libfontconfig1-dev) && \
|
||||
TEMP_PACKAGES+=(libpng-dev) && \
|
||||
TEMP_PACKAGES+=(libjpeg-dev) && \
|
||||
TEMP_PACKAGES+=(libssl-dev) && \
|
||||
TEMP_PACKAGES+=(libxft-dev) && \
|
||||
TEMP_PACKAGES+=(make) && \
|
||||
TEMP_PACKAGES+=(python3-dev) && \
|
||||
TEMP_PACKAGES+=(python3-setuptools) && \
|
||||
TEMP_PACKAGES+=(python3-wheel) && \
|
||||
# Packages kept in the image
|
||||
KEPT_PACKAGES+=(bash) && \
|
||||
KEPT_PACKAGES+=(ca-certificates) && \
|
||||
KEPT_PACKAGES+=(chrpath) && \
|
||||
KEPT_PACKAGES+=(locales) && \
|
||||
KEPT_PACKAGES+=(locales-all) && \
|
||||
KEPT_PACKAGES+=(libfreetype6) && \
|
||||
KEPT_PACKAGES+=(libfontconfig1) && \
|
||||
KEPT_PACKAGES+=(p7zip-full) && \
|
||||
KEPT_PACKAGES+=(python3) && \
|
||||
KEPT_PACKAGES+=(python3-pip) && \
|
||||
KEPT_PACKAGES+=(unrar-free) && \
|
||||
# Install packages
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
${KEPT_PACKAGES[@]} \
|
||||
${TEMP_PACKAGES[@]} \
|
||||
&& \
|
||||
# Install required python modules
|
||||
python -m pip install --upgrade pip && \
|
||||
python -m venv /opt/venv && \
|
||||
python -m pip install -r /opt/kcc/requirements.txt
|
||||
|
||||
|
||||
######################################################################################
|
||||
|
||||
FROM --platform=linux/arm/v7 python:3.12-slim-bullseye as compile-armv7
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
|
||||
|
||||
ENV LC_ALL=C.UTF-8 \
|
||||
LANG=C.UTF-8 \
|
||||
LANGUAGE=en_US:en
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
COPY requirements.txt /opt/kcc/
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
RUN set -x && \
|
||||
TEMP_PACKAGES=() && \
|
||||
KEPT_PACKAGES=() && \
|
||||
# Packages only required during build
|
||||
TEMP_PACKAGES+=(build-essential) && \
|
||||
TEMP_PACKAGES+=(cmake) && \
|
||||
TEMP_PACKAGES+=(libffi-dev) && \
|
||||
TEMP_PACKAGES+=(libfreetype6-dev) && \
|
||||
TEMP_PACKAGES+=(libfontconfig1-dev) && \
|
||||
TEMP_PACKAGES+=(libpng-dev) && \
|
||||
TEMP_PACKAGES+=(libjpeg-dev) && \
|
||||
TEMP_PACKAGES+=(libssl-dev) && \
|
||||
TEMP_PACKAGES+=(libxft-dev) && \
|
||||
TEMP_PACKAGES+=(make) && \
|
||||
TEMP_PACKAGES+=(python3-dev) && \
|
||||
TEMP_PACKAGES+=(python3-setuptools) && \
|
||||
TEMP_PACKAGES+=(python3-wheel) && \
|
||||
# Packages kept in the image
|
||||
KEPT_PACKAGES+=(bash) && \
|
||||
KEPT_PACKAGES+=(ca-certificates) && \
|
||||
KEPT_PACKAGES+=(chrpath) && \
|
||||
KEPT_PACKAGES+=(locales) && \
|
||||
KEPT_PACKAGES+=(locales-all) && \
|
||||
KEPT_PACKAGES+=(libfreetype6) && \
|
||||
KEPT_PACKAGES+=(libfontconfig1) && \
|
||||
KEPT_PACKAGES+=(p7zip-full) && \
|
||||
KEPT_PACKAGES+=(python3) && \
|
||||
KEPT_PACKAGES+=(python3-pip) && \
|
||||
KEPT_PACKAGES+=(unrar-free) && \
|
||||
# Install packages
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
${KEPT_PACKAGES[@]} \
|
||||
${TEMP_PACKAGES[@]} \
|
||||
&& \
|
||||
# Install required python modules
|
||||
python -m pip install --upgrade pip && \
|
||||
python -m venv /opt/venv && \
|
||||
python -m pip install --upgrade pillow psutil requests python-slugify raven packaging mozjpeg-lossless-optimization natsort distro
|
||||
|
||||
|
||||
######################################################################################
|
||||
FROM --platform=linux/amd64 python:3.12-slim-bullseye as build-amd64
|
||||
COPY --from=compile-amd64 /opt/venv /opt/venv
|
||||
|
||||
FROM --platform=linux/arm64 python:3.12-slim-bullseye as build-arm64
|
||||
COPY --from=compile-arm64 /opt/venv /opt/venv
|
||||
|
||||
FROM --platform=linux/arm/v7 python:3.12-slim-bullseye as build-armv7
|
||||
COPY --from=compile-armv7 /opt/venv /opt/venv
|
||||
######################################################################################
|
||||
|
||||
# Select final stage based on TARGETARCH ARG
|
||||
FROM build-${TARGETARCH}${TARGETVARIANT}
|
||||
LABEL com.kcc.name="Kindle Comic Converter base image"
|
||||
LABEL com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi"
|
||||
LABEL org.opencontainers.image.description='Kindle Comic Converter base image'
|
||||
LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc'
|
||||
LABEL org.opencontainers.image.source='https://github.com/ciromattia/kcc'
|
||||
LABEL org.opencontainers.image.authors='darodi'
|
||||
LABEL org.opencontainers.image.url='https://github.com/ciromattia/kcc'
|
||||
LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc'
|
||||
LABEL org.opencontainers.image.vendor='ciromattia'
|
||||
LABEL org.opencontainers.image.licenses='ISC'
|
||||
LABEL org.opencontainers.image.title="Kindle Comic Converter"
|
||||
|
||||
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
WORKDIR /app
|
||||
RUN DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
|
||||
apt-get install -y p7zip-full unrar-free && \
|
||||
ln -s /app/kindlegen /bin/kindlegen && \
|
||||
echo docker-base-20240928 > /IMAGE_VERSION
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
exclude kindlecomicconverter/sentry.py
|
||||
269
README.md
269
README.md
@@ -1,15 +1,62 @@
|
||||
<img src="header.jpg" alt="Header Image" width="400">
|
||||
|
||||
# KCC
|
||||
|
||||
|
||||
|
||||
[](https://github.com/ciromattia/kcc/releases)
|
||||
[](https://github.com/ciromattia/kcc/pkgs/container/kcc)
|
||||
[](https://github.com/ciromattia/kcc/releases)
|
||||
|
||||
|
||||
**Kindle Comic Converter** is a Python app to convert comic/manga files or folders to EPUB, Panel View MOBI or E-Ink optimized CBZ.
|
||||
It was initially developed for Kindle but since version 4.6 it outputs valid EPUB 3.0 so _**despite its name, KCC is
|
||||
actually a comic/manga to EPUB converter that every e-reader owner can happily use**_.
|
||||
It can also optionally optimize images by applying a number of transformations.
|
||||
**Kindle Comic Converter** optimizes black & white (or color) comics and manga for E-ink ereaders
|
||||
like Kindle, Kobo, ReMarkable, and more.
|
||||
Pages display in fullscreen without margins,
|
||||
with proper fixed layout support.
|
||||
Supported input formats include JPG/PNG image files in folders, archives, or PDFs.
|
||||
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
|
||||
KCC runs on Windows, macOS, and Linux.
|
||||
|
||||
Just drop your input files into the KCC window, hit convert, and USB drop the output files onto your device's `documents` folder!
|
||||
|
||||
https://github.com/user-attachments/assets/da73d625-e082-482d-91a4-ae4765e96fd7
|
||||
|
||||
**WARNING**: Kindle Scribe 2025 support may not be possible. Does not work well currently.
|
||||
|
||||
**NEW**: PDF output is now supported for direct conversion to reMarkable devices!
|
||||
When using a reMarkable profile (Rmk1, Rmk2, RmkPP), the format automatically defaults to PDF
|
||||
for optimal compatibility with your device's native PDF reader.
|
||||
|
||||
The absolute highest quality source files are print quality DRM-free PDFs from Kodansha/[Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1)/Fanatical,
|
||||
which can be directly converted by KCC.
|
||||
|
||||
Its main feature is various optional image processing steps to look good on eink screens,
|
||||
which have different requirements than normal LCD screens.
|
||||
Combining that with downscaling to your specific device's screen resolution
|
||||
can result in filesize reductions of hundreds of MB per volume with no visible quality loss on eink.
|
||||
This can also improve battery life, page turn speed, and general performance
|
||||
on underpowered ereaders with small memory and storage capacities.
|
||||
|
||||
KCC avoids many common formatting issues (some of which occur [even on the Kindle Store](https://github.com/ciromattia/kcc/wiki/Kindle-Store-bad-formatting)), such as:
|
||||
1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain.
|
||||
2) unneccessary margins at the bottom of the screen
|
||||
3) Not utilizing the full 1860x2480 resolution of the 10" Kindle Scribe
|
||||
4) incorrect page turn direction for manga that's read right to left
|
||||
5) unaligned two page spreads in landscape, where pages are shifted over by 1
|
||||
6) Removing without blur the rainbow effect on color eink Kaleido 3 due to manga screentones
|
||||
|
||||
The GUI looks like this, built in Qt6, with my most commonly used settings:
|
||||
|
||||

|
||||
|
||||
Simply drag and drop your files/folders into the KCC window,
|
||||
adjust your settings (hover over each option to see details in a tooltip),
|
||||
and hit convert to create ereader optimized files.
|
||||
You can change the default output directory by holding `Shift` while clicking the convert button.
|
||||
Then just drag and drop the generated output files onto your device's documents folder via USB.
|
||||
If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac.
|
||||
|
||||
YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=QQ6zJcMF2Iw
|
||||
|
||||
Installation tutorial: https://www.youtube.com/watch?v=IR2Fhcm9658
|
||||
|
||||
### A word of warning
|
||||
**KCC** _is not_ [Amazon's Kindle Comic Creator](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1001103761) nor is in any way endorsed by Amazon.
|
||||
@@ -22,15 +69,29 @@ If you have some **technical** problems using KCC please [file an issue here](ht
|
||||
If you can fix an open issue, fork & make a pull request.
|
||||
|
||||
If you find **KCC** valuable you can consider donating to the authors:
|
||||
- Ciro Mattia Gonano (founder, active 2013-2014):
|
||||
- [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=D8WNYNPBGDAS2)
|
||||
- [](http://flattr.com/thing/2260449/ciromattiakcc-on-GitHub)
|
||||
- Paweł Jastrzębski (active 2013-2019):
|
||||
- [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YTTJ4LK2JDHPS)
|
||||
- [](https://jastrzeb.ski/donate/)
|
||||
- Alex Xu (active 2023-Present)
|
||||
- [](https://www.paypal.com/donate/?business=QFJVE7A6LCP6U&no_recurring=0&item_name=Kindle+Comic+Converter¤cy_code=USD)
|
||||
- Ciro Mattia Gonano (founder, active 2012-2014):
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=D8WNYNPBGDAS2)
|
||||
|
||||
- Paweł Jastrzębski (active 2013-2019):
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YTTJ4LK2JDHPS)
|
||||
[](https://jastrzeb.ski/donate/)
|
||||
|
||||
- Alex Xu (active 2023-Present)
|
||||
|
||||
[](https://ko-fi.com/Q5Q41BW8HS)
|
||||
|
||||
## Commissions
|
||||
|
||||
This section is subject to change:
|
||||
|
||||
Email (for commisions and inquiries): `kindle.comic.converter` gmail
|
||||
|
||||
|
||||
## Sponsors
|
||||
|
||||
- Free code signing on Windows provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
|
||||
|
||||
## DOWNLOADS
|
||||
|
||||
@@ -41,22 +102,46 @@ Click on **Assets** of the latest release.
|
||||
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)
|
||||
- `kcc_macos_i386_*.*.*.dmg` (older Mac with Intel chip macOS 14+)
|
||||
|
||||
There are also legacy macOS 10.14+ and Windows 7 experimental versions available.
|
||||
|
||||
The `c2e` and `c2p` versions are command line tools for power users.
|
||||
|
||||
On Windows 11, you may need to run in compatibility mode for an older Windows version.
|
||||
|
||||
On Mac, right click open to get past the security warning.
|
||||
On macOS, if you get a `can't be opened` error, follow: https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unknown-developer-mh40616/mac
|
||||
|
||||
For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation
|
||||
|
||||
## FAQ
|
||||
|
||||
- [Kindle Scribe cover guide](https://github.com/ciromattia/kcc/issues/508) (also works for older Kindles)
|
||||
- Should I use Calibre?
|
||||
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre can break the formatting.
|
||||
Viewing KCC output in Calibre will also not work properly.
|
||||
On 7th gen and later Kindles running firmware 5.15.1+, you can get cover thumbnails simply by USB dropping into documents folder.
|
||||
On 6th gen and older, you can get cover thumbnails by keeping Kindle plugged in during conversion.
|
||||
If you are careful to not modify the file however, you can still use Calibre, but direct USB dropping is reccomended.
|
||||
- Blank pages?
|
||||
- May happen when [using PNG with Kindle Scribe](https://github.com/ciromattia/kcc/issues/665) or [any format with a Kindle Colorsoft](https://github.com/ciromattia/kcc/issues/768). Solve by using JPG with Kindle Scribe or buying a Kobo Colour. Happens more often when turning pages really fast.
|
||||
Going back a few pages and exiting and re-entering book should fix it temporarily.
|
||||
- What output format should I use?
|
||||
- MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable.
|
||||
- All options have additional information in tooltips if you hover over the option.
|
||||
- To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB
|
||||
- Kindle panel view not working?
|
||||
- Virtual panel view is enabled in Aa menu on your Kindle, not in KCC as of 7.4
|
||||
- Right to left mode not working?
|
||||
- RTL mode only affects splitting order for CBZ output. Your cbz reader itself sets the page turn direction.
|
||||
- Colors inverted?
|
||||
- Disable Kindle dark mode
|
||||
- Cannot connect Kindle Scribe or 2024+ Kindle to macOS
|
||||
- Use official MTP [Amazon USB File Transfer app](https://www.amazon.com/gp/help/customer/display.html/ref=hp_Connect_USB_MTP?nodeId=TCUBEdEkbIhK07ysFu)
|
||||
(no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps.
|
||||
- How to make AZW3 instead of MOBI?
|
||||
- The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons.
|
||||
- [Windows 7 support](https://github.com/ciromattia/kcc/issues/678)
|
||||
- [Combine files/chapters](https://github.com/ciromattia/kcc/issues/612#issuecomment-2117985011)
|
||||
- [Flatpak mobi conversion stuck](https://github.com/ciromattia/kcc/wiki/Installation#linux)
|
||||
- Image too dark?
|
||||
- The default gamma correction of 1.8 makes the image darker, and is useful for faded/gray artwork/text. Disable by setting gamma = 1.0
|
||||
- Huge margins / slow page turns?
|
||||
- You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB.
|
||||
|
||||
## PREREQUISITES
|
||||
|
||||
@@ -70,9 +155,13 @@ If you have issues detecting it, get stuck on the MOBI conversion step, or use L
|
||||
|
||||
### 7-Zip
|
||||
|
||||
This is no longer required as of KCC 6.1.
|
||||
This is optional but will make conversions much faster.
|
||||
|
||||
If you still need it, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation#7-zip
|
||||
This is required for certain files and advanced features.
|
||||
|
||||
KCC will ask you to install if needed.
|
||||
|
||||
Refer to the wiki to install: https://github.com/ciromattia/kcc/wiki/Installation#7-zip
|
||||
|
||||
## INPUT FORMATS
|
||||
**KCC** can understand and convert, at the moment, the following input types:
|
||||
@@ -98,33 +187,44 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
|
||||
### Profiles:
|
||||
|
||||
```
|
||||
'K1': ("Kindle 1", (600, 670), Palette4, 1.8),
|
||||
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
|
||||
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
|
||||
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
|
||||
'K578': ("Kindle", (600, 800), Palette16, 1.8),
|
||||
'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),
|
||||
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
|
||||
'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8),
|
||||
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
|
||||
'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),
|
||||
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8),
|
||||
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8),
|
||||
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8),
|
||||
'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),
|
||||
'OTHER': ("Other", (0, 0), Palette16, 1.8),
|
||||
'K1': ("Kindle 1", (600, 670), Palette4, 1.0),
|
||||
'K2': ("Kindle 2", (600, 670), Palette15, 1.0),
|
||||
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
|
||||
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.0),
|
||||
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.0),
|
||||
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0),
|
||||
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.0),
|
||||
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.0),
|
||||
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
|
||||
'KPW34': ("Kindle Paperwhite 3/4", (1072, 1448), Palette16, 1.0),
|
||||
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
|
||||
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0),
|
||||
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0),
|
||||
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
|
||||
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
|
||||
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
|
||||
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
|
||||
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
|
||||
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.0),
|
||||
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.0),
|
||||
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.0),
|
||||
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.0),
|
||||
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.0),
|
||||
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.0),
|
||||
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.0),
|
||||
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.0),
|
||||
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.0),
|
||||
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.0),
|
||||
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.0),
|
||||
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.0),
|
||||
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.0),
|
||||
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.0),
|
||||
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.0),
|
||||
'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.0),
|
||||
'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.0),
|
||||
'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.0),
|
||||
'RmkPPMove': ("reMarkable Paper Pro Move", (954, 1696), Palette16, 1.0),
|
||||
'OTHER': ("Other", (0, 0), Palette16, 1.0),
|
||||
```
|
||||
|
||||
### Standalone `kcc-c2e.py` usage:
|
||||
@@ -137,7 +237,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
|
||||
@@ -146,24 +247,33 @@ MAIN:
|
||||
the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others]
|
||||
|
||||
PROCESSING:
|
||||
-n, --noprocessing Do not modify image and ignore any profil or processing option
|
||||
-n, --noprocessing Do not modify image and ignore any profile or processing option
|
||||
--pdfextract Use legacy PDF image extraction method from KCC 8 and earlier.
|
||||
-u, --upscale Resize images smaller than device's resolution
|
||||
-s, --stretch Stretch images to device's resolution
|
||||
-r SPLITTER, --splitter SPLITTER
|
||||
Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]
|
||||
-g GAMMA, --gamma GAMMA
|
||||
Apply gamma correction to linearize the image [Default=Auto]
|
||||
--autolevel Set most common dark pixel value to be black point for leveling.
|
||||
--noautocontrast Disable autocontrast
|
||||
--colorautocontrast Force autocontrast for all pages. Skipped when near blacks and whites don't exist
|
||||
-c CROPPING, --cropping CROPPING
|
||||
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
|
||||
--coverfill Center-crop only the cover to fill target device screen
|
||||
--forcecolor Don't convert images to grayscale
|
||||
--forcepng Create PNG files instead JPEG
|
||||
--mozjpeg Create JPEG files using mozJpeg
|
||||
--jpeg-quality The JPEG quality, on a scale from 0 (worst) to 95 (best). Default 85 for most devices.
|
||||
--maximizestrips Turn 1x4 strips to 2x2 strips
|
||||
-d, --delete Delete source file(s) or a directory. It's not recoverable.
|
||||
|
||||
@@ -172,10 +282,19 @@ OUTPUT SETTINGS:
|
||||
Output generated file to specified directory or file
|
||||
-t TITLE, --title TITLE
|
||||
Comic title [Default=filename or directory name]
|
||||
--metadatatitle Write title using ComicInfo.xml or other embedded metadata. 0: Don't use Title from metadata 1: Combine Title with default schema 2: Use Title only [Default=0]
|
||||
-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]
|
||||
Output format (Available options: Auto, MOBI, EPUB, CBZ, PDF, KFX, MOBI+EPUB) [Default=Auto]
|
||||
--nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'
|
||||
-b BATCHSPLIT, --batchsplit BATCHSPLIT
|
||||
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
|
||||
--spreadshift Shift first page to opposite side in landscape for two page spread alignment
|
||||
--norotate Do not rotate double page spreads in spread splitter option.
|
||||
--rotatefirst Put rotated spread first in spread splitter option.
|
||||
--filefusion Combines all input files into a single file.
|
||||
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies
|
||||
|
||||
CUSTOM PROFILE:
|
||||
--customwidth CUSTOMWIDTH
|
||||
@@ -211,15 +330,26 @@ 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`.
|
||||
If new objects have been added, verify that correct tab order has been applied by using [Tab Order Editing Mode](https://doc.qt.io/qt-6/designer-tab-order.html).
|
||||
Then use the `gen_ui_files` scripts to autogenerate the python UI.
|
||||
|
||||
An example PR adding a new checkbox is here: https://github.com/ciromattia/kcc/pull/785
|
||||
|
||||
video of adding a new checkbox: https://youtu.be/g3I8DU74C7g
|
||||
|
||||
Do not use `git merge` to merge master from upstream,
|
||||
use the "Sync fork" button on your fork on GitHub in your branch
|
||||
to avoid weird looking merges in pull requests.
|
||||
|
||||
When making changes, be aware of how your change might affect file splitting/chunking
|
||||
or chapter alignment.
|
||||
|
||||
### Windows install from source
|
||||
|
||||
@@ -238,8 +368,16 @@ venv\Scripts\activate.bat
|
||||
python kcc.py
|
||||
```
|
||||
|
||||
You can build a `.exe` of KCC like the downloads we offer with
|
||||
|
||||
```
|
||||
python setup.py build_binary
|
||||
```
|
||||
|
||||
### macOS install from source
|
||||
|
||||
If the system installed Python gives you issues, please install the latest Python from either brew or the official website.
|
||||
|
||||
One time setup and running for the first time:
|
||||
```
|
||||
python3 -m venv venv
|
||||
@@ -255,6 +393,12 @@ source venv/bin/activate
|
||||
python kcc.py
|
||||
```
|
||||
|
||||
You can build a `.app` of KCC like the downloads we offer with
|
||||
|
||||
```
|
||||
python setup.py build_binary
|
||||
```
|
||||
|
||||
## CREDITS
|
||||
**KCC** is made by
|
||||
|
||||
@@ -272,6 +416,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)
|
||||
@@ -284,12 +434,15 @@ The app relies and includes the following scripts:
|
||||
|
||||
## PRIVACY
|
||||
**KCC** is initiating internet connections in two cases:
|
||||
* During startup - Version check.
|
||||
* During startup - Version check and announcement check.
|
||||
* When error occurs - Automatic reporting on Windows and macOS.
|
||||
|
||||
## KNOWN ISSUES
|
||||
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.
|
||||
|
||||
## Verification
|
||||
Impact-Site-Verification: ffe48fc7-4f0c-40fd-bd2e-59f4d7205180
|
||||
|
||||
14
appveyor.yml
14
appveyor.yml
@@ -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*
|
||||
22
entrypoint.sh
Normal file
22
entrypoint.sh
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
MODE=${KCC_MODE:-c2e}
|
||||
|
||||
case "$MODE" in
|
||||
"c2e")
|
||||
echo "Starting C2E..."
|
||||
exec c2e "$@"
|
||||
;;
|
||||
|
||||
"c2p")
|
||||
echo "Starting C2P..."
|
||||
exec c2p "$@"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Error: Unknown mode '$MODE'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1,16 +0,0 @@
|
||||
name: kcc
|
||||
channels:
|
||||
- conda-forge
|
||||
- defaults
|
||||
dependencies:
|
||||
- python=3.11
|
||||
- Pillow>=5.2.0
|
||||
- psutil>=5.9.5
|
||||
- python-slugify>=1.2.1
|
||||
- raven>=6.0.0
|
||||
- distro
|
||||
- natsort[fast]>=8.4.0
|
||||
- pip
|
||||
- pip:
|
||||
- mozjpeg-lossless-optimization>=1.1.2
|
||||
- pyside6>=6.5.1
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -27,4 +28,9 @@
|
||||
<file>../icons/document_new.png</file>
|
||||
<file>../icons/folder_new.png</file>
|
||||
</qresource>
|
||||
<qresource prefix="Brand">
|
||||
<file>../icons/kofi_symbol.png</file>
|
||||
<file>../icons/Humble_H-Red.png</file>
|
||||
<file>../icons/Bindle_Red.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
1325
gui/KCC.ui
1325
gui/KCC.ui
File diff suppressed because it is too large
Load Diff
@@ -62,56 +62,66 @@
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="volumeLine"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Number:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="3" column="1">
|
||||
<widget class="QLineEdit" name="numberLine"/>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Writer:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<item row="4" column="1">
|
||||
<widget class="QLineEdit" name="writerLine"/>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Penciller:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<item row="5" column="1">
|
||||
<widget class="QLineEdit" name="pencillerLine"/>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Inker:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<item row="6" column="1">
|
||||
<widget class="QLineEdit" name="inkerLine"/>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Colorist:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<item row="7" column="1">
|
||||
<widget class="QLineEdit" name="coloristLine"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>Title:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="titleLine"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -182,6 +192,18 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>seriesLine</tabstop>
|
||||
<tabstop>volumeLine</tabstop>
|
||||
<tabstop>titleLine</tabstop>
|
||||
<tabstop>numberLine</tabstop>
|
||||
<tabstop>writerLine</tabstop>
|
||||
<tabstop>pencillerLine</tabstop>
|
||||
<tabstop>inkerLine</tabstop>
|
||||
<tabstop>coloristLine</tabstop>
|
||||
<tabstop>okButton</tabstop>
|
||||
<tabstop>cancelButton</tabstop>
|
||||
</tabstops>
|
||||
<resources>
|
||||
<include location="KCC.qrc"/>
|
||||
</resources>
|
||||
|
||||
BIN
header.jpg
Normal file
BIN
header.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 921 KiB |
BIN
icons/Bindle_Red.png
Normal file
BIN
icons/Bindle_Red.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
BIN
icons/Humble_H-Red.png
Normal file
BIN
icons/Humble_H-Red.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
icons/Rmk.png
Normal file
BIN
icons/Rmk.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
BIN
icons/kofi_symbol.png
Normal file
BIN
icons/kofi_symbol.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
@@ -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()
|
||||
|
||||
@@ -11,7 +11,7 @@ a = Analysis(['kcc-c2e.py'],
|
||||
hiddenimports=['_cffi_backend'],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
excludes=['pkg_resources'],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -11,7 +11,7 @@ a = Analysis(['kcc-c2p.py'],
|
||||
hiddenimports=['_cffi_backend'],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
excludes=['pkg_resources'],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
|
||||
105
kcc.py
105
kcc.py
@@ -18,63 +18,76 @@
|
||||
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
if sys.version_info < (3, 8, 0):
|
||||
print('ERROR: This is a Python 3.8+ script!')
|
||||
sys.exit(1)
|
||||
|
||||
# OS specific workarounds
|
||||
import os
|
||||
if sys.platform.startswith('darwin'):
|
||||
# prioritize KC2 since it optionally also installs KP3
|
||||
mac_paths = [
|
||||
'/Applications/Kindle Comic Creator/Kindle Comic Creator.app/Contents/MacOS',
|
||||
'/Applications/Kindle Previewer 3.app/Contents/lib/fc/bin/',
|
||||
]
|
||||
if getattr(sys, 'frozen', False):
|
||||
os.environ['PATH'] += os.pathsep + os.pathsep.join(mac_paths +
|
||||
[
|
||||
'/opt/homebrew/bin',
|
||||
'/usr/local/bin',
|
||||
'/usr/bin',
|
||||
'/bin',
|
||||
]
|
||||
)
|
||||
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
|
||||
else:
|
||||
os.environ['PATH'] += os.pathsep + os.pathsep.join(mac_paths)
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
elif sys.platform.startswith('win'):
|
||||
# prioritize KC2 since it optionally also installs KP3
|
||||
win_paths = [
|
||||
os.path.expandvars('%LOCALAPPDATA%\\Amazon\\KC2'),
|
||||
os.path.expandvars('%LOCALAPPDATA%\\Amazon\\Kindle Previewer 3\\lib\\fc\\bin\\'),
|
||||
os.path.expandvars('%UserProfile%\\Kindle Previewer 3\\lib\\fc\\bin\\'),
|
||||
'C:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin',
|
||||
'D:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin',
|
||||
'E:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin',
|
||||
'C:\\Program Files\\7-Zip',
|
||||
'D:\\Program Files\\7-Zip',
|
||||
'E:\\Program Files\\7-Zip',
|
||||
]
|
||||
if getattr(sys, 'frozen', False):
|
||||
os.environ['PATH'] += os.pathsep + os.pathsep.join(win_paths)
|
||||
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
|
||||
else:
|
||||
os.environ['PATH'] += os.pathsep + os.pathsep.join(win_paths)
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
# Load additional Sentry configuration
|
||||
# if getattr(sys, 'frozen', False):
|
||||
# try:
|
||||
# import kindlecomicconverter.sentry
|
||||
# except ImportError:
|
||||
# pass
|
||||
def modify_path():
|
||||
if platform.system() == 'Darwin':
|
||||
mac_paths = [
|
||||
'/Applications/Kindle Comic Creator/Kindle Comic Creator.app/Contents/MacOS',
|
||||
'/Applications/Kindle Previewer 3.app/Contents/lib/fc/bin/',
|
||||
]
|
||||
if getattr(sys, 'frozen', False):
|
||||
os.environ['PATH'] += os.pathsep + os.pathsep.join(mac_paths +
|
||||
[
|
||||
'/opt/homebrew/bin',
|
||||
'/usr/local/bin',
|
||||
'/usr/bin',
|
||||
'/bin',
|
||||
]
|
||||
)
|
||||
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
|
||||
else:
|
||||
os.environ['PATH'] += os.pathsep + os.pathsep.join(mac_paths)
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
elif platform.system() == 'Linux':
|
||||
if getattr(sys, 'frozen', False):
|
||||
os.environ['PATH'] += os.pathsep + os.pathsep.join(
|
||||
[
|
||||
str(Path.home() / ".local" / "bin"),
|
||||
'/opt/homebrew/bin',
|
||||
'/usr/local/bin',
|
||||
'/usr/bin',
|
||||
'/bin',
|
||||
]
|
||||
)
|
||||
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
|
||||
else:
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
elif platform.system() == 'Windows':
|
||||
win_paths = [
|
||||
os.path.expandvars('%LOCALAPPDATA%\\Amazon\\KC2'),
|
||||
os.path.expandvars('%LOCALAPPDATA%\\Amazon\\Kindle Previewer 3\\lib\\fc\\bin\\'),
|
||||
os.path.expandvars('%UserProfile%\\Kindle Previewer 3\\lib\\fc\\bin\\'),
|
||||
'C:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin',
|
||||
'D:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin',
|
||||
'E:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin',
|
||||
'C:\\Program Files\\7-Zip',
|
||||
'D:\\Program Files\\7-Zip',
|
||||
'E:\\Program Files\\7-Zip',
|
||||
]
|
||||
if getattr(sys, 'frozen', False):
|
||||
os.environ['PATH'] += os.pathsep + os.pathsep.join(win_paths)
|
||||
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
|
||||
else:
|
||||
os.environ['PATH'] += os.pathsep + os.pathsep.join(win_paths)
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
from multiprocessing import freeze_support, set_start_method
|
||||
from kindlecomicconverter.startup import start
|
||||
|
||||
if __name__ == "__main__":
|
||||
modify_path()
|
||||
set_start_method('spawn')
|
||||
freeze_support()
|
||||
start()
|
||||
|
||||
39
kcc.spec
Normal file
39
kcc.spec
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(['kcc.py'],
|
||||
pathex=['.'],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=['_cffi_backend'],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=['pkg_resources'],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False)
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='kcc',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=False,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None , icon='icons\\comic2ebook.ico')
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'MetaEditor.ui'
|
||||
##
|
||||
## Created by: Qt User Interface Compiler version 6.5.2
|
||||
## Created by: Qt User Interface Compiler version 6.9.3
|
||||
##
|
||||
## 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")
|
||||
@@ -60,52 +60,62 @@ class Ui_editorDialog(object):
|
||||
self.label_3 = QLabel(self.editorWidget)
|
||||
self.label_3.setObjectName(u"label_3")
|
||||
|
||||
self.gridLayout.addWidget(self.label_3, 2, 0, 1, 1)
|
||||
self.gridLayout.addWidget(self.label_3, 3, 0, 1, 1)
|
||||
|
||||
self.numberLine = QLineEdit(self.editorWidget)
|
||||
self.numberLine.setObjectName(u"numberLine")
|
||||
|
||||
self.gridLayout.addWidget(self.numberLine, 2, 1, 1, 1)
|
||||
self.gridLayout.addWidget(self.numberLine, 3, 1, 1, 1)
|
||||
|
||||
self.label_4 = QLabel(self.editorWidget)
|
||||
self.label_4.setObjectName(u"label_4")
|
||||
|
||||
self.gridLayout.addWidget(self.label_4, 3, 0, 1, 1)
|
||||
self.gridLayout.addWidget(self.label_4, 4, 0, 1, 1)
|
||||
|
||||
self.writerLine = QLineEdit(self.editorWidget)
|
||||
self.writerLine.setObjectName(u"writerLine")
|
||||
|
||||
self.gridLayout.addWidget(self.writerLine, 3, 1, 1, 1)
|
||||
self.gridLayout.addWidget(self.writerLine, 4, 1, 1, 1)
|
||||
|
||||
self.label_5 = QLabel(self.editorWidget)
|
||||
self.label_5.setObjectName(u"label_5")
|
||||
|
||||
self.gridLayout.addWidget(self.label_5, 4, 0, 1, 1)
|
||||
self.gridLayout.addWidget(self.label_5, 5, 0, 1, 1)
|
||||
|
||||
self.pencillerLine = QLineEdit(self.editorWidget)
|
||||
self.pencillerLine.setObjectName(u"pencillerLine")
|
||||
|
||||
self.gridLayout.addWidget(self.pencillerLine, 4, 1, 1, 1)
|
||||
self.gridLayout.addWidget(self.pencillerLine, 5, 1, 1, 1)
|
||||
|
||||
self.label_6 = QLabel(self.editorWidget)
|
||||
self.label_6.setObjectName(u"label_6")
|
||||
|
||||
self.gridLayout.addWidget(self.label_6, 5, 0, 1, 1)
|
||||
self.gridLayout.addWidget(self.label_6, 6, 0, 1, 1)
|
||||
|
||||
self.inkerLine = QLineEdit(self.editorWidget)
|
||||
self.inkerLine.setObjectName(u"inkerLine")
|
||||
|
||||
self.gridLayout.addWidget(self.inkerLine, 5, 1, 1, 1)
|
||||
self.gridLayout.addWidget(self.inkerLine, 6, 1, 1, 1)
|
||||
|
||||
self.label_7 = QLabel(self.editorWidget)
|
||||
self.label_7.setObjectName(u"label_7")
|
||||
|
||||
self.gridLayout.addWidget(self.label_7, 6, 0, 1, 1)
|
||||
self.gridLayout.addWidget(self.label_7, 7, 0, 1, 1)
|
||||
|
||||
self.coloristLine = QLineEdit(self.editorWidget)
|
||||
self.coloristLine.setObjectName(u"coloristLine")
|
||||
|
||||
self.gridLayout.addWidget(self.coloristLine, 6, 1, 1, 1)
|
||||
self.gridLayout.addWidget(self.coloristLine, 7, 1, 1, 1)
|
||||
|
||||
self.label_8 = QLabel(self.editorWidget)
|
||||
self.label_8.setObjectName(u"label_8")
|
||||
|
||||
self.gridLayout.addWidget(self.label_8, 2, 0, 1, 1)
|
||||
|
||||
self.titleLine = QLineEdit(self.editorWidget)
|
||||
self.titleLine.setObjectName(u"titleLine")
|
||||
|
||||
self.gridLayout.addWidget(self.titleLine, 2, 1, 1, 1)
|
||||
|
||||
|
||||
self.verticalLayout.addWidget(self.editorWidget)
|
||||
@@ -117,7 +127,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 +139,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 +148,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)
|
||||
@@ -146,6 +156,15 @@ class Ui_editorDialog(object):
|
||||
|
||||
self.verticalLayout.addWidget(self.optionWidget)
|
||||
|
||||
QWidget.setTabOrder(self.seriesLine, self.volumeLine)
|
||||
QWidget.setTabOrder(self.volumeLine, self.titleLine)
|
||||
QWidget.setTabOrder(self.titleLine, self.numberLine)
|
||||
QWidget.setTabOrder(self.numberLine, self.writerLine)
|
||||
QWidget.setTabOrder(self.writerLine, self.pencillerLine)
|
||||
QWidget.setTabOrder(self.pencillerLine, self.inkerLine)
|
||||
QWidget.setTabOrder(self.inkerLine, self.coloristLine)
|
||||
QWidget.setTabOrder(self.coloristLine, self.okButton)
|
||||
QWidget.setTabOrder(self.okButton, self.cancelButton)
|
||||
|
||||
self.retranslateUi(editorDialog)
|
||||
|
||||
@@ -161,6 +180,7 @@ class Ui_editorDialog(object):
|
||||
self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None))
|
||||
self.label_6.setText(QCoreApplication.translate("editorDialog", u"Inker:", None))
|
||||
self.label_7.setText(QCoreApplication.translate("editorDialog", u"Colorist:", None))
|
||||
self.label_8.setText(QCoreApplication.translate("editorDialog", u"Title:", None))
|
||||
self.statusLabel.setText("")
|
||||
self.okButton.setText(QCoreApplication.translate("editorDialog", u"Save", None))
|
||||
self.cancelButton.setText(QCoreApplication.translate("editorDialog", u"Cancel", None))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
__version__ = '6.2.2'
|
||||
__version__ = '9.5.0'
|
||||
__license__ = 'ISC'
|
||||
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,13 +18,17 @@
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
#
|
||||
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
from argparse import ArgumentParser
|
||||
from shutil import rmtree, copytree, move
|
||||
from shutil import rmtree
|
||||
from multiprocessing import Pool
|
||||
from PIL import Image, ImageChops, ImageOps, ImageDraw
|
||||
from .shared import getImageFileName, walkLevel, walkSort, sanitizeTrace
|
||||
from PIL import Image, ImageChops, ImageOps, ImageDraw, ImageFilter, ImageFile
|
||||
from PIL.Image import Dither
|
||||
from .shared import dot_clean, getImageFileName, walkLevel, walkSort, sanitizeTrace
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
|
||||
|
||||
def mergeDirectoryTick(output):
|
||||
@@ -44,6 +48,7 @@ def mergeDirectory(work):
|
||||
imagesValid = []
|
||||
sizes = []
|
||||
targetHeight = 0
|
||||
dot_clean(directory)
|
||||
for root, _, files in walkLevel(directory, 0):
|
||||
for name in files:
|
||||
if getImageFileName(name) is not None:
|
||||
@@ -57,18 +62,19 @@ def mergeDirectory(work):
|
||||
imagesValid.append(i[0])
|
||||
# Silently drop directories that contain too many images
|
||||
# 131072 = GIMP_MAX_IMAGE_SIZE / 4
|
||||
if targetHeight > 131072:
|
||||
return None
|
||||
if targetHeight > 131072 * 4:
|
||||
raise RuntimeError(f'Image too tall at {targetHeight} pixels. {targetWidth} pixels wide. Try using separate chapter folders or file fusion.')
|
||||
result = Image.new('RGB', (targetWidth, targetHeight))
|
||||
y = 0
|
||||
for i in imagesValid:
|
||||
img = Image.open(i).convert('RGB')
|
||||
if img.size[0] < targetWidth or img.size[0] > targetWidth:
|
||||
widthPercent = (targetWidth / float(img.size[0]))
|
||||
heightSize = int((float(img.size[1]) * float(widthPercent)))
|
||||
img = ImageOps.fit(img, (targetWidth, heightSize), method=Image.BICUBIC, centering=(0.5, 0.5))
|
||||
result.paste(img, (0, y))
|
||||
y += img.size[1]
|
||||
with Image.open(i) as img:
|
||||
img = img.convert('RGB')
|
||||
if img.size[0] < targetWidth or img.size[0] > targetWidth:
|
||||
widthPercent = (targetWidth / float(img.size[0]))
|
||||
heightSize = int((float(img.size[1]) * float(widthPercent)))
|
||||
img = ImageOps.fit(img, (targetWidth, heightSize), method=Image.BICUBIC, centering=(0.5, 0.5))
|
||||
result.paste(img, (0, y))
|
||||
y += img.size[1]
|
||||
os.remove(i)
|
||||
savePath = os.path.split(imagesValid[0])
|
||||
result.save(os.path.join(savePath[0], os.path.splitext(savePath[1])[0] + '.png'), 'PNG')
|
||||
@@ -100,7 +106,11 @@ def splitImage(work):
|
||||
Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
|
||||
Image.MAX_IMAGE_PIXELS = 1000000000
|
||||
imgOrg = Image.open(filePath).convert('RGB')
|
||||
imgProcess = Image.open(filePath).convert('1')
|
||||
# I experimented with custom vertical edge kernel [-1, 2, -1] but got poor results
|
||||
imgEdges = Image.open(filePath).convert('L').filter(ImageFilter.FIND_EDGES)
|
||||
# threshold of 8 is too high. 5 is too low.
|
||||
imgProcess = imgEdges.point(lambda p: 255 if p > 6 else 0).convert('1', dither=Dither.NONE)
|
||||
|
||||
widthImg, heightImg = imgOrg.size
|
||||
if heightImg > opt.height:
|
||||
if opt.debug:
|
||||
@@ -111,47 +121,71 @@ def splitImage(work):
|
||||
yWork = 0
|
||||
panelDetected = False
|
||||
panels = []
|
||||
# check git history for how these constant values changed
|
||||
h_pad = int(widthImg / 20)
|
||||
v_pad = int(widthImg / 80)
|
||||
if v_pad % 2:
|
||||
v_pad += 1
|
||||
while yWork < heightImg:
|
||||
tmpImg = imgProcess.crop((4, yWork, widthImg-4, yWork + 4))
|
||||
tmpImg = imgProcess.crop((h_pad, yWork, widthImg - h_pad, yWork + v_pad))
|
||||
solid = detectSolid(tmpImg)
|
||||
if not solid and not panelDetected:
|
||||
panelDetected = True
|
||||
panelY1 = yWork - 2
|
||||
if heightImg - yWork <= 5:
|
||||
panelY1 = yWork
|
||||
if heightImg - yWork <= (v_pad // 2):
|
||||
if not solid and panelDetected:
|
||||
panelY2 = heightImg
|
||||
panelDetected = False
|
||||
panels.append((panelY1, panelY2, panelY2 - panelY1))
|
||||
if solid and panelDetected:
|
||||
panelDetected = False
|
||||
panelY2 = yWork + 6
|
||||
panelY2 = yWork
|
||||
# skip short panel at start
|
||||
if panelY1 < v_pad * 2 and panelY2 - panelY1 < v_pad * 2:
|
||||
continue
|
||||
panels.append((panelY1, panelY2, panelY2 - panelY1))
|
||||
yWork += 5
|
||||
yWork += v_pad // 2
|
||||
|
||||
max_width = 1072
|
||||
virtual_width = min((max_width, opt.width, widthImg))
|
||||
if opt.width > max_width:
|
||||
virtual_height = int(opt.height/max_width*virtual_width)
|
||||
else:
|
||||
virtual_height = int(opt.height/opt.width*virtual_width)
|
||||
opt.height = virtual_height
|
||||
|
||||
# Split too big panels
|
||||
panelsProcessed = []
|
||||
for panel in panels:
|
||||
# 1.52 too high
|
||||
if panel[2] <= opt.height * 1.5:
|
||||
panelsProcessed.append(panel)
|
||||
elif panel[2] < opt.height * 2:
|
||||
elif panel[2] <= opt.height * 2:
|
||||
diff = panel[2] - opt.height
|
||||
panelsProcessed.append((panel[0], panel[1] - diff, opt.height))
|
||||
panelsProcessed.append((panel[1] - opt.height, panel[1], opt.height))
|
||||
else:
|
||||
parts = round(panel[2] / opt.height)
|
||||
# split super long panels with overlap
|
||||
parts = math.ceil(panel[2] / opt.height)
|
||||
diff = panel[2] // parts
|
||||
for x in range(0, parts):
|
||||
panelsProcessed.append((panel[0] + (x * diff), panel[1] - ((parts - x - 1) * diff), diff))
|
||||
panelsProcessed.append((panel[0], panel[0] + opt.height, opt.height))
|
||||
for x in range(1, parts - 1):
|
||||
start = panel[0] + (x * diff)
|
||||
panelsProcessed.append((start, start + opt.height, opt.height))
|
||||
panelsProcessed.append((panel[1] - opt.height, panel[1], opt.height))
|
||||
|
||||
if opt.debug:
|
||||
for panel in panelsProcessed:
|
||||
draw.rectangle(((0, panel[0]), (widthImg, panel[1])), (0, 255, 0, 128), (0, 0, 255, 255))
|
||||
debugImage = Image.alpha_composite(imgOrg.convert(mode='RGBA'), drawImg)
|
||||
# debugImage.show()
|
||||
debugImage.save(os.path.join(path, os.path.splitext(name)[0] + '-debug.png'), 'PNG')
|
||||
|
||||
# Create virtual pages
|
||||
pages = []
|
||||
currentPage = []
|
||||
# TODO: 1.25 way too high, 1.1 too high, 1.05 slightly too high(?), optimized for 2 page landscape reading
|
||||
# opt.height = max_height = virtual_height * 1.00
|
||||
pageLeft = opt.height
|
||||
panelNumber = 0
|
||||
for panel in panelsProcessed:
|
||||
@@ -181,14 +215,14 @@ 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:
|
||||
return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2])
|
||||
|
||||
|
||||
def main(argv=None, qtgui=None):
|
||||
def main(argv=None, job_progress='', qtgui=None):
|
||||
global args, GUI, splitWorkerPool, splitWorkerOutput, mergeWorkerPool, mergeWorkerOutput
|
||||
parser = ArgumentParser(prog="kcc-c2p", usage="kcc-c2p [options] [input]", add_help=False)
|
||||
|
||||
@@ -200,6 +234,8 @@ def main(argv=None, qtgui=None):
|
||||
" with spaces.")
|
||||
main_options.add_argument("-y", "--height", type=int, dest="height", default=0,
|
||||
help="Height of the target device screen")
|
||||
main_options.add_argument("-x", "--width", type=int, dest="width", default=0,
|
||||
help="Width of the target device screen")
|
||||
main_options.add_argument("-i", "--in-place", action="store_true", dest="inPlace", default=False,
|
||||
help="Overwrite source directory")
|
||||
main_options.add_argument("-m", "--merge", action="store_true", dest="merge", default=False,
|
||||
@@ -218,16 +254,14 @@ def main(argv=None, qtgui=None):
|
||||
return 1
|
||||
if args.height > 0:
|
||||
for sourceDir in args.input:
|
||||
targetDir = sourceDir + "-Splitted"
|
||||
targetDir = sourceDir
|
||||
if os.path.isdir(sourceDir):
|
||||
rmtree(targetDir, True)
|
||||
copytree(sourceDir, targetDir)
|
||||
work = []
|
||||
pagenumber = 1
|
||||
splitWorkerOutput = []
|
||||
splitWorkerPool = Pool(maxtasksperchild=10)
|
||||
if args.merge:
|
||||
print("Merging images...")
|
||||
print(f"{job_progress}Merging images...")
|
||||
directoryNumer = 1
|
||||
mergeWork = []
|
||||
mergeWorkerOutput = []
|
||||
@@ -239,7 +273,7 @@ def main(argv=None, qtgui=None):
|
||||
directoryNumer += 1
|
||||
mergeWork.append([os.path.join(root, directory)])
|
||||
if GUI:
|
||||
GUI.progressBarTick.emit('Combining images')
|
||||
GUI.progressBarTick.emit(f'{job_progress}Combining images')
|
||||
GUI.progressBarTick.emit(str(directoryNumer))
|
||||
for i in mergeWork:
|
||||
mergeWorkerPool.apply_async(func=mergeDirectory, args=(i, ), callback=mergeDirectoryTick)
|
||||
@@ -252,7 +286,8 @@ def main(argv=None, qtgui=None):
|
||||
rmtree(targetDir, True)
|
||||
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0],
|
||||
mergeWorkerOutput[0][1])
|
||||
print("Splitting images...")
|
||||
print(f"{job_progress}Splitting images...")
|
||||
dot_clean(targetDir)
|
||||
for root, _, files in os.walk(targetDir, False):
|
||||
for name in files:
|
||||
if getImageFileName(name) is not None:
|
||||
@@ -261,7 +296,7 @@ def main(argv=None, qtgui=None):
|
||||
else:
|
||||
os.remove(os.path.join(root, name))
|
||||
if GUI:
|
||||
GUI.progressBarTick.emit('Splitting images')
|
||||
GUI.progressBarTick.emit(f'{job_progress}Splitting images')
|
||||
GUI.progressBarTick.emit(str(pagenumber))
|
||||
GUI.progressBarTick.emit('tick')
|
||||
if len(work) > 0:
|
||||
@@ -269,6 +304,7 @@ def main(argv=None, qtgui=None):
|
||||
splitWorkerPool.apply_async(func=splitImage, args=(i, ), callback=splitImageTick)
|
||||
splitWorkerPool.close()
|
||||
splitWorkerPool.join()
|
||||
dot_clean(targetDir)
|
||||
if GUI and not GUI.conversionAlive:
|
||||
rmtree(targetDir, True)
|
||||
raise UserWarning("Conversion interrupted.")
|
||||
@@ -276,12 +312,9 @@ def main(argv=None, qtgui=None):
|
||||
rmtree(targetDir, True)
|
||||
raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0],
|
||||
splitWorkerOutput[0][1])
|
||||
if args.inPlace:
|
||||
rmtree(sourceDir)
|
||||
move(targetDir, sourceDir)
|
||||
else:
|
||||
rmtree(targetDir, True)
|
||||
raise UserWarning("Source directory is empty.")
|
||||
raise UserWarning("C2P: Source directory is empty.")
|
||||
else:
|
||||
raise UserWarning("Provided input is not a directory.")
|
||||
else:
|
||||
|
||||
@@ -18,16 +18,19 @@
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
#
|
||||
|
||||
from functools import cached_property
|
||||
from functools import cached_property, lru_cache
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import distro
|
||||
from subprocess import STDOUT, PIPE, CalledProcessError
|
||||
from xml.dom.minidom import parseString
|
||||
from xml.parsers.expat import ExpatError
|
||||
from .shared import subprocess_run
|
||||
from .shared import IMAGE_TYPES, subprocess_run
|
||||
|
||||
EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.'
|
||||
SEVENZIP = '7zz' if platform.system() == 'Darwin' else '7z'
|
||||
TAR = 'bsdtar' if platform.system() == 'Linux' else 'tar'
|
||||
|
||||
|
||||
class ComicArchive:
|
||||
@@ -35,21 +38,22 @@ class ComicArchive:
|
||||
self.filepath = filepath
|
||||
if not os.path.isfile(self.filepath):
|
||||
raise OSError('File not found.')
|
||||
self.dirname, self.basename = os.path.split(filepath)
|
||||
|
||||
@cached_property
|
||||
def type(self):
|
||||
extraction_commands = [
|
||||
['7z', 'l', '-y', '-p1', self.filepath],
|
||||
[SEVENZIP, 'l', '-y', '-p1', self.basename],
|
||||
]
|
||||
|
||||
if distro.id() == 'fedora':
|
||||
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
||||
extraction_commands.append(
|
||||
['unrar', 'l', '-y', '-p1', self.filepath],
|
||||
['unrar', 'l', '-y', '-p1', self.basename],
|
||||
)
|
||||
|
||||
for cmd in extraction_commands:
|
||||
try:
|
||||
process = subprocess_run(cmd, capture_output=True, check=True)
|
||||
process = subprocess_run(cmd, capture_output=True, check=True, cwd=self.dirname)
|
||||
for line in process.stdout.splitlines():
|
||||
if b'Type =' in line:
|
||||
return line.rstrip().decode().split(' = ')[1].upper()
|
||||
@@ -63,28 +67,33 @@ class ComicArchive:
|
||||
def extract(self, targetdir):
|
||||
if not os.path.isdir(targetdir):
|
||||
raise OSError('Target directory doesn\'t exist.')
|
||||
|
||||
if Path(self.basename).suffix.lower() in IMAGE_TYPES:
|
||||
raise UserWarning('Put images into folder and drag and drop folder into KCC window.')
|
||||
|
||||
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],
|
||||
[TAR, '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.basename, '-C', targetdir],
|
||||
[SEVENZIP, 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.basename],
|
||||
]
|
||||
|
||||
if platform.system() == 'Darwin':
|
||||
extraction_commands.append(
|
||||
['unar', self.filepath, '-f', '-o', targetdir]
|
||||
['unar', self.basename, '-D', '-f', '-o', targetdir]
|
||||
)
|
||||
|
||||
if distro.id() == 'fedora':
|
||||
extraction_commands.reverse()
|
||||
|
||||
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
||||
extraction_commands.append(
|
||||
['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.filepath, targetdir]
|
||||
['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.basename, targetdir]
|
||||
)
|
||||
|
||||
for cmd in extraction_commands:
|
||||
try:
|
||||
subprocess_run(cmd, capture_output=True, check=True)
|
||||
return targetdir
|
||||
subprocess_run(cmd, capture_output=True, check=True, cwd=self.dirname)
|
||||
return targetdir
|
||||
except FileNotFoundError:
|
||||
missing.append(cmd[0])
|
||||
except CalledProcessError:
|
||||
@@ -98,17 +107,30 @@ class ComicArchive:
|
||||
def addFile(self, sourcefile):
|
||||
if self.type in ['RAR', 'RAR5']:
|
||||
raise NotImplementedError
|
||||
process = subprocess_run(['7z', 'a', '-y', self.filepath, sourcefile],
|
||||
stdout=PIPE, stderr=STDOUT)
|
||||
process = subprocess_run([SEVENZIP, 'a', '-y', self.basename, sourcefile],
|
||||
stdout=PIPE, stderr=STDOUT, cwd=self.dirname)
|
||||
if process.returncode != 0:
|
||||
raise OSError('Failed to add the file.')
|
||||
|
||||
def extractMetadata(self):
|
||||
process = subprocess_run(['7z', 'x', '-y', '-so', self.filepath, 'ComicInfo.xml'],
|
||||
stdout=PIPE, stderr=STDOUT)
|
||||
process = subprocess_run([SEVENZIP, 'x', '-y', '-so', self.basename, 'ComicInfo.xml'],
|
||||
stdout=PIPE, stderr=STDOUT, cwd=self.dirname)
|
||||
if process.returncode != 0:
|
||||
raise OSError(EXTRACTION_ERROR)
|
||||
try:
|
||||
return parseString(process.stdout)
|
||||
except ExpatError:
|
||||
return None
|
||||
|
||||
@lru_cache
|
||||
def available_archive_tools():
|
||||
available = []
|
||||
|
||||
for tool in [TAR, SEVENZIP, 'unar', 'unrar']:
|
||||
try:
|
||||
subprocess_run([tool], stdout=PIPE, stderr=STDOUT)
|
||||
available.append(tool)
|
||||
except (FileNotFoundError, CalledProcessError):
|
||||
pass
|
||||
|
||||
return available
|
||||
|
||||
28
kindlecomicconverter/common_crop.py
Normal file
28
kindlecomicconverter/common_crop.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -20,11 +20,18 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import io
|
||||
import os
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from functools import cached_property
|
||||
import mozjpeg_lossless_optimization
|
||||
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter
|
||||
from .shared import md5Checksum
|
||||
from PIL import Image, ImageOps, ImageFile, ImageChops, ImageDraw
|
||||
|
||||
from .rainbow_artifacts_eraser import erase_rainbow_artifacts
|
||||
from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin
|
||||
from .inter_panel_crop_alg import crop_empty_inter_panel
|
||||
|
||||
AUTO_CROP_THRESHOLD = 0.015
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
|
||||
|
||||
class ProfileData:
|
||||
@@ -79,20 +86,28 @@ class ProfileData:
|
||||
]
|
||||
|
||||
ProfilesKindleEBOK = {
|
||||
'K1': ("Kindle 1", (600, 670), Palette4, 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),
|
||||
'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),
|
||||
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
|
||||
'K1': ("Kindle 1", (600, 670), Palette4, 1.0),
|
||||
'K2': ("Kindle 2", (600, 670), Palette15, 1.0),
|
||||
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.0),
|
||||
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.0),
|
||||
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.0),
|
||||
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.0),
|
||||
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
|
||||
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.0),
|
||||
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0),
|
||||
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0),
|
||||
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
|
||||
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
|
||||
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
|
||||
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
|
||||
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
|
||||
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
|
||||
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0),
|
||||
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
|
||||
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
|
||||
}
|
||||
|
||||
ProfilesKindle = {
|
||||
@@ -101,27 +116,35 @@ class ProfileData:
|
||||
}
|
||||
|
||||
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),
|
||||
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8),
|
||||
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8),
|
||||
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8),
|
||||
'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),
|
||||
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.0),
|
||||
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.0),
|
||||
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.0),
|
||||
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.0),
|
||||
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.0),
|
||||
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.0),
|
||||
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.0),
|
||||
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.0),
|
||||
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.0),
|
||||
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.0),
|
||||
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.0),
|
||||
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.0),
|
||||
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.0),
|
||||
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.0),
|
||||
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.0),
|
||||
}
|
||||
|
||||
ProfilesRemarkable = {
|
||||
'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.0),
|
||||
'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.0),
|
||||
'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.0),
|
||||
'RmkPPMove': ("reMarkable Paper Pro Move", (954, 1696), Palette16, 1.0),
|
||||
}
|
||||
|
||||
Profiles = {
|
||||
**ProfilesKindle,
|
||||
**ProfilesKobo,
|
||||
'OTHER': ("Other", (0, 0), Palette16, 1.8),
|
||||
**ProfilesRemarkable,
|
||||
'OTHER': ("Other", (0, 0), Palette16, 1.0),
|
||||
}
|
||||
|
||||
|
||||
@@ -132,8 +155,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')
|
||||
self.color = self.colorCheck()
|
||||
|
||||
# Detect corruption in source image, let caller catch any exceptions triggered.
|
||||
srcImgPath = os.path.join(source[0], source[1])
|
||||
# Image.open(srcImgPath).verify()
|
||||
with Image.open(srcImgPath) as im:
|
||||
self.image = im.copy()
|
||||
|
||||
self.fill = self.fillCheck()
|
||||
# backwards compatibility for Pillow >9.1.0
|
||||
if not hasattr(Image, 'Resampling'):
|
||||
@@ -164,10 +192,13 @@ class ComicPageParser:
|
||||
new_image = Image.new("RGB", (int(width / 2), int(height*2)))
|
||||
new_image.paste(pageone, (0, 0))
|
||||
new_image.paste(pagetwo, (0, height))
|
||||
self.payload.append(['N', self.source, new_image, self.color, self.fill])
|
||||
self.payload.append(['N', self.source, new_image, self.fill])
|
||||
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth \
|
||||
and not self.opt.webtoon and self.opt.splitter == 1:
|
||||
self.payload.append(['R', self.source, self.image.rotate(90, Image.Resampling.BICUBIC, True), self.color, self.fill])
|
||||
spread = self.image
|
||||
if not self.opt.norotate:
|
||||
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
||||
self.payload.append(['R', self.source, spread, self.fill])
|
||||
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
|
||||
if self.opt.splitter != 1:
|
||||
if width > height:
|
||||
@@ -182,35 +213,15 @@ class ComicPageParser:
|
||||
else:
|
||||
pageone = self.image.crop(leftbox)
|
||||
pagetwo = self.image.crop(rightbox)
|
||||
self.payload.append(['S1', self.source, pageone, self.color, self.fill])
|
||||
self.payload.append(['S2', self.source, pagetwo, self.color, self.fill])
|
||||
self.payload.append(['S1', self.source, pageone, self.fill])
|
||||
self.payload.append(['S2', self.source, pagetwo, self.fill])
|
||||
if self.opt.splitter > 0:
|
||||
self.payload.append(['R', self.source, self.image.rotate(90, Image.Resampling.BICUBIC, True),
|
||||
self.color, self.fill])
|
||||
spread = self.image
|
||||
if not self.opt.norotate:
|
||||
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
||||
self.payload.append(['R', self.source, spread, self.fill])
|
||||
else:
|
||||
self.payload.append(['N', self.source, self.image, self.color, self.fill])
|
||||
|
||||
def colorCheck(self):
|
||||
if self.opt.webtoon:
|
||||
return True
|
||||
else:
|
||||
img = self.image.copy()
|
||||
bands = img.getbands()
|
||||
if bands == ('R', 'G', 'B') or bands == ('R', 'G', 'B', 'A'):
|
||||
thumb = img.resize((40, 40))
|
||||
SSE, bias = 0, [0, 0, 0]
|
||||
bias = ImageStat.Stat(thumb).mean[:3]
|
||||
bias = [b - sum(bias) / 3 for b in bias]
|
||||
for pixel in thumb.getdata():
|
||||
mu = sum(pixel) / 3
|
||||
SSE += sum((pixel[i] - mu - bias[i]) * (pixel[i] - mu - bias[i]) for i in [0, 1, 2])
|
||||
MSE = float(SSE) / (40 * 40)
|
||||
if MSE > 22:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
self.payload.append(['N', self.source, self.image, self.fill])
|
||||
|
||||
def fillCheck(self):
|
||||
if self.opt.bordersColor:
|
||||
@@ -252,164 +263,295 @@ class ComicPageParser:
|
||||
|
||||
|
||||
class ComicPage:
|
||||
def __init__(self, options, mode, path, image, color, fill):
|
||||
def __init__(self, options, mode, path, image, fill):
|
||||
self.opt = options
|
||||
_, self.size, self.palette, self.gamma = self.opt.profileData
|
||||
if self.opt.hq:
|
||||
self.size = (int(self.size[0] * 1.5), int(self.size[1] * 1.5))
|
||||
self.kindle_scribe_azw3 = (options.profile == 'KS') and (options.format in ('MOBI', 'EPUB'))
|
||||
self.image = image
|
||||
self.color = color
|
||||
self.original_color_mode = image.mode
|
||||
# TODO: color check earlier
|
||||
self.image = image.convert("RGB")
|
||||
self.fill = fill
|
||||
self.rotated = False
|
||||
self.orgPath = os.path.join(path[0], path[1])
|
||||
self.targetPathStart = os.path.join(path[0], os.path.splitext(path[1])[0])
|
||||
if 'N' in mode:
|
||||
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC'
|
||||
self.targetPathOrder = '-kcc-x'
|
||||
elif 'R' in mode:
|
||||
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC-A'
|
||||
self.rotated = True
|
||||
self.targetPathOrder = '-kcc-a' if options.rotatefirst else '-kcc-d'
|
||||
if not options.norotate:
|
||||
self.rotated = True
|
||||
elif 'S1' in mode:
|
||||
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC-B'
|
||||
self.targetPathOrder = '-kcc-b'
|
||||
elif 'S2' in mode:
|
||||
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC-C'
|
||||
self.targetPathOrder = '-kcc-c'
|
||||
# backwards compatibility for Pillow >9.1.0
|
||||
if not hasattr(Image, 'Resampling'):
|
||||
Image.Resampling = Image
|
||||
|
||||
@cached_property
|
||||
def color(self):
|
||||
if self.original_color_mode in ("L", "1"):
|
||||
return False
|
||||
if self.opt.webtoon:
|
||||
return True
|
||||
if self.calculate_color():
|
||||
return True
|
||||
return False
|
||||
|
||||
# cut off pixels from both ends of the histogram to remove jpg compression artifacts
|
||||
# for better accuracy, you could split the image in half and analyze each half separately
|
||||
def histograms_cutoff(self, cb_hist, cr_hist, cutoff=(2, 2)):
|
||||
if cutoff == (0, 0):
|
||||
return cb_hist, cr_hist
|
||||
|
||||
for h in cb_hist, cr_hist:
|
||||
# get number of pixels
|
||||
n = sum(h)
|
||||
# remove cutoff% pixels from the low end
|
||||
cut = int(n * cutoff[0] // 100)
|
||||
for lo in range(256):
|
||||
if cut > h[lo]:
|
||||
cut = cut - h[lo]
|
||||
h[lo] = 0
|
||||
else:
|
||||
h[lo] -= cut
|
||||
cut = 0
|
||||
if cut <= 0:
|
||||
break
|
||||
# remove cutoff% samples from the high end
|
||||
cut = int(n * cutoff[1] // 100)
|
||||
for hi in range(255, -1, -1):
|
||||
if cut > h[hi]:
|
||||
cut = cut - h[hi]
|
||||
h[hi] = 0
|
||||
else:
|
||||
h[hi] -= cut
|
||||
cut = 0
|
||||
if cut <= 0:
|
||||
break
|
||||
return cb_hist, cr_hist
|
||||
|
||||
def color_precision(self, cb_hist_original, cr_hist_original, cutoff, diff_threshold):
|
||||
cb_hist, cr_hist = self.histograms_cutoff(cb_hist_original.copy(), cr_hist_original.copy(), cutoff)
|
||||
|
||||
cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
|
||||
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
|
||||
cb_spread = cb_nonzero[-1] - cb_nonzero[0]
|
||||
cr_spread = cr_nonzero[-1] - cr_nonzero[0]
|
||||
|
||||
# bias adjustment, don't go lower than 7
|
||||
SPREAD_THRESHOLD = 7
|
||||
if self.opt.forcecolor:
|
||||
if any([
|
||||
cb_nonzero[0] > 128,
|
||||
cr_nonzero[0] > 128,
|
||||
cb_nonzero[-1] < 128,
|
||||
cr_nonzero[-1] < 128,
|
||||
]):
|
||||
return True, True
|
||||
elif cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
|
||||
return True, False
|
||||
|
||||
DIFF_THRESHOLD = diff_threshold
|
||||
if any([
|
||||
cb_nonzero[0] <= 128 - DIFF_THRESHOLD,
|
||||
cr_nonzero[0] <= 128 - DIFF_THRESHOLD,
|
||||
cb_nonzero[-1] >= 128 + DIFF_THRESHOLD,
|
||||
cr_nonzero[-1] >= 128 + DIFF_THRESHOLD,
|
||||
]):
|
||||
return True, True
|
||||
|
||||
return False, None
|
||||
|
||||
def calculate_color(self):
|
||||
img = self.image.convert("YCbCr")
|
||||
_, cb, cr = img.split()
|
||||
cb_hist_original = cb.histogram()
|
||||
cr_hist_original = cr.histogram()
|
||||
|
||||
# you can increase 22 but don't increase 10. 4 maybe can go higher
|
||||
for cutoff, diff_threshold in [((0, 0), 22), ((.2, .2), 10), ((3, 3), 4)]:
|
||||
done, decision = self.color_precision(cb_hist_original, cr_hist_original, cutoff, diff_threshold)
|
||||
if done:
|
||||
return decision
|
||||
return False
|
||||
|
||||
def saveToDir(self):
|
||||
try:
|
||||
flags = []
|
||||
if not self.opt.forcecolor and not self.opt.forcepng:
|
||||
self.image = self.image.convert('L')
|
||||
if self.rotated:
|
||||
flags.append('Rotated')
|
||||
if self.fill != 'white':
|
||||
flags.append('BlackBackground')
|
||||
if self.opt.forcepng:
|
||||
self.image.info["transparency"] = None
|
||||
self.targetPath += '.png'
|
||||
self.image.save(self.targetPath, 'PNG', optimize=1)
|
||||
if self.opt.kindle_scribe_azw3 and self.image.size[1] > 1920:
|
||||
w, h = self.image.size
|
||||
targetPath = self.save_with_codec(self.image.crop((0, 0, w, 1920)), self.targetPathStart + self.targetPathOrder + '-above')
|
||||
self.save_with_codec(self.image.crop((0, 1920, w, h)), self.targetPathStart + self.targetPathOrder + '-below')
|
||||
elif self.opt.kindle_scribe_azw3:
|
||||
targetPath = self.save_with_codec(self.image, self.targetPathStart + self.targetPathOrder + '-whole')
|
||||
else:
|
||||
self.targetPath += '.jpg'
|
||||
if self.opt.mozjpeg:
|
||||
with io.BytesIO() as output:
|
||||
self.image.save(output, format="JPEG", optimize=1, quality=85)
|
||||
input_jpeg_bytes = output.getvalue()
|
||||
output_jpeg_bytes = mozjpeg_lossless_optimization.optimize(input_jpeg_bytes)
|
||||
with open(self.targetPath, "wb") as output_jpeg_file:
|
||||
output_jpeg_file.write(output_jpeg_bytes)
|
||||
else:
|
||||
self.image.save(self.targetPath, 'JPEG', optimize=1, quality=85)
|
||||
return [md5Checksum(self.targetPath), flags, self.orgPath]
|
||||
targetPath = self.save_with_codec(self.image, self.targetPathStart + self.targetPathOrder)
|
||||
if os.path.isfile(self.orgPath):
|
||||
os.remove(self.orgPath)
|
||||
return [Path(targetPath).name, flags]
|
||||
except IOError as err:
|
||||
raise RuntimeError('Cannot save image. ' + str(err))
|
||||
|
||||
def autocontrastImage(self):
|
||||
def save_with_codec(self, image, targetPath):
|
||||
if self.opt.forcepng:
|
||||
image.info.pop('transparency', None)
|
||||
if self.opt.iskindle and ('MOBI' in self.opt.format or 'EPUB' in self.opt.format):
|
||||
targetPath += '.gif'
|
||||
image.save(targetPath, 'GIF', optimize=1, interlace=False)
|
||||
else:
|
||||
targetPath += '.png'
|
||||
image.save(targetPath, 'PNG', optimize=1)
|
||||
else:
|
||||
targetPath += '.jpg'
|
||||
if self.opt.mozjpeg:
|
||||
with io.BytesIO() as output:
|
||||
image.save(output, format="JPEG", optimize=1, quality=self.opt.jpegquality)
|
||||
input_jpeg_bytes = output.getvalue()
|
||||
output_jpeg_bytes = mozjpeg_lossless_optimization.optimize(input_jpeg_bytes)
|
||||
with open(targetPath, "wb") as output_jpeg_file:
|
||||
output_jpeg_file.write(output_jpeg_bytes)
|
||||
else:
|
||||
image.save(targetPath, 'JPEG', optimize=1, quality=self.opt.jpegquality)
|
||||
return targetPath
|
||||
|
||||
def gammaCorrectImage(self):
|
||||
gamma = self.opt.gamma
|
||||
if gamma < 0.1:
|
||||
gamma = self.gamma
|
||||
if self.gamma != 1.0 and self.color:
|
||||
gamma = 1.0
|
||||
if gamma == 1.0:
|
||||
self.image = ImageOps.autocontrast(self.image)
|
||||
pass
|
||||
else:
|
||||
self.image = ImageOps.autocontrast(Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma)))
|
||||
self.image = Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma))
|
||||
|
||||
def autocontrastImage(self):
|
||||
if self.opt.webtoon:
|
||||
return
|
||||
if self.opt.noautocontrast:
|
||||
return
|
||||
if self.color and not self.opt.colorautocontrast:
|
||||
return
|
||||
|
||||
# if image is extremely low contrast, that was probably intentional
|
||||
extrema = self.image.convert('L').getextrema()
|
||||
if extrema[1] - extrema[0] < (255 - 32 * 3):
|
||||
return
|
||||
|
||||
if self.opt.autolevel:
|
||||
self.autolevelImage()
|
||||
|
||||
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
|
||||
|
||||
def autolevelImage(self):
|
||||
img = self.image
|
||||
if self.color:
|
||||
img = self.image.convert("YCbCr")
|
||||
y, cb, cr = img.split()
|
||||
img = y
|
||||
else:
|
||||
img = img.convert('L')
|
||||
h = img.histogram()
|
||||
most_common_dark_pixel_count = max(h[:64])
|
||||
black_point = h.index(most_common_dark_pixel_count)
|
||||
bp = black_point
|
||||
img = img.point(lambda p: p if p > bp else bp)
|
||||
if self.color:
|
||||
self.image = Image.merge(mode='YCbCr', bands=[img, cb, cr]).convert('RGB')
|
||||
else:
|
||||
self.image = img
|
||||
|
||||
def convertToGrayscale(self):
|
||||
self.image = self.image.convert('L')
|
||||
|
||||
def quantizeImage(self):
|
||||
colors = len(self.palette) // 3
|
||||
if colors < 256:
|
||||
self.palette += self.palette[:3] * (256 - colors)
|
||||
# remove all color pixels from image, since colorCheck() has some tolerance
|
||||
# quantize with a small number of color pixels in a mostly b/w image can have unexpected results
|
||||
self.image = self.image.convert("RGB")
|
||||
|
||||
palImg = Image.new('P', (1, 1))
|
||||
palImg.putpalette(self.palette)
|
||||
self.image = self.image.convert('L')
|
||||
self.image = self.image.convert('RGB')
|
||||
# Quantize is deprecated but new function call it internally anyway...
|
||||
self.image = self.image.quantize(palette=palImg)
|
||||
|
||||
def optimizeForDisplay(self, eraserainbow, is_color):
|
||||
# Erase rainbow artifacts for grayscale and color images by removing spectral frequencies that cause Moire interference with color filter array
|
||||
if eraserainbow and all(dim > 1 for dim in self.image.size):
|
||||
self.image = erase_rainbow_artifacts(self.image, is_color)
|
||||
|
||||
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)
|
||||
if self.opt.norotate and self.targetPathOrder in ('-kcc-a', '-kcc-d') and not self.opt.kindle_scribe_azw3:
|
||||
# TODO: Kindle Scribe case
|
||||
if self.opt.kindle_azw3 and any(dim > 1920 for dim in self.image.size):
|
||||
self.image = ImageOps.contain(self.image, (1920, 1920), Image.Resampling.LANCZOS)
|
||||
elif self.image.size[0] > self.size[0] * 2 or self.image.size[1] > self.size[1]:
|
||||
self.image = ImageOps.contain(self.image, (self.size[0] * 2, self.size[1]), Image.Resampling.LANCZOS)
|
||||
return
|
||||
|
||||
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 in ('CBZ', 'PDF') or self.opt.kfx) and not self.opt.white_borders:
|
||||
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
|
||||
else:
|
||||
if self.kindle_scribe_azw3:
|
||||
self.size = (1860, 1920)
|
||||
self.image = ImageOps.contain(self.image, self.size, method=method)
|
||||
|
||||
def resize_method(self):
|
||||
if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
|
||||
if self.image.size[0] < self.size[0] and self.image.size[1] < self.size[1]:
|
||||
return Image.Resampling.BICUBIC
|
||||
else:
|
||||
return Image.Resampling.LANCZOS
|
||||
|
||||
def 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
|
||||
|
||||
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:
|
||||
w, h = self.image.size
|
||||
left, upper, right, lower = bbox
|
||||
# don't crop more than 10% of image
|
||||
bbox = (min(0.1*w, left), min(0.1*h, upper), max(0.9*w, right), max(0.9*h, lower))
|
||||
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:
|
||||
w, h = self.image.size
|
||||
left, upper, right, lower = bbox
|
||||
# don't crop more than 10% of image
|
||||
bbox = (min(0.1*w, left), min(0.1*h, upper), max(0.9*w, right), max(0.9*h, lower))
|
||||
self.maybeCrop(bbox, minimum)
|
||||
|
||||
def cropInterPanelEmptySections(self, direction):
|
||||
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill)
|
||||
|
||||
class Cover:
|
||||
def __init__(self, source, target, opt, tomeid):
|
||||
def __init__(self, source, opt):
|
||||
self.options = opt
|
||||
self.source = source
|
||||
self.target = target
|
||||
if tomeid == 0:
|
||||
self.tomeid = 1
|
||||
else:
|
||||
self.tomeid = tomeid
|
||||
self.image = Image.open(source)
|
||||
# backwards compatibility for Pillow >9.1.0
|
||||
if not hasattr(Image, 'Resampling'):
|
||||
@@ -418,22 +560,59 @@ class Cover:
|
||||
|
||||
def process(self):
|
||||
self.image = self.image.convert('RGB')
|
||||
self.image = ImageOps.autocontrast(self.image)
|
||||
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
|
||||
if not self.options.forcecolor:
|
||||
self.image = self.image.convert('L')
|
||||
self.image.thumbnail(self.options.profileData[1], Image.Resampling.LANCZOS)
|
||||
self.save()
|
||||
self.crop_main_cover()
|
||||
|
||||
def save(self):
|
||||
size = list(self.options.profileData[1])
|
||||
if self.options.kindle_scribe_azw3:
|
||||
size[0] = min(size[0], 1920)
|
||||
size[1] = min(size[1], 1920)
|
||||
if self.options.coverfill and not self.options.kindle_scribe_azw3:
|
||||
# TODO: Kindle Scribe case
|
||||
self.image = ImageOps.fit(self.image, tuple(size), Image.Resampling.LANCZOS, centering=(0.5, 0.5))
|
||||
else:
|
||||
self.image.thumbnail(tuple(size), Image.Resampling.LANCZOS)
|
||||
|
||||
def crop_main_cover(self):
|
||||
w, h = self.image.size
|
||||
if w / h > 2:
|
||||
if self.options.righttoleft:
|
||||
self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h))
|
||||
else:
|
||||
self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h))
|
||||
elif w / h > 1.34:
|
||||
if self.options.righttoleft:
|
||||
self.image = self.image.crop((0, 0, w/2 - w * 0.03, h))
|
||||
else:
|
||||
self.image = self.image.crop((w/2 + w * 0.03, 0, w, h))
|
||||
|
||||
def save_to_epub(self, target, tomeid, len_tomes=0):
|
||||
try:
|
||||
self.image.save(self.target, "JPEG", optimize=1, quality=85)
|
||||
if tomeid == 0:
|
||||
self.image.save(target, "JPEG", optimize=1, quality=self.options.jpegquality)
|
||||
else:
|
||||
copy = self.image.copy()
|
||||
draw = ImageDraw.Draw(copy)
|
||||
w, h = copy.size
|
||||
draw.text(
|
||||
xy=(w/2, h * .85),
|
||||
text=f'{tomeid}/{len_tomes}',
|
||||
anchor='ms',
|
||||
font_size=h//7,
|
||||
fill=255,
|
||||
stroke_fill=0,
|
||||
stroke_width=25
|
||||
)
|
||||
copy.save(target, "JPEG", optimize=1, quality=self.options.jpegquality)
|
||||
except IOError:
|
||||
raise RuntimeError('Failed to save cover.')
|
||||
|
||||
def saveToKindle(self, kindle, asin):
|
||||
self.image = self.image.resize((300, 470), Image.Resampling.LANCZOS)
|
||||
self.image = ImageOps.contain(self.image, (300, 470), Image.Resampling.LANCZOS)
|
||||
try:
|
||||
self.image.save(os.path.join(kindle.path.split('documents')[0], 'system', 'thumbnails',
|
||||
'thumbnail_' + asin + '_EBOK_portrait.jpg'), 'JPEG', optimize=1, quality=85)
|
||||
'thumbnail_' + asin + '_EBOK_portrait.jpg'), 'JPEG', optimize=1, quality=self.options.jpegquality)
|
||||
except IOError:
|
||||
raise RuntimeError('Failed to upload cover.')
|
||||
|
||||
78
kindlecomicconverter/inter_panel_crop_alg.py
Normal file
78
kindlecomicconverter/inter_panel_crop_alg.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from PIL import Image, ImageFilter, ImageOps, ImageFile
|
||||
import numpy as np
|
||||
from typing import Literal
|
||||
from .common_crop import threshold_from_power, group_close_values
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
|
||||
|
||||
'''
|
||||
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_temp)
|
||||
|
||||
if background_color != 'white':
|
||||
img_temp = ImageOps.invert(img_temp)
|
||||
|
||||
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 []
|
||||
|
||||
|
||||
|
||||
@@ -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,6 +32,8 @@ 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', 'apfs') and 'rw' in drive[3]):
|
||||
|
||||
@@ -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:
|
||||
@@ -122,4 +123,4 @@ class MetadataParser:
|
||||
cbx.addFile(tmpXML)
|
||||
except OSError as e:
|
||||
raise UserWarning(e)
|
||||
rmtree(workdir)
|
||||
rmtree(workdir, True)
|
||||
|
||||
206
kindlecomicconverter/page_number_crop_alg.py
Normal file
206
kindlecomicconverter/page_number_crop_alg.py
Normal file
@@ -0,0 +1,206 @@
|
||||
from PIL import ImageOps, ImageFilter, ImageFile
|
||||
import numpy as np
|
||||
from .common_crop import threshold_from_power, group_close_values
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
|
||||
|
||||
'''
|
||||
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)
|
||||
ignore_pixels_near_edge(bw_img)
|
||||
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)
|
||||
|
||||
ignore_pixels_near_edge(bw_img)
|
||||
|
||||
return bw_img.getbbox()
|
||||
|
||||
def ignore_pixels_near_edge(bw_img):
|
||||
w, h = bw_img.size
|
||||
edge_bbox = [
|
||||
(0, 0, w, int(0.02 * h)),
|
||||
(0, int(0.98 * h), w, h),
|
||||
(0, 0, int(0.02 * w), h),
|
||||
(int(0.98 * w), 0, w, h)
|
||||
]
|
||||
for box in edge_bbox:
|
||||
edge = bw_img.crop(box)
|
||||
h = edge.histogram()
|
||||
if not edge.height or not edge.width:
|
||||
continue
|
||||
imperfections = h[255] / (edge.height * edge.width)
|
||||
if imperfections > 0 and imperfections < .02:
|
||||
bw_img.paste(im=0, box=box)
|
||||
|
||||
|
||||
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
|
||||
@@ -22,8 +22,6 @@
|
||||
#
|
||||
|
||||
import os
|
||||
from random import choice
|
||||
from string import ascii_uppercase, digits
|
||||
|
||||
# skip stray images a few pixels in size in some PDFs
|
||||
# typical images are many thousands in length
|
||||
@@ -32,10 +30,9 @@ STRAY_IMAGE_LENGTH_THRESHOLD = 300
|
||||
|
||||
|
||||
class PdfJpgExtract:
|
||||
def __init__(self, fname):
|
||||
def __init__(self, fname, fullPath):
|
||||
self.fname = fname
|
||||
self.filename = os.path.splitext(fname)
|
||||
self.path = self.filename[0] + "-KCC-" + ''.join(choice(ascii_uppercase + digits) for _ in range(3))
|
||||
self.path = fullPath
|
||||
|
||||
def getPath(self):
|
||||
return self.path
|
||||
@@ -48,7 +45,6 @@ class PdfJpgExtract:
|
||||
endfix = 2
|
||||
i = 0
|
||||
njpg = 0
|
||||
os.makedirs(self.path)
|
||||
while True:
|
||||
istream = pdf.find(b"stream", i)
|
||||
if istream < 0:
|
||||
@@ -71,9 +67,9 @@ class PdfJpgExtract:
|
||||
continue
|
||||
|
||||
jpg = pdf[istart:iend]
|
||||
jpgfile = open(self.path + "/jpg%d.jpg" % njpg, "wb")
|
||||
jpgfile = open(os.path.join(self.path, "jpg%d.jpg" % njpg), "wb")
|
||||
jpgfile.write(jpg)
|
||||
jpgfile.close()
|
||||
njpg += 1
|
||||
|
||||
return self.path, njpg
|
||||
return njpg
|
||||
|
||||
249
kindlecomicconverter/rainbow_artifacts_eraser.py
Normal file
249
kindlecomicconverter/rainbow_artifacts_eraser.py
Normal file
@@ -0,0 +1,249 @@
|
||||
import numpy as np
|
||||
from PIL import Image, ImageFile
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
|
||||
|
||||
def fourier_transform_image(img):
|
||||
"""
|
||||
Memory-optimized version that modifies the array in place when possible.
|
||||
"""
|
||||
# Convert with minimal copy
|
||||
img_array = np.asarray(img, dtype=np.float32)
|
||||
|
||||
# Use rfft2 if the image is real to save memory
|
||||
# and computation time (approximately 2x faster)
|
||||
fft_result = np.fft.rfft2(img_array)
|
||||
|
||||
return fft_result
|
||||
|
||||
def attenuate_diagonal_frequencies(fft_spectrum, freq_threshold=0.30, target_angle=135,
|
||||
angle_tolerance=10, attenuation_factor=0.10):
|
||||
"""
|
||||
Attenuates specific frequencies in the Fourier domain (optimized version for rfft2).
|
||||
|
||||
Args:
|
||||
fft_spectrum: Result of 2D real Fourier transform (from rfft2)
|
||||
freq_threshold: Frequency threshold in cycles/pixel (default: 0.3, theoretical max: 0.5)
|
||||
target_angle: Target angle in degrees (default: 135)
|
||||
angle_tolerance: Angular tolerance in degrees (default: 15)
|
||||
attenuation_factor: Attenuation factor (0.1 = 90% attenuation)
|
||||
|
||||
Returns:
|
||||
np.ndarray: Modified FFT with applied attenuation (same format as input)
|
||||
"""
|
||||
|
||||
# Get dimensions of the rfft2 result
|
||||
if fft_spectrum.ndim == 2:
|
||||
height, width_rfft = fft_spectrum.shape
|
||||
else: # 3D array (color channels)
|
||||
height, width_rfft = fft_spectrum.shape[:2]
|
||||
|
||||
# For rfft2, the original width is (width_rfft - 1) * 2
|
||||
width_original = (width_rfft - 1) * 2
|
||||
|
||||
# Create frequency grids for rfft2 format
|
||||
freq_y = np.fft.fftfreq(height, d=1.0)
|
||||
freq_x = np.fft.rfftfreq(width_original, d=1.0) # Use rfftfreq for the X dimension
|
||||
|
||||
|
||||
# Use broadcasting to create grids without meshgrid (more efficient)
|
||||
freq_y_grid = freq_y.reshape(-1, 1) # Column
|
||||
freq_x_grid = freq_x.reshape(1, -1) # Row
|
||||
|
||||
# Calculate squared radial frequencies (avoid sqrt)
|
||||
freq_radial_sq = freq_x_grid**2 + freq_y_grid**2
|
||||
freq_threshold_sq = freq_threshold**2
|
||||
|
||||
# Frequency condition
|
||||
freq_condition = freq_radial_sq >= freq_threshold_sq
|
||||
|
||||
# Early exit if no frequency satisfies the condition
|
||||
if not np.any(freq_condition):
|
||||
return fft_spectrum
|
||||
|
||||
# Calculate angles only where necessary
|
||||
# Use atan2 directly with broadcasting
|
||||
angles_rad = np.arctan2(freq_y_grid, freq_x_grid)
|
||||
|
||||
# Convert to degrees and normalize in a single operation
|
||||
angles_deg = np.rad2deg(angles_rad) % 360
|
||||
|
||||
# Calculation of complementary angle
|
||||
target_angle_2 = (target_angle + 180) % 360
|
||||
|
||||
# Calulation of perpendicular angles (135° + 45° to maximize compatibility until we know for sure which angle configure for each device)
|
||||
target_angle_3 = (target_angle + 90) % 360
|
||||
target_angle_4 = (target_angle_3 + 180) % 360
|
||||
|
||||
# Create angular conditions in a vectorized way
|
||||
angle_condition = np.zeros_like(angles_deg, dtype=bool)
|
||||
|
||||
# Process both angles simultaneously
|
||||
for angle in [target_angle, target_angle_2, target_angle_3, target_angle_4]:
|
||||
min_angle = (angle - angle_tolerance) % 360
|
||||
max_angle = (angle + angle_tolerance) % 360
|
||||
|
||||
if min_angle > max_angle: # Interval crosses 0°
|
||||
angle_condition |= (angles_deg >= min_angle) | (angles_deg <= max_angle)
|
||||
else: # Normal interval
|
||||
angle_condition |= (angles_deg >= min_angle) & (angles_deg <= max_angle)
|
||||
|
||||
# Combine conditions
|
||||
combined_condition = freq_condition & angle_condition
|
||||
|
||||
# Apply attenuation directly (avoid creating a full mask)
|
||||
if attenuation_factor == 0:
|
||||
# Special case: complete suppression
|
||||
if fft_spectrum.ndim == 2:
|
||||
fft_spectrum[combined_condition] = 0
|
||||
else: # 3D array
|
||||
fft_spectrum[combined_condition, :] = 0
|
||||
return fft_spectrum
|
||||
elif attenuation_factor == 1:
|
||||
# Special case: no attenuation
|
||||
return fft_spectrum
|
||||
else:
|
||||
# General case: partial attenuation
|
||||
if fft_spectrum.ndim == 2:
|
||||
fft_spectrum[combined_condition] *= attenuation_factor
|
||||
else: # 3D array
|
||||
fft_spectrum[combined_condition, :] *= attenuation_factor
|
||||
return fft_spectrum
|
||||
|
||||
def inverse_fourier_transform_image(fft_spectrum, is_color, original_shape=None):
|
||||
"""
|
||||
Performs an optimized inverse Fourier transform to reconstruct a PIL image.
|
||||
|
||||
Args:
|
||||
fft_spectrum: Fourier transform result (complex array from rfft2)
|
||||
is_color: Boolean indicating if the image is to be treated as color
|
||||
|
||||
Returns:
|
||||
PIL.Image: Reconstructed image
|
||||
"""
|
||||
# Perform inverse Fourier transform with original shape if provided
|
||||
if original_shape is not None:
|
||||
img_reconstructed = np.fft.irfft2(fft_spectrum, s=original_shape)
|
||||
else:
|
||||
img_reconstructed = np.fft.irfft2(fft_spectrum)
|
||||
|
||||
# Normalize values between 0 and 255
|
||||
img_reconstructed = np.clip(img_reconstructed, 0, 255)
|
||||
img_reconstructed = img_reconstructed.astype(np.uint8)
|
||||
|
||||
# Convert to PIL image
|
||||
if is_color and img_reconstructed.ndim == 3:
|
||||
pil_image = Image.fromarray(img_reconstructed, mode='RGB')
|
||||
else:
|
||||
pil_image = Image.fromarray(img_reconstructed, mode='L')
|
||||
|
||||
return pil_image
|
||||
|
||||
def rgb_to_yuv(rgb_array):
|
||||
"""
|
||||
Convert RGB to YUV color space.
|
||||
Y = luminance, U and V = chrominance
|
||||
"""
|
||||
# Coefficients for RGB to YUV conversion
|
||||
rgb_to_yuv_matrix = np.array([
|
||||
[0.299, 0.587, 0.114], # Y
|
||||
[-0.14713, -0.28886, 0.436], # U
|
||||
[0.615, -0.51499, -0.10001] # V
|
||||
])
|
||||
|
||||
# Reshape for matrix multiplication
|
||||
original_shape = rgb_array.shape
|
||||
rgb_flat = rgb_array.reshape(-1, 3)
|
||||
|
||||
# Apply transformation
|
||||
yuv_flat = rgb_flat @ rgb_to_yuv_matrix.T
|
||||
|
||||
# Reshape back
|
||||
yuv_array = yuv_flat.reshape(original_shape)
|
||||
|
||||
return yuv_array
|
||||
|
||||
def yuv_to_rgb(yuv_array):
|
||||
"""
|
||||
Convert YUV to RGB color space.
|
||||
"""
|
||||
# Coefficients for YUV to RGB conversion
|
||||
yuv_to_rgb_matrix = np.array([
|
||||
[1.0, 0.0, 1.13983], # R
|
||||
[1.0, -0.39465, -0.58060], # G
|
||||
[1.0, 2.03211, 0.0] # B
|
||||
])
|
||||
|
||||
# Reshape for matrix multiplication
|
||||
original_shape = yuv_array.shape
|
||||
yuv_flat = yuv_array.reshape(-1, 3)
|
||||
|
||||
# Apply transformation
|
||||
rgb_flat = yuv_flat @ yuv_to_rgb_matrix.T
|
||||
|
||||
# Reshape back
|
||||
rgb_array = rgb_flat.reshape(original_shape)
|
||||
|
||||
return rgb_array
|
||||
|
||||
def erase_rainbow_artifacts(img, is_color):
|
||||
"""
|
||||
Remove rainbow artifacts from grayscale or color images.
|
||||
|
||||
Args:
|
||||
img: PIL Image (grayscale or RGB)
|
||||
is_color: Boolean indicating if the image is to be treated as color
|
||||
|
||||
Returns:
|
||||
PIL.Image: Cleaned image
|
||||
"""
|
||||
# Auto-detect color mode if not specified
|
||||
if is_color is None:
|
||||
color = img.mode in ('RGB', 'RGBA', 'L') and len(np.array(img).shape) == 3
|
||||
|
||||
if is_color and img.mode in ('RGB', 'RGBA'):
|
||||
# Convert to RGB if needed
|
||||
if img.mode == 'RGBA':
|
||||
img = img.convert('RGB')
|
||||
|
||||
# Convert to numpy array
|
||||
img_array = np.array(img, dtype=np.float32)
|
||||
|
||||
# Convert to YUV color space
|
||||
yuv_array = rgb_to_yuv(img_array)
|
||||
|
||||
# Extract luminance channel (Y)
|
||||
luminance = yuv_array[:, :, 0]
|
||||
|
||||
# Process only the luminance channel
|
||||
fft_spectrum = fourier_transform_image(luminance)
|
||||
clean_spectrum = attenuate_diagonal_frequencies(fft_spectrum)
|
||||
clean_luminance = np.fft.irfft2(clean_spectrum, s=luminance.shape)
|
||||
|
||||
# Normalize and clip luminance
|
||||
clean_luminance = np.clip(clean_luminance, 0, 255)
|
||||
|
||||
# Replace luminance in YUV array
|
||||
yuv_array[:, :, 0] = clean_luminance
|
||||
|
||||
# Convert back to RGB
|
||||
rgb_array = yuv_to_rgb(yuv_array)
|
||||
rgb_array = np.clip(rgb_array, 0, 255).astype(np.uint8)
|
||||
|
||||
# Convert back to PIL image
|
||||
clean_image = Image.fromarray(rgb_array, mode='RGB')
|
||||
|
||||
else:
|
||||
# Grayscale processing (original behavior)
|
||||
if img.mode != 'L':
|
||||
img = img.convert('L')
|
||||
|
||||
# Get original image dimensions
|
||||
original_shape = (img.height, img.width)
|
||||
|
||||
fft_spectrum = fourier_transform_image(img)
|
||||
clean_spectrum = attenuate_diagonal_frequencies(fft_spectrum)
|
||||
clean_image = inverse_fourier_transform_image(clean_spectrum, is_color, original_shape)
|
||||
|
||||
return clean_image
|
||||
@@ -19,7 +19,6 @@
|
||||
#
|
||||
|
||||
import os
|
||||
from hashlib import md5
|
||||
from html.parser import HTMLParser
|
||||
import subprocess
|
||||
from packaging.version import Version
|
||||
@@ -28,6 +27,9 @@ import sys
|
||||
from traceback import format_tb
|
||||
|
||||
|
||||
IMAGE_TYPES = ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.avif')
|
||||
|
||||
|
||||
class HTMLStripper(HTMLParser):
|
||||
def __init__(self):
|
||||
HTMLParser.__init__(self)
|
||||
@@ -46,11 +48,17 @@ class HTMLStripper(HTMLParser):
|
||||
pass
|
||||
|
||||
|
||||
def dot_clean(filetree):
|
||||
for root, _, files in os.walk(filetree, topdown=False):
|
||||
for name in files:
|
||||
if name.startswith('._') or name == '.DS_Store':
|
||||
if os.path.exists(os.path.join(root, name)):
|
||||
os.remove(os.path.join(root, name))
|
||||
|
||||
|
||||
def getImageFileName(imgfile):
|
||||
name, ext = os.path.splitext(imgfile)
|
||||
ext = ext.lower()
|
||||
if (name.startswith('.') and len(name) == 1) or ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp']:
|
||||
return None
|
||||
return [name, ext]
|
||||
|
||||
|
||||
@@ -74,16 +82,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,10 +101,10 @@ def dependencyCheck(level):
|
||||
if level > 2:
|
||||
try:
|
||||
from PySide6.QtCore import qVersion as qtVersion
|
||||
if Version('6.5.1') > Version(qtVersion()):
|
||||
missing.append('PySide 6.5.1+')
|
||||
if Version('6.0.0') > Version(qtVersion()):
|
||||
missing.append('PySide 6.0.0')
|
||||
except ImportError:
|
||||
missing.append('PySide 6.5.1+')
|
||||
missing.append('PySide 6.0.0+')
|
||||
try:
|
||||
import raven
|
||||
except ImportError:
|
||||
@@ -129,10 +127,16 @@ def dependencyCheck(level):
|
||||
missing.append('python-slugify 1.2.1+')
|
||||
try:
|
||||
from PIL import __version__ as pillowVersion
|
||||
if Version('5.2.0') > Version(pillowVersion):
|
||||
missing.append('Pillow 5.2.0+')
|
||||
if Version('8.3.0') > Version(pillowVersion):
|
||||
missing.append('Pillow 8.3.0+')
|
||||
except ImportError:
|
||||
missing.append('Pillow 5.2.0+')
|
||||
missing.append('Pillow 8.3.0+')
|
||||
try:
|
||||
from pymupdf import __version__ as pymupdfVersion
|
||||
if Version('1.16.1') > Version(pymupdfVersion):
|
||||
missing.append('PyMuPDF 1.16.1+')
|
||||
except ImportError:
|
||||
missing.append('PyMuPDF 1.16.1+')
|
||||
if len(missing) > 0:
|
||||
print('ERROR: ' + ', '.join(missing) + ' is not installed!')
|
||||
sys.exit(1)
|
||||
|
||||
11
requirements-docker.txt
Normal file
11
requirements-docker.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
python-slugify>=1.2.1
|
||||
packaging>=23.2
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
# Below requirements are compiled in Dockefile
|
||||
# numpy==2.3.4
|
||||
# PyMuPDF==1.26.6
|
||||
12
requirements-osx-legacy.txt
Normal file
12
requirements-osx-legacy.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
PySide6==6.4.3
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
python-slugify>=1.2.1
|
||||
raven>=6.0.0
|
||||
packaging>=23.2
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
numpy<2
|
||||
PyMuPDF>=1.26.1
|
||||
12
requirements-win7.txt
Normal file
12
requirements-win7.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
PySide6==6.1.3
|
||||
Pillow>=9
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
python-slugify>=1.2.1
|
||||
raven>=6.0.0
|
||||
packaging>=23.2
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
numpy==1.23.0
|
||||
PyMuPDF>=1.16
|
||||
@@ -1,10 +1,12 @@
|
||||
PySide6>=6.5.1
|
||||
Pillow>=5.2.0
|
||||
PySide6>6
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
python-slugify>=1.2.1
|
||||
python-slugify>=1.2.1,<9.0.0
|
||||
raven>=6.0.0
|
||||
packaging>=23.2
|
||||
mozjpeg-lossless-optimization>=1.1.2
|
||||
natsort[fast]>=8.4.0
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
numpy>=1.22.4
|
||||
PyMuPDF>=1.18.0
|
||||
|
||||
92
setup.py
92
setup.py
@@ -8,6 +8,8 @@ Install as Python package:
|
||||
|
||||
Create EXE/APP:
|
||||
python3 setup.py build_binary
|
||||
python3 setup.py build_c2e
|
||||
python3 setup.py build_c2p
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -38,10 +40,17 @@ class BuildBinaryCommand(setuptools.Command):
|
||||
if sys.platform == 'darwin':
|
||||
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')
|
||||
min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET', '')
|
||||
if min_os.startswith('10.1'):
|
||||
os.system(f'appdmg kcc.json dist/kcc_osx_{min_os.replace(".", "_")}_legacy_{VERSION}.dmg')
|
||||
else:
|
||||
os.system(f'appdmg kcc.json dist/kcc_macos_{platform.processor()}_{VERSION}.dmg')
|
||||
sys.exit(0)
|
||||
elif sys.platform == 'win32':
|
||||
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n KCC_' + VERSION + ' -w --noupx kcc.py')
|
||||
if os.getenv('WINDOWS_7'):
|
||||
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_win7_legacy_' + VERSION + ' -w --noupx kcc.py')
|
||||
else:
|
||||
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(
|
||||
@@ -50,10 +59,75 @@ class BuildBinaryCommand(setuptools.Command):
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
class BuildC2ECommand(setuptools.Command):
|
||||
description = 'build binary c2e release'
|
||||
user_options = []
|
||||
|
||||
def initialize_options(self):
|
||||
pass
|
||||
|
||||
def finalize_options(self):
|
||||
pass
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def run(self):
|
||||
VERSION = __version__
|
||||
if sys.platform == 'darwin':
|
||||
os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "KCC C2E" -c -s kcc-c2e.py')
|
||||
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
|
||||
sys.exit(0)
|
||||
elif sys.platform == 'win32':
|
||||
if os.getenv('WINDOWS_7'):
|
||||
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2e_win7_legacy_' + VERSION + ' -c --noupx kcc-c2e.py')
|
||||
else:
|
||||
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2e_' + VERSION + ' -c --noupx kcc-c2e.py')
|
||||
sys.exit(0)
|
||||
elif sys.platform == 'linux':
|
||||
os.system(
|
||||
'pyinstaller --hidden-import=_cffi_backend --hidden-import=queue -y -F -i icons/comic2ebook.ico -n kcc_c2e_linux_' + VERSION + ' kcc-c2e.py')
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
class BuildC2PCommand(setuptools.Command):
|
||||
description = 'build binary c2p release'
|
||||
user_options = []
|
||||
|
||||
def initialize_options(self):
|
||||
pass
|
||||
|
||||
def finalize_options(self):
|
||||
pass
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def run(self):
|
||||
VERSION = __version__
|
||||
if sys.platform == 'darwin':
|
||||
os.system('pyinstaller --hidden-import=_cffi_backend -y -n "KCC C2P" -c -s kcc-c2p.py')
|
||||
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
|
||||
sys.exit(0)
|
||||
elif sys.platform == 'win32':
|
||||
if os.getenv('WINDOWS_7'):
|
||||
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2p_win7_legacy_' + VERSION + ' -c --noupx kcc-c2p.py')
|
||||
else:
|
||||
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2p_' + VERSION + ' -c --noupx kcc-c2p.py')
|
||||
sys.exit(0)
|
||||
elif sys.platform == 'linux':
|
||||
os.system(
|
||||
'pyinstaller --hidden-import=_cffi_backend --hidden-import=queue -y -F -i icons/comic2ebook.ico -n kcc_c2p_linux_' + VERSION + ' kcc-c2p.py')
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
setuptools.setup(
|
||||
cmdclass={
|
||||
'build_binary': BuildBinaryCommand,
|
||||
'build_c2e': BuildC2ECommand,
|
||||
'build_c2p': BuildC2PCommand,
|
||||
},
|
||||
name=NAME,
|
||||
version=VERSION,
|
||||
@@ -74,15 +148,17 @@ setuptools.setup(
|
||||
},
|
||||
packages=['kindlecomicconverter'],
|
||||
install_requires=[
|
||||
'pyside6>=6.5.1',
|
||||
'Pillow>=5.2.0',
|
||||
'PySide6>=6.0.0',
|
||||
'Pillow>=9.3.0',
|
||||
'psutil>=5.9.5',
|
||||
'requests>=2.31.0',
|
||||
'python-slugify>=1.2.1,<9.0.0',
|
||||
'raven>=6.0.0',
|
||||
'requests>=2.31.0',
|
||||
'mozjpeg-lossless-optimization>=1.1.2',
|
||||
'natsort[fast]>=8.4.0',
|
||||
'distro',
|
||||
'mozjpeg-lossless-optimization>=1.2.0',
|
||||
'natsort>=8.4.0',
|
||||
'distro>=1.8.0',
|
||||
'numpy>=1.22.4',
|
||||
'PyMuPDF>=1.16.1',
|
||||
],
|
||||
classifiers=[],
|
||||
zip_safe=False,
|
||||
|
||||
Reference in New Issue
Block a user