Compare commits
1526 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2878e5d41b | |||
| bd691989a9 | |||
| d4aeb798c7 | |||
| 997a514e2a | |||
| a3672f7a1e | |||
| 1b48a9fc5e | |||
| d5dde46989 | |||
| 92c85c18e9 | |||
| e5122cc188 | |||
| 61be6aa78e | |||
| d6834063c1 | |||
| 1a8d74de4a | |||
| a0a194ecf1 | |||
| 290578d66e | |||
| f97398d481 | |||
| a7a9f35686 | |||
| d5146d02fc | |||
| b0374e127d | |||
| 894dbfc8a2 | |||
| 72f98bb032 | |||
| 96bf14d386 | |||
| d4e1565e4a | |||
| c3030e8bd1 | |||
| c5744117e3 | |||
| cd2eeb4d0f | |||
| e7b7054b0e | |||
| 6f26bd5874 | |||
| d5ca8fb407 | |||
| 8d61a9e558 | |||
| 8aaedf274d | |||
| 5782a44e7b | |||
| e4c918f0f3 | |||
| 8f4072bfab | |||
| 61f3097be5 | |||
| fa33ef8f89 | |||
| 232bac00a9 | |||
| d19a4754fa | |||
| 9a93cc4b17 | |||
| e0471b2dc9 | |||
| a87eb318cf | |||
| 87987c9ebf | |||
| f5fe8d93b0 | |||
| 249f823f01 | |||
| 3a9d4f274d | |||
| b5de6fd39d | |||
| b4b9e41a0c | |||
| 9b9181a715 | |||
| 472fdc97b5 | |||
| 6fdfddd7d9 | |||
| 34fb68ac65 | |||
| b4d72cd581 | |||
| 1dead9af8f | |||
| 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 | |||
| 421e6bcb64 | |||
| 5168cd73c4 | |||
| f7f19b99da | |||
| 1131bab41f | |||
| 8d204668a7 | |||
| 99d94ceaa7 | |||
| 8ff401cc3a | |||
| fb7d92d737 | |||
| 1ca8b2c11b | |||
| 40e0b4853b | |||
| 005313f978 | |||
| add2ef9faa | |||
| 0c98acd606 | |||
| fe902ec213 | |||
| 4e9714e6f8 | |||
| 1337ad7fe3 | |||
| 511c7c1580 | |||
| 16dd034af4 | |||
| d22794555f | |||
| ab089adbca | |||
| 2c770f4562 | |||
| 4c73006bd8 | |||
| 1093dbf65a | |||
| df9990c692 | |||
| 114f4c9e57 | |||
| c2c477475d | |||
| a0f8d0b5cf | |||
| 2fc32bae58 | |||
| db25a0939c | |||
| 1bd7506140 | |||
| 4fc5cc9dfb | |||
| 89289412a3 | |||
| 193297c8fc | |||
| 367d71e4ad | |||
| cdde0f18cd | |||
| 9fe82a9dd4 | |||
| e958edd135 | |||
| 6738b70487 | |||
| 44094bdb21 | |||
| a9a2f47e30 | |||
| c35dd137ea | |||
| 1ea008c8f3 | |||
| cbc1ed5db4 | |||
| 6bf662bea3 | |||
| cfae306731 | |||
| f6475f4c61 | |||
| 9646518575 | |||
| 937dd3984a | |||
| 4e6f6ec8e8 | |||
| 4b36fdbfa2 | |||
| d8141af4eb | |||
| cf38c4d445 | |||
| 283b2da1a0 | |||
| 4a9f693574 | |||
| 9a48887edc | |||
| 0981ec3c6d | |||
| e0e6606736 | |||
| 518e67c132 | |||
| 6c593dac1f | |||
| 9d065676db | |||
| 9520e59a29 | |||
| 461a7fda88 | |||
| 5ccbb770a6 | |||
| 49bf80f5d8 | |||
| 33cc324381 | |||
| 6ba5539813 | |||
| f725196106 | |||
| c8ff88ed0f | |||
| 33fed662fe | |||
| bc7cd17916 | |||
| c5e1e18ac2 | |||
| ee31b784cb | |||
| 48541404ee | |||
| acbebcfd40 | |||
| dd6273b864 | |||
| e8502f008a | |||
| 169a41e7d2 | |||
| 57cf669cdd | |||
| 0b4f089b8e | |||
| 7eb985337c | |||
| d62690e8bf | |||
| 3988f2012f | |||
| 6cdd9d5909 | |||
| c29a4beac9 | |||
| 5f7bdef325 | |||
| 54b5d698ee | |||
| a99c63acea | |||
| 251df2e7ba | |||
| b1379b7c59 | |||
| 983bde1691 | |||
| e79e5a311c | |||
| 39c73dbc0f | |||
| 915c9389ef | |||
| 89d887710e | |||
| 2e9bc5381a | |||
| ab1ce158c7 | |||
| 6278adfb25 | |||
| 6e6c13047e | |||
| b528dab711 | |||
| 2ffefee928 | |||
| a5e5407363 | |||
| da1ba64bd2 | |||
| 6dcaf9a6d1 | |||
| 3090a47f20 | |||
| 1e537915d4 | |||
| 7273ca25b8 | |||
| 68da853e42 | |||
| 431862a2e9 | |||
| 65062f8984 | |||
| 8122fa1e45 | |||
| 13fedff77b | |||
| 61b1207a3e | |||
| 60e9f075b8 | |||
| 424118b7cd | |||
| 4e45402b8f | |||
| 615790c278 | |||
| 0d417b8e11 | |||
| 6836c20377 | |||
| f75ea6dfe8 | |||
| 77afa77d32 | |||
| f73d889b6e | |||
| 8e04ccde18 | |||
| cc1e5db0aa | |||
| c5c88095ee | |||
| 1318b9c0f2 | |||
| 9339abb267 | |||
| 154707a412 | |||
| 217f571f3d | |||
| 531cea88e6 | |||
| a5202458dc | |||
| 5902d88d98 | |||
| e7e41715d0 | |||
| 62d1c7c488 | |||
| 95678adfd6 | |||
| 4923dac8f0 | |||
| 935727c1db | |||
| b0e38a700a | |||
| 23961243b6 | |||
| c98d6179c3 | |||
| 37200bdca0 | |||
| 0193bcd00a | |||
| 0bbe9348a2 | |||
| d16628dc59 | |||
| 85c1801417 | |||
| b28ee08d01 | |||
| dba927c351 | |||
| dd5fd621db | |||
| 21a167b3ee | |||
| 1c9eeee52d | |||
| 611ee31526 | |||
| 7c4fdf9d1a | |||
| b14f59e77a | |||
| b4ec0b4a74 | |||
| a90b4c82c5 | |||
| 390d58bf08 | |||
| e1aa6cd0af | |||
| 718fda2f0c | |||
| 9d7904f63b | |||
| ec58964c7c | |||
| 0b687ebadc | |||
| 9bc1f92c8c | |||
| 56f6c6962f | |||
| dfd15ab572 | |||
| 5588ad9250 | |||
| 0c2adb517e | |||
| 9ab1cd359c | |||
| 41d24e77e1 | |||
| 17206ddf8b | |||
| 896c05a72f | |||
| f83106f35b | |||
| dfca136a2d | |||
| 92ced5f415 | |||
| d18275d525 | |||
| c049adc3a1 | |||
| a3ce26983e | |||
| fe8195cfed | |||
| 3a1737e8d0 | |||
| fb0c0231f3 | |||
| 2e807e23e1 | |||
| 27841abb83 | |||
| b71d056559 | |||
| a822dfa3ae | |||
| 70de379987 | |||
| 16e275bb1f | |||
| b225de7b97 | |||
| 4a89446914 | |||
| 64521de577 | |||
| 38b14fd734 | |||
| 4fa72780a1 | |||
| c979486e28 | |||
| 03bd67cf2f | |||
| b44af66484 | |||
| 754395f1b3 | |||
| 95e73ea1dd | |||
| f466701b5d | |||
| bc6b26862f | |||
| b4d86fed7f | |||
| b5806ece0e | |||
| 85a99f0b05 | |||
| 4fea796763 | |||
| ca8bdf7e1f | |||
| 33d57d5025 | |||
| cceb9cf61f | |||
| 9ee375c6e4 | |||
| a50345a26d | |||
| be4b35b705 | |||
| 608e79a9fc | |||
| 7468512ec8 | |||
| 2256df0785 | |||
| 378a3caccc | |||
| ab7d629ba9 | |||
| b54299ff76 | |||
| 1d3fc9cc92 | |||
| 49313ce030 | |||
| c0d17c1803 | |||
| 7f883f98ba | |||
| 2c19898952 | |||
| 12c663bc05 | |||
| d8957dc4a6 | |||
| 9b45633279 | |||
| b5a0126051 | |||
| 182a292f70 | |||
| 7ab96c5573 | |||
| 1fbfaeae01 | |||
| 2462d11b5e | |||
| 44e6ffe4e3 | |||
| 9be2a4a492 | |||
| 3ea9a486bf | |||
| 95f138887e | |||
| a7a01f8269 | |||
| 6987c8b1cd | |||
| 6e10636356 | |||
| 63bd55313f | |||
| 324047bebc | |||
| 842a729c75 | |||
| 27d3eab8d5 | |||
| 8035dedae8 | |||
| b39076a9f4 | |||
| 237f343e62 | |||
| ab60d67ab7 | |||
| 79715c6a06 | |||
| 6cd073809e | |||
| 9dac000a04 | |||
| 674121620f | |||
| f6e2ceae91 | |||
| 143f6eb0f9 | |||
| 7cac6d4008 | |||
| aa4456bdb1 | |||
| c91be77588 | |||
| 65a42c1063 | |||
| 6519eb0453 | |||
| 9fdbf095d3 | |||
| 4ec4c9966c | |||
| 5a51c5234e | |||
| b7c6fd30e4 | |||
| 515b83637f | |||
| c3dad087d3 | |||
| 67e913ed9e | |||
| 6ce0f76fe0 | |||
| 5f5157c1d4 | |||
| 4c13ef0f6c | |||
| 50dc7fbffe | |||
| a060498ac7 | |||
| 35bba68a72 | |||
| 0b056a8fa8 | |||
| a6f9e84251 | |||
| 259800e48b | |||
| 28e170f1d2 | |||
| 7120c76025 | |||
| 4891913b5c | |||
| cd83b2899c | |||
| 535c2c220b | |||
| 409f077c3e | |||
| 3ecb2ba877 | |||
| c07a9657ef | |||
| 7f719a22ad | |||
| 332d3d455e | |||
| 2070a977ae | |||
| 8f8d0d68a3 | |||
| 5a8deb4623 | |||
| a7ea795df5 | |||
| a2ffd259b8 | |||
| 93e6b51466 | |||
| 7904662f25 | |||
| 6792c2d366 | |||
| ef4a91e44d | |||
| a2a405e5f5 | |||
| a63a46a741 | |||
| 2591b53a09 | |||
| 1c615ffc20 | |||
| 6eb05b3a8f | |||
| 968b083fb2 | |||
| 205907ef1e | |||
| 33ef8275c3 | |||
| eec2099515 | |||
| 9027265b7c | |||
| 34e2af3389 | |||
| 5ecddaceab | |||
| fe554c20aa | |||
| 6acf1a1802 | |||
| ac81a6be4b | |||
| 829a5f25e7 | |||
| a695a4c151 | |||
| 7c0b78350d | |||
| aa978ab246 | |||
| 7524c50657 | |||
| 9d8663a925 | |||
| 08ed304f8e | |||
| 658d2f3281 | |||
| f44bf5993e | |||
| 458c28281f | |||
| 968a1afa1d | |||
| 2c875673bd | |||
| 1a0be83d63 | |||
| 36a7dc49ec | |||
| 40d1ae63da | |||
| 3d3621c6ec | |||
| d77f04a84e | |||
| ec51d6fc51 | |||
| b7861d9d9e | |||
| 0b4503af21 | |||
| 437ffb9b10 | |||
| 066d1401bd | |||
| 9babe68d2a | |||
| 67de77538c | |||
| c3e950f2ec | |||
| ac2934aba2 | |||
| a5064a0c0a | |||
| 2d712e796d | |||
| cc3da40fd7 | |||
| a53c272bd0 | |||
| 6526b139fd | |||
| 283d6101cd | |||
| 02dab3c6ee | |||
| 1895aa127d | |||
| c01ff83fce | |||
| 4b670f3754 | |||
| 23b1560fa2 | |||
| 62350608dc | |||
| 8048b91fa8 | |||
| 2e9b3389e4 | |||
| 40e1ab4cf3 | |||
| d2c12c89e6 | |||
| 4647fd1f1d | |||
| 010ad3c88c | |||
| 4b0a94a8a0 | |||
| 807a2d1dff | |||
| e1470cca15 | |||
| 02b9081e37 | |||
| 495db88a9e | |||
| 2bea546a9d | |||
| ee042ef98d | |||
| aea7c0fafb | |||
| 45c1afcad4 | |||
| b8e314f6ca | |||
| d76eea9f43 | |||
| 2e55f22355 | |||
| 30b8770e34 | |||
| 9ad161489f | |||
| bdb459cfab | |||
| 2e85556543 | |||
| 93ebbbd0af | |||
| dd5c907bad | |||
| 64fb4a9eca | |||
| 284c577894 | |||
| c68c9892e4 | |||
| aa00ea3aa2 | |||
| 88f005824c | |||
| 2a2bfae112 | |||
| 583eec787f | |||
| 9ce691aecb | |||
| d1a07d7ffa | |||
| b545f7ad48 | |||
| 9e01797d28 | |||
| c68c5f25bf | |||
| a04bf5262f | |||
| b09b2527d9 | |||
| 94b372f47d | |||
| b978adcc7c | |||
| 9dee4432ad | |||
| 15055c6c0c | |||
| 3f948a10b0 | |||
| 1c942d81db | |||
| b36a5a0a93 | |||
| 2ca07430a0 | |||
| 3132aa8a21 | |||
| e4dccfe603 | |||
| 4c56141b80 | |||
| 73c2e4b136 | |||
| c28e9a6ef0 | |||
| 558bf07f7f | |||
| eaa458a9c7 | |||
| 91b6869638 | |||
| 25331f5d81 | |||
| 9b25182393 | |||
| 189c03529a | |||
| c9cf635229 | |||
| f78dc3cd8f | |||
| 4079314b61 | |||
| 04cf732d50 | |||
| 9015614b1a | |||
| e817c8b258 | |||
| cb9059c231 | |||
| 75d8be0951 | |||
| 161abdab18 | |||
| 4b1c7b3124 | |||
| 5481c0cfe5 | |||
| 9543b573e3 | |||
| df0bafe4b6 | |||
| b2e58127cb | |||
| 21174338ff | |||
| 241801f9cb | |||
| a2086618a2 | |||
| 7d81de6834 | |||
| 7305ccffc5 | |||
| 72b5027021 | |||
| 1152655061 | |||
| f25c25a121 | |||
| fff7eeca2b | |||
| a93da2136b | |||
| dc3498b74c | |||
| f317a5c430 | |||
| 3bedc3b928 | |||
| a2651747cd | |||
| af2c4e7250 | |||
| f93ced8939 | |||
| 8a0ba682c3 | |||
| 1a7b6baaf3 | |||
| 0f8daaf9f3 | |||
| 1fa6d315b1 | |||
| 112917754a | |||
| 65774c6f12 | |||
| 7b3ce8827f | |||
| b12825045b | |||
| ac9f3a5d87 | |||
| 00969a3739 | |||
| 21f738b44a | |||
| f2238b16a6 | |||
| 14f677ec68 | |||
| 7b3bf4618f | |||
| eab63a0f74 | |||
| 2128104db7 | |||
| c6179b0064 | |||
| 1d4319be2e | |||
| f5a738e2d4 | |||
| 477d834a91 | |||
| c8698f6d99 | |||
| 0988601842 | |||
| 57e9637c81 | |||
| a7440e06a9 | |||
| a9ed1e7610 | |||
| b1bc140ad3 | |||
| 9014ed53d4 | |||
| cad05904f3 | |||
| 10386d8af3 | |||
| c991feb9ce | |||
| d26eb7cdcd | |||
| 351084b703 | |||
| e861e7f6e8 | |||
| 370c9d4df7 | |||
| 8e5704683c | |||
| c65e1c8dea | |||
| 677622c103 | |||
| af0ebb85a0 | |||
| 8af029ac92 | |||
| a268e12a90 | |||
| d621335e6c | |||
| ec1d9c2d93 | |||
| 85b9dbbf83 | |||
| feeced44bf | |||
| cbea18398b | |||
| 4c9857f14d | |||
| 6b58ef4557 | |||
| 24d697c965 | |||
| 8b07d4eb69 | |||
| e6c5ac915f | |||
| b22e4757a3 | |||
| 91b06016bb | |||
| 5631391245 | |||
| c33887b7b7 | |||
| 8d82f58f09 | |||
| 36985f5169 | |||
| 9d190c1585 | |||
| 3834850317 | |||
| 84fc23b979 | |||
| 77748afdbd | |||
| 431e2ffaf2 | |||
| 16df4cd083 | |||
| 1aa34347c1 | |||
| 561af90b06 | |||
| 00d239e1d8 | |||
| 26bd2d3ed0 | |||
| e7aa49b70c | |||
| da41edc2f1 | |||
| ecbf60fb28 | |||
| 57b571b6c2 | |||
| 44bdc0245b | |||
| 1ec07fe4ec | |||
| 5f8f7e0919 | |||
| f404b9090d | |||
| 68521f7c63 | |||
| f5dd813c4c | |||
| 7924c492b3 | |||
| 2fc21c33e2 | |||
| cb76504acb | |||
| db6b0eddfe | |||
| 7d529a2acc | |||
| ad3ff35aaa | |||
| c62eeeb712 | |||
| 5a36a13105 | |||
| 12684d6562 | |||
| c5f68ae12a | |||
| 7bd9c766cc | |||
| c6b1417d9c | |||
| 98bf28a713 | |||
| f2d6d5b458 | |||
| 5de492ffb6 | |||
| 5c2c6ed825 | |||
| c2730ab01c | |||
| bfba66d47d | |||
| b5bc2f8e00 | |||
| 917eaef548 | |||
| 3187ebb054 | |||
| b9276e9ede | |||
| 147d815057 | |||
| 180123fee2 | |||
| 2768e622f2 | |||
| b629b45d46 | |||
| f66c83425c | |||
| 68b4b7114d | |||
| 36f8c82eaf | |||
| bd665c3261 | |||
| 94586fa590 | |||
| 89806dfcfc | |||
| c9eb73ab90 | |||
| 2edcc0369a | |||
| bc0a52b848 | |||
| 5ae72bf06d | |||
| 24c32643c1 | |||
| 40b988f964 | |||
| ac794eff85 | |||
| eaa387a9d6 | |||
| d0f5d6dac4 | |||
| b174534c1c | |||
| 19d486a4ee | |||
| 652762b709 | |||
| e6df87f8fd | |||
| 1a9cd0beb5 | |||
| 108e351126 | |||
| dfe3e10470 | |||
| 787ad9fa66 | |||
| f10e8869a9 | |||
| 715ada328f | |||
| 56f23ab488 | |||
| 996af59e00 | |||
| 37aa84c4aa | |||
| 50574632e6 | |||
| 0afb9e8c0b | |||
| 7511c7eed6 | |||
| 836a4146f9 | |||
| 15a240ccea | |||
| 0722ddf8b0 | |||
| b3159b94e7 | |||
| ef5207c990 | |||
| db77d89817 | |||
| 4571fadadb | |||
| 94f56238ae | |||
| 5efb5d6dbb | |||
| 623f615dd9 | |||
| 39fbbc42b3 | |||
| 99405ab8a6 | |||
| aadfca8306 | |||
| 5450502c2a | |||
| c976b06413 | |||
| b6facda95b | |||
| 3f608eb602 | |||
| 104cd04994 | |||
| b323204628 | |||
| 56195d301d | |||
| 287723ca6f | |||
| 90490149c7 | |||
| 2210f484df | |||
| d5502e85b0 | |||
| 59b26cfc8b | |||
| a722e5fa49 | |||
| fce3072dca | |||
| f53cdf9cd7 | |||
| de74318c69 | |||
| b137e69510 | |||
| 888663fa4c | |||
| 8a2ba96ac5 | |||
| cb6b0e0a7b | |||
| 49d2a7fbab | |||
| 3d84b27d58 | |||
| e98a23d0c0 | |||
| 7f80dacea8 | |||
| 6efb3dcef3 | |||
| 942a828b7e | |||
| df5ee1badf | |||
| e3ab28642d | |||
| a1831c45d5 | |||
| 52bea9957a | |||
| a71339ec34 | |||
| c5f09c44b8 | |||
| 64b199ef74 | |||
| 8dd0b0e694 | |||
| 181a2e8ab4 | |||
| 3fdff845b7 | |||
| 2ee5dc310b | |||
| f32e9560b5 | |||
| 621827c1c2 | |||
| dc312f36c2 | |||
| 4573ff6ec2 | |||
| d77498405b | |||
| e491fca445 | |||
| d22ee1a488 | |||
| 7ebcccd8a2 | |||
| 9a691c3c63 | |||
| 2b04a0298e | |||
| 9867f63d00 | |||
| 866f8898be | |||
| 9f2ac7a176 | |||
| 634213f380 | |||
| ce82b5ec66 | |||
| 062b239f2f | |||
| f5f5c05f1e | |||
| a229ba44bf | |||
| 27e5cc3b00 | |||
| 63d752280a | |||
| 60b7a90589 | |||
| 63413fa4ba | |||
| 3a536df626 | |||
| 35bc4a2987 | |||
| 3bb8cc7778 | |||
| bd53c6108d | |||
| 3d8bcb4020 | |||
| 162c146bed | |||
| e4750fc965 | |||
| ebe7d910de | |||
| aa96381eb5 | |||
| a2b9b5aa8b | |||
| 1e5bfc9f66 | |||
| 25a68ebdea | |||
| 786d2a9e1f | |||
| 4133cd21ba | |||
| 237ef728e1 | |||
| 991bf5d4a9 | |||
| aa8b78b4e4 | |||
| c2e6a0b791 | |||
| 42cf9e099f | |||
| d99064596a | |||
| b1e7a88353 | |||
| f8610e1cd7 | |||
| 562dad02fa | |||
| 66b867c1ca | |||
| b46ada9596 | |||
| aaa2de81fc | |||
| b0c6315fee | |||
| 509d78c0a6 | |||
| 65ef77bace | |||
| 170064853c | |||
| c7e9d883fa | |||
| a8521dbc9a | |||
| 0f44629273 | |||
| f12361e7cf | |||
| 450076e6e8 | |||
| 6d445ae151 | |||
| 921511dcf2 | |||
| d76a624a82 | |||
| cf3df581e1 | |||
| 87009f27a6 | |||
| cccbd36463 | |||
| 19b438844d | |||
| 878e92b527 | |||
| 116dce09fd | |||
| 3af30faee9 | |||
| cf0b6b3484 | |||
| 96a8c1d354 | |||
| 20712b6c42 | |||
| 84d836bf0e | |||
| d944d6385e | |||
| a38013eabc | |||
| def9e42a61 | |||
| 34aaeab8b1 | |||
| eaff6cc633 | |||
| 54f48d2156 | |||
| 42d845cf07 | |||
| e5e53d3aa7 | |||
| f952634971 | |||
| 19ff6a51cc | |||
| 8fbe558f65 | |||
| 6bdb0ab942 | |||
| 7656a85708 | |||
| d016bade8e | |||
| 3cc99c6221 | |||
| 93f5d105cf | |||
| c1c44bdf88 | |||
| 72132ea908 | |||
| 29f901f92a | |||
| 22b7258aa3 | |||
| c0f788bd67 | |||
| 8f10e93c08 | |||
| 4a473e3716 | |||
| 89d31cb8e5 | |||
| 80b65b12b7 | |||
| e835502837 | |||
| 5bcdc78725 | |||
| acb4dfad8f | |||
| 7f5de29174 | |||
| 11007402cd | |||
| 0cf92fc48f | |||
| 953942ca00 | |||
| c46ca8b507 | |||
| 48d3bee225 | |||
| 3b0e5cc309 | |||
| e5be31f9d5 | |||
| 17ea85c31f | |||
| 572e1422bf | |||
| af263073b5 | |||
| 7facf2d620 | |||
| eef3ff434b | |||
| fa94e18a6a | |||
| c680cfd5c5 | |||
| 3e8469611d | |||
| 39ab475156 | |||
| 636de67a17 | |||
| d80c18f652 | |||
| 9f68e009f1 | |||
| 557bd2bbbf | |||
| d33c53b691 | |||
| ddd223c2ec | |||
| 50f5b600b1 | |||
| d94df8390a | |||
| 8b33331929 | |||
| 86a9dde1eb | |||
| 33dec77063 | |||
| fe06e2fa19 | |||
| 7b5e3eaafd | |||
| 0a30f1ffb9 | |||
| 8687604d26 | |||
| 0a9fd6c439 | |||
| c95a9395de | |||
| 77066d7a9f | |||
| c8e5b7de9a | |||
| 3e11a88a7c | |||
| a7e4968836 | |||
| 6d9e2d3c03 | |||
| 0789e7a353 | |||
| ff97a85552 | |||
| 33cfd92cef | |||
| 58513ef59f | |||
| 6056e3e767 | |||
| 1b1ed7c4ab | |||
| 5b44e4bddd | |||
| 54592969a4 | |||
| 38007ab3d5 | |||
| bdd10c7617 | |||
| c0f4bc021a | |||
| 34d6af93a6 | |||
| 0df481dabb | |||
| 55c5b91411 | |||
| be745f4602 | |||
| 8bf5ad0f12 | |||
| 1b723b2fee | |||
| 6095eb98c4 | |||
| 95bb070a6b | |||
| 75a338304a | |||
| 1c28305b83 | |||
| 1f1abb80be | |||
| 3addc4563a | |||
| e37e7566e2 | |||
| eedf537902 | |||
| 5e8bd52433 | |||
| 910e8a6cf9 | |||
| edfc2467a3 | |||
| 205170efe4 | |||
| fceac407ee | |||
| 4c5ebe5949 | |||
| d12ad26e79 | |||
| bd57665b55 | |||
| c3b8d48813 | |||
| c8933e7326 | |||
| c95c64ccff | |||
| c5bcd4c2e4 | |||
| 9e402897c5 | |||
| cbf87df551 | |||
| ea01492e1f | |||
| 82445d5d3a | |||
| 839bc4cc9e | |||
| 98c6a569bf | |||
| 85ad95c0d0 | |||
| 1d0498fdce | |||
| ee390c34e9 | |||
| b853615f7d | |||
| 5b2bb30902 | |||
| da4788a6c6 | |||
| 3022635a71 | |||
| 38d3ae6a6a | |||
| 3f0eea44f4 | |||
| 5077f60881 | |||
| 279f70a6bd | |||
| 01f3596fde | |||
| 23d3577a06 | |||
| 22c75951e0 | |||
| 3d3fb5dc61 | |||
| e4a8213a2c | |||
| b2f71ae163 | |||
| 53256b0e34 | |||
| 4ebd3e046d | |||
| d07d49d2e7 | |||
| 111bfb9302 | |||
| 143b2a7ff5 | |||
| e7b47d28d9 | |||
| 97b44a89d4 | |||
| 27e01657b1 | |||
| 43638813d7 | |||
| f781b6785c | |||
| 9168cd4109 | |||
| 4c49c6ffc9 | |||
| 4890727692 | |||
| 7907a45ca2 | |||
| 727bbba815 | |||
| 877da36e5a | |||
| ab8effbc32 | |||
| a58d98f0dc | |||
| 431cb73e61 | |||
| 0ee02f2efd | |||
| 8d5b2a9e88 | |||
| 1085673010 | |||
| d327a72749 | |||
| 74add23c14 | |||
| 9b400573c8 | |||
| a8c3ef7d00 | |||
| a484582b70 | |||
| 69b956904c | |||
| 6e3888f295 | |||
| 1bfa0eb9c7 | |||
| 08a342c909 | |||
| a28ad2b0c7 | |||
| 4c96de9cdf | |||
| 3a645abe6f | |||
| 27bd6f96e7 | |||
| a98fac2e95 | |||
| f645b65a9e | |||
| 6eaf8cc374 | |||
| 61c0b691ab | |||
| 394cefb2de | |||
| 30d6a55e3c | |||
| e32018e8f6 | |||
| 6b002a8475 | |||
| 97e23c8f50 | |||
| e558ffd807 | |||
| 71d158ca45 | |||
| 5f8e5e0be9 | |||
| ff91eb1407 | |||
| 877a859ef4 | |||
| f8b29cd967 | |||
| 3d2554c557 | |||
| b7d7204d40 | |||
| 876d26d174 | |||
| 3ccb1a63aa | |||
| c8bb9b4f5f | |||
| 723be29118 | |||
| 2865915cdf | |||
| d3e0c2bb6e | |||
| 5e7ae73861 | |||
| 61206b2169 | |||
| 92e2a8913b | |||
| ad827828d7 | |||
| faf16084a3 | |||
| 38d2b55456 | |||
| abcebc54e8 | |||
| 40cb963c99 | |||
| 9d267a6cc4 | |||
| 462f24149b | |||
| 4744b62f91 | |||
| 08244e7fdc | |||
| f64fb1bee1 | |||
| 743c3b2466 | |||
| aefa36fef8 | |||
| d7f6503196 | |||
| 3375b8c898 | |||
| 52e5919ccd | |||
| 1c2e57adb6 | |||
| 2e99d6ee0a | |||
| 6744815f77 | |||
| 0ba44ab2d3 | |||
| a6006450de | |||
| c20e2ba451 | |||
| 7a0b387c1c | |||
| fdfe5fbe39 | |||
| 35751efad5 | |||
| 7005c9e40a | |||
| 118cf25ff7 | |||
| 01f65220ec | |||
| 078c7b78da | |||
| 25f00db9cf | |||
| 576ee17c4d | |||
| e7f654f56a | |||
| 014277b574 | |||
| 043a49152f | |||
| 40592d197d | |||
| 80e3d0eab4 | |||
| 40da090fe8 | |||
| 795ccd9782 | |||
| 57bfd6b968 | |||
| 02d1fe308d | |||
| 3e117f46d5 | |||
| dbb14e37fa | |||
| 8f2b534a68 | |||
| 224d2b89b1 | |||
| 4c2b5b010b | |||
| b98a1fa100 | |||
| bb51b53e11 | |||
| 19bd2431aa | |||
| 977ece64e3 | |||
| f8f362fc10 | |||
| fdc265eaea | |||
| 941a4fa14f | |||
| e4fdbdaf8d | |||
| c09e050084 | |||
| d12a94401d | |||
| e14d0d7c7f | |||
| ee9c94aceb | |||
| 4b4424ae92 | |||
| 8a9f11b0f5 | |||
| fd493dba9a | |||
| 75bc7c51b5 | |||
| aab166c466 | |||
| 94c49f500f | |||
| f353c4edf2 | |||
| acb3e5058d | |||
| 6858aecda7 | |||
| 4411ca18aa | |||
| dc9dbed683 | |||
| 860673bb0e | |||
| ac54a40af5 | |||
| 92df52867d | |||
| ab209c6f39 | |||
| 69dd23f27b | |||
| c5577317f9 | |||
| 0d7cf6f37d | |||
| 1d425b3cd6 | |||
| 9b62a45100 | |||
| db1f745a50 | |||
| d56e9ef298 | |||
| f278b7c2c5 | |||
| 916f3ca9ec | |||
| 30c695d988 | |||
| 7dcb120edf | |||
| e570e25b24 | |||
| 3cbc7f163a | |||
| 21a8001f56 | |||
| ebafb0006a | |||
| 37e42b2f0b | |||
| 1ae5b210b0 | |||
| 3f2fb67c04 | |||
| dded30d6a6 | |||
| 047a585cc2 | |||
| 8cb7aebf86 | |||
| 439eedcec7 | |||
| ed94e6508c | |||
| 9f33e756d5 | |||
| 9fee6568cd | |||
| 2a169d50ec | |||
| b2d90c2ac1 | |||
| 64cd253618 | |||
| 98bc6fbe4c | |||
| 70e41eb0e3 | |||
| b5e08df5d3 | |||
| 46034b0629 | |||
| 109da1056e | |||
| e046565f22 | |||
| ff99472278 | |||
| a5a2c2a93f | |||
| 4cf7a531c2 | |||
| b101dc780d | |||
| 1c7ff5c597 | |||
| 880f220355 | |||
| a1d2acc845 | |||
| 60caf896bd | |||
| f9d0ec864d | |||
| 6ca1aa532c |
@@ -0,0 +1,40 @@
|
|||||||
|
.git
|
||||||
|
.github
|
||||||
|
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
KindleComicConverter.egg-info
|
||||||
|
|
||||||
|
.dockerignore
|
||||||
|
.gitignore
|
||||||
|
.travis.yml
|
||||||
|
|
||||||
|
Dockerfile
|
||||||
|
venv
|
||||||
|
.venv
|
||||||
|
__pycache__/
|
||||||
|
*/__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
*.md
|
||||||
|
*.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
|
||||||
@@ -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']
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
Add a screenshot of your KCC settings.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. macOS, Linux, Windows 11]
|
||||||
|
- Device [e.g. Kindle Paperwhite 3rd gen, Kobo Libra 2]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pip"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
|
||||||
|
# Enable version updates for Docker
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
# Look for a `Dockerfile` in the `root` directory
|
||||||
|
directory: "/"
|
||||||
|
# Check for updates once a week
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
|
||||||
|
# Maintain dependencies for GitHub Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "beta_release" ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ "beta_release" ]
|
||||||
|
schedule:
|
||||||
|
- cron: '42 22 * * 3'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'python' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||||
|
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
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.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
|
||||||
|
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||||
|
# queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
|
||||||
|
# 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@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
|
||||||
|
|
||||||
|
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||||
|
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||||
|
|
||||||
|
# - run: |
|
||||||
|
# echo "Run, Build Application using script"
|
||||||
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v4
|
||||||
|
with:
|
||||||
|
category: "/language:${{matrix.language}}"
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
name: Build and Publish Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
|
|
||||||
|
# Don't trigger if it's just a documentation update
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
- '**.MD'
|
||||||
|
- '**.yml'
|
||||||
|
- 'docs/**'
|
||||||
|
- 'LICENSE'
|
||||||
|
- '.gitattributes'
|
||||||
|
- '.gitignore'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
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@v3
|
||||||
|
|
||||||
|
- 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
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||||
|
|
||||||
|
name: build KCC for Linux
|
||||||
|
|
||||||
|
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: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: 3.11
|
||||||
|
cache: 'pip'
|
||||||
|
- name: Install python dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libpng-dev libjpeg-dev p7zip-full p7zip-rar python3-pip squashfs-tools libfuse2 libxcb-cursor0
|
||||||
|
python -m pip install --upgrade pip certifi pyinstaller --no-binary pyinstaller
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
- name: build binary
|
||||||
|
run: |
|
||||||
|
python setup.py build_binary
|
||||||
|
chmod +x dist/kcc_linux*
|
||||||
|
# issue with this action, disabled and commented out
|
||||||
|
# see https://github.com/AppImageCrafters/build-appimage/issues/5
|
||||||
|
# see https://appimage-builder.readthedocs.io/en/latest/intro/install.html#install-appimagetool
|
||||||
|
# - name: Build AppImage
|
||||||
|
# uses: AppImageCrafters/build-appimage-action@master
|
||||||
|
# env:
|
||||||
|
# UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync
|
||||||
|
# with:
|
||||||
|
# recipe: AppImageBuilder.yml
|
||||||
|
- name: Build AppImage
|
||||||
|
run: |
|
||||||
|
wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
|
||||||
|
chmod +x appimage-builder-x86_64.AppImage
|
||||||
|
sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
|
||||||
|
appimage-builder --recipe AppImageBuilder.yml --skip-test
|
||||||
|
env:
|
||||||
|
UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync
|
||||||
|
- name: upload artifact
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: AppImage
|
||||||
|
path: './*.AppImage*'
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
generate_release_notes: false
|
||||||
|
files: |
|
||||||
|
LICENSE.txt
|
||||||
|
*.AppImage*
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||||
|
|
||||||
|
name: build KCC for mac os
|
||||||
|
|
||||||
|
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, macos-14 ]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
env:
|
||||||
|
MACOSX_DEPLOYMENT_TARGET: '14.0'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: 3.11
|
||||||
|
cache: 'pip'
|
||||||
|
- name: Install python dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip pyinstaller certifi
|
||||||
|
pip install -r requirements.txt
|
||||||
|
- name: Install the Apple certificate and provisioning profile
|
||||||
|
# TODO signing
|
||||||
|
# https://federicoterzi.com/blog/automatic-code-signing-and-notarization-for-macos-apps-using-github-actions/
|
||||||
|
if: ${{ false }}
|
||||||
|
env:
|
||||||
|
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||||
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
|
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
|
||||||
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# create variables
|
||||||
|
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
||||||
|
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
|
||||||
|
# import certificate and provisioning profile from secrets
|
||||||
|
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
|
||||||
|
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
|
||||||
|
|
||||||
|
# create temporary keychain
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
# import certificate to keychain
|
||||||
|
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||||
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
# apply provisioning profile
|
||||||
|
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||||
|
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm install -g appdmg
|
||||||
|
- name: build binary
|
||||||
|
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
|
||||||
|
run: |
|
||||||
|
python setup.py build_binary
|
||||||
|
- name: upload build
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: mac-os-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: |
|
||||||
|
dist/*.dmg
|
||||||
|
- name: Clean up keychain and provisioning profile
|
||||||
|
# TODO signing
|
||||||
|
if: ${{ false }}
|
||||||
|
# if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
rm ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision
|
||||||
@@ -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 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
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||||
|
|
||||||
|
name: build KCC for windows
|
||||||
|
|
||||||
|
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:
|
||||||
|
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@v6
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: 3.11
|
||||||
|
cache: 'pip'
|
||||||
|
- name: Install dependencies
|
||||||
|
env:
|
||||||
|
PYINSTALLER_COMPILE_BOOTLOADER: 1
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install certifi pyinstaller --no-binary pyinstaller
|
||||||
|
- name: build binary
|
||||||
|
run: |
|
||||||
|
python setup.py ${{ matrix.command }}
|
||||||
|
- name: upload-unsigned-artifact
|
||||||
|
id: upload-unsigned-artifact
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
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: false
|
||||||
|
files: |
|
||||||
|
dist/*.exe
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# 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
|
||||||
|
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
|
||||||
|
- 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: false
|
||||||
|
files: |
|
||||||
|
dist/*.exe
|
||||||
@@ -1,7 +1,17 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
*.cbz
|
Pipfile
|
||||||
*.cbr
|
Pipfile.lock
|
||||||
.idea
|
setup.bat
|
||||||
build
|
kindlecomicconverter/sentry.py
|
||||||
awkcc
|
other/windows/kindlegen.exe
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
KindleComicConverter*.egg-info/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
win7
|
||||||
|
osx10.11
|
||||||
|
/venv/
|
||||||
|
/kindlegen*
|
||||||
|
/kcc.bat
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# appimage-builder recipe see https://appimage-builder.readthedocs.io for details
|
||||||
|
version: 1
|
||||||
|
script:
|
||||||
|
- rm -rf AppDir || true
|
||||||
|
- mkdir -p AppDir/usr/share/icons/hicolor/64x64/apps/
|
||||||
|
- cp -a dist/kcc_linux* AppDir/ && mv AppDir/kcc_linux* AppDir/kcc_linux
|
||||||
|
- cp icons/comic2ebook.png AppDir/usr/share/icons/hicolor/64x64/apps/
|
||||||
|
AppDir:
|
||||||
|
path: AppDir
|
||||||
|
app_info:
|
||||||
|
id: com.github.ciromattia.kcc
|
||||||
|
name: kindleComicConverter
|
||||||
|
icon: comic2ebook
|
||||||
|
version: latest
|
||||||
|
exec: ./kcc_linux
|
||||||
|
exec_args: $@
|
||||||
|
apt:
|
||||||
|
arch:
|
||||||
|
- amd64
|
||||||
|
allow_unauthenticated: true
|
||||||
|
sources:
|
||||||
|
- sourceline: deb http://archive.ubuntu.com/ubuntu jammy main restricted
|
||||||
|
- sourceline: deb http://archive.ubuntu.com/ubuntu jammy-updates main restricted
|
||||||
|
- sourceline: deb http://archive.ubuntu.com/ubuntu jammy universe
|
||||||
|
- sourceline: deb http://archive.ubuntu.com/ubuntu jammy-updates universe
|
||||||
|
- sourceline: deb http://archive.ubuntu.com/ubuntu jammy multiverse
|
||||||
|
- sourceline: deb http://archive.ubuntu.com/ubuntu jammy-updates multiverse
|
||||||
|
- sourceline: deb http://archive.ubuntu.com/ubuntu jammy-backports main restricted
|
||||||
|
universe multiverse
|
||||||
|
- sourceline: deb http://security.ubuntu.com/ubuntu jammy-security main restricted
|
||||||
|
- sourceline: deb http://security.ubuntu.com/ubuntu jammy-security universe
|
||||||
|
- sourceline: deb http://security.ubuntu.com/ubuntu jammy-security multiverse
|
||||||
|
include:
|
||||||
|
- libc6:amd64
|
||||||
|
files:
|
||||||
|
include: []
|
||||||
|
exclude:
|
||||||
|
- usr/share/man
|
||||||
|
- usr/share/doc/*/README.*
|
||||||
|
- usr/share/doc/*/changelog.*
|
||||||
|
- usr/share/doc/*/NEWS.*
|
||||||
|
- usr/share/doc/*/TODO.*
|
||||||
|
test:
|
||||||
|
fedora-30:
|
||||||
|
image: appimagecrafters/tests-env:fedora-30
|
||||||
|
command: ./AppRun
|
||||||
|
use_host_x: true
|
||||||
|
debian-stable:
|
||||||
|
image: appimagecrafters/tests-env:debian-stable
|
||||||
|
command: ./AppRun
|
||||||
|
use_host_x: true
|
||||||
|
archlinux-latest:
|
||||||
|
image: appimagecrafters/tests-env:archlinux-latest
|
||||||
|
command: ./AppRun
|
||||||
|
use_host_x: true
|
||||||
|
centos-7:
|
||||||
|
image: appimagecrafters/tests-env:centos-7
|
||||||
|
command: ./AppRun
|
||||||
|
use_host_x: true
|
||||||
|
ubuntu-xenial:
|
||||||
|
image: appimagecrafters/tests-env:ubuntu-xenial
|
||||||
|
command: ./AppRun
|
||||||
|
use_host_x: true
|
||||||
|
AppImage:
|
||||||
|
arch: x86_64
|
||||||
|
update-information: !ENV ${UPDATE_INFO}
|
||||||
|
sign-key: None
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
# CHANGELOG
|
||||||
|
|
||||||
|
|
||||||
|
#### 5.6.2:
|
||||||
|
* build pipeline : drop pypi by @darodi in [#465](https://github.com/ciromattia/kcc/pull/465)
|
||||||
|
* supporting Kindle Previewer by @darodi in [#466](https://github.com/ciromattia/kcc/pull/466)
|
||||||
|
* Bump actions/upload-artifact from 2 to 3 by @dependabot in [#468](https://github.com/ciromattia/kcc/pull/468)
|
||||||
|
* new appImage by @darodi in [#483](https://github.com/ciromattia/kcc/pull/483)
|
||||||
|
* Bump actions/checkout from 2 to 3 by @dependabot in [#484](https://github.com/ciromattia/kcc/pull/484)
|
||||||
|
* supporting Kindle Previewer by @darodi in [#486](https://github.com/ciromattia/kcc/pull/486)
|
||||||
|
* comic2ebook/func: Add a delete option (closes #458) by @Constantin1489 in [#485](https://github.com/ciromattia/kcc/pull/485)
|
||||||
|
* Add command line executables to CI/pipelines by @darodi in [#487](https://github.com/ciromattia/kcc/pull/487)
|
||||||
|
* gui/func: Add a 'delete after conversion' button (closes #458) by @Constantin1489 in [#488](https://github.com/ciromattia/kcc/pull/488)
|
||||||
|
* fix crashes on png transparency by @axu2 in [#494](https://github.com/ciromattia/kcc/pull/494)
|
||||||
|
* Update python-slugify requirement from <8.0.0,>=1.2.1 to >=1.2.1,<9.0.0 by @dependabot in [#473](https://github.com/ciromattia/kcc/pull/473)
|
||||||
|
* Updates GUI text for new Homebrew version by @thatrobotdev in [#491](https://github.com/ciromattia/kcc/pull/491)
|
||||||
|
* Even with EPUB-200MB option selected, created file is above 200MB by @darodi in [#503](https://github.com/ciromattia/kcc/pull/503)
|
||||||
|
* limit kindle scribe image size to (1440, 1920) when using kindlegen by @darodi in [#514](https://github.com/ciromattia/kcc/pull/514)
|
||||||
|
* add 7z to PATH by @axu2 in [#513](https://github.com/ciromattia/kcc/pull/513)
|
||||||
|
* use unrar for fedora only by @darodi in [#515](https://github.com/ciromattia/kcc/pull/515)
|
||||||
|
|
||||||
|
|
||||||
|
#### 5.6.1:
|
||||||
|
* Fix pillow backwards compatibility, add mozjpeg-lossless-optimization to setup.py by @corylk in #461
|
||||||
|
* fix in fedora: 7z doesn't support rar archives, use unrar by @AlicesReflexion in #370
|
||||||
|
* Using communicate instead of terminate by @catsout in #459
|
||||||
|
* use copyfile and delete instead of shutil.move fix #386 by @StudioEtrange in #387
|
||||||
|
|
||||||
|
|
||||||
|
#### 5.6.0:
|
||||||
|
* Fix Docker 7z missing [darodi/kcc#31](https://github.com/darodi/kcc/issues/31), thanks [@darodi](https://github.com/darodi)
|
||||||
|
* update to python 3.11, thanks [@darodi](https://github.com/darodi)
|
||||||
|
* Bump python from 3.8-slim-buster to 3.11-slim-buster dependabot[bot]
|
||||||
|
* More precise type in slugify dependency check, thanks [@bamless](https://github.com/bamless)
|
||||||
|
* Fix 'slugify' dependency check, thanks [@bamless](https://github.com/bamless)
|
||||||
|
* Update python-slugify requirement from <3.0.0,>=1.2.1 to >=1.2.1,<8.0.0 dependabot[bot]
|
||||||
|
* Bump actions/setup-python from 3 to 4 [darodi/kcc#32](https://github.com/darodi/kcc/issues/32) dependabot[bot]
|
||||||
|
* Bump actions/setup-node from 2 to 3 [darodi/kcc#34](https://github.com/darodi/kcc/issues/34) dependabot[bot]
|
||||||
|
* Fix Docker 7z missing [darodi/kcc#31](https://github.com/darodi/kcc/issues/31), thanks [@darodi](https://github.com/darodi)
|
||||||
|
* Spread splitter not keeping aspect ratio [darodi/kcc#27](https://github.com/darodi/kcc/issues/27), thanks [@darodi](https://github.com/darodi)
|
||||||
|
* activate batchsplit only for EPUB-200 [darodi/kcc#24](https://github.com/darodi/kcc/issues/24), thanks [@darodi](https://github.com/darodi)
|
||||||
|
* Feature Request: allow split for epub and set target (email, web upload) [darodi/kcc#21](https://github.com/darodi/kcc/issues/21), thanks [@darodi](https://github.com/darodi)
|
||||||
|
* Adding a switch on GUI interface in order to control cropping options [darodi/kcc#18](https://github.com/darodi/kcc/issues/18), thanks [@darodi](https://github.com/darodi)
|
||||||
|
* profiles: add Kindle11 and Kindle Scribe [darodi/kcc#16](https://github.com/darodi/kcc/issues/16), thanks [@darodi](https://github.com/darodi)
|
||||||
|
* Update with new Kobo models [darodi/kcc#15](https://github.com/darodi/kcc/issues/15), thanks [@lennie420](https://github.com/lennie420)
|
||||||
|
* Keep epub file when selecting another type of output file [darodi/kcc#12](https://github.com/darodi/kcc/issues/12), thanks [@darodi](https://github.com/darodi)
|
||||||
|
* fix Using 'Disable processing' option using my processed image get an error [darodi/kcc#1](https://github.com/darodi/kcc/issues/1), thanks [@darodi](https://github.com/darodi)
|
||||||
|
* KFX Output in GUI [darodi/kcc#9](https://github.com/darodi/kcc/issues/9), thanks [@darodi](https://github.com/darodi)
|
||||||
|
* Linux version and appImage [darodi/kcc#6](https://github.com/darodi/kcc/issues/6), thanks [@darodi](https://github.com/darodi)
|
||||||
|
* docker image for command line [darodi/kcc#5](https://github.com/darodi/kcc/issues/5), thanks [@darodi](https://github.com/darodi)
|
||||||
|
* Fix type error in autocontrastImage [ciromattia/kcc#432](https://github.com/ciromattia/kcc/issues/432), thanks [@darodi](https://github.com/darodi)
|
||||||
|
* Option to turn 1x4 strips into 2x2 strips [ciromattia/kcc#439](https://github.com/ciromattia/kcc/issues/439), thanks [@darodi](https://github.com/darodi)
|
||||||
|
* Option in GUI to have PNG instead of jpg images [darodi/kcc#3](https://github.com/darodi/kcc/issues/3), thanks [@darodi](https://github.com/darodi)
|
||||||
|
* Use MozJPEG as the JPEG encoder : cli and GUI option [ciromattia/kcc#416](https://github.com/ciromattia/kcc/pull/416), thanks [@darodi](https://github.com/darodi)
|
||||||
|
* Disable all image transformation : cli and GUI option [ciromattia/kcc#388](https://github.com/ciromattia/kcc/pull/388), thanks [@StudioEtrange](https://github.com/StudioEtrange)
|
||||||
|
* file selector add All `*.*` [ciromattia/kcc#412](https://github.com/ciromattia/kcc/pull/412), thanks [@StudioEtrange](https://github.com/StudioEtrange)
|
||||||
|
* Clarify Pillow version requirement [ciromattia/kcc#366](https://github.com/ciromattia/kcc/pull/366), thanks [@clach04](https://github.com/clach04)
|
||||||
|
* sync requirements between setup.py and requirements.txt [ciromattia/kcc#411](https://github.com/ciromattia/kcc/pull/411), thanks [@StudioEtrange](https://github.com/StudioEtrange)
|
||||||
|
* Add profile for Kindle PW5/Signature [ciromattia/kcc#405](https://github.com/ciromattia/kcc/pull/405), thanks [@Einlar](https://github.com/Einlar), [@darodi](https://github.com/darodi)
|
||||||
|
* Fixed the skipped/missed images and/or panels [ciromattia/kcc#393](https://github.com/ciromattia/kcc/pull/393), thanks [@FulyaDemirkan](https://github.com/FulyaDemirkan)
|
||||||
|
* Add profiles for the Kobo Clara HD and Libra H2O [ciromattia/kcc#331](https://github.com/ciromattia/kcc/pull/331), thanks [@fbriere](https://github.com/fbriere)
|
||||||
|
|
||||||
|
#### 5.5.2:
|
||||||
|
* Fixed KindleGen detection on macOS 10.15
|
||||||
|
* Fixed multiple smaller issues
|
||||||
|
|
||||||
|
#### 5.5.1:
|
||||||
|
* Fixes some stability issues
|
||||||
|
|
||||||
|
#### 5.5.0:
|
||||||
|
* Added support for WebP format
|
||||||
|
* Added profiles for Kindle Paperwhite 4 and Kobo Forma
|
||||||
|
* All archives are now handled by 7z
|
||||||
|
* Removed MCD support
|
||||||
|
* Fixed multiple smaller issues
|
||||||
|
|
||||||
|
#### 5.4.5:
|
||||||
|
* Fixed EPUB output for non-Kindle devices
|
||||||
|
|
||||||
|
#### 5.4.4:
|
||||||
|
* Minor bug fixes
|
||||||
|
|
||||||
|
#### 5.4.3:
|
||||||
|
* Fixed conversion crash on Windows
|
||||||
|
|
||||||
|
#### 5.4.2:
|
||||||
|
* Added Kindle Oasis 2 profile
|
||||||
|
* Allowed metadata editor to edit directories
|
||||||
|
* Fixed image stretching when HQ Panel View option was enabled
|
||||||
|
* Fixed possible problem with directory sort order
|
||||||
|
|
||||||
|
#### 5.4.1:
|
||||||
|
* Minor bug fixes and tweaks
|
||||||
|
* Implemented new binary build pipeline
|
||||||
|
|
||||||
|
#### 5.4:
|
||||||
|
* Reimplemented high quality Panel View option
|
||||||
|
* Improved webtoon splitter
|
||||||
|
* Fixed page splitter
|
||||||
|
|
||||||
|
#### 5.3.1:
|
||||||
|
* Small increase of output quality
|
||||||
|
* Improved error reporting
|
||||||
|
* Internal changes and tweaks
|
||||||
|
|
||||||
|
#### 5.3:
|
||||||
|
* Vastly improved output compatibility for non-Kindle devices
|
||||||
|
* Enabled old pinch zoom for Kindle devices
|
||||||
|
* Re-enabled Panel View support for Kindle Keyboard
|
||||||
|
* Partially re-enabled OS X file association mechanism
|
||||||
|
* Fixed multiple smaller issues
|
||||||
|
|
||||||
|
#### 5.2.1:
|
||||||
|
* Improved directory parsing
|
||||||
|
* Tweaked margin detection algorithm
|
||||||
|
* Improved error reporting
|
||||||
|
|
||||||
|
#### 5.2:
|
||||||
|
* Added new Panel View options
|
||||||
|
* Implemented new margin detection algorithm
|
||||||
|
* Removed HQ Panel View mode
|
||||||
|
* Fixed multiple smaller issues
|
||||||
|
|
||||||
|
#### 5.1.3:
|
||||||
|
* Added Kobo Aura ONE profile
|
||||||
|
* Fixed few small bugs
|
||||||
|
|
||||||
|
#### 5.1.2:
|
||||||
|
* Fixed error reporting
|
||||||
|
|
||||||
|
#### 5.1.1:
|
||||||
|
* Fixed multiple GUI bugs
|
||||||
|
|
||||||
|
#### 5.1:
|
||||||
|
* GUI now can be resized and high DPI support was somewhat improved
|
||||||
|
* Added profile for Kindle Oasis
|
||||||
|
* Implemented new error reporting mechanism
|
||||||
|
* CLI version now support additional cropping options
|
||||||
|
* Fixed permission issues on Windows
|
||||||
|
* Fixed multiple smaller issues
|
||||||
|
|
||||||
|
#### 5.0.1:
|
||||||
|
* Fixed Panel View placement issues
|
||||||
|
* Decreased application startup time
|
||||||
|
* Fixed multiple smaller issues
|
||||||
|
|
||||||
|
#### 5.0:
|
||||||
|
* Major overhaul of internal mechanisms and GUI
|
||||||
|
* Added cover upload feature
|
||||||
|
* Tweaked Webtoon parsing mode
|
||||||
|
* Fixed multiple smaller issues
|
||||||
|
* Migrated build enviroment to PyInstaller
|
||||||
|
|
||||||
|
#### 4.6.5:
|
||||||
|
* Fixed multiple Windows and OS X issues
|
||||||
|
* Allowed Linux release to use older PyQT5 version
|
||||||
|
|
||||||
|
#### 4.6.4:
|
||||||
|
* Fixed multiple Windows specific problems
|
||||||
|
* Improved error handling
|
||||||
|
* Improved color detection algorithm
|
||||||
|
* New, slimmer OS X release
|
||||||
|
|
||||||
|
#### 4.6.3:
|
||||||
|
* Implemented remote bug reporting
|
||||||
|
* Minor bug fixes and GUI tweaks
|
||||||
|
|
||||||
|
#### 4.6.2:
|
||||||
|
* Fixed critical MOBI header bug
|
||||||
|
* Fixed metadata encoding error
|
||||||
|
|
||||||
|
#### 4.6.1:
|
||||||
|
* Fixed KEPUB TOC generator
|
||||||
|
* Added warning about too small input files
|
||||||
|
* ComicRack Summary metadata field is now parsed
|
||||||
|
* Small tweaks of KEPUB output
|
||||||
|
|
||||||
|
#### 4.6:
|
||||||
|
* KEPUB is now default output for all Kobo profiles
|
||||||
|
* EPUB output now produce fully valid EPUB 3.0.1
|
||||||
|
* Added profile for Kindle Paperwhite 3
|
||||||
|
* Dropped official support of all Kindle Fire models and Kindle for Android
|
||||||
|
* Other minor tweaks
|
||||||
|
|
||||||
|
#### 4.5.1:
|
||||||
|
* Added Kobo Glo HD profile
|
||||||
|
* Fixed RAR/CBR parsing anomalies
|
||||||
|
* Minor bug fixes and tweaks
|
||||||
|
|
||||||
|
#### 4.5:
|
||||||
|
* Added simple ComicRack metadata editor
|
||||||
|
* Re-enabled Manga Cover Database support
|
||||||
|
* ComicRack bookmarks are now parsed
|
||||||
|
* Fixed glitches in Kindle Voyage profile
|
||||||
|
* Fixed problems with directory locks on Windows
|
||||||
|
* Fixed sorting anomalies
|
||||||
|
* Improved conversion speed
|
||||||
|
|
||||||
|
#### 4.4.1:
|
||||||
|
* Fixed problems with OSX GUI
|
||||||
|
* Added one missing DLL to Windows installer
|
||||||
|
|
||||||
|
#### 4.4:
|
||||||
|
* Improved speed and quality of conversion
|
||||||
|
* Added RAR5 support
|
||||||
|
* Dropped BMP and TIFF support
|
||||||
|
* Fixed some WebToon mode bugs
|
||||||
|
* Fixed CBR parsing on OSX
|
||||||
|
|
||||||
|
#### 4.3.1:
|
||||||
|
* Fixed Kindle Voyage profile
|
||||||
|
* Fixed some bugs in OS X release
|
||||||
|
* CLI version now support multiple input files at once
|
||||||
|
* Disabled MCB support
|
||||||
|
* Other minor tweaks
|
||||||
|
|
||||||
|
#### 4.3:
|
||||||
|
* Added profiles for Kindle Voyage and Kobo Aura H2O
|
||||||
|
* Added missing features to CLI version
|
||||||
|
* Other minor bug fixes
|
||||||
|
|
||||||
|
#### 4.2.1:
|
||||||
|
* Improved margin color detection
|
||||||
|
* Fixed random crashes of MOBI processing step
|
||||||
|
* Fixed resizing problems in high quality mode
|
||||||
|
* Fixed some MCD support bugs
|
||||||
|
* Default output format for Kindle DX is now CBZ
|
||||||
|
|
||||||
|
#### 4.2:
|
||||||
|
* Added [Manga Cover Database](http://manga.joentjuh.nl/) support
|
||||||
|
* Officially dropped Windows XP support
|
||||||
|
* Fixed _Other_ profile
|
||||||
|
* Fixed problems with page order on stock KOBO CBZ reader
|
||||||
|
* Many other small bug fixes and tweaks
|
||||||
|
|
||||||
|
#### 4.1:
|
||||||
|
* Thanks to code contributed by Kevin Hendricks speed of MOBI creation was greatly increased
|
||||||
|
* Improved performance on Windows
|
||||||
|
* Improved MOBI splitting and changed maximal size of output file
|
||||||
|
* Fixed _No optimization_ mode
|
||||||
|
* Multiple small tweaks nad minor bug fixes
|
||||||
|
|
||||||
|
#### 4.0.2:
|
||||||
|
* Fixed some Windows and OSX specific bugs
|
||||||
|
* Fixed problem with marigns when using HQ mode
|
||||||
|
|
||||||
|
#### 4.0.1:
|
||||||
|
* Fixed file lock problems that plagued some Windows users
|
||||||
|
* Fixed content server failing to start on Windows
|
||||||
|
* Improved performance of WebToon splitter
|
||||||
|
* Tweaked margin color detection
|
||||||
|
|
||||||
|
#### 4.0:
|
||||||
|
* KCC now use Python 3.3 and Qt 5.2
|
||||||
|
* Full UTF-8 awareness
|
||||||
|
* CBZ output now support Manga mode
|
||||||
|
* Improved Panel View support and margin color detection
|
||||||
|
* Added drag&drop support
|
||||||
|
* Output directory can be now selected
|
||||||
|
* Windows release now have auto-updater
|
||||||
|
* Names of chapters on Kindle should be now more user friendly
|
||||||
|
* Fixed OSX file association support
|
||||||
|
* Many extensive internal changes and tweaks
|
||||||
|
|
||||||
|
#### 3.7.2:
|
||||||
|
* Fixed problems with HQ mode
|
||||||
|
|
||||||
|
#### 3.7.1:
|
||||||
|
* Hotfixed Kobo profiles
|
||||||
|
|
||||||
|
#### 3.7:
|
||||||
|
* Added profiles for KOBO devices
|
||||||
|
* Improved Panel View support
|
||||||
|
* Improved WebToon splitter
|
||||||
|
* Improved margin color autodetection
|
||||||
|
* Tweaked EPUB output
|
||||||
|
* Fixed stretching option
|
||||||
|
* GUI tweaks and minor bugfixes
|
||||||
|
|
||||||
|
#### 3.6.2:
|
||||||
|
* Fixed previous PNG output fix
|
||||||
|
* Fixed Panel View anomalies
|
||||||
|
|
||||||
|
#### 3.6.1:
|
||||||
|
* Fixed PNG output
|
||||||
|
|
||||||
|
#### 3.6:
|
||||||
|
* Increased quality of Panel View zoom
|
||||||
|
* Creation of multipart MOBI output is now faster on machines with 4GB+ RAM
|
||||||
|
* Automatic gamma correction now distinguishes color and grayscale images
|
||||||
|
* Added ComicRack metadata parser
|
||||||
|
* Implemented new method to detect border color in non-webtoon comics
|
||||||
|
* Upscaling is now enabled by default for Kindle Fire HD/HDX
|
||||||
|
* Windows nad Linux releases now have tray icon
|
||||||
|
* Fixed Kindle Fire HDX 7" output
|
||||||
|
* Increased target resolution for Kindle DX/DXG CBZ output
|
||||||
|
|
||||||
|
#### 3.5:
|
||||||
|
* Added simple content server - Converted files can be now delivered wireless
|
||||||
|
* Added proper Windows installer
|
||||||
|
* Improved multiprocessing speed
|
||||||
|
* GUI tweaks and minor bug fixes
|
||||||
|
|
||||||
|
#### 3.4:
|
||||||
|
* Improved PNG output
|
||||||
|
* Increased quality of upscaling
|
||||||
|
* Added support of file association - KCC can now open CBZ, CBR, CB7, ZIP, RAR, 7Z and PDF files directly
|
||||||
|
* Paths that contain UTF-8 characters are now supported
|
||||||
|
* Migrated to new version of Pillow library
|
||||||
|
* Merged DX and DXG profiles
|
||||||
|
* Many other minor bug fixes and GUI tweaks
|
||||||
|
|
||||||
|
#### 3.3:
|
||||||
|
* Margins are now automatically omitted in Panel View mode
|
||||||
|
* Margin color fill is now autodetected
|
||||||
|
* Created MOBI files are not longer marked as _Personal_ on newer Kindle models
|
||||||
|
* Layout of panels in Panel View mode is now automatically adjusted to content
|
||||||
|
* Fixed Kindle 2/DX/DXG profiles - no more blank pages
|
||||||
|
* All Kindle Fire profiles now support hiqh quality Panel View
|
||||||
|
* Added support of 7z/CB7 files
|
||||||
|
* Added Kindle Fire HDX profile
|
||||||
|
* Support for Virtual Panel View was removed
|
||||||
|
* Profiles for Kindle Keyboard, Touch and Non-Touch are now merged
|
||||||
|
* Windows release is now bundled with UnRAR and 7za
|
||||||
|
* Small GUI tweaks
|
||||||
|
|
||||||
|
#### 3.2:
|
||||||
|
* Too big EPUB files are now splitted before conversion to MOBI
|
||||||
|
* Added experimental parser of manga webtoons
|
||||||
|
* Improved error handling
|
||||||
|
|
||||||
|
#### 3.2.1:
|
||||||
|
* Hotfixed crash occurring on OS with Russian locale
|
||||||
|
|
||||||
|
#### 3.1:
|
||||||
|
* Added profile: Kindle for Android
|
||||||
|
* Add file/directory dialogs now support multiselect
|
||||||
|
* Many small fixes and tweaks
|
||||||
|
|
||||||
|
#### 3.0:
|
||||||
|
* New QT GUI
|
||||||
|
* Merge with AWKCC
|
||||||
|
* Added ultra quality mode
|
||||||
|
* Added support for custom width/height
|
||||||
|
* Added option to disable color conversion
|
||||||
|
|
||||||
|
#### 2.10:
|
||||||
|
* Multiprocessing support
|
||||||
|
* Kindle Fire support (color EPUB/MOBI)
|
||||||
|
* Panel View support for horizontal content
|
||||||
|
* Fixed panel order for horizontal pages when --rotate is enabled
|
||||||
|
* Disabled cropping and page number cutting for blank pages
|
||||||
|
* Fixed some slugify issues with specific file naming conventions (#50, #51)
|
||||||
|
|
||||||
|
#### 2.9
|
||||||
|
* Added support for generating a plain CBZ (skipping all the EPUB/MOBI generation) (#45)
|
||||||
|
* Prevent output file overwriting the source one: if a duplicate name is detected, append _kcc to the name
|
||||||
|
* Rarfile library updated to 2.6
|
||||||
|
* Added GIF, TIFF and BMP to supported formats (#42)
|
||||||
|
* Filenames slugifications (#28, #31, #9, #8)
|
||||||
|
|
||||||
|
#### 2.8
|
||||||
|
* Updated rarfile library
|
||||||
|
* Panel View support + HQ support (#36) - new option: --nopanelviewhq
|
||||||
|
* Split profiles for K4NT and K4T
|
||||||
|
* Rewrite of Landscape Mode support (huge readability improvement for KPW)
|
||||||
|
* Upscale use now BILINEAR method
|
||||||
|
* Added generic CSS file
|
||||||
|
* Optimized archive extraction for zip/rar files (#40)
|
||||||
|
|
||||||
|
#### 2.7
|
||||||
|
* Lots of GUI improvements (#27, #13)
|
||||||
|
* Added gamma support within --gamma option (defaults to profile-specified gamma) (#26, #27)
|
||||||
|
* Added --nodithering option to prevent dithering optimizations (#27)
|
||||||
|
* EPUB margins support (#30)
|
||||||
|
* Fixed no file added if file has no spaces on Windows (#25)
|
||||||
|
* Gracefully exit if unrar missing (#15)
|
||||||
|
* Do not call kindlegen if source EPUB is bigger than 320MB (#17)
|
||||||
|
* Get filetype from magic number (#14)
|
||||||
|
* PDF conversion works again
|
||||||
|
|
||||||
|
#### 2.6
|
||||||
|
* Added --rotate option to rotate landscape images instead of splitting them (#16, #24)
|
||||||
|
* Added --output option to customize EPUB output dir/file (#22)
|
||||||
|
* Add rendition:layout and rendition:orientation EPUB meta tags (supported by new kindlegen 2.8)
|
||||||
|
* Fixed natural sorting for files (#18)
|
||||||
|
|
||||||
|
#### 2.5
|
||||||
|
* Added --black-borders option to set added borders black when page's ratio is not the device's one (#11).
|
||||||
|
* Fixes EPUB containing zipped itself (#10)
|
||||||
|
|
||||||
|
#### 2.4
|
||||||
|
* Use temporary directory as workdir (fixes converting from external volumes and zipfiles renaming)
|
||||||
|
* Fixed "add folders" from GUI.
|
||||||
|
|
||||||
|
#### 2.3
|
||||||
|
* Fixed win32 EPUB generation, folder handling, filenames with spaces and subfolders
|
||||||
|
|
||||||
|
#### 2.2:
|
||||||
|
* Added (valid!) EPUB 2.0 output
|
||||||
|
* Rename .zip files to .cbz to avoid overwriting
|
||||||
|
|
||||||
|
#### 2.1
|
||||||
|
* Added basic error reporting
|
||||||
|
|
||||||
|
#### 2.0
|
||||||
|
* GUI! AppleScript is gone and Tk is used to provide cross-platform GUI support.
|
||||||
|
|
||||||
|
#### 1.5
|
||||||
|
* Added subfolder support for multiple chapters.
|
||||||
|
|
||||||
|
#### 1.4.1
|
||||||
|
* Fixed a serious bug on resizing when img ratio was bigger than device one
|
||||||
|
|
||||||
|
#### 1.4
|
||||||
|
* Added some options for controlling image optimization
|
||||||
|
* Further optimization (ImageOps, page numbering cut, autocontrast)
|
||||||
|
|
||||||
|
#### 1.3
|
||||||
|
* Fixed an issue in OPF generation for device resolution
|
||||||
|
* Reworked options system (call with -h option to get the inline help)
|
||||||
|
|
||||||
|
#### 1.2
|
||||||
|
* Comic optimizations! Split pages not target-oriented (landscape with portrait target or portrait with landscape target), add palette and other image optimizations from Mangle. WARNING: PIL is required for all image mangling!
|
||||||
|
|
||||||
|
#### 1.1.1
|
||||||
|
* Added support for CBZ/CBR files in Kindle Comic Converter
|
||||||
|
|
||||||
|
#### 1.1
|
||||||
|
* Added support for CBZ/CBR files in comic2ebook.py
|
||||||
|
|
||||||
|
#### 1.0
|
||||||
|
* Initial version
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# STAGE 1: BUILDER
|
||||||
|
# Contains all build tools and dev dependencies, will be discarded
|
||||||
|
FROM python:3.13-slim-bullseye AS builder
|
||||||
|
|
||||||
|
# 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" && \
|
||||||
|
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}
|
||||||
|
|
||||||
|
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"]
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
ISC LICENSE
|
ISC LICENSE
|
||||||
|
|
||||||
Copyright (c) 2012 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 (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
|
Permission to use, copy, modify, and/or distribute this software for
|
||||||
any purpose with or without fee is hereby granted, provided that the
|
any purpose with or without fee is hereby granted, provided that the
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
include README.md MANIFEST.in LICENSE.txt
|
|
||||||
@@ -1,187 +1,453 @@
|
|||||||
# KCC
|
<img src="header.jpg" alt="Header Image" width="400">
|
||||||
|
|
||||||
`KCC` (a.k.a. `KindleComicConverter`) is a Python app to convert comic files or folders to ePub or Panel View MOBI.
|
# KCC
|
||||||
It was initally developed for Kindle but since v2.2 it outputs valid ePub 2.0 so _**despite its name, KCC is
|
|
||||||
actually a comic 2 epub converter that every ereader owner can happily use**_.
|
|
||||||
|
|
||||||
It can also optionally optimize images by applying a number of transformations.
|
[](https://github.com/ciromattia/kcc/releases)
|
||||||
|
[](https://github.com/ciromattia/kcc/pkgs/container/kcc)
|
||||||
|
[](https://github.com/ciromattia/kcc/releases)
|
||||||
|
|
||||||
|
|
||||||
|
**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 like CBZ, 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
|
### 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.
|
**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.
|
||||||
Amazon's tool is for comic _publishers_ and involves a lot of manual effort, while **KCC** is for comic _readers_.
|
Amazon's tool is for comic publishers and involves a lot of manual effort, while **KCC** is for comic/manga readers.
|
||||||
If you want to read some comments over *Amazon's kc2* you can take a look at [this](http://www.mobileread.com/forums/showthread.php?t=207461&page=7#96) and [that](http://www.mobileread.com/forums/showthread.php?t=211047) threads on Mobileread.
|
_KC2_ in no way is a replacement for **KCC** so you can be quite confident we are going to carry on developing our little monster ;-)
|
||||||
_kc2_ in no way is a replacement for **KCC** so you can be quite confident we'll going to carry on developing our little monster ;)
|
|
||||||
|
|
||||||
## BINARY RELEASES
|
### Issues / new features / donations
|
||||||
You can find the latest released binary at the following links:
|
If you have general questions about usage, feedback etc. please [post it here](http://www.mobileread.com/forums/showthread.php?t=207461).
|
||||||
- OS X: [https://dl.dropbox.com/u/16806101/KindleComicConverter_osx_2.10.zip](https://dl.dropbox.com/u/16806101/KindleComicConverter_osx_2.10.zip)
|
If you have some **technical** problems using KCC please [file an issue here](https://github.com/ciromattia/kcc/issues/new).
|
||||||
- Win64: [https://dl.dropbox.com/u/16806101/KindleComicConverter_win-amd64_2.10.zip](https://dl.dropbox.com/u/16806101/KindleComicConverter_win-amd64_2.10.zip)
|
If you can fix an open issue, fork & make a pull request.
|
||||||
- Win32: [http://pawelj.vulturis.eu/Shared/KindleComicConverter_win-x86_2.10.zip](http://pawelj.vulturis.eu/Shared/KindleComicConverter_win-x86_2.10.zip)
|
|
||||||
- Linux: Just download sourcecode and launch `python kcc.py` *(Provided you have Python and Pillow installed)*
|
|
||||||
|
|
||||||
## AWKCC .NET GUI
|
If you find **KCC** valuable you can consider donating to the authors:
|
||||||

|
- Ciro Mattia Gonano (founder, active 2012-2014):
|
||||||
|
|
||||||
[All-in-one package for Windows users](http://www.mobileread.com/forums/showpost.php?p=2444957&postcount=3)
|
[](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
|
||||||
|
|
||||||
|
- **https://github.com/ciromattia/kcc/releases**
|
||||||
|
|
||||||
|
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 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 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
|
||||||
|
- 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.
|
||||||
|
Additionally, it will break page numbers.
|
||||||
|
Viewing KCC output in Calibre will also not work properly.
|
||||||
|
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. You can try PDF output.
|
||||||
|
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 or Kindle Scribe 2025.
|
||||||
|
- 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.
|
||||||
|
- Huge margins / slow page turns?
|
||||||
|
- You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB.
|
||||||
|
|
||||||
|
## PREREQUISITES
|
||||||
|
|
||||||
|
You'll need to install various tools to access important but optional features. Close and re-open KCC to get KCC to detect them.
|
||||||
|
|
||||||
|
### KindleGen
|
||||||
|
|
||||||
|
On Windows and macOS, install [Kindle Previewer](https://www.amazon.com/Kindle-Previewer/b?ie=UTF8&node=21381691011) and `kindlegen` will be autodetected from it.
|
||||||
|
|
||||||
|
If you have issues detecting it, get stuck on the MOBI conversion step, or use Linux AppImage or Flatpak, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation#kindlegen
|
||||||
|
|
||||||
|
### 7-Zip
|
||||||
|
|
||||||
|
This is optional but will make conversions much faster.
|
||||||
|
|
||||||
|
This is required for certain files and advanced features.
|
||||||
|
|
||||||
|
KCC will ask you to install if needed.
|
||||||
|
|
||||||
|
Refer to the wiki to install: https://github.com/ciromattia/kcc/wiki/Installation#7-zip
|
||||||
|
|
||||||
## INPUT FORMATS
|
## INPUT FORMATS
|
||||||
`kcc` can understand and convert, at the moment, the following file types:
|
**KCC** can understand and convert, at the moment, the following input types:
|
||||||
- PNG, JPG, GIF, TIFF, BMP
|
- Folders containing: PNG, JPG, GIF or WebP files
|
||||||
- Folders
|
- CBZ, ZIP *(With `7z` executable)*
|
||||||
- CBZ, ZIP
|
- CBR, RAR *(With `7z` executable)*
|
||||||
- CBR, RAR *(With `unrar` executable)*
|
- CB7, 7Z *(With `7z` executable)*
|
||||||
- PDF *(Extracting only contained JPG images)*
|
- PDF *(Only extracting JPG images)*
|
||||||
|
|
||||||
## OPTIONAL REQUIREMENTS
|
|
||||||
- `kindlegen` v2.7+ in a directory reachable by your PATH or in KCC directory *(For .mobi generation)*
|
|
||||||
- [unrar](http://www.rarlab.com/download.htm) *(For CBR support)*
|
|
||||||
|
|
||||||
### For compiling/running from source:
|
|
||||||
- Python 2.7+ (Included in MacOS and Linux, follow the [official documentation](http://www.python.org/getit/windows/) to install on Windows)
|
|
||||||
- [Pillow](http://pypi.python.org/pypi/Pillow/) for comic optimizations like split double pages, resize to optimal resolution, improve contrast and palette, etc.
|
|
||||||
Please refer to official documentation for installing into your system.
|
|
||||||
|
|
||||||
## USAGE
|
## USAGE
|
||||||
|
|
||||||
### GUI
|
Should be pretty self-explanatory. All options have detailed information in tooltips.
|
||||||
|
After completed conversion, you should find ready file alongside the original input file (same directory).
|
||||||
|
|
||||||
Should be pretty self-explanatory, just keep in mind that it's still in development ;)
|
Please check [our wiki](https://github.com/ciromattia/kcc/wiki/) for more details.
|
||||||
While working it seems frozen, I'll try to fix the aesthetics later.
|
|
||||||
Conversion being done, you should find an .epub and a .mobi files alongside the original input file (same directory)
|
|
||||||
|
|
||||||
### Standalone `comic2ebook.py` usage:
|
CLI version of **KCC** is intended for power users. It allows using options that might not be compatible and decrease the quality of output.
|
||||||
|
CLI version has reduced dependencies, on Debian based distributions this commands should install all needed dependencies:
|
||||||
|
```
|
||||||
|
sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugify
|
||||||
|
```
|
||||||
|
|
||||||
|
### Profiles:
|
||||||
|
|
||||||
```
|
```
|
||||||
Usage: comic2ebook.py [options] comic_file|comic_folder
|
'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),
|
||||||
|
'KPW6': ("Kindle Paperwhite 6", (1272, 1696), Palette16, 1.0),
|
||||||
|
'KO': ("Kindle Oasis 2/3", (1264, 1680), Palette16, 1.0),
|
||||||
|
'KCS': ("Kindle Colorsoft", (1272, 1696), 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),
|
||||||
|
```
|
||||||
|
|
||||||
Options:
|
### Standalone `kcc-c2e.py` usage:
|
||||||
--version show program's version number and exit
|
|
||||||
-h, --help show this help message and exit
|
```
|
||||||
-p PROFILE, --profile=PROFILE
|
usage: kcc-c2e [options] [input]
|
||||||
Device profile (Choose one among K1, K2, K3, K4NT, K4T, KDX, KDXG, KHD, KF, KFHD, KFHD8) [Default=KHD]
|
|
||||||
-t TITLE, --title=TITLE
|
MANDATORY:
|
||||||
Comic title [Default=filename]
|
input Full path to comic folder or file(s) to be processed.
|
||||||
-m, --manga-style Manga style (Right-to-left reading and splitting) [Default=False]
|
|
||||||
-c, --cbz-output Outputs a CBZ archive and does not generate EPUB
|
MAIN:
|
||||||
--nopanelviewhq Disable high quality Panel View [Default=False]
|
-p PROFILE, --profile PROFILE
|
||||||
--noprocessing Do not apply image preprocessing (Page splitting and optimizations) [Default=True]
|
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)
|
||||||
--forcepng Create PNG files instead JPEG (For non-Kindle devices) [Default=False]
|
[Default=KV]
|
||||||
--gamma=GAMMA Apply gamma correction to linearize the image [Default=Auto]
|
-m, --manga-style Manga style (right-to-left reading and splitting)
|
||||||
--upscale Resize images smaller than device's resolution [Default=False]
|
-q, --hq Try to increase the quality of magnification
|
||||||
--stretch Stretch images to device's resolution [Default=False]
|
-2, --two-panel Display two not four panels in Panel View mode
|
||||||
--blackborders Use black borders instead of white ones when not stretching and ratio is not like the device's one [Default=False]
|
-w, --webtoon Webtoon processing mode
|
||||||
--rotate Rotate landscape pages instead of splitting them [Default=False]
|
--ts TARGETSIZE, --targetsize TARGETSIZE
|
||||||
--nosplitrotate Disable splitting and rotation [Default=False]
|
the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others]
|
||||||
--nocutpagenumbers Do not try to cut page numbering on images [Default=True]
|
|
||||||
-o OUTPUT, --output=OUTPUT
|
PROCESSING:
|
||||||
Output generated file (EPUB or CBZ) to specified directory or file
|
-n, --noprocessing Do not modify image and ignore any profile or processing option
|
||||||
-v, --verbose Verbose output [Default=False]
|
--pdfextract Use legacy PDF image extraction method from KCC 8 and earlier.
|
||||||
|
--pdfwidth Render vector PDFs based on device width instead of height.
|
||||||
|
-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
|
||||||
|
--nosmartcovercrop Disable attempt to crop main cover from wide image
|
||||||
|
--coverfill Center-crop only the cover to fill target device screen
|
||||||
|
--forcecolor Don't convert images to grayscale
|
||||||
|
--forcepng Create PNG files instead JPEG for black and white images
|
||||||
|
--webp Replace JPG with lossy WEBP and PNG with lossless WEBP
|
||||||
|
--force-png-rgb Force color images to be saved as PNG
|
||||||
|
--pnglegacy Use a more compatible 8 bit PNG instead of 4 bit.
|
||||||
|
--noquantize Don't quantize PNG images to 16 colors
|
||||||
|
--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.
|
||||||
|
--tempdir Create temporary files directory on source file drive.
|
||||||
|
|
||||||
|
OUTPUT SETTINGS:
|
||||||
|
-o OUTPUT, --output OUTPUT
|
||||||
|
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, 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.
|
||||||
|
--rotateright Rotate double page spreads in opposite direction.
|
||||||
|
--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
|
||||||
|
Replace screen width provided by device profile
|
||||||
|
--customheight CUSTOMHEIGHT
|
||||||
|
Replace screen height provided by device profile
|
||||||
|
|
||||||
|
OTHER:
|
||||||
|
-h, --help Show this help message and exit
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standalone `kcc-c2p.py` usage:
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: kcc-c2p [options] [input]
|
||||||
|
|
||||||
|
MANDATORY:
|
||||||
|
input Full path to comic folder(s) to be processed. Separate multiple inputs with spaces.
|
||||||
|
|
||||||
|
MAIN:
|
||||||
|
-y HEIGHT, --height HEIGHT
|
||||||
|
Height of the target device screen
|
||||||
|
-i, --in-place Overwrite source directory
|
||||||
|
-m, --merge Combine every directory into a single image before splitting
|
||||||
|
|
||||||
|
OTHER:
|
||||||
|
-d, --debug Create debug file for every split image
|
||||||
|
-h, --help Show this help message and exit
|
||||||
|
```
|
||||||
|
|
||||||
|
## INSTALL FROM SOURCE
|
||||||
|
|
||||||
|
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 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 `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
|
||||||
|
|
||||||
|
One time setup and running for the first time:
|
||||||
|
```
|
||||||
|
python -m venv venv
|
||||||
|
venv\Scripts\activate.bat
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python kcc.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Every time you close Command Prompt, you will need to re-activate the virtual environment and re-run:
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python kcc.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Every time you close Terminal, you will need to reactivate the virtual environment and re-run:
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
## CREDITS
|
||||||
KCC is made by [Ciro Mattia Gonano](http://github.com/ciromattia) and [Paweł Jastrzębski](http://github.com/AcidWeb)
|
**KCC** is made by
|
||||||
|
|
||||||
This script born as a cross-platform alternative to `KindleComicParser` by **Dc5e** (published in [this mobileread forum thread](http://www.mobileread.com/forums/showthread.php?t=192783))
|
- [Ciro Mattia Gonano](http://github.com/ciromattia)
|
||||||
|
- [Paweł Jastrzębski](http://github.com/AcidWeb)
|
||||||
|
- [Darodi](http://github.com/darodi)
|
||||||
|
- [Alex Xu](http://github.com/axu2)
|
||||||
|
|
||||||
The app relies and includes the following scripts/binaries:
|
This script born as a cross-platform alternative to `KindleComicParser` by **Dc5e** (published [here](http://www.mobileread.com/forums/showthread.php?t=192783)).
|
||||||
|
|
||||||
- `KindleStrip` script © 2010-2012 by **Paul Durrant** and released in public domain
|
The app relies and includes the following scripts:
|
||||||
([mobileread forum thread](http://www.mobileread.com/forums/showthread.php?t=96903))
|
|
||||||
- `rarfile.py` script © 2005-2011 **Marko Kreen** <markokr@gmail.com>, released with ISC License
|
|
||||||
- the icon is by **Nikolay Verin** ([http://ncrow.deviantart.com/](http://ncrow.deviantart.com/)) and released under [CC Attribution-NonCommercial-ShareAlike 3.0 Unported](http://creativecommons.org/licenses/by-nc-sa/3.0/) License
|
|
||||||
- `image.py` class from **Alex Yatskov**'s [Mangle](http://foosoft.net/mangle/) with subsequent [proDOOMman](https://github.com/proDOOMman/Mangle)'s and [Birua](https://github.com/Birua/Mangle)'s patches
|
|
||||||
- `magic.py` from [python-magic](https://github.com/ahupp/python-magic) library
|
|
||||||
|
|
||||||
## CHANGELOG
|
- `DualMetaFix` script by **K. Hendricks**. Released with GPL-3 License.
|
||||||
####1.00
|
- `image.py` class from **Alex Yatskov**'s [Mangle](https://github.com/FooSoft/mangle/) with subsequent [proDOOMman](https://github.com/proDOOMman/Mangle)'s and [Birua](https://github.com/Birua/Mangle)'s patches.
|
||||||
* Initial version
|
- 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.
|
||||||
|
|
||||||
####1.10
|
## SAMPLE FILES CREATED BY KCC
|
||||||
* Added support for CBZ/CBR files in comic2ebook.py
|
|
||||||
|
|
||||||
####1.11
|
https://www.mediafire.com/folder/ixh40veo6hrc5/kcc_samples
|
||||||
* Added support for CBZ/CBR files in KindleComicConverter
|
|
||||||
|
|
||||||
####1.20
|
Older links (dead):
|
||||||
* Comic optimizations! Split pages not target-oriented (landscape with portrait target or portrait with landscape target), add palette and other image optimizations from Mangle. WARNING: PIL is required for all image mangling!
|
|
||||||
|
|
||||||
####1.30
|
|
||||||
* Fixed an issue in OPF generation for device resolution
|
|
||||||
* Reworked options system (call with -h option to get the inline help)
|
|
||||||
|
|
||||||
####1.40
|
* [Kindle Oasis 2 / 3](http://kcc.iosphe.re/Samples/Ubunchu!-KO.mobi)
|
||||||
* Added some options for controlling image optimization
|
* [Kindle Paperwhite 3 / 4 / Voyage / Oasis](http://kcc.iosphe.re/Samples/Ubunchu!-KV.mobi)
|
||||||
* Further optimization (ImageOps, page numbering cut, autocontrast)
|
* [Kindle Paperwhite 1 / 2](http://kcc.iosphe.re/Samples/Ubunchu!-KPW.mobi)
|
||||||
|
* [Kindle](http://kcc.iosphe.re/Samples/Ubunchu!-K578.mobi)
|
||||||
|
* [Kobo Aura](http://kcc.iosphe.re/Samples/Ubunchu-KoA.kepub.epub)
|
||||||
|
* [Kobo Aura HD](http://kcc.iosphe.re/Samples/Ubunchu-KoAHD.kepub.epub)
|
||||||
|
* [Kobo Aura H2O](http://kcc.iosphe.re/Samples/Ubunchu-KoAH2O.kepub.epub)
|
||||||
|
* [Kobo Aura ONE](http://kcc.iosphe.re/Samples/Ubunchu-KoAO.kepub.epub)
|
||||||
|
* [Kobo Forma](http://kcc.iosphe.re/Samples/Ubunchu-KoF.kepub.epub)
|
||||||
|
|
||||||
####1.41
|
## PRIVACY
|
||||||
* Fixed a serious bug on resizing when img ratio was bigger than device one
|
**KCC** is initiating internet connections in two cases:
|
||||||
|
* During startup - Version check and announcement check.
|
||||||
|
* When error occurs - Automatic reporting on Windows and macOS.
|
||||||
|
|
||||||
####1.50
|
## KNOWN ISSUES
|
||||||
* Added subfolder support for multiple chapters.
|
Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).
|
||||||
|
|
||||||
####2.0
|
|
||||||
* GUI! AppleScript is gone and Tk is used to provide cross-platform GUI support.
|
|
||||||
|
|
||||||
####2.1
|
|
||||||
* Added basic error reporting
|
|
||||||
|
|
||||||
#### 2.2:
|
|
||||||
* Added (valid!) ePub 2.0 output
|
|
||||||
* Rename .zip files to .cbz to avoid overwriting
|
|
||||||
|
|
||||||
####2.3
|
|
||||||
* Fixed win32 ePub generation, folder handling, filenames with spaces and subfolders
|
|
||||||
|
|
||||||
####2.4
|
|
||||||
* Use temporary directory as workdir (fixes converting from external volumes and zipfiles renaming)
|
|
||||||
* Fixed "add folders" from GUI.
|
|
||||||
|
|
||||||
####2.5
|
|
||||||
* Added --black-borders option to set added borders black when page's ratio is not the device's one (#11).
|
|
||||||
* Fixes epub containing zipped itself (#10)
|
|
||||||
|
|
||||||
####2.6
|
|
||||||
* Added --rotate option to rotate landscape images instead of splitting them (#16, #24)
|
|
||||||
* Added --output option to customize ePub output dir/file (#22)
|
|
||||||
* Add rendition:layout and rendition:orientation ePub meta tags (supported by new kindlegen 2.8)
|
|
||||||
* Fixed natural sorting for files (#18)
|
|
||||||
|
|
||||||
####2.7
|
|
||||||
* Lots of GUI improvements (#27, #13)
|
|
||||||
* Added gamma support within --gamma option (defaults to profile-specified gamma) (#26, #27)
|
|
||||||
* Added --nodithering option to prevent dithering optimizations (#27)
|
|
||||||
* Epub margins support (#30)
|
|
||||||
* Fixed no file added if file has no spaces on Windows (#25)
|
|
||||||
* Gracefully exit if unrar missing (#15)
|
|
||||||
* Do not call kindlegen if source epub is bigger than 320MB (#17)
|
|
||||||
* Get filetype from magic number (#14)
|
|
||||||
* PDF conversion works again
|
|
||||||
|
|
||||||
####2.8
|
|
||||||
* Updated rarfile library
|
|
||||||
* Panel View support + HQ support (#36) - new option: --nopanelviewhq
|
|
||||||
* Split profiles for K4NT and K4T
|
|
||||||
* Rewrite of Landscape Mode support (huge readability improvement for KPW)
|
|
||||||
* Upscale use now BILINEAR method
|
|
||||||
* Added generic CSS file
|
|
||||||
* Optimized archive extraction for zip/rar files (#40)
|
|
||||||
|
|
||||||
####2.9
|
|
||||||
* Added support for generating a plain CBZ (skipping all the EPUB/Mobi generation) (#45)
|
|
||||||
* Prevent output file overwriting the source one: if a duplicate name is detected, append _kcc to the name
|
|
||||||
* Rarfile library updated to 2.6
|
|
||||||
* Added GIF, TIFF and BMP to supported formats (#42)
|
|
||||||
* Filenames slugifications (#28, #31, #9, #8)
|
|
||||||
|
|
||||||
####2.10:
|
|
||||||
* Multiprocessing support
|
|
||||||
* Kindle Fire support (color ePub/Mobi)
|
|
||||||
* Panel View support for horizontal content
|
|
||||||
* Fixed panel order for horizontal pages when --rotate is enabled
|
|
||||||
* Disabled cropping and page number cutting for blank pages
|
|
||||||
* Fixed some slugify issues with specific file naming conventions (#50, #51)
|
|
||||||
|
|
||||||
## COPYRIGHT
|
## COPYRIGHT
|
||||||
|
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.
|
||||||
|
|
||||||
Copyright (c) 2012-2013 Ciro Mattia Gonano and Paweł Jastrzębski.
|
## Verification
|
||||||
KCC is released under ISC LICENSE; see LICENSE.txt for further details.
|
Impact-Site-Verification: ffe48fc7-4f0c-40fd-bd2e-59f4d7205180
|
||||||
|
|||||||
@@ -0,0 +1,320 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="48px"
|
||||||
|
height="48px"
|
||||||
|
id="svg3832"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="0.47 r22583"
|
||||||
|
sodipodi:docname="appimage-assistant_alt3.svg">
|
||||||
|
<defs
|
||||||
|
id="defs3834">
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3308-4-6-931-761-0"
|
||||||
|
id="linearGradient2975"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="24.3125"
|
||||||
|
y1="22.96875"
|
||||||
|
x2="24.3125"
|
||||||
|
y2="41.03125" />
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3308-4-6-931-761-0">
|
||||||
|
<stop
|
||||||
|
id="stop2919-2"
|
||||||
|
style="stop-color:#ffffff;stop-opacity:1"
|
||||||
|
offset="0" />
|
||||||
|
<stop
|
||||||
|
id="stop2921-76"
|
||||||
|
style="stop-color:#ffffff;stop-opacity:0"
|
||||||
|
offset="1" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient4222"
|
||||||
|
id="linearGradient2979"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0,0.3704967,-0.3617496,0,33.508315,6.1670925)"
|
||||||
|
x1="7.6485429"
|
||||||
|
y1="26.437023"
|
||||||
|
x2="41.861729"
|
||||||
|
y2="26.437023" />
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient4222">
|
||||||
|
<stop
|
||||||
|
id="stop4224"
|
||||||
|
style="stop-color:#ffffff;stop-opacity:1"
|
||||||
|
offset="0" />
|
||||||
|
<stop
|
||||||
|
id="stop4226"
|
||||||
|
style="stop-color:#ffffff;stop-opacity:0"
|
||||||
|
offset="1" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3308-4-6-931-761"
|
||||||
|
id="linearGradient2982"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(0,0.9999987)"
|
||||||
|
x1="23.99999"
|
||||||
|
y1="4.999989"
|
||||||
|
x2="23.99999"
|
||||||
|
y2="43" />
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3308-4-6-931-761">
|
||||||
|
<stop
|
||||||
|
id="stop2919"
|
||||||
|
style="stop-color:#ffffff;stop-opacity:1"
|
||||||
|
offset="0" />
|
||||||
|
<stop
|
||||||
|
id="stop2921"
|
||||||
|
style="stop-color:#ffffff;stop-opacity:0"
|
||||||
|
offset="1" />
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3575"
|
||||||
|
id="radialGradient2985"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0,1.0262008,-1.6561124,9.4072203e-4,-56.097482,-45.332325)"
|
||||||
|
cx="48.42384"
|
||||||
|
cy="-48.027504"
|
||||||
|
fx="48.42384"
|
||||||
|
fy="-48.027504"
|
||||||
|
r="38.212933" />
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3575">
|
||||||
|
<stop
|
||||||
|
id="stop3577"
|
||||||
|
style="stop-color:#fafafa;stop-opacity:1"
|
||||||
|
offset="0" />
|
||||||
|
<stop
|
||||||
|
id="stop3579"
|
||||||
|
style="stop-color:#e6e6e6;stop-opacity:1"
|
||||||
|
offset="1" />
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3993"
|
||||||
|
id="radialGradient2990"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0,2.0478765,-2.7410544,-8.6412258e-8,47.161382,-8.837436)"
|
||||||
|
cx="9.3330879"
|
||||||
|
cy="8.4497671"
|
||||||
|
fx="9.3330879"
|
||||||
|
fy="8.4497671"
|
||||||
|
r="19.99999" />
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3993">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#a3c0d0;stop-opacity:1"
|
||||||
|
id="stop3995" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#427da1;stop-opacity:1"
|
||||||
|
id="stop4001" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient2508"
|
||||||
|
id="linearGradient2992"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(0,0.9674382)"
|
||||||
|
x1="14.048676"
|
||||||
|
y1="44.137306"
|
||||||
|
x2="14.048676"
|
||||||
|
y2="4.0000005" />
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient2508">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#2e4a5a;stop-opacity:1"
|
||||||
|
id="stop2510" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#6e8796;stop-opacity:1"
|
||||||
|
id="stop2512" />
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient
|
||||||
|
cx="4.9929786"
|
||||||
|
cy="43.5"
|
||||||
|
r="2.5"
|
||||||
|
fx="4.9929786"
|
||||||
|
fy="43.5"
|
||||||
|
id="radialGradient2873-966-168"
|
||||||
|
xlink:href="#linearGradient3688-166-749"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(2.003784,0,0,1.4,27.98813,-17.4)" />
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3688-166-749">
|
||||||
|
<stop
|
||||||
|
id="stop2883"
|
||||||
|
style="stop-color:#181818;stop-opacity:1"
|
||||||
|
offset="0" />
|
||||||
|
<stop
|
||||||
|
id="stop2885"
|
||||||
|
style="stop-color:#181818;stop-opacity:0"
|
||||||
|
offset="1" />
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient
|
||||||
|
cx="4.9929786"
|
||||||
|
cy="43.5"
|
||||||
|
r="2.5"
|
||||||
|
fx="4.9929786"
|
||||||
|
fy="43.5"
|
||||||
|
id="radialGradient2875-742-326"
|
||||||
|
xlink:href="#linearGradient3688-464-309"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(2.003784,0,0,1.4,-20.01187,-104.4)" />
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3688-464-309">
|
||||||
|
<stop
|
||||||
|
id="stop2889"
|
||||||
|
style="stop-color:#181818;stop-opacity:1"
|
||||||
|
offset="0" />
|
||||||
|
<stop
|
||||||
|
id="stop2891"
|
||||||
|
style="stop-color:#181818;stop-opacity:0"
|
||||||
|
offset="1" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
x1="25.058096"
|
||||||
|
y1="47.027729"
|
||||||
|
x2="25.058096"
|
||||||
|
y2="39.999443"
|
||||||
|
id="linearGradient2877-634-617"
|
||||||
|
xlink:href="#linearGradient3702-501-757"
|
||||||
|
gradientUnits="userSpaceOnUse" />
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3702-501-757">
|
||||||
|
<stop
|
||||||
|
id="stop2895"
|
||||||
|
style="stop-color:#181818;stop-opacity:0"
|
||||||
|
offset="0" />
|
||||||
|
<stop
|
||||||
|
id="stop2897"
|
||||||
|
style="stop-color:#181818;stop-opacity:1"
|
||||||
|
offset="0.5" />
|
||||||
|
<stop
|
||||||
|
id="stop2899"
|
||||||
|
style="stop-color:#181818;stop-opacity:0"
|
||||||
|
offset="1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="7"
|
||||||
|
inkscape:cx="24"
|
||||||
|
inkscape:cy="24"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:grid-bbox="true"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:window-width="603"
|
||||||
|
inkscape:window-height="484"
|
||||||
|
inkscape:window-x="417"
|
||||||
|
inkscape:window-y="162"
|
||||||
|
inkscape:window-maximized="0" />
|
||||||
|
<metadata
|
||||||
|
id="metadata3837">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
id="layer1"
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer">
|
||||||
|
<g
|
||||||
|
style="display:inline"
|
||||||
|
id="g2036"
|
||||||
|
transform="matrix(1.1,0,0,0.4444449,-2.4000022,25.11107)">
|
||||||
|
<g
|
||||||
|
style="opacity:0.4"
|
||||||
|
id="g3712"
|
||||||
|
transform="matrix(1.052632,0,0,1.285713,-1.263158,-13.42854)">
|
||||||
|
<rect
|
||||||
|
style="fill:url(#radialGradient2873-966-168);fill-opacity:1;stroke:none"
|
||||||
|
id="rect2801"
|
||||||
|
y="40"
|
||||||
|
x="38"
|
||||||
|
height="7"
|
||||||
|
width="5" />
|
||||||
|
<rect
|
||||||
|
style="fill:url(#radialGradient2875-742-326);fill-opacity:1;stroke:none"
|
||||||
|
id="rect3696"
|
||||||
|
transform="scale(-1,-1)"
|
||||||
|
y="-47"
|
||||||
|
x="-10"
|
||||||
|
height="7"
|
||||||
|
width="5" />
|
||||||
|
<rect
|
||||||
|
style="fill:url(#linearGradient2877-634-617);fill-opacity:1;stroke:none"
|
||||||
|
id="rect3700"
|
||||||
|
y="40"
|
||||||
|
x="10"
|
||||||
|
height="7.0000005"
|
||||||
|
width="28" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<rect
|
||||||
|
style="fill:url(#radialGradient2990);fill-opacity:1;stroke:url(#linearGradient2992);stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
|
||||||
|
id="rect5505"
|
||||||
|
y="5.4674392"
|
||||||
|
x="4.5"
|
||||||
|
ry="2.2322156"
|
||||||
|
rx="2.2322156"
|
||||||
|
height="39"
|
||||||
|
width="39" />
|
||||||
|
<path
|
||||||
|
style="opacity:0.05;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.00178742;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
id="path4294-1"
|
||||||
|
d="m 21,6.9687498 a 2.0165107,2.0165107 0 0 0 -2.03125,2.03125 l 0,3.9687502 -1.15625,0 a 2.0165107,2.0165107 0 0 0 -1.5,3.375 l 5.0625,5.75 c -0.06312,0.110777 -0.178724,0.246032 -0.21875,0.34375 -0.195898,0.478256 -0.25,0.83653 -0.25,1.21875 l 0,0.125 L 20.8125,23.6875 C 20.534322,23.409323 20.213169,23.162739 19.71875,22.96875 19.47154,22.87176 19.185456,22.791748 18.75,22.8125 c -0.435456,0.02075 -1.054055,0.210302 -1.46875,0.625 L 15.75,24.96875 c -0.414689,0.414689 -0.604245,1.033294 -0.625,1.46875 -0.02075,0.435456 0.05925,0.721537 0.15625,0.96875 C 15.475241,27.900677 15.721817,28.221821 16,28.5 l 0.09375,0.09375 -0.125,0 c -0.382218,0 -0.740493,0.0541 -1.21875,0.25 -0.239128,0.09795 -0.538285,0.214988 -0.84375,0.53125 -0.305465,0.316262 -0.625,0.914788 -0.625,1.53125 l 0,2.1875 c 0,0.616465 0.319536,1.214989 0.625,1.53125 0.305464,0.316261 0.604622,0.433301 0.84375,0.53125 0.478256,0.195898 0.83653,0.25 1.21875,0.25 l 0.125,0 L 16,35.5 c -0.278175,0.278176 -0.52476,0.599329 -0.71875,1.09375 -0.09699,0.24721 -0.177003,0.533292 -0.15625,0.96875 0.02075,0.435458 0.210304,1.054058 0.625,1.46875 l 1.53125,1.53125 c 0.414691,0.414697 1.033292,0.604245 1.46875,0.625 0.435458,0.02076 0.721537,-0.05926 0.96875,-0.15625 0.494425,-0.19399 0.81557,-0.440568 1.09375,-0.71875 l 0.09375,-0.09375 0,0.125 c 0,0.38222 0.0541,0.740495 0.25,1.21875 0.09795,0.239127 0.214989,0.538285 0.53125,0.84375 0.316261,0.305465 0.914783,0.625 1.53125,0.625 l 2.1875,0 c 0.616466,0 1.214989,-0.319534 1.53125,-0.625 0.316261,-0.305466 0.433302,-0.604622 0.53125,-0.84375 0.195896,-0.478255 0.25,-0.836532 0.25,-1.21875 l 0,-0.125 0.09375,0.09375 c 0.278176,0.278175 0.599329,0.52476 1.09375,0.71875 0.24721,0.09699 0.533292,0.177003 0.96875,0.15625 0.435458,-0.02075 1.054058,-0.210304 1.46875,-0.625 L 32.875,39.03125 C 33.289697,38.616559 33.479245,37.997958 33.5,37.5625 33.52076,37.127042 33.44074,36.840963 33.34375,36.59375 33.14976,36.099325 32.903182,35.77818 32.625,35.5 l -0.09375,-0.09375 0.125,0 c 0.38222,0 0.740494,-0.0541 1.21875,-0.25 0.239128,-0.09795 0.538286,-0.214988 0.84375,-0.53125 0.305464,-0.316262 0.625,-0.914787 0.625,-1.53125 l 0,-2.1875 c 0,-0.61646 -0.319535,-1.214987 -0.625,-1.53125 -0.305465,-0.316263 -0.604621,-0.433301 -0.84375,-0.53125 -0.478257,-0.195898 -0.836532,-0.25 -1.21875,-0.25 l -0.125,0 L 32.625,28.5 c 0.278177,-0.278177 0.52476,-0.599329 0.71875,-1.09375 C 33.44074,27.15904 33.520753,26.872957 33.5,26.4375 33.47925,26.002043 33.289697,25.383443 32.875,24.96875 L 31.34375,23.4375 c -0.414688,-0.414694 -1.03329,-0.604245 -1.46875,-0.625 -0.43546,-0.02076 -0.721537,0.05925 -0.96875,0.15625 -0.494426,0.193991 -0.815572,0.44057 -1.09375,0.71875 l -0.09375,0.09375 0,-0.125 c 0,-0.382218 -0.0541,-0.740493 -0.25,-1.21875 -0.09112,-0.22245 -0.228127,-0.500183 -0.5,-0.78125 l 4.71875,-5.3125 a 2.0165107,2.0165107 0 0 0 -1.5,-3.375 l -1.15625,0 0,-3.9687502 A 2.0165107,2.0165107 0 0 0 27,6.9687498 l -6,0 z M 24.3125,31.25 c 0.427097,0 0.75,0.322904 0.75,0.75 0,0.427096 -0.322903,0.75 -0.75,0.75 -0.427094,0 -0.75,-0.322906 -0.75,-0.75 0,-0.427094 0.322906,-0.75 0.75,-0.75 z" />
|
||||||
|
<path
|
||||||
|
style="opacity:0.05;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.00178742;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
id="path4294"
|
||||||
|
d="m 20.90625,8.0312498 a 0.96385067,0.96385067 0 0 0 -0.875,0.96875 l 0,5.0312502 -2.21875,0 A 0.96385067,0.96385067 0 0 0 17.09375,15.625 l 5.78125,6.53125 c -0.158814,0.0616 -0.341836,0.0951 -0.4375,0.1875 -0.169161,0.163386 -0.252971,0.323419 -0.3125,0.46875 -0.119058,0.290663 -0.15625,0.566746 -0.15625,0.84375 l 0,1.65625 C 21.718163,25.40233 21.485871,25.509772 21.25,25.625 l -1.1875,-1.1875 c -0.199651,-0.19965 -0.421433,-0.352095 -0.71875,-0.46875 -0.148659,-0.05833 -0.329673,-0.104846 -0.5625,-0.09375 -0.232827,0.0111 -0.53583,0.09833 -0.75,0.3125 L 16.5,25.71875 c -0.214168,0.214168 -0.301403,0.517173 -0.3125,0.75 -0.0111,0.232827 0.03542,0.41384 0.09375,0.5625 0.116655,0.297321 0.269096,0.519099 0.46875,0.71875 l 1.1875,1.1875 c -0.115228,0.235871 -0.222668,0.468163 -0.3125,0.71875 l -1.65625,0 c -0.277003,0 -0.553087,0.03719 -0.84375,0.15625 -0.145332,0.05953 -0.305363,0.143338 -0.46875,0.3125 -0.163387,0.169162 -0.3125,0.46403 -0.3125,0.78125 l 0,2.1875 c 0,0.317221 0.149114,0.612089 0.3125,0.78125 0.163386,0.169161 0.323419,0.252971 0.46875,0.3125 0.290663,0.119058 0.566746,0.15625 0.84375,0.15625 l 1.65625,0 c 0.08983,0.250587 0.197272,0.482879 0.3125,0.71875 L 16.75,36.25 c -0.199649,0.19965 -0.352095,0.421432 -0.46875,0.71875 -0.05833,0.148659 -0.104846,0.329672 -0.09375,0.5625 0.0111,0.232828 0.09833,0.535831 0.3125,0.75 l 1.53125,1.53125 c 0.214168,0.214172 0.517172,0.301403 0.75,0.3125 0.232828,0.0111 0.41384,-0.03542 0.5625,-0.09375 0.29732,-0.116655 0.519098,-0.269096 0.71875,-0.46875 L 21.25,38.375 c 0.235871,0.115228 0.468164,0.222668 0.71875,0.3125 l 0,1.65625 c 0,0.277003 0.03719,0.553087 0.15625,0.84375 0.05953,0.145331 0.143339,0.305364 0.3125,0.46875 0.169161,0.163386 0.464028,0.3125 0.78125,0.3125 l 2.1875,0 c 0.317221,0 0.612089,-0.149113 0.78125,-0.3125 0.169161,-0.163387 0.252971,-0.323419 0.3125,-0.46875 0.119057,-0.290663 0.15625,-0.566748 0.15625,-0.84375 l 0,-1.65625 c 0.250586,-0.08983 0.482879,-0.197272 0.71875,-0.3125 l 1.1875,1.1875 c 0.19965,0.199649 0.421432,0.352095 0.71875,0.46875 0.148659,0.05833 0.329672,0.104846 0.5625,0.09375 0.232828,-0.0111 0.535831,-0.09833 0.75,-0.3125 L 32.125,38.28125 c 0.214172,-0.214168 0.301403,-0.517172 0.3125,-0.75 0.0111,-0.232828 -0.03542,-0.41384 -0.09375,-0.5625 C 32.227095,36.67143 32.074654,36.449652 31.875,36.25 L 30.6875,35.0625 C 30.802728,34.82663 30.910168,34.594337 31,34.34375 l 1.65625,0 c 0.277004,0 0.553087,-0.03719 0.84375,-0.15625 0.145332,-0.05953 0.305364,-0.143339 0.46875,-0.3125 0.163386,-0.169161 0.3125,-0.46403 0.3125,-0.78125 l 0,-2.1875 c 0,-0.317219 -0.149114,-0.612088 -0.3125,-0.78125 C 33.805364,29.955838 33.645332,29.872029 33.5,29.8125 33.209336,29.693442 32.933253,29.65625 32.65625,29.65625 l -1.65625,0 C 30.91017,29.405663 30.802728,29.17337 30.6875,28.9375 L 31.875,27.75 c 0.19965,-0.19965 0.352095,-0.421432 0.46875,-0.71875 0.05833,-0.148659 0.104846,-0.329672 0.09375,-0.5625 -0.0111,-0.232828 -0.09833,-0.535831 -0.3125,-0.75 L 30.59375,24.1875 c -0.214167,-0.21417 -0.517171,-0.301403 -0.75,-0.3125 -0.232829,-0.0111 -0.41384,0.03542 -0.5625,0.09375 -0.29732,0.116656 -0.519099,0.269097 -0.71875,0.46875 L 27.375,25.625 c -0.235871,-0.115228 -0.468163,-0.222668 -0.71875,-0.3125 l 0,-1.65625 c 0,-0.277003 -0.03719,-0.553087 -0.15625,-0.84375 -0.05953,-0.145332 -0.143338,-0.305363 -0.3125,-0.46875 -0.169162,-0.163387 -0.46403,-0.3125 -0.78125,-0.3125 l -0.15625,0 5.65625,-6.40625 A 0.96385067,0.96385067 0 0 0 30.1875,14.03125 l -2.21875,0 0,-5.0312502 A 0.96385067,0.96385067 0 0 0 27,8.0312498 l -6,0 a 0.96385067,0.96385067 0 0 0 -0.09375,0 z M 24.3125,30.1875 c 1.002113,0 1.8125,0.810388 1.8125,1.8125 0,1.002112 -0.810387,1.8125 -1.8125,1.8125 C 23.31039,33.8125 22.5,33.002111 22.5,32 c 0,-1.002111 0.81039,-1.8125 1.8125,-1.8125 z" />
|
||||||
|
<path
|
||||||
|
style="fill:url(#radialGradient2985);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.00178742;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
id="path2317"
|
||||||
|
d="M 21,8.9999996 21,15 17.8125,15 24,22 30.1875,15 27,15 l 0,-6.0000004 -6,0 z M 23.21875,23 c -0.172892,0 -0.28125,0.294922 -0.28125,0.65625 l 0,2.28125 C 22.24145,26.095996 21.585954,26.379869 21,26.75 l -1.625,-1.625 c -0.255498,-0.255497 -0.533998,-0.372253 -0.65625,-0.25 l -1.53125,1.53125 c -0.122254,0.122254 -0.0055,0.400753 0.25,0.65625 l 1.625,1.625 c -0.37013,0.585953 -0.654003,1.24145 -0.8125,1.9375 l -2.28125,0 c -0.361328,0 -0.65625,0.108357 -0.65625,0.28125 l 0,2.1875 c 0,0.172892 0.294922,0.28125 0.65625,0.28125 l 2.28125,0 c 0.158497,0.69605 0.44237,1.351546 0.8125,1.9375 l -1.625,1.625 c -0.255497,0.255498 -0.372254,0.533997 -0.25,0.65625 l 1.53125,1.53125 c 0.122252,0.122254 0.400752,0.0055 0.65625,-0.25 L 21,37.25 c 0.585954,0.37013 1.24145,0.654002 1.9375,0.8125 l 0,2.28125 C 22.9375,40.705077 23.045858,41 23.21875,41 l 2.1875,0 c 0.172893,0 0.28125,-0.294924 0.28125,-0.65625 l 0,-2.28125 c 0.69605,-0.158498 1.351546,-0.44237 1.9375,-0.8125 l 1.625,1.625 c 0.255498,0.255497 0.533997,0.372254 0.65625,0.25 l 1.53125,-1.53125 c 0.122254,-0.122252 0.0055,-0.400752 -0.25,-0.65625 l -1.625,-1.625 c 0.370129,-0.585954 0.654003,-1.24145 0.8125,-1.9375 l 2.28125,0 c 0.361329,0 0.65625,-0.108358 0.65625,-0.28125 l 0,-2.1875 c 0,-0.172893 -0.294921,-0.28125 -0.65625,-0.28125 l -2.28125,0 c -0.158497,-0.69605 -0.442371,-1.351547 -0.8125,-1.9375 l 1.625,-1.625 c 0.255497,-0.255497 0.372254,-0.533997 0.25,-0.65625 L 29.90625,24.875 C 29.783997,24.752745 29.505498,24.8695 29.25,25.125 l -1.625,1.625 c -0.585954,-0.370131 -1.24145,-0.654004 -1.9375,-0.8125 l 0,-2.28125 C 25.6875,23.294922 25.579143,23 25.40625,23 l -2.1875,0 z m 1.09375,6.21875 c 1.528616,0 2.78125,1.252635 2.78125,2.78125 0,1.528615 -1.252634,2.78125 -2.78125,2.78125 -1.528614,0 -2.78125,-1.252635 -2.78125,-2.78125 0,-1.528615 1.252636,-2.78125 2.78125,-2.78125 z" />
|
||||||
|
<rect
|
||||||
|
style="opacity:0.4;fill:none;stroke:url(#linearGradient2982);stroke-width:0.99999976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
|
||||||
|
id="rect6741"
|
||||||
|
y="6.4999886"
|
||||||
|
x="5.4999981"
|
||||||
|
ry="1.365193"
|
||||||
|
rx="1.365193"
|
||||||
|
height="37.000011"
|
||||||
|
width="36.999985" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:url(#linearGradient2979);stroke-width:0.99829447;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible"
|
||||||
|
id="path2777"
|
||||||
|
d="M 28.926376,15.466668 24,21.177578 18.963089,15.5 21.5,15.5 l 0,-6.0000004 5,0 0,6.0000004 2.426376,-0.03333 z" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:url(#linearGradient2975);stroke-width:1;stroke-opacity:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
|
||||||
|
id="path4243"
|
||||||
|
d="m 23.4375,23.46875 c -0.01166,0.05381 -0.03125,0.100205 -0.03125,0.1875 l 0,2.28125 a 0.48185467,0.48185467 0 0 1 -0.375,0.46875 c -0.638467,0.145384 -1.238423,0.407111 -1.78125,0.75 a 0.48185467,0.48185467 0 0 1 -0.59375,-0.0625 l -1.625,-1.625 C 18.9779,25.4154 18.9477,25.40242 18.90625,25.375 l -1.21875,1.21875 c 0.02742,0.04145 0.0404,0.07165 0.09375,0.125 l 1.625,1.625 a 0.48185467,0.48185467 0 0 1 0.0625,0.59375 c -0.342888,0.542826 -0.604615,1.142782 -0.75,1.78125 a 0.48185467,0.48185467 0 0 1 -0.46875,0.375 l -2.28125,0 c -0.08729,0 -0.133695,0.01959 -0.1875,0.03125 l 0,1.75 c 0.05381,0.01166 0.100205,0.03125 0.1875,0.03125 l 2.28125,0 a 0.48185467,0.48185467 0 0 1 0.46875,0.375 c 0.145385,0.638468 0.407112,1.238423 0.75,1.78125 a 0.48185467,0.48185467 0 0 1 -0.0625,0.59375 l -1.625,1.625 c -0.05335,0.05335 -0.06633,0.08355 -0.09375,0.125 l 1.21875,1.21875 c 0.04145,-0.02742 0.07165,-0.0404 0.125,-0.09375 l 1.625,-1.625 A 0.48185467,0.48185467 0 0 1 21.25,36.84375 c 0.542827,0.342888 1.142781,0.604614 1.78125,0.75 a 0.48185467,0.48185467 0 0 1 0.375,0.46875 l 0,2.28125 c 0,0.08729 0.01959,0.133695 0.03125,0.1875 l 1.75,0 c 0.01166,-0.0538 0.03125,-0.100206 0.03125,-0.1875 l 0,-2.28125 a 0.48185467,0.48185467 0 0 1 0.375,-0.46875 c 0.638469,-0.145386 1.238423,-0.407112 1.78125,-0.75 a 0.48185467,0.48185467 0 0 1 0.59375,0.0625 l 1.625,1.625 c 0.05335,0.05335 0.08355,0.06633 0.125,0.09375 l 1.21875,-1.21875 c -0.02742,-0.04145 -0.0404,-0.07165 -0.09375,-0.125 l -1.625,-1.625 a 0.48185467,0.48185467 0 0 1 -0.0625,-0.59375 c 0.342888,-0.542828 0.604615,-1.142783 0.75,-1.78125 a 0.48185467,0.48185467 0 0 1 0.46875,-0.375 l 2.28125,0 c 0.08729,0 0.133695,-0.01959 0.1875,-0.03125 l 0,-1.75 c -0.0538,-0.01166 -0.100204,-0.03125 -0.1875,-0.03125 l -2.28125,0 a 0.48185467,0.48185467 0 0 1 -0.46875,-0.375 c -0.145385,-0.638467 -0.407113,-1.238424 -0.75,-1.78125 a 0.48185467,0.48185467 0 0 1 0.0625,-0.59375 l 1.625,-1.625 c 0.05335,-0.05335 0.06633,-0.08355 0.09375,-0.125 L 29.71875,25.375 c -0.04145,0.02742 -0.07165,0.0404 -0.125,0.09375 l -1.625,1.625 a 0.48185467,0.48185467 0 0 1 -0.59375,0.0625 c -0.542827,-0.342889 -1.142783,-0.604616 -1.78125,-0.75 a 0.48185467,0.48185467 0 0 1 -0.375,-0.46875 l 0,-2.28125 c 0,-0.0873 -0.01959,-0.133695 -0.03125,-0.1875 l -1.75,0 z m 0.875,5.28125 c 1.791829,0 3.25,1.458172 3.25,3.25 0,1.791828 -1.458171,3.25 -3.25,3.25 -1.791827,0 -3.25,-1.458172 -3.25,-3.25 0,-1.791828 1.458173,-3.25 3.25,-3.25 z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 21 KiB |
@@ -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,284 +0,0 @@
|
|||||||
#!python
|
|
||||||
"""Bootstrap setuptools installation
|
|
||||||
|
|
||||||
If you want to use setuptools in your package's setup.py, just include this
|
|
||||||
file in the same directory with it, and add this to the top of your setup.py::
|
|
||||||
|
|
||||||
from ez_setup import use_setuptools
|
|
||||||
use_setuptools()
|
|
||||||
|
|
||||||
If you want to require a specific version of setuptools, set a download
|
|
||||||
mirror, or use an alternate download directory, you can do so by supplying
|
|
||||||
the appropriate options to ``use_setuptools()``.
|
|
||||||
|
|
||||||
This file can also be run as a script to install or upgrade setuptools.
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
DEFAULT_VERSION = "0.6c11"
|
|
||||||
DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
|
|
||||||
|
|
||||||
md5_data = {
|
|
||||||
'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
|
|
||||||
'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
|
|
||||||
'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
|
|
||||||
'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
|
|
||||||
'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
|
|
||||||
'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
|
|
||||||
'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
|
|
||||||
'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
|
|
||||||
'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
|
|
||||||
'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
|
|
||||||
'setuptools-0.6c10-py2.3.egg': 'ce1e2ab5d3a0256456d9fc13800a7090',
|
|
||||||
'setuptools-0.6c10-py2.4.egg': '57d6d9d6e9b80772c59a53a8433a5dd4',
|
|
||||||
'setuptools-0.6c10-py2.5.egg': 'de46ac8b1c97c895572e5e8596aeb8c7',
|
|
||||||
'setuptools-0.6c10-py2.6.egg': '58ea40aef06da02ce641495523a0b7f5',
|
|
||||||
'setuptools-0.6c11-py2.3.egg': '2baeac6e13d414a9d28e7ba5b5a596de',
|
|
||||||
'setuptools-0.6c11-py2.4.egg': 'bd639f9b0eac4c42497034dec2ec0c2b',
|
|
||||||
'setuptools-0.6c11-py2.5.egg': '64c94f3bf7a72a13ec83e0b24f2749b2',
|
|
||||||
'setuptools-0.6c11-py2.6.egg': 'bfa92100bd772d5a213eedd356d64086',
|
|
||||||
'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
|
|
||||||
'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
|
|
||||||
'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
|
|
||||||
'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
|
|
||||||
'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
|
|
||||||
'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
|
|
||||||
'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
|
|
||||||
'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
|
|
||||||
'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
|
|
||||||
'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
|
|
||||||
'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
|
|
||||||
'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
|
|
||||||
'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
|
|
||||||
'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
|
|
||||||
'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
|
|
||||||
'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
|
|
||||||
'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
|
|
||||||
'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
|
|
||||||
'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
|
|
||||||
'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
|
|
||||||
'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
|
|
||||||
'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
|
|
||||||
'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
|
|
||||||
'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a',
|
|
||||||
}
|
|
||||||
|
|
||||||
import sys, os
|
|
||||||
try: from hashlib import md5
|
|
||||||
except ImportError: from md5 import md5
|
|
||||||
|
|
||||||
def _validate_md5(egg_name, data):
|
|
||||||
if egg_name in md5_data:
|
|
||||||
digest = md5(data).hexdigest()
|
|
||||||
if digest != md5_data[egg_name]:
|
|
||||||
print >>sys.stderr, (
|
|
||||||
"md5 validation of %s failed! (Possible download problem?)"
|
|
||||||
% egg_name
|
|
||||||
)
|
|
||||||
sys.exit(2)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def use_setuptools(
|
|
||||||
version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
|
|
||||||
download_delay=15
|
|
||||||
):
|
|
||||||
"""Automatically find/download setuptools and make it available on sys.path
|
|
||||||
|
|
||||||
`version` should be a valid setuptools version number that is available
|
|
||||||
as an egg for download under the `download_base` URL (which should end with
|
|
||||||
a '/'). `to_dir` is the directory where setuptools will be downloaded, if
|
|
||||||
it is not already available. If `download_delay` is specified, it should
|
|
||||||
be the number of seconds that will be paused before initiating a download,
|
|
||||||
should one be required. If an older version of setuptools is installed,
|
|
||||||
this routine will print a message to ``sys.stderr`` and raise SystemExit in
|
|
||||||
an attempt to abort the calling script.
|
|
||||||
"""
|
|
||||||
was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
|
|
||||||
def do_download():
|
|
||||||
egg = download_setuptools(version, download_base, to_dir, download_delay)
|
|
||||||
sys.path.insert(0, egg)
|
|
||||||
import setuptools; setuptools.bootstrap_install_from = egg
|
|
||||||
try:
|
|
||||||
import pkg_resources
|
|
||||||
except ImportError:
|
|
||||||
return do_download()
|
|
||||||
try:
|
|
||||||
pkg_resources.require("setuptools>="+version); return
|
|
||||||
except pkg_resources.VersionConflict, e:
|
|
||||||
if was_imported:
|
|
||||||
print >>sys.stderr, (
|
|
||||||
"The required version of setuptools (>=%s) is not available, and\n"
|
|
||||||
"can't be installed while this script is running. Please install\n"
|
|
||||||
" a more recent version first, using 'easy_install -U setuptools'."
|
|
||||||
"\n\n(Currently using %r)"
|
|
||||||
) % (version, e.args[0])
|
|
||||||
sys.exit(2)
|
|
||||||
except pkg_resources.DistributionNotFound:
|
|
||||||
pass
|
|
||||||
|
|
||||||
del pkg_resources, sys.modules['pkg_resources'] # reload ok
|
|
||||||
return do_download()
|
|
||||||
|
|
||||||
def download_setuptools(
|
|
||||||
version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
|
|
||||||
delay = 15
|
|
||||||
):
|
|
||||||
"""Download setuptools from a specified location and return its filename
|
|
||||||
|
|
||||||
`version` should be a valid setuptools version number that is available
|
|
||||||
as an egg for download under the `download_base` URL (which should end
|
|
||||||
with a '/'). `to_dir` is the directory where the egg will be downloaded.
|
|
||||||
`delay` is the number of seconds to pause before an actual download attempt.
|
|
||||||
"""
|
|
||||||
import urllib2, shutil
|
|
||||||
egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
|
|
||||||
url = download_base + egg_name
|
|
||||||
saveto = os.path.join(to_dir, egg_name)
|
|
||||||
src = dst = None
|
|
||||||
if not os.path.exists(saveto): # Avoid repeated downloads
|
|
||||||
try:
|
|
||||||
from distutils import log
|
|
||||||
if delay:
|
|
||||||
log.warn("""
|
|
||||||
---------------------------------------------------------------------------
|
|
||||||
This script requires setuptools version %s to run (even to display
|
|
||||||
help). I will attempt to download it for you (from
|
|
||||||
%s), but
|
|
||||||
you may need to enable firewall access for this script first.
|
|
||||||
I will start the download in %d seconds.
|
|
||||||
|
|
||||||
(Note: if this machine does not have network access, please obtain the file
|
|
||||||
|
|
||||||
%s
|
|
||||||
|
|
||||||
and place it in this directory before rerunning this script.)
|
|
||||||
---------------------------------------------------------------------------""",
|
|
||||||
version, download_base, delay, url
|
|
||||||
); from time import sleep; sleep(delay)
|
|
||||||
log.warn("Downloading %s", url)
|
|
||||||
src = urllib2.urlopen(url)
|
|
||||||
# Read/write all in one block, so we don't create a corrupt file
|
|
||||||
# if the download is interrupted.
|
|
||||||
data = _validate_md5(egg_name, src.read())
|
|
||||||
dst = open(saveto,"wb"); dst.write(data)
|
|
||||||
finally:
|
|
||||||
if src: src.close()
|
|
||||||
if dst: dst.close()
|
|
||||||
return os.path.realpath(saveto)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv, version=DEFAULT_VERSION):
|
|
||||||
"""Install or upgrade setuptools and EasyInstall"""
|
|
||||||
try:
|
|
||||||
import setuptools
|
|
||||||
except ImportError:
|
|
||||||
egg = None
|
|
||||||
try:
|
|
||||||
egg = download_setuptools(version, delay=0)
|
|
||||||
sys.path.insert(0,egg)
|
|
||||||
from setuptools.command.easy_install import main
|
|
||||||
return main(list(argv)+[egg]) # we're done here
|
|
||||||
finally:
|
|
||||||
if egg and os.path.exists(egg):
|
|
||||||
os.unlink(egg)
|
|
||||||
else:
|
|
||||||
if setuptools.__version__ == '0.0.1':
|
|
||||||
print >>sys.stderr, (
|
|
||||||
"You have an obsolete version of setuptools installed. Please\n"
|
|
||||||
"remove it from your system entirely before rerunning this script."
|
|
||||||
)
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
req = "setuptools>="+version
|
|
||||||
import pkg_resources
|
|
||||||
try:
|
|
||||||
pkg_resources.require(req)
|
|
||||||
except pkg_resources.VersionConflict:
|
|
||||||
try:
|
|
||||||
from setuptools.command.easy_install import main
|
|
||||||
except ImportError:
|
|
||||||
from easy_install import main
|
|
||||||
main(list(argv)+[download_setuptools(delay=0)])
|
|
||||||
sys.exit(0) # try to force an exit
|
|
||||||
else:
|
|
||||||
if argv:
|
|
||||||
from setuptools.command.easy_install import main
|
|
||||||
main(argv)
|
|
||||||
else:
|
|
||||||
print "Setuptools version",version,"or greater has been installed."
|
|
||||||
print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
|
|
||||||
|
|
||||||
def update_md5(filenames):
|
|
||||||
"""Update our built-in md5 registry"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
for name in filenames:
|
|
||||||
base = os.path.basename(name)
|
|
||||||
f = open(name,'rb')
|
|
||||||
md5_data[base] = md5(f.read()).hexdigest()
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
data = [" %r: %r,\n" % it for it in md5_data.items()]
|
|
||||||
data.sort()
|
|
||||||
repl = "".join(data)
|
|
||||||
|
|
||||||
import inspect
|
|
||||||
srcfile = inspect.getsourcefile(sys.modules[__name__])
|
|
||||||
f = open(srcfile, 'rb'); src = f.read(); f.close()
|
|
||||||
|
|
||||||
match = re.search("\nmd5_data = {\n([^}]+)}", src)
|
|
||||||
if not match:
|
|
||||||
print >>sys.stderr, "Internal error!"
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
src = src[:match.start(1)] + repl + src[match.end(1):]
|
|
||||||
f = open(srcfile,'w')
|
|
||||||
f.write(src)
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__=='__main__':
|
|
||||||
if len(sys.argv)>2 and sys.argv[1]=='--md5update':
|
|
||||||
update_md5(sys.argv[2:])
|
|
||||||
else:
|
|
||||||
main(sys.argv[1:])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<RCC>
|
||||||
|
<qresource prefix="Icon">
|
||||||
|
<file>../icons/comic2ebook.png</file>
|
||||||
|
</qresource>
|
||||||
|
<qresource prefix="Devices">
|
||||||
|
<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>
|
||||||
|
<file>../icons/EPUB.png</file>
|
||||||
|
<file>../icons/MOBI.png</file>
|
||||||
|
<file>../icons/KFX.png</file>
|
||||||
|
</qresource>
|
||||||
|
<qresource prefix="Status">
|
||||||
|
<file>../icons/error.png</file>
|
||||||
|
<file>../icons/info.png</file>
|
||||||
|
<file>../icons/warning.png</file>
|
||||||
|
</qresource>
|
||||||
|
<qresource prefix="Other">
|
||||||
|
<file>../icons/wiki.png</file>
|
||||||
|
<file>../icons/editor.png</file>
|
||||||
|
<file>../icons/list_background.png</file>
|
||||||
|
<file>../icons/clear.png</file>
|
||||||
|
<file>../icons/convert.png</file>
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>editorDialog</class>
|
||||||
|
<widget class="QDialog" name="editorDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>400</width>
|
||||||
|
<height>260</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>400</width>
|
||||||
|
<height>260</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Metadata editor</string>
|
||||||
|
</property>
|
||||||
|
<property name="windowIcon">
|
||||||
|
<iconset resource="KCC.qrc">
|
||||||
|
<normaloff>:/Icon/icons/comic2ebook.png</normaloff>:/Icon/icons/comic2ebook.png</iconset>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>5</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QWidget" name="editorWidget" native="true">
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="label_1">
|
||||||
|
<property name="text">
|
||||||
|
<string>Series:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QLineEdit" name="seriesLine"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>Volume:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QLineEdit" name="volumeLine"/>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="text">
|
||||||
|
<string>Number:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="QLineEdit" name="numberLine"/>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0">
|
||||||
|
<widget class="QLabel" name="label_4">
|
||||||
|
<property name="text">
|
||||||
|
<string>Writer:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="1">
|
||||||
|
<widget class="QLineEdit" name="writerLine"/>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="0">
|
||||||
|
<widget class="QLabel" name="label_5">
|
||||||
|
<property name="text">
|
||||||
|
<string>Penciller:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="1">
|
||||||
|
<widget class="QLineEdit" name="pencillerLine"/>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="0">
|
||||||
|
<widget class="QLabel" name="label_6">
|
||||||
|
<property name="text">
|
||||||
|
<string>Inker:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="1">
|
||||||
|
<widget class="QLineEdit" name="inkerLine"/>
|
||||||
|
</item>
|
||||||
|
<item row="7" column="0">
|
||||||
|
<widget class="QLabel" name="label_7">
|
||||||
|
<property name="text">
|
||||||
|
<string>Colorist:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<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>
|
||||||
|
<item>
|
||||||
|
<widget class="QWidget" name="optionWidget" native="true">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="statusLabel">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="okButton">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>30</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Save</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="KCC.qrc">
|
||||||
|
<normaloff>:/Other/icons/convert.png</normaloff>:/Other/icons/convert.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="cancelButton">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>30</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Cancel</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="KCC.qrc">
|
||||||
|
<normaloff>:/Other/icons/clear.png</normaloff>:/Other/icons/clear.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</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>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
|
After Width: | Height: | Size: 921 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 328 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 345 KiB After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
|
||||||
|
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and/or distribute this software for
|
||||||
|
# any purpose with or without fee is hereby granted, provided that the
|
||||||
|
# above copyright notice and this permission notice appear in all
|
||||||
|
# copies.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||||
|
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
||||||
|
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
||||||
|
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
|
||||||
|
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||||
|
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import 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)
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
block_cipher = None
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(['kcc-c2e.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-c2e',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=False,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None , icon='icons\\comic2ebook.ico')
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
|
||||||
|
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and/or distribute this software for
|
||||||
|
# any purpose with or without fee is hereby granted, provided that the
|
||||||
|
# above copyright notice and this permission notice appear in all
|
||||||
|
# copies.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||||
|
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
||||||
|
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
||||||
|
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
|
||||||
|
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||||
|
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import 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)
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
block_cipher = None
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(['kcc-c2p.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-c2p',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=False,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None , icon='icons\\comic2ebook.ico')
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"title": "Kindle Comic Converter",
|
||||||
|
"icon": "icons/comic2ebook.icns",
|
||||||
|
"background": "icons/WizardOSX.png",
|
||||||
|
"icon-size": 160,
|
||||||
|
"contents": [
|
||||||
|
{ "x": 180, "y": 300, "type": "file", "path": "dist/Kindle Comic Converter.app" },
|
||||||
|
{ "x": 520, "y": 300, "type": "link", "path": "/Applications" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# Copyright (c) 2012 Ciro Mattia Gonano <ciromattia@gmail.com>
|
# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
|
||||||
|
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
|
||||||
#
|
#
|
||||||
# Permission to use, copy, modify, and/or distribute this software for
|
# Permission to use, copy, modify, and/or distribute this software for
|
||||||
# any purpose with or without fee is hereby granted, provided that the
|
# any purpose with or without fee is hereby granted, provided that the
|
||||||
@@ -15,27 +17,78 @@
|
|||||||
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||||
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
# PERFORMANCE OF THIS SOFTWARE.
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
#
|
|
||||||
__version__ = '2.10'
|
|
||||||
__license__ = 'ISC'
|
|
||||||
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
|
|
||||||
from Tkinter import *
|
|
||||||
from kcc import gui
|
|
||||||
from sys import platform
|
|
||||||
from multiprocessing import freeze_support
|
|
||||||
import os
|
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)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
freeze_support()
|
|
||||||
root = Tk()
|
|
||||||
root.resizable(width=False, height=False)
|
|
||||||
root.config(padx=5, pady=5, takefocus=True)
|
|
||||||
root.title("Kindle Comic Converter v" + __version__)
|
|
||||||
#root.wm_attributes("-topmost", 1)
|
|
||||||
if platform == 'darwin':
|
|
||||||
os.environ['PATH'] = '/usr/local/bin:' + os.environ['PATH']
|
|
||||||
elif platform == 'win32':
|
|
||||||
root.iconbitmap(default='comic2ebook.ico')
|
|
||||||
gui.MainWindow(master=root)
|
|
||||||
root.mainloop()
|
|
||||||
|
|||||||
@@ -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')
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
__version__ = '2.10'
|
|
||||||
__license__ = 'ISC'
|
|
||||||
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# Copyright (c) 2012 Ciro Mattia Gonano <ciromattia@gmail.com>
|
|
||||||
#
|
|
||||||
# Permission to use, copy, modify, and/or distribute this software for
|
|
||||||
# any purpose with or without fee is hereby granted, provided that the
|
|
||||||
# above copyright notice and this permission notice appear in all
|
|
||||||
# copies.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
|
||||||
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
|
||||||
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
|
||||||
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
|
|
||||||
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
||||||
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
# PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
#
|
|
||||||
__license__ = 'ISC'
|
|
||||||
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
|
|
||||||
import os
|
|
||||||
import zipfile
|
|
||||||
import rarfile
|
|
||||||
|
|
||||||
|
|
||||||
class CBxArchive:
|
|
||||||
def __init__(self, origFileName):
|
|
||||||
self.origFileName = origFileName
|
|
||||||
if zipfile.is_zipfile(origFileName):
|
|
||||||
self.compressor = 'zip'
|
|
||||||
elif rarfile.is_rarfile(origFileName):
|
|
||||||
self.compressor = 'rar'
|
|
||||||
else:
|
|
||||||
self.compressor = None
|
|
||||||
|
|
||||||
def isCbxFile(self):
|
|
||||||
return self.compressor is not None
|
|
||||||
|
|
||||||
def extractCBZ(self, targetdir):
|
|
||||||
cbzFile = zipfile.ZipFile(self.origFileName)
|
|
||||||
filelist = []
|
|
||||||
for f in cbzFile.namelist():
|
|
||||||
if f.startswith('__MACOSX') or f.endswith('.DS_Store'):
|
|
||||||
pass # skip MacOS special files
|
|
||||||
elif f.endswith('/'):
|
|
||||||
try:
|
|
||||||
os.makedirs(os.path.join(targetdir, f))
|
|
||||||
except:
|
|
||||||
pass # the dir exists so we are going to extract the images only.
|
|
||||||
else:
|
|
||||||
filelist.append(f)
|
|
||||||
cbzFile.extractall(targetdir, filelist)
|
|
||||||
|
|
||||||
def extractCBR(self, targetdir):
|
|
||||||
cbrFile = rarfile.RarFile(self.origFileName)
|
|
||||||
filelist = []
|
|
||||||
for f in cbrFile.namelist():
|
|
||||||
if f.startswith('__MACOSX') or f.endswith('.DS_Store'):
|
|
||||||
pass # skip MacOS special files
|
|
||||||
elif f.endswith('/'):
|
|
||||||
try:
|
|
||||||
os.makedirs(os.path.join(targetdir, f))
|
|
||||||
except:
|
|
||||||
pass # the dir exists so we are going to extract the images only.
|
|
||||||
else:
|
|
||||||
filelist.append(f)
|
|
||||||
cbrFile.extractall(targetdir, filelist)
|
|
||||||
|
|
||||||
def extract(self, targetdir):
|
|
||||||
print "\n" + targetdir + "\n"
|
|
||||||
if self.compressor == 'rar':
|
|
||||||
self.extractCBR(targetdir)
|
|
||||||
elif self.compressor == 'zip':
|
|
||||||
self.extractCBZ(targetdir)
|
|
||||||
adir = os.listdir(targetdir)
|
|
||||||
if len(adir) == 1 and os.path.isdir(os.path.join(targetdir, adir[0])):
|
|
||||||
import shutil
|
|
||||||
for f in os.listdir(os.path.join(targetdir, adir[0])):
|
|
||||||
shutil.move(os.path.join(targetdir, adir[0], f), targetdir)
|
|
||||||
os.rmdir(os.path.join(targetdir, adir[0]))
|
|
||||||
return targetdir
|
|
||||||
@@ -1,738 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Copyright (c) 2012 Ciro Mattia Gonano <ciromattia@gmail.com>
|
|
||||||
#
|
|
||||||
# Permission to use, copy, modify, and/or distribute this software for
|
|
||||||
# any purpose with or without fee is hereby granted, provided that the
|
|
||||||
# above copyright notice and this permission notice appear in all
|
|
||||||
# copies.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
|
||||||
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
|
||||||
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
|
||||||
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
|
|
||||||
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
||||||
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
# PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
#
|
|
||||||
__version__ = '2.10'
|
|
||||||
__license__ = 'ISC'
|
|
||||||
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import re
|
|
||||||
from shutil import move
|
|
||||||
from shutil import copyfile
|
|
||||||
from shutil import copytree
|
|
||||||
from shutil import rmtree
|
|
||||||
from shutil import make_archive
|
|
||||||
from optparse import OptionParser
|
|
||||||
from multiprocessing import Pool, freeze_support
|
|
||||||
import image
|
|
||||||
import cbxarchive
|
|
||||||
import pdfjpgextract
|
|
||||||
|
|
||||||
|
|
||||||
def buildHTML(path, imgfile):
|
|
||||||
filename = getImageFileName(imgfile)
|
|
||||||
if filename is not None:
|
|
||||||
# All files marked with this sufix need horizontal Panel View.
|
|
||||||
if "_rotated" in str(filename):
|
|
||||||
rotate = True
|
|
||||||
else:
|
|
||||||
rotate = False
|
|
||||||
htmlpath = ''
|
|
||||||
postfix = ''
|
|
||||||
backref = 1
|
|
||||||
head = path
|
|
||||||
while True:
|
|
||||||
head, tail = os.path.split(head)
|
|
||||||
if tail == 'Images':
|
|
||||||
htmlpath = os.path.join(head, 'Text', postfix)
|
|
||||||
break
|
|
||||||
postfix = tail + "/" + postfix
|
|
||||||
backref += 1
|
|
||||||
if not os.path.exists(htmlpath):
|
|
||||||
os.makedirs(htmlpath)
|
|
||||||
htmlfile = os.path.join(htmlpath, filename[0] + '.html')
|
|
||||||
f = open(htmlfile, "w")
|
|
||||||
f.writelines(["<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" ",
|
|
||||||
"\"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n",
|
|
||||||
"<html xmlns=\"http://www.w3.org/1999/xhtml\">\n",
|
|
||||||
"<head>\n",
|
|
||||||
"<title>", filename[0], "</title>\n",
|
|
||||||
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n",
|
|
||||||
"<link href=\"", "../" * (backref - 1),
|
|
||||||
"style.css\" type=\"text/css\" rel=\"stylesheet\"/>\n",
|
|
||||||
"</head>\n",
|
|
||||||
"<body>\n",
|
|
||||||
"<div class=\"fs\">\n",
|
|
||||||
"<div><img src=\"", "../" * backref, "Images/", postfix, imgfile, "\" alt=\"",
|
|
||||||
imgfile, "\" class=\"singlePage\"/></div>\n"
|
|
||||||
])
|
|
||||||
if options.panelview:
|
|
||||||
if rotate:
|
|
||||||
if options.righttoleft:
|
|
||||||
f.writelines(["<div id=\"BoxTL\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
|
|
||||||
"'{\"targetId\":\"BoxTL-Panel-Parent\", \"ordinal\":1}'></a></div>\n",
|
|
||||||
"<div id=\"BoxTR\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
|
|
||||||
"'{\"targetId\":\"BoxTR-Panel-Parent\", \"ordinal\":3}'></a></div>\n",
|
|
||||||
"<div id=\"BoxBL\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
|
|
||||||
"'{\"targetId\":\"BoxBL-Panel-Parent\", \"ordinal\":2}'></a></div>\n",
|
|
||||||
"<div id=\"BoxBR\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify="
|
|
||||||
"'{\"targetId\":\"BoxBR-Panel-Parent\", \"ordinal\":4}'></a></div>\n"
|
|
||||||
])
|
|
||||||
else:
|
|
||||||
f.writelines(["<div id=\"BoxTL\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
|
|
||||||
"'{\"targetId\":\"BoxTL-Panel-Parent\", \"ordinal\":2}'></a></div>\n",
|
|
||||||
"<div id=\"BoxTR\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
|
|
||||||
"'{\"targetId\":\"BoxTR-Panel-Parent\", \"ordinal\":4}'></a></div>\n",
|
|
||||||
"<div id=\"BoxBL\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
|
|
||||||
"'{\"targetId\":\"BoxBL-Panel-Parent\", \"ordinal\":1}'></a></div>\n",
|
|
||||||
"<div id=\"BoxBR\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify="
|
|
||||||
"'{\"targetId\":\"BoxBR-Panel-Parent\", \"ordinal\":3}'></a></div>\n"
|
|
||||||
])
|
|
||||||
else:
|
|
||||||
if options.righttoleft:
|
|
||||||
f.writelines(["<div id=\"BoxTL\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
|
|
||||||
"'{\"targetId\":\"BoxTL-Panel-Parent\", \"ordinal\":2}'></a></div>\n",
|
|
||||||
"<div id=\"BoxTR\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
|
|
||||||
"'{\"targetId\":\"BoxTR-Panel-Parent\", \"ordinal\":1}'></a></div>\n",
|
|
||||||
"<div id=\"BoxBL\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
|
|
||||||
"'{\"targetId\":\"BoxBL-Panel-Parent\", \"ordinal\":4}'></a></div>\n",
|
|
||||||
"<div id=\"BoxBR\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify="
|
|
||||||
"'{\"targetId\":\"BoxBR-Panel-Parent\", \"ordinal\":3}'></a></div>\n"
|
|
||||||
])
|
|
||||||
else:
|
|
||||||
f.writelines(["<div id=\"BoxTL\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
|
|
||||||
"'{\"targetId\":\"BoxTL-Panel-Parent\", \"ordinal\":1}'></a></div>\n",
|
|
||||||
"<div id=\"BoxTR\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
|
|
||||||
"'{\"targetId\":\"BoxTR-Panel-Parent\", \"ordinal\":2}'></a></div>\n",
|
|
||||||
"<div id=\"BoxBL\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
|
|
||||||
"'{\"targetId\":\"BoxBL-Panel-Parent\", \"ordinal\":3}'></a></div>\n",
|
|
||||||
"<div id=\"BoxBR\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify="
|
|
||||||
"'{\"targetId\":\"BoxBR-Panel-Parent\", \"ordinal\":4}'></a></div>\n"
|
|
||||||
])
|
|
||||||
|
|
||||||
f.writelines(["<div id=\"BoxTL-Panel-Parent\" class=\"target-mag-parent\"><div id=\"BoxTL-Panel\" class=\"",
|
|
||||||
"target-mag\"><img src=\"", "../" * backref, "Images/", postfix, imgfile, "\" alt=\"",
|
|
||||||
imgfile, "\"/></div></div>\n",
|
|
||||||
"<div id=\"BoxTR-Panel-Parent\" class=\"target-mag-parent\"><div id=\"BoxTR-Panel\" class=\"",
|
|
||||||
"target-mag\"><img src=\"", "../" * backref, "Images/", postfix, imgfile, "\" alt=\"",
|
|
||||||
imgfile, "\"/></div></div>\n",
|
|
||||||
"<div id=\"BoxBL-Panel-Parent\" class=\"target-mag-parent\"><div id=\"BoxBL-Panel\" class=\"",
|
|
||||||
"target-mag\"><img src=\"", "../" * backref, "Images/", postfix, imgfile, "\" alt=\"",
|
|
||||||
imgfile, "\"/></div></div>\n",
|
|
||||||
"<div id=\"BoxBR-Panel-Parent\" class=\"target-mag-parent\"><div id=\"BoxBR-Panel\" class=\"",
|
|
||||||
"target-mag\"><img src=\"", "../" * backref, "Images/", postfix, imgfile, "\" alt=\"",
|
|
||||||
imgfile, "\"/></div></div>\n"
|
|
||||||
])
|
|
||||||
f.writelines(["</div>\n</body>\n</html>"])
|
|
||||||
f.close()
|
|
||||||
return path, imgfile
|
|
||||||
|
|
||||||
|
|
||||||
def buildBlankHTML(path):
|
|
||||||
f = open(os.path.join(path, 'blank.html'), "w")
|
|
||||||
f.writelines(["<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" ",
|
|
||||||
"\"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n",
|
|
||||||
"<html xmlns=\"http://www.w3.org/1999/xhtml\">\n",
|
|
||||||
"<head>\n",
|
|
||||||
"<title></title>\n",
|
|
||||||
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n",
|
|
||||||
"</head>\n",
|
|
||||||
"<body>\n",
|
|
||||||
"</body>\n",
|
|
||||||
"</html>"])
|
|
||||||
f.close()
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def buildNCX(dstdir, title, chapters):
|
|
||||||
ncxfile = os.path.join(dstdir, 'OEBPS', 'toc.ncx')
|
|
||||||
f = open(ncxfile, "w")
|
|
||||||
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
|
||||||
"<!DOCTYPE ncx PUBLIC \"-//NISO//DTD ncx 2005-1//EN\" ",
|
|
||||||
"\"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd\">\n",
|
|
||||||
"<ncx version=\"2005-1\" xml:lang=\"en-US\" xmlns=\"http://www.daisy.org/z3986/2005/ncx/\">\n",
|
|
||||||
"<head>\n",
|
|
||||||
"<meta name=\"dtb:uid\" content=\"015ffaec-9340-42f8-b163-a0c5ab7d0611\"/>\n",
|
|
||||||
"<meta name=\"dtb:depth\" content=\"2\"/>\n",
|
|
||||||
"<meta name=\"dtb:totalPageCount\" content=\"0\"/>\n",
|
|
||||||
"<meta name=\"dtb:maxPageNumber\" content=\"0\"/>\n",
|
|
||||||
"</head>\n",
|
|
||||||
"<docTitle><text>", title, "</text></docTitle>\n",
|
|
||||||
"<navMap>"
|
|
||||||
])
|
|
||||||
for chapter in chapters:
|
|
||||||
folder = chapter[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\')
|
|
||||||
title = os.path.basename(folder)
|
|
||||||
filename = getImageFileName(os.path.join(folder, chapter[1]))
|
|
||||||
f.write("<navPoint id=\"" + folder.replace('/', '_').replace('\\', '_') + "\"><navLabel><text>" + title
|
|
||||||
+ "</text></navLabel><content src=\"" + filename[0] + ".html\"/></navPoint>\n")
|
|
||||||
f.write("</navMap>\n</ncx>")
|
|
||||||
f.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def buildOPF(profile, dstdir, title, filelist, cover=None):
|
|
||||||
opffile = os.path.join(dstdir, 'OEBPS', 'content.opf')
|
|
||||||
# read the first file resolution
|
|
||||||
profilelabel, deviceres, palette, gamma, panelviewsize = image.ProfileData.Profiles[profile]
|
|
||||||
imgres = str(deviceres[0]) + "x" + str(deviceres[1])
|
|
||||||
if options.righttoleft:
|
|
||||||
writingmode = "horizontal-rl"
|
|
||||||
facing = "right"
|
|
||||||
facing1 = "right"
|
|
||||||
facing2 = "left"
|
|
||||||
else:
|
|
||||||
writingmode = "horizontal-lr"
|
|
||||||
facing = "left"
|
|
||||||
facing1 = "left"
|
|
||||||
facing2 = "right"
|
|
||||||
from uuid import uuid4
|
|
||||||
uuid = str(uuid4())
|
|
||||||
uuid = uuid.encode('utf-8')
|
|
||||||
f = open(opffile, "w")
|
|
||||||
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
|
||||||
"<package version=\"2.0\" unique-identifier=\"BookID\" xmlns=\"http://www.idpf.org/2007/opf\">\n",
|
|
||||||
"<metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\" ",
|
|
||||||
"xmlns:opf=\"http://www.idpf.org/2007/opf\">\n",
|
|
||||||
"<dc:title>", title, "</dc:title>\n",
|
|
||||||
"<dc:language>en-US</dc:language>\n",
|
|
||||||
"<dc:identifier id=\"BookID\" opf:scheme=\"UUID\">", uuid, "</dc:identifier>\n",
|
|
||||||
"<meta name=\"RegionMagnification\" content=\"true\"/>\n",
|
|
||||||
"<meta name=\"cover\" content=\"cover\"/>\n",
|
|
||||||
"<meta name=\"book-type\" content=\"comic\"/>\n",
|
|
||||||
"<meta name=\"zero-gutter\" content=\"true\"/>\n",
|
|
||||||
"<meta name=\"zero-margin\" content=\"true\"/>\n",
|
|
||||||
"<meta name=\"fixed-layout\" content=\"true\"/>\n"
|
|
||||||
])
|
|
||||||
if options.landscapemode:
|
|
||||||
f.writelines(["<meta name=\"rendition:orientation\" content=\"auto\"/>\n",
|
|
||||||
"<meta name=\"orientation-lock\" content=\"none\"/>\n"])
|
|
||||||
else:
|
|
||||||
f.writelines(["<meta name=\"rendition:orientation\" content=\"portrait\"/>\n",
|
|
||||||
"<meta name=\"orientation-lock\" content=\"portrait\"/>\n"])
|
|
||||||
f.writelines(["<meta name=\"original-resolution\" content=\"", imgres, "\"/>\n",
|
|
||||||
"<meta name=\"primary-writing-mode\" content=\"", writingmode, "\"/>\n",
|
|
||||||
"<meta name=\"rendition:layout\" content=\"pre-paginated\"/>\n",
|
|
||||||
"</metadata>\n<manifest>\n<item id=\"ncx\" href=\"toc.ncx\" ",
|
|
||||||
"media-type=\"application/x-dtbncx+xml\"/>\n"])
|
|
||||||
if cover is not None:
|
|
||||||
filename = getImageFileName(cover.replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\'))
|
|
||||||
if '.png' == filename[1]:
|
|
||||||
mt = 'image/png'
|
|
||||||
else:
|
|
||||||
mt = 'image/jpeg'
|
|
||||||
f.write("<item id=\"cover\" href=\"" + filename[0] + filename[1] + "\" media-type=\"" + mt + "\"/>\n")
|
|
||||||
reflist = []
|
|
||||||
for path in filelist:
|
|
||||||
folder = path[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\')
|
|
||||||
filename = getImageFileName(path[1])
|
|
||||||
uniqueid = os.path.join(folder, filename[0]).replace('/', '_').replace('\\', '_')
|
|
||||||
reflist.append(uniqueid)
|
|
||||||
f.write("<item id=\"page_" + uniqueid + "\" href=\""
|
|
||||||
+ folder.replace('Images', 'Text') + "/" + filename[0]
|
|
||||||
+ ".html\" media-type=\"application/xhtml+xml\"/>\n")
|
|
||||||
if '.png' == filename[1]:
|
|
||||||
mt = 'image/png'
|
|
||||||
else:
|
|
||||||
mt = 'image/jpeg'
|
|
||||||
f.write("<item id=\"img_" + uniqueid + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\""
|
|
||||||
+ mt + "\"/>\n")
|
|
||||||
if options.landscapemode and splitCount > 0:
|
|
||||||
splitCountUsed = 1
|
|
||||||
while splitCountUsed <= splitCount:
|
|
||||||
f.write("<item id=\"blank-page" + str(splitCountUsed) +
|
|
||||||
"\" href=\"Text/blank.html\" media-type=\"application/xhtml+xml\"/>\n")
|
|
||||||
splitCountUsed += 1
|
|
||||||
f.write("<item id=\"css\" href=\"Text/style.css\" media-type=\"text/css\"/>\n")
|
|
||||||
f.write("</manifest>\n<spine toc=\"ncx\">\n")
|
|
||||||
splitCountUsed = 1
|
|
||||||
for entry in reflist:
|
|
||||||
if entry.endswith("-kcca"):
|
|
||||||
# noinspection PyRedundantParentheses
|
|
||||||
if ((options.righttoleft and facing == 'left') or (not options.righttoleft and facing == 'right')) and\
|
|
||||||
options.landscapemode:
|
|
||||||
f.write("<itemref idref=\"blank-page" + str(splitCountUsed) + "\" properties=\"layout-blank\"/>\n")
|
|
||||||
splitCountUsed += 1
|
|
||||||
if options.landscapemode:
|
|
||||||
f.write("<itemref idref=\"page_" + entry + "\" properties=\"page-spread-" + facing1 + "\"/>\n")
|
|
||||||
else:
|
|
||||||
f.write("<itemref idref=\"page_" + entry + "\"/>\n")
|
|
||||||
elif entry.endswith("-kccb"):
|
|
||||||
if options.landscapemode:
|
|
||||||
f.write("<itemref idref=\"page_" + entry + "\" properties=\"page-spread-" + facing2 + "\"/>\n")
|
|
||||||
else:
|
|
||||||
f.write("<itemref idref=\"page_" + entry + "\"/>\n")
|
|
||||||
if options.righttoleft:
|
|
||||||
facing = "right"
|
|
||||||
else:
|
|
||||||
facing = "left"
|
|
||||||
else:
|
|
||||||
if options.landscapemode:
|
|
||||||
f.write("<itemref idref=\"page_" + entry + "\" properties=\"page-spread-" + facing + "\"/>\n")
|
|
||||||
else:
|
|
||||||
f.write("<itemref idref=\"page_" + entry + "\"/>\n")
|
|
||||||
if facing == 'right':
|
|
||||||
facing = 'left'
|
|
||||||
else:
|
|
||||||
facing = 'right'
|
|
||||||
f.write("</spine>\n<guide>\n</guide>\n</package>\n")
|
|
||||||
f.close()
|
|
||||||
os.mkdir(os.path.join(dstdir, 'META-INF'))
|
|
||||||
f = open(os.path.join(dstdir, 'mimetype'), 'w')
|
|
||||||
f.write('application/epub+zip')
|
|
||||||
f.close()
|
|
||||||
f = open(os.path.join(dstdir, 'META-INF', 'container.xml'), 'w')
|
|
||||||
f.writelines(["<?xml version=\"1.0\"?>\n",
|
|
||||||
"<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">\n",
|
|
||||||
"<rootfiles>\n",
|
|
||||||
"<rootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/>\n",
|
|
||||||
"</rootfiles>\n",
|
|
||||||
"</container>"])
|
|
||||||
f.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def getImageFileName(imgfile):
|
|
||||||
filename = os.path.splitext(imgfile)
|
|
||||||
if filename[0].startswith('.') or\
|
|
||||||
(filename[1].lower() != '.png' and
|
|
||||||
filename[1].lower() != '.jpg' and
|
|
||||||
filename[1].lower() != '.gif' and
|
|
||||||
filename[1].lower() != '.tif' and
|
|
||||||
filename[1].lower() != '.tiff' and
|
|
||||||
filename[1].lower() != '.bmp' and
|
|
||||||
filename[1].lower() != '.jpeg'):
|
|
||||||
return None
|
|
||||||
return filename
|
|
||||||
|
|
||||||
|
|
||||||
def applyImgOptimization(img, isSplit, toRight, options):
|
|
||||||
img.cropWhiteSpace(10.0)
|
|
||||||
if options.cutpagenumbers:
|
|
||||||
img.cutPageNumber()
|
|
||||||
img.resizeImage(options.upscale, options.stretch, options.black_borders, isSplit, toRight, options.landscapemode,
|
|
||||||
options.nopanelviewhq)
|
|
||||||
img.optimizeImage(options.gamma)
|
|
||||||
if options.forcepng:
|
|
||||||
img.quantizeImage()
|
|
||||||
|
|
||||||
|
|
||||||
def dirImgProcess(path):
|
|
||||||
global options, splitCount
|
|
||||||
work = []
|
|
||||||
pagenumber = 0
|
|
||||||
pagenumbermodifier = 0
|
|
||||||
pool = Pool()
|
|
||||||
for (dirpath, dirnames, filenames) in os.walk(path):
|
|
||||||
for afile in filenames:
|
|
||||||
if getImageFileName(afile) is not None:
|
|
||||||
pagenumber += 1
|
|
||||||
work.append([afile, dirpath, pagenumber, options])
|
|
||||||
splitpages = pool.map(fileImgProcess, work)
|
|
||||||
pool.close()
|
|
||||||
pool.join()
|
|
||||||
splitpages = filter(None, splitpages)
|
|
||||||
splitpages.sort()
|
|
||||||
for page in splitpages:
|
|
||||||
if (page + pagenumbermodifier) % 2 == 0:
|
|
||||||
splitCount += 1
|
|
||||||
pagenumbermodifier += 1
|
|
||||||
pagenumbermodifier += 1
|
|
||||||
|
|
||||||
|
|
||||||
def fileImgProcess(work):
|
|
||||||
afile = work[0]
|
|
||||||
dirpath = work[1]
|
|
||||||
pagenumber = work[2]
|
|
||||||
options = work[3]
|
|
||||||
output = None
|
|
||||||
if options.verbose:
|
|
||||||
print "Optimizing " + afile + " for " + options.profile
|
|
||||||
else:
|
|
||||||
print ".",
|
|
||||||
img = image.ComicPage(os.path.join(dirpath, afile), options.profile)
|
|
||||||
if options.nosplitrotate:
|
|
||||||
split = None
|
|
||||||
else:
|
|
||||||
split = img.splitPage(dirpath, options.righttoleft, options.rotate)
|
|
||||||
if split is not None and split is not "R":
|
|
||||||
if options.verbose:
|
|
||||||
print "Splitted " + afile
|
|
||||||
if options.righttoleft:
|
|
||||||
toRight1 = False
|
|
||||||
toRight2 = True
|
|
||||||
else:
|
|
||||||
toRight1 = True
|
|
||||||
toRight2 = False
|
|
||||||
output = pagenumber
|
|
||||||
img0 = image.ComicPage(split[0], options.profile)
|
|
||||||
applyImgOptimization(img0, True, toRight1, options)
|
|
||||||
img0.saveToDir(dirpath, options.forcepng, options.forcecolor)
|
|
||||||
img1 = image.ComicPage(split[1], options.profile)
|
|
||||||
applyImgOptimization(img1, True, toRight2, options)
|
|
||||||
img1.saveToDir(dirpath, options.forcepng, options.forcecolor)
|
|
||||||
else:
|
|
||||||
applyImgOptimization(img, False, False, options)
|
|
||||||
img.saveToDir(dirpath, options.forcepng, options.forcecolor, split)
|
|
||||||
return output
|
|
||||||
|
|
||||||
|
|
||||||
def genEpubStruct(path):
|
|
||||||
global options
|
|
||||||
filelist = []
|
|
||||||
chapterlist = []
|
|
||||||
cover = None
|
|
||||||
_, deviceres, _, _, panelviewsize = image.ProfileData.Profiles[options.profile]
|
|
||||||
sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
|
|
||||||
os.mkdir(os.path.join(path, 'OEBPS', 'Text'))
|
|
||||||
f = open(os.path.join(path, 'OEBPS', 'Text', 'style.css'), 'w')
|
|
||||||
# DON'T COMPRESS CSS. KINDLE WILL FAIL TO PARSE IT.
|
|
||||||
# Generic Panel View support + Margins fix for Non-Kindle devices.
|
|
||||||
f.writelines(["@page {\n",
|
|
||||||
"margin-bottom: 0;\n",
|
|
||||||
"margin-top: 0\n",
|
|
||||||
"}\n",
|
|
||||||
"body {\n",
|
|
||||||
"display: block;\n",
|
|
||||||
"margin-bottom: 0;\n",
|
|
||||||
"margin-left: 0;\n",
|
|
||||||
"margin-right: 0;\n",
|
|
||||||
"margin-top: 0;\n",
|
|
||||||
"padding-bottom: 0;\n",
|
|
||||||
"padding-left: 0;\n",
|
|
||||||
"padding-right: 0;\n",
|
|
||||||
"padding-top: 0;\n",
|
|
||||||
"text-align: left\n",
|
|
||||||
"}\n",
|
|
||||||
"div.fs {\n",
|
|
||||||
"height: ", str(deviceres[1]), "px;\n",
|
|
||||||
"width: ", str(deviceres[0]), "px;\n",
|
|
||||||
"position: relative;\n",
|
|
||||||
"display: block;\n",
|
|
||||||
"text-align: center\n",
|
|
||||||
"}\n",
|
|
||||||
"div.fs a {\n",
|
|
||||||
"display: block;\n",
|
|
||||||
"width : 100%;\n",
|
|
||||||
"height: 100%;\n",
|
|
||||||
"}\n",
|
|
||||||
"div.fs div {\n",
|
|
||||||
"position: absolute;\n",
|
|
||||||
"}\n",
|
|
||||||
"img.singlePage {\n",
|
|
||||||
"position: absolute;\n",
|
|
||||||
"height: ", str(deviceres[1]), "px;\n",
|
|
||||||
"width: ", str(deviceres[0]), "px;\n",
|
|
||||||
"}\n",
|
|
||||||
"div.target-mag-parent {\n",
|
|
||||||
"width:100%;\n",
|
|
||||||
"height:100%;\n",
|
|
||||||
"display:none;\n",
|
|
||||||
"}\n",
|
|
||||||
"div.target-mag {\n",
|
|
||||||
"position: absolute;\n",
|
|
||||||
"display: block;\n",
|
|
||||||
"overflow: hidden;\n",
|
|
||||||
"}\n",
|
|
||||||
"div.target-mag img {\n",
|
|
||||||
"position: absolute;\n",
|
|
||||||
"height: ", str(panelviewsize[1]), "px;\n",
|
|
||||||
"width: ", str(panelviewsize[0]), "px;\n",
|
|
||||||
"}\n",
|
|
||||||
"#BoxTL {\n",
|
|
||||||
"top: 0;\n",
|
|
||||||
"left: 0;\n",
|
|
||||||
"height: 50%;\n",
|
|
||||||
"width: 50%;\n",
|
|
||||||
"}\n",
|
|
||||||
"#BoxTR {\n",
|
|
||||||
"top: 0;\n",
|
|
||||||
"right: 0;\n",
|
|
||||||
"height: 50%;\n",
|
|
||||||
"width: 50%;\n",
|
|
||||||
"}\n",
|
|
||||||
"#BoxBL {\n",
|
|
||||||
"bottom: 0;\n",
|
|
||||||
"left: 0;\n",
|
|
||||||
"height: 50%;\n",
|
|
||||||
"width: 50%;\n",
|
|
||||||
"}\n",
|
|
||||||
"#BoxBR {\n",
|
|
||||||
"bottom: 0;\n",
|
|
||||||
"right: 0;\n",
|
|
||||||
"height: 50%;\n",
|
|
||||||
"width: 50%;\n",
|
|
||||||
"}\n",
|
|
||||||
"#BoxTL-Panel {\n",
|
|
||||||
"top: 0;\n",
|
|
||||||
"left: 0;\n",
|
|
||||||
"height: 100%;\n",
|
|
||||||
"width: 100%;\n",
|
|
||||||
"}\n",
|
|
||||||
"#BoxTL-Panel img {\n",
|
|
||||||
"top: 0%;\n",
|
|
||||||
"left: 0%;\n",
|
|
||||||
"}\n",
|
|
||||||
"#BoxTR-Panel {\n",
|
|
||||||
"top: 0;\n",
|
|
||||||
"right: 0;\n",
|
|
||||||
"height: 100%;\n",
|
|
||||||
"width: 100%;\n",
|
|
||||||
"}\n",
|
|
||||||
"#BoxTR-Panel img {\n",
|
|
||||||
"top: 0%;\n",
|
|
||||||
"right: 0%;\n",
|
|
||||||
"}\n",
|
|
||||||
"#BoxBL-Panel {\n",
|
|
||||||
"bottom: 0;\n",
|
|
||||||
"left: 0;\n",
|
|
||||||
"height: 100%;\n",
|
|
||||||
"width: 100%;\n",
|
|
||||||
"}\n",
|
|
||||||
"#BoxBL-Panel img {\n",
|
|
||||||
"bottom: 0%;\n",
|
|
||||||
"left: 0%;\n",
|
|
||||||
"}\n",
|
|
||||||
"#BoxBR-Panel {\n",
|
|
||||||
"bottom: 0;\n",
|
|
||||||
"right: 0;\n",
|
|
||||||
"height: 100%;\n",
|
|
||||||
"width: 100%;\n",
|
|
||||||
"}\n",
|
|
||||||
"#BoxBR-Panel img {\n",
|
|
||||||
"bottom: 0%;\n",
|
|
||||||
"right: 0%;\n",
|
|
||||||
"}"
|
|
||||||
])
|
|
||||||
f.close()
|
|
||||||
for (dirpath, dirnames, filenames) in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
|
||||||
chapter = False
|
|
||||||
for afile in filenames:
|
|
||||||
filename = getImageFileName(afile)
|
|
||||||
if filename is not None:
|
|
||||||
filelist.append(buildHTML(dirpath, afile))
|
|
||||||
if not chapter:
|
|
||||||
chapterlist.append((dirpath.replace('Images', 'Text'), filelist[-1][1]))
|
|
||||||
chapter = True
|
|
||||||
if cover is None:
|
|
||||||
cover = os.path.join(filelist[-1][0], 'cover' + getImageFileName(filelist[-1][1])[1])
|
|
||||||
copyfile(os.path.join(filelist[-1][0], filelist[-1][1]), cover)
|
|
||||||
buildNCX(path, options.title, chapterlist)
|
|
||||||
# ensure we're sorting files alphabetically
|
|
||||||
convert = lambda text: int(text) if text.isdigit() else text
|
|
||||||
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
|
|
||||||
filelist.sort(key=lambda name: (alphanum_key(name[0].lower()), alphanum_key(name[1].lower())))
|
|
||||||
buildOPF(options.profile, path, options.title, filelist, cover)
|
|
||||||
if options.landscapemode and splitCount > 0:
|
|
||||||
filelist.append(buildBlankHTML(os.path.join(path, 'OEBPS', 'Text')))
|
|
||||||
|
|
||||||
|
|
||||||
def getWorkFolder(afile):
|
|
||||||
workdir = tempfile.mkdtemp()
|
|
||||||
if os.path.isdir(afile):
|
|
||||||
try:
|
|
||||||
import shutil
|
|
||||||
os.rmdir(workdir) # needed for copytree() fails if dst already exists
|
|
||||||
copytree(afile, workdir)
|
|
||||||
path = workdir
|
|
||||||
except OSError:
|
|
||||||
raise
|
|
||||||
elif afile.lower().endswith('.pdf'):
|
|
||||||
pdf = pdfjpgextract.PdfJpgExtract(afile)
|
|
||||||
path = pdf.extract()
|
|
||||||
else:
|
|
||||||
cbx = cbxarchive.CBxArchive(afile)
|
|
||||||
if cbx.isCbxFile():
|
|
||||||
try:
|
|
||||||
path = cbx.extract(workdir)
|
|
||||||
except OSError:
|
|
||||||
print 'Unrar not found, please download from ' + \
|
|
||||||
'http://www.rarlab.com/download.htm and put into your PATH.'
|
|
||||||
sys.exit(21)
|
|
||||||
else:
|
|
||||||
raise TypeError
|
|
||||||
move(path, path + "_temp")
|
|
||||||
move(path + "_temp", os.path.join(path, 'OEBPS', 'Images'))
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def slugify(value):
|
|
||||||
"""
|
|
||||||
Normalizes string, converts to lowercase, removes non-alpha characters,
|
|
||||||
and converts spaces to hyphens.
|
|
||||||
"""
|
|
||||||
import unicodedata
|
|
||||||
value = unicodedata.normalize('NFKD', unicode(value, 'latin1')).encode('ascii', 'ignore')
|
|
||||||
value = re.sub('[^\w\s\.-]', '', value).strip().lower()
|
|
||||||
value = re.sub('[-\.\s]+', '-', value)
|
|
||||||
value = re.sub(r'([0-9]+)', r'00000\1', value)
|
|
||||||
value = re.sub(r'0*([0-9]{6,})', r'\1', value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def sanitizeTree(filetree):
|
|
||||||
for root, dirs, files in os.walk(filetree, False):
|
|
||||||
for name in files:
|
|
||||||
if name.startswith('.') or name.lower() == 'thumbs.db':
|
|
||||||
os.remove(os.path.join(root, name))
|
|
||||||
else:
|
|
||||||
splitname = os.path.splitext(name)
|
|
||||||
slugified = slugify(splitname[0])
|
|
||||||
while os.path.exists(os.path.join(root, slugified + splitname[1])):
|
|
||||||
slugified += "1"
|
|
||||||
os.rename(os.path.join(root, name), os.path.join(root, slugified + splitname[1]))
|
|
||||||
for name in dirs:
|
|
||||||
if name.startswith('.'):
|
|
||||||
os.remove(os.path.join(root, name))
|
|
||||||
else:
|
|
||||||
os.rename(os.path.join(root, name), os.path.join(root, slugify(name)))
|
|
||||||
|
|
||||||
|
|
||||||
def Copyright():
|
|
||||||
print ('comic2ebook v%(__version__)s. '
|
|
||||||
'Written 2013 by Ciro Mattia Gonano and Pawel Jastrzebski.' % globals())
|
|
||||||
|
|
||||||
|
|
||||||
def Usage():
|
|
||||||
print "Generates EPUB/CBZ comic ebook from a bunch of images."
|
|
||||||
parser.print_help()
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
global parser, options, epub_path, splitCount
|
|
||||||
usage = "Usage: %prog [options] comic_file|comic_folder"
|
|
||||||
parser = OptionParser(usage=usage, version=__version__)
|
|
||||||
parser.add_option("-p", "--profile", action="store", dest="profile", default="KHD",
|
|
||||||
help="Device profile (Choose one among K1, K2, K3, K4NT, K4T, KDX, KDXG, KHD, KF, KFHD, KFHD8) "
|
|
||||||
"[Default=KHD]")
|
|
||||||
parser.add_option("-t", "--title", action="store", dest="title", default="defaulttitle",
|
|
||||||
help="Comic title [Default=filename]")
|
|
||||||
parser.add_option("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
|
|
||||||
help="Manga style (Right-to-left reading and splitting) [Default=False]")
|
|
||||||
parser.add_option("-c", "--cbz-output", action="store_true", dest="cbzoutput", default=False,
|
|
||||||
help="Outputs a CBZ archive and does not generate EPUB")
|
|
||||||
parser.add_option("--nopanelviewhq", action="store_true", dest="nopanelviewhq", default=False,
|
|
||||||
help="Disable high quality Panel View [Default=False]")
|
|
||||||
parser.add_option("--noprocessing", action="store_false", dest="imgproc", default=True,
|
|
||||||
help="Do not apply image preprocessing (Page splitting and optimizations) [Default=True]")
|
|
||||||
parser.add_option("--forcepng", action="store_true", dest="forcepng", default=False,
|
|
||||||
help="Create PNG files instead JPEG (For non-Kindle devices) [Default=False]")
|
|
||||||
parser.add_option("--gamma", type="float", dest="gamma", default="0.0",
|
|
||||||
help="Apply gamma correction to linearize the image [Default=Auto]")
|
|
||||||
parser.add_option("--upscale", action="store_true", dest="upscale", default=False,
|
|
||||||
help="Resize images smaller than device's resolution [Default=False]")
|
|
||||||
parser.add_option("--stretch", action="store_true", dest="stretch", default=False,
|
|
||||||
help="Stretch images to device's resolution [Default=False]")
|
|
||||||
parser.add_option("--blackborders", action="store_true", dest="black_borders", default=False,
|
|
||||||
help="Use black borders instead of white ones when not stretching and ratio "
|
|
||||||
+ "is not like the device's one [Default=False]")
|
|
||||||
parser.add_option("--rotate", action="store_true", dest="rotate", default=False,
|
|
||||||
help="Rotate landscape pages instead of splitting them [Default=False]")
|
|
||||||
parser.add_option("--nosplitrotate", action="store_true", dest="nosplitrotate", default=False,
|
|
||||||
help="Disable splitting and rotation [Default=False]")
|
|
||||||
parser.add_option("--nocutpagenumbers", action="store_false", dest="cutpagenumbers", default=True,
|
|
||||||
help="Do not try to cut page numbering on images [Default=True]")
|
|
||||||
parser.add_option("-o", "--output", action="store", dest="output", default=None,
|
|
||||||
help="Output generated file (EPUB or CBZ) to specified directory or file")
|
|
||||||
parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False,
|
|
||||||
help="Verbose output [Default=False]")
|
|
||||||
options, args = parser.parse_args(argv)
|
|
||||||
checkOptions()
|
|
||||||
if len(args) != 1:
|
|
||||||
parser.print_help()
|
|
||||||
return
|
|
||||||
path = getWorkFolder(args[0])
|
|
||||||
if options.title == 'defaulttitle':
|
|
||||||
options.title = os.path.splitext(os.path.basename(args[0]))[0]
|
|
||||||
splitCount = 0
|
|
||||||
if options.imgproc:
|
|
||||||
print "Processing images..."
|
|
||||||
dirImgProcess(path + "/OEBPS/Images/")
|
|
||||||
if options.cbzoutput:
|
|
||||||
# if CBZ output wanted, compress all images and return filepath
|
|
||||||
print "\nCreating CBZ file..."
|
|
||||||
filepath = getOutputFilename(args[0], options.output, '.cbz')
|
|
||||||
make_archive(path + '_comic', 'zip', path + '/OEBPS/Images')
|
|
||||||
else:
|
|
||||||
print "\nCreating ePub structure..."
|
|
||||||
genEpubStruct(path)
|
|
||||||
# actually zip the ePub
|
|
||||||
filepath = getOutputFilename(args[0], options.output, '.epub')
|
|
||||||
make_archive(path + '_comic', 'zip', path)
|
|
||||||
move(path + '_comic.zip', filepath)
|
|
||||||
rmtree(path)
|
|
||||||
return filepath
|
|
||||||
|
|
||||||
|
|
||||||
def getOutputFilename(srcpath, wantedname, ext):
|
|
||||||
if not ext.startswith('.'):
|
|
||||||
ext = '.' + ext
|
|
||||||
if wantedname is not None:
|
|
||||||
if wantedname.endswith(ext):
|
|
||||||
filename = os.path.abspath(wantedname)
|
|
||||||
elif os.path.isdir(srcpath):
|
|
||||||
filename = os.path.abspath(options.output) + "/" + os.path.basename(srcpath) + ext
|
|
||||||
else:
|
|
||||||
filename = os.path.abspath(options.output) + "/" \
|
|
||||||
+ os.path.basename(os.path.splitext(srcpath)[0]) + ext
|
|
||||||
elif os.path.isdir(srcpath):
|
|
||||||
filename = srcpath + ext
|
|
||||||
else:
|
|
||||||
filename = os.path.splitext(srcpath)[0] + ext
|
|
||||||
if os.path.isfile(filename):
|
|
||||||
filename = os.path.splitext(filename)[0] + '_kcc' + ext
|
|
||||||
return filename
|
|
||||||
|
|
||||||
|
|
||||||
def checkOptions():
|
|
||||||
global options
|
|
||||||
# Landscape mode is only supported by Kindle Touch and Paperwhite.
|
|
||||||
if options.profile == 'K4T' or options.profile == 'KHD':
|
|
||||||
options.landscapemode = True
|
|
||||||
else:
|
|
||||||
options.landscapemode = False
|
|
||||||
# Older Kindle don't support Virtual Panel View. We providing them configuration that will fake that feature.
|
|
||||||
if options.profile == 'K3' or options.profile == 'K4NT':
|
|
||||||
# Real Panel View
|
|
||||||
options.panelview = True
|
|
||||||
else:
|
|
||||||
# Virtual Panel View
|
|
||||||
options.panelview = False
|
|
||||||
# Older Kindle don't need higher resolution files due lack of Panel View.
|
|
||||||
# Kindle Fire family have very high resolution. Bigger images are not needed.
|
|
||||||
if options.profile == 'K1' or options.profile == 'K2' or options.profile == 'KDX' or options.profile == 'KDXG'\
|
|
||||||
or options.profile == 'KF' or options.profile == 'KFHD' or options.profile == 'KFHD8':
|
|
||||||
options.nopanelviewhq = True
|
|
||||||
# Disabling grayscale conversion for Kindle Fire family.
|
|
||||||
# Forcing JPEG output. For now code can't provide color PNG files.
|
|
||||||
if options.profile == 'KF' or options.profile == 'KFHD' or options.profile == 'KFHD8':
|
|
||||||
options.forcecolor = True
|
|
||||||
options.forcepng = False
|
|
||||||
else:
|
|
||||||
options.forcecolor = False
|
|
||||||
# Mixing vertical and horizontal pages require real Panel View.
|
|
||||||
# Landscape mode don't work correcly without Virtual Panel View.
|
|
||||||
if options.rotate:
|
|
||||||
options.panelview = True
|
|
||||||
options.landscapemode = False
|
|
||||||
|
|
||||||
|
|
||||||
def getEpubPath():
|
|
||||||
global epub_path
|
|
||||||
return epub_path
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
freeze_support()
|
|
||||||
Copyright()
|
|
||||||
main(sys.argv[1:])
|
|
||||||
sys.exit(0)
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright (c) 2013 Ciro Mattia Gonano <ciromattia@gmail.com>
|
|
||||||
#
|
|
||||||
# Permission to use, copy, modify, and/or distribute this software for
|
|
||||||
# any purpose with or without fee is hereby granted, provided that the
|
|
||||||
# above copyright notice and this permission notice appear in all
|
|
||||||
# copies.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
|
||||||
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
|
||||||
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
|
||||||
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
|
|
||||||
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
||||||
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
# PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
__license__ = 'ISC'
|
|
||||||
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
|
|
||||||
from Tkinter import *
|
|
||||||
import tkFileDialog
|
|
||||||
import tkMessageBox
|
|
||||||
import ttk
|
|
||||||
import comic2ebook
|
|
||||||
import kindlestrip
|
|
||||||
from image import ProfileData
|
|
||||||
from subprocess import call
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import stat
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
|
|
||||||
class MainWindow:
|
|
||||||
|
|
||||||
def clear_files(self):
|
|
||||||
self.filelist = []
|
|
||||||
self.refresh_list()
|
|
||||||
|
|
||||||
def change_gamma(self):
|
|
||||||
if self.aEntry['state'] == DISABLED:
|
|
||||||
self.aEntry['state'] = NORMAL
|
|
||||||
else:
|
|
||||||
self.aEntry['state'] = DISABLED
|
|
||||||
|
|
||||||
def open_files(self):
|
|
||||||
filetypes = [('All files', '.*'), ('Comic files', ('*.cbr', '*.cbz', '*.zip', '*.rar', '*.pdf'))]
|
|
||||||
f = tkFileDialog.askopenfilenames(title="Choose files", filetypes=filetypes)
|
|
||||||
if not isinstance(f, tuple):
|
|
||||||
f = self.master.tk.splitlist(f)
|
|
||||||
self.filelist.extend(f)
|
|
||||||
self.refresh_list()
|
|
||||||
|
|
||||||
def open_folder(self):
|
|
||||||
f = tkFileDialog.askdirectory(title="Choose folder:")
|
|
||||||
self.filelist.extend([f])
|
|
||||||
self.refresh_list()
|
|
||||||
|
|
||||||
def refresh_list(self):
|
|
||||||
self.filelocation.config(state=NORMAL)
|
|
||||||
self.filelocation.delete(0, END)
|
|
||||||
for afile in self.filelist:
|
|
||||||
self.filelocation.insert(END, afile)
|
|
||||||
self.filelocation.config(state=DISABLED)
|
|
||||||
try:
|
|
||||||
if len(self.filelist) > 0:
|
|
||||||
self.submit['state'] = NORMAL
|
|
||||||
else:
|
|
||||||
self.submit['state'] = DISABLED
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def initialize(self):
|
|
||||||
self.filelocation = Listbox(self.master)
|
|
||||||
self.filelocation.grid(row=0, columnspan=4, sticky=W + E + N + S)
|
|
||||||
self.refresh_list()
|
|
||||||
|
|
||||||
self.clear_file = Button(self.master, text="Clear files", command=self.clear_files)
|
|
||||||
self.clear_file.grid(row=4, column=0, sticky=W + E + N + S)
|
|
||||||
self.open_file = Button(self.master, text="Add files", command=self.open_files)
|
|
||||||
self.open_file.grid(row=4, column=1, sticky=W + E + N + S)
|
|
||||||
self.open_folder = Button(self.master, text="Add folder", command=self.open_folder)
|
|
||||||
self.open_folder.grid(row=4, column=2, sticky=W + E + N + S)
|
|
||||||
|
|
||||||
self.profile = StringVar()
|
|
||||||
profiles = sorted(ProfileData.ProfileLabels.iterkeys())
|
|
||||||
self.profile.set(profiles[-1])
|
|
||||||
w = apply(OptionMenu, (self.master, self.profile) + tuple(profiles))
|
|
||||||
w.grid(row=4, column=3, sticky=W + E + N + S)
|
|
||||||
|
|
||||||
self.options = {
|
|
||||||
'Aepub_only': IntVar(None, 0),
|
|
||||||
'Bcbz_only': IntVar(None, 0),
|
|
||||||
'Cmangastyle': IntVar(None, 0),
|
|
||||||
'Dnopanelviewhq': IntVar(None, 0),
|
|
||||||
'Eimage_preprocess': IntVar(None, 0),
|
|
||||||
'Fforcepng': IntVar(None, 0),
|
|
||||||
'Gimage_gamma': DoubleVar(None, 0.0),
|
|
||||||
'Himage_upscale': IntVar(None, 0),
|
|
||||||
'Iimage_stretch': IntVar(None, 0),
|
|
||||||
'Jblack_borders': IntVar(None, 0),
|
|
||||||
'Krotate': IntVar(None, 0),
|
|
||||||
'Lnosplitrotate': IntVar(None, 0),
|
|
||||||
'Mcut_page_numbers': IntVar(None, 0)
|
|
||||||
}
|
|
||||||
self.optionlabels = {
|
|
||||||
'Aepub_only': "Generate EPUB only",
|
|
||||||
'Bcbz_only': "Generate CBZ only (skip EPUB/Mobi generation)",
|
|
||||||
'Cmangastyle': "Manga mode",
|
|
||||||
'Dnopanelviewhq': "Disable high quality Panel View",
|
|
||||||
'Eimage_preprocess': "Disable image optimizations",
|
|
||||||
'Fforcepng': "Create PNG files instead of JPEG",
|
|
||||||
'Gimage_gamma': "Custom gamma correction",
|
|
||||||
'Himage_upscale': "Allow image upscaling",
|
|
||||||
'Iimage_stretch': "Stretch images",
|
|
||||||
'Jblack_borders': "Use black borders (instead of white ones)",
|
|
||||||
'Krotate': "Rotate images (instead of splitting them)",
|
|
||||||
'Lnosplitrotate': "Disable both splitting and rotation",
|
|
||||||
'Mcut_page_numbers': "Disable page numbers cutting"
|
|
||||||
}
|
|
||||||
self.optionsButtons = {}
|
|
||||||
for key in sorted(self.options):
|
|
||||||
if isinstance(self.options[key], IntVar) or isinstance(self.options[key], BooleanVar):
|
|
||||||
self.optionsButtons[key] = Checkbutton(self.master, text=self.optionlabels[key],
|
|
||||||
variable=self.options[key])
|
|
||||||
self.optionsButtons[key].grid(columnspan=4, sticky=W + N + S)
|
|
||||||
elif isinstance(self.options[key], DoubleVar):
|
|
||||||
self.optionsButtons[key] = Checkbutton(self.master, text=self.optionlabels[key],
|
|
||||||
command=self.change_gamma)
|
|
||||||
self.optionsButtons[key].grid(columnspan=4, sticky=W + N + S)
|
|
||||||
self.aEntry = Entry(self.master, textvariable=self.options[key])
|
|
||||||
self.aEntry['state'] = DISABLED
|
|
||||||
self.aEntry.grid(column=3, row=(self.master.grid_size()[1] - 1), sticky=W + N + S)
|
|
||||||
|
|
||||||
self.submit = Button(self.master, text="CONVERT", command=self.start_conversion, fg="red", state=DISABLED)
|
|
||||||
self.submit.grid(columnspan=4, sticky=W + E + N + S)
|
|
||||||
aLabel = Label(self.master, text="File progress:", anchor=W, justify=LEFT)
|
|
||||||
aLabel.grid(column=0, sticky=E)
|
|
||||||
self.progress_file = ttk.Progressbar(orient=HORIZONTAL, length=200, mode='determinate', maximum=4)
|
|
||||||
self.progress_file.grid(column=1, columnspan=3, row=(self.master.grid_size()[1] - 1), sticky=W + E + N + S)
|
|
||||||
aLabel = Label(self.master, text="Overall progress:", anchor=W, justify=LEFT)
|
|
||||||
aLabel.grid(column=0, sticky=E)
|
|
||||||
self.progress_overall = ttk.Progressbar(orient=HORIZONTAL, length=200, mode='determinate')
|
|
||||||
self.progress_overall.grid(column=1, columnspan=3, row=(self.master.grid_size()[1] - 1), sticky=W + E + N + S)
|
|
||||||
|
|
||||||
retcode = call("kindlegen", shell=True)
|
|
||||||
if retcode == 1:
|
|
||||||
self.optionsButtons['Aepub_only'].select()
|
|
||||||
self.optionsButtons['Aepub_only']['state'] = DISABLED
|
|
||||||
|
|
||||||
def start_conversion(self):
|
|
||||||
self.submit['state'] = DISABLED
|
|
||||||
self.master.update()
|
|
||||||
self.convert()
|
|
||||||
self.submit['state'] = NORMAL
|
|
||||||
self.master.update()
|
|
||||||
|
|
||||||
def convert(self):
|
|
||||||
if len(self.filelist) < 1:
|
|
||||||
tkMessageBox.showwarning('No files selected!', "Please choose files to convert.")
|
|
||||||
return
|
|
||||||
profilekey = ProfileData.ProfileLabels[self.profile.get()]
|
|
||||||
argv = ["-p", profilekey]
|
|
||||||
if self.options['Bcbz_only'].get() == 1:
|
|
||||||
argv.append("-c")
|
|
||||||
if self.options['Cmangastyle'].get() == 1:
|
|
||||||
argv.append("-m")
|
|
||||||
if self.options['Dnopanelviewhq'].get() == 1:
|
|
||||||
argv.append("--nopanelviewhq")
|
|
||||||
if self.options['Eimage_preprocess'].get() == 1:
|
|
||||||
argv.append("--noprocessing")
|
|
||||||
if self.options['Fforcepng'].get() == 1:
|
|
||||||
argv.append("--forcepng")
|
|
||||||
if self.options['Gimage_gamma'].get() != 0.0:
|
|
||||||
argv.append("--gamma")
|
|
||||||
argv.append(self.options['Gimage_gamma'].get())
|
|
||||||
if self.options['Himage_upscale'].get() == 1:
|
|
||||||
argv.append("--upscale")
|
|
||||||
if self.options['Iimage_stretch'].get() == 1:
|
|
||||||
argv.append("--stretch")
|
|
||||||
if self.options['Jblack_borders'].get() == 1:
|
|
||||||
argv.append("--blackborders")
|
|
||||||
if self.options['Krotate'].get() == 1:
|
|
||||||
argv.append("--rotate")
|
|
||||||
if self.options['Lnosplitrotate'].get() == 1:
|
|
||||||
argv.append("--nosplitrotate")
|
|
||||||
if self.options['Mcut_page_numbers'].get() == 1:
|
|
||||||
argv.append("--nocutpagenumbers")
|
|
||||||
errors = False
|
|
||||||
left_files = len(self.filelist)
|
|
||||||
filenum = 0
|
|
||||||
self.progress_overall['value'] = 0
|
|
||||||
self.progress_overall['maximum'] = left_files
|
|
||||||
for entry in self.filelist:
|
|
||||||
self.progress_overall['value'] = filenum
|
|
||||||
self.progress_file['value'] = 1
|
|
||||||
self.master.update()
|
|
||||||
filenum += 1
|
|
||||||
subargv = list(argv)
|
|
||||||
try:
|
|
||||||
subargv.append(entry)
|
|
||||||
epub_path = comic2ebook.main(subargv)
|
|
||||||
self.progress_file['value'] = 2
|
|
||||||
self.master.update()
|
|
||||||
except Exception as err:
|
|
||||||
type_, value_, traceback_ = sys.exc_info()
|
|
||||||
tkMessageBox.showerror('KCC Error', "Error on file %s:\n%s\nTraceback:\n%s" %
|
|
||||||
(subargv[-1], str(err), traceback.format_tb(traceback_)))
|
|
||||||
errors = True
|
|
||||||
continue
|
|
||||||
if self.options['Aepub_only'].get() == 0 and self.options['Bcbz_only'].get() == 0:
|
|
||||||
try:
|
|
||||||
if os.path.getsize(epub_path) > 314572800:
|
|
||||||
# do not call kindlegen if source is bigger than 300MB
|
|
||||||
tkMessageBox.showwarning('KindleGen Warning',
|
|
||||||
"ePub file %s is bigger than 300MB, not suitable for kindlegen" %
|
|
||||||
epub_path)
|
|
||||||
continue
|
|
||||||
retcode = call("kindlegen \"" + epub_path + "\"", shell=True)
|
|
||||||
if retcode < 0:
|
|
||||||
print >>sys.stderr, "Child was terminated by signal", -retcode
|
|
||||||
else:
|
|
||||||
print >>sys.stderr, "Child returned", retcode
|
|
||||||
self.progress_file['value'] = 3
|
|
||||||
self.master.update()
|
|
||||||
except OSError as e:
|
|
||||||
tkMessageBox.showerror('KindleGen Error', "Error on file %s:\n%s" % (epub_path, e))
|
|
||||||
errors = True
|
|
||||||
continue
|
|
||||||
mobifile = epub_path.replace('.epub', '.mobi')
|
|
||||||
try:
|
|
||||||
shutil.move(mobifile, mobifile + '_tostrip')
|
|
||||||
kindlestrip.main((mobifile + '_tostrip', mobifile))
|
|
||||||
os.remove(mobifile + '_tostrip')
|
|
||||||
self.progress_file['value'] = 4
|
|
||||||
self.master.update()
|
|
||||||
except Exception, err:
|
|
||||||
tkMessageBox.showerror('KindleStrip Error', "Error on file %s:\n%s" % (mobifile, str(err)))
|
|
||||||
errors = True
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
self.progress_file['value'] = 4
|
|
||||||
self.master.update()
|
|
||||||
if errors:
|
|
||||||
tkMessageBox.showwarning("Done", "Conversion completed with errors.")
|
|
||||||
else:
|
|
||||||
tkMessageBox.showinfo("Done", "Conversion successful!")
|
|
||||||
# reset progressbars
|
|
||||||
self.progress_overall['value'] = 0
|
|
||||||
self.progress_file['value'] = 0
|
|
||||||
self.master.update()
|
|
||||||
|
|
||||||
def remove_readonly(self, fn, path):
|
|
||||||
if fn is os.rmdir:
|
|
||||||
os.chmod(path, stat.S_IWRITE)
|
|
||||||
os.rmdir(path)
|
|
||||||
elif fn is os.remove:
|
|
||||||
os.chmod(path, stat.S_IWRITE)
|
|
||||||
os.remove(path)
|
|
||||||
|
|
||||||
def __init__(self, master):
|
|
||||||
self.filelist = []
|
|
||||||
self.master = master
|
|
||||||
self.initialize()
|
|
||||||
@@ -1,389 +0,0 @@
|
|||||||
# Copyright (C) 2010 Alex Yatskov
|
|
||||||
# Copyright (C) 2011 Stanislav (proDOOMman) Kosolapov <prodoomman@gmail.com>
|
|
||||||
# Copyright (C) 2012-2013 Ciro Mattia Gonano <ciromattia@gmail.com>
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
__license__ = 'ISC'
|
|
||||||
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
|
|
||||||
import os
|
|
||||||
from PIL import Image, ImageOps, ImageStat, ImageChops
|
|
||||||
|
|
||||||
|
|
||||||
class ImageFlags:
|
|
||||||
Orient = 1 << 0
|
|
||||||
Resize = 1 << 1
|
|
||||||
Frame = 1 << 2
|
|
||||||
Quantize = 1 << 3
|
|
||||||
Stretch = 1 << 4
|
|
||||||
|
|
||||||
|
|
||||||
class ProfileData:
|
|
||||||
Palette4 = [
|
|
||||||
0x00, 0x00, 0x00,
|
|
||||||
0x55, 0x55, 0x55,
|
|
||||||
0xaa, 0xaa, 0xaa,
|
|
||||||
0xff, 0xff, 0xff
|
|
||||||
]
|
|
||||||
|
|
||||||
Palette15 = [
|
|
||||||
0x00, 0x00, 0x00,
|
|
||||||
0x11, 0x11, 0x11,
|
|
||||||
0x22, 0x22, 0x22,
|
|
||||||
0x33, 0x33, 0x33,
|
|
||||||
0x44, 0x44, 0x44,
|
|
||||||
0x55, 0x55, 0x55,
|
|
||||||
0x66, 0x66, 0x66,
|
|
||||||
0x77, 0x77, 0x77,
|
|
||||||
0x88, 0x88, 0x88,
|
|
||||||
0x99, 0x99, 0x99,
|
|
||||||
0xaa, 0xaa, 0xaa,
|
|
||||||
0xbb, 0xbb, 0xbb,
|
|
||||||
0xcc, 0xcc, 0xcc,
|
|
||||||
0xdd, 0xdd, 0xdd,
|
|
||||||
0xff, 0xff, 0xff,
|
|
||||||
]
|
|
||||||
|
|
||||||
Palette16 = [
|
|
||||||
0x00, 0x00, 0x00,
|
|
||||||
0x11, 0x11, 0x11,
|
|
||||||
0x22, 0x22, 0x22,
|
|
||||||
0x33, 0x33, 0x33,
|
|
||||||
0x44, 0x44, 0x44,
|
|
||||||
0x55, 0x55, 0x55,
|
|
||||||
0x66, 0x66, 0x66,
|
|
||||||
0x77, 0x77, 0x77,
|
|
||||||
0x88, 0x88, 0x88,
|
|
||||||
0x99, 0x99, 0x99,
|
|
||||||
0xaa, 0xaa, 0xaa,
|
|
||||||
0xbb, 0xbb, 0xbb,
|
|
||||||
0xcc, 0xcc, 0xcc,
|
|
||||||
0xdd, 0xdd, 0xdd,
|
|
||||||
0xee, 0xee, 0xee,
|
|
||||||
0xff, 0xff, 0xff,
|
|
||||||
]
|
|
||||||
|
|
||||||
Profiles = {
|
|
||||||
'K1': ("Kindle 1", (600, 800), Palette4, 1.8, (900, 1200)),
|
|
||||||
'K2': ("Kindle 2", (600, 800), Palette15, 1.8, (900, 1200)),
|
|
||||||
'K3': ("Kindle Keyboard", (600, 800), Palette16, 1.8, (900, 1200)),
|
|
||||||
'K4NT': ("Kindle Non-Touch", (600, 800), Palette16, 1.8, (900, 1200)),
|
|
||||||
'K4T': ("Kindle Touch", (600, 800), Palette16, 1.8, (900, 1200)),
|
|
||||||
'KHD': ("Kindle Paperwhite", (758, 1024), Palette16, 1.8, (1137, 1536)),
|
|
||||||
'KDX': ("Kindle DX", (824, 1200), Palette15, 1.8, (1236, 1800)),
|
|
||||||
'KDXG': ("Kindle DXG", (824, 1200), Palette16, 1.8, (1236, 1800)),
|
|
||||||
'KF': ("Kindle Fire", (600, 1024), Palette16, 1.0, (900, 1536)),
|
|
||||||
'KFHD': ("Kindle Fire HD 7\"", (800, 1280), Palette16, 1.0, (1200, 1920)),
|
|
||||||
'KFHD8': ("Kindle Fire HD 8.9\"", (1200, 1920), Palette16, 1.0, (1800, 2880))
|
|
||||||
}
|
|
||||||
|
|
||||||
ProfileLabels = {
|
|
||||||
"Kindle 1": 'K1',
|
|
||||||
"Kindle 2": 'K2',
|
|
||||||
"Kindle 3/Keyboard": 'K3',
|
|
||||||
"Kindle 4/Non-Touch": 'K4NT',
|
|
||||||
"Kindle 4/Touch": 'K4T',
|
|
||||||
"Kindle Paperwhite": 'KHD',
|
|
||||||
"Kindle DX": 'KDX',
|
|
||||||
"Kindle DXG": 'KDXG',
|
|
||||||
"Kindle Fire": 'KF',
|
|
||||||
"Kindle Fire HD 7\"": 'KFHD',
|
|
||||||
"Kindle Fire HD 8.9\"": 'KFHD8'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ComicPage:
|
|
||||||
def __init__(self, source, device):
|
|
||||||
try:
|
|
||||||
self.profile = device
|
|
||||||
self.profile_label, self.size, self.palette, self.gamma, self.panelviewsize = ProfileData.Profiles[device]
|
|
||||||
except KeyError:
|
|
||||||
raise RuntimeError('Unexpected output device %s' % device)
|
|
||||||
try:
|
|
||||||
self.origFileName = source
|
|
||||||
self.image = Image.open(source)
|
|
||||||
except IOError:
|
|
||||||
raise RuntimeError('Cannot read image file %s' % source)
|
|
||||||
self.image = self.image.convert('RGB')
|
|
||||||
|
|
||||||
def saveToDir(self, targetdir, forcepng, color, sufix=None):
|
|
||||||
filename = os.path.basename(self.origFileName)
|
|
||||||
try:
|
|
||||||
if not color:
|
|
||||||
self.image = self.image.convert('L') # convert to grayscale
|
|
||||||
# Sufix is used to recognise which files need horizontal Panel View.
|
|
||||||
if sufix == "R":
|
|
||||||
sufix = "_rotated"
|
|
||||||
else:
|
|
||||||
sufix = ""
|
|
||||||
os.remove(os.path.join(targetdir, filename))
|
|
||||||
if forcepng:
|
|
||||||
self.image.save(os.path.join(targetdir, os.path.splitext(filename)[0] + sufix + ".png"), "PNG")
|
|
||||||
else:
|
|
||||||
self.image.save(os.path.join(targetdir, os.path.splitext(filename)[0] + sufix + ".jpg"), "JPEG")
|
|
||||||
except IOError as e:
|
|
||||||
raise RuntimeError('Cannot write image in directory %s: %s' % (targetdir, e))
|
|
||||||
|
|
||||||
def optimizeImage(self, gamma):
|
|
||||||
if gamma < 0.1:
|
|
||||||
gamma = self.gamma
|
|
||||||
if gamma == 1.0:
|
|
||||||
self.image = ImageOps.autocontrast(self.image)
|
|
||||||
else:
|
|
||||||
self.image = ImageOps.autocontrast(Image.eval(self.image, lambda a: 255 * (a / 255.) ** gamma))
|
|
||||||
|
|
||||||
def quantizeImage(self):
|
|
||||||
self.image = self.image.convert('L') # convert to grayscale
|
|
||||||
self.image = self.image.convert("RGB") # convert back to RGB
|
|
||||||
colors = len(self.palette) / 3
|
|
||||||
if colors < 256:
|
|
||||||
self.palette += self.palette[:3] * (256 - colors)
|
|
||||||
palImg = Image.new('P', (1, 1))
|
|
||||||
palImg.putpalette(self.palette)
|
|
||||||
self.image = self.image.quantize(palette=palImg)
|
|
||||||
|
|
||||||
def resizeImage(self, upscale=False, stretch=False, black_borders=False, isSplit=False, toRight=False,
|
|
||||||
landscapeMode=False, noPanelViewHQ=False):
|
|
||||||
method = Image.ANTIALIAS
|
|
||||||
if black_borders:
|
|
||||||
fill = 'black'
|
|
||||||
else:
|
|
||||||
fill = 'white'
|
|
||||||
if noPanelViewHQ:
|
|
||||||
size = (self.size[0], self.size[1])
|
|
||||||
else:
|
|
||||||
size = (self.panelviewsize[0], self.panelviewsize[1])
|
|
||||||
if isSplit and landscapeMode:
|
|
||||||
upscale = True
|
|
||||||
if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
|
|
||||||
if not upscale:
|
|
||||||
borderw = (self.size[0] - self.image.size[0]) / 2
|
|
||||||
borderh = (self.size[1] - self.image.size[1]) / 2
|
|
||||||
self.image = ImageOps.expand(self.image, border=(borderw, borderh), fill=fill)
|
|
||||||
return self.image
|
|
||||||
else:
|
|
||||||
method = Image.BILINEAR
|
|
||||||
if stretch: # if stretching call directly resize() without other considerations.
|
|
||||||
self.image = self.image.resize(size, method)
|
|
||||||
return self.image
|
|
||||||
ratioDev = float(self.size[0]) / float(self.size[1])
|
|
||||||
if (float(self.image.size[0]) / float(self.image.size[1])) < ratioDev:
|
|
||||||
if isSplit and landscapeMode:
|
|
||||||
diff = int(self.image.size[1] * ratioDev) - self.image.size[0]
|
|
||||||
self.image = ImageOps.expand(self.image, border=(diff / 2, 0), fill=fill)
|
|
||||||
tempImg = Image.new(self.image.mode, (self.image.size[0] + diff, self.image.size[1]), fill)
|
|
||||||
if toRight:
|
|
||||||
tempImg.paste(self.image, (diff, 0))
|
|
||||||
else:
|
|
||||||
tempImg.paste(self.image, (0, 0))
|
|
||||||
self.image = tempImg
|
|
||||||
else:
|
|
||||||
diff = int(self.image.size[1] * ratioDev) - self.image.size[0]
|
|
||||||
self.image = ImageOps.expand(self.image, border=(diff / 2, 0), fill=fill)
|
|
||||||
elif (float(self.image.size[0]) / float(self.image.size[1])) > ratioDev:
|
|
||||||
diff = int(self.image.size[0] / ratioDev) - self.image.size[1]
|
|
||||||
self.image = ImageOps.expand(self.image, border=(0, diff / 2), fill=fill)
|
|
||||||
self.image = ImageOps.fit(self.image, size, method=method, centering=(0.5, 0.5))
|
|
||||||
return self.image
|
|
||||||
|
|
||||||
def splitPage(self, targetdir, righttoleft=False, rotate=False):
|
|
||||||
width, height = self.image.size
|
|
||||||
dstwidth, dstheight = self.size
|
|
||||||
#print "Image is %d x %d" % (width,height)
|
|
||||||
# only split if origin is not oriented the same as target
|
|
||||||
if (width > height) != (dstwidth > dstheight):
|
|
||||||
if rotate:
|
|
||||||
self.image = self.image.rotate(90)
|
|
||||||
return "R"
|
|
||||||
else:
|
|
||||||
if width > height:
|
|
||||||
# source is landscape, so split by the width
|
|
||||||
leftbox = (0, 0, width / 2, height)
|
|
||||||
rightbox = (width / 2, 0, width, height)
|
|
||||||
else:
|
|
||||||
# source is portrait and target is landscape, so split by the height
|
|
||||||
leftbox = (0, 0, width, height / 2)
|
|
||||||
rightbox = (0, height / 2, width, height)
|
|
||||||
filename = os.path.splitext(os.path.basename(self.origFileName))
|
|
||||||
fileone = targetdir + '/' + filename[0] + '-kcca' + filename[1]
|
|
||||||
filetwo = targetdir + '/' + filename[0] + '-kccb' + filename[1]
|
|
||||||
try:
|
|
||||||
if righttoleft:
|
|
||||||
pageone = self.image.crop(rightbox)
|
|
||||||
pagetwo = self.image.crop(leftbox)
|
|
||||||
else:
|
|
||||||
pageone = self.image.crop(leftbox)
|
|
||||||
pagetwo = self.image.crop(rightbox)
|
|
||||||
pageone.save(fileone)
|
|
||||||
pagetwo.save(filetwo)
|
|
||||||
os.remove(self.origFileName)
|
|
||||||
except IOError as e:
|
|
||||||
raise RuntimeError('Cannot write image in directory %s: %s' % (targetdir, e))
|
|
||||||
return fileone, filetwo
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def cutPageNumber(self):
|
|
||||||
if ImageChops.invert(self.image).getbbox() is not None:
|
|
||||||
widthImg, heightImg = self.image.size
|
|
||||||
delta = 2
|
|
||||||
diff = delta
|
|
||||||
fixedThreshold = 5
|
|
||||||
if ImageStat.Stat(self.image).var[0] < 2 * fixedThreshold:
|
|
||||||
return self.image
|
|
||||||
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] < fixedThreshold\
|
|
||||||
and diff < heightImg:
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
pageNumberCut1 = diff
|
|
||||||
if diff < delta:
|
|
||||||
diff = delta
|
|
||||||
oldStat = ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0]
|
|
||||||
diff += delta
|
|
||||||
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] - oldStat > 0\
|
|
||||||
and diff < heightImg / 4:
|
|
||||||
oldStat = ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0]
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
pageNumberCut2 = diff
|
|
||||||
diff += delta
|
|
||||||
oldStat = ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg - pageNumberCut2))).var[0]
|
|
||||||
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg - pageNumberCut2))).var[0]\
|
|
||||||
< fixedThreshold + oldStat and diff < heightImg / 4:
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
pageNumberCut3 = diff
|
|
||||||
delta = 5
|
|
||||||
diff = delta
|
|
||||||
while ImageStat.Stat(self.image.crop((0, heightImg - pageNumberCut2, diff, heightImg))).var[0] < fixedThreshold\
|
|
||||||
and diff < widthImg:
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
pageNumberX1 = diff
|
|
||||||
diff = delta
|
|
||||||
while ImageStat.Stat(self.image.crop((widthImg - diff, heightImg - pageNumberCut2,
|
|
||||||
widthImg, heightImg))).var[0] < fixedThreshold and diff < widthImg:
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
pageNumberX2 = widthImg - diff
|
|
||||||
if pageNumberCut3 - pageNumberCut1 > 2 * delta\
|
|
||||||
and float(pageNumberX2 - pageNumberX1) / float(pageNumberCut2 - pageNumberCut1) <= 9.0\
|
|
||||||
and ImageStat.Stat(self.image.crop((0, heightImg - pageNumberCut3, widthImg, heightImg))).var[0]\
|
|
||||||
/ ImageStat.Stat(self.image).var[0] < 0.1\
|
|
||||||
and pageNumberCut3 < heightImg / 4 - delta:
|
|
||||||
diff = pageNumberCut3
|
|
||||||
else:
|
|
||||||
diff = pageNumberCut1
|
|
||||||
self.image = self.image.crop((0, 0, widthImg, heightImg - diff))
|
|
||||||
return self.image
|
|
||||||
|
|
||||||
def cropWhiteSpace(self, threshold):
|
|
||||||
if ImageChops.invert(self.image).getbbox() is not None:
|
|
||||||
widthImg, heightImg = self.image.size
|
|
||||||
delta = 10
|
|
||||||
diff = delta
|
|
||||||
# top
|
|
||||||
while ImageStat.Stat(self.image.crop((0, 0, widthImg, diff))).var[0] < threshold and diff < heightImg:
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
# print "Top crop: %s"%diff
|
|
||||||
self.image = self.image.crop((0, diff, widthImg, heightImg))
|
|
||||||
widthImg, heightImg = self.image.size
|
|
||||||
diff = delta
|
|
||||||
# left
|
|
||||||
while ImageStat.Stat(self.image.crop((0, 0, diff, heightImg))).var[0] < threshold and diff < widthImg:
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
# print "Left crop: %s"%diff
|
|
||||||
self.image = self.image.crop((diff, 0, widthImg, heightImg))
|
|
||||||
widthImg, heightImg = self.image.size
|
|
||||||
diff = delta
|
|
||||||
# down
|
|
||||||
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] < threshold\
|
|
||||||
and diff < heightImg:
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
# print "Down crop: %s"%diff
|
|
||||||
self.image = self.image.crop((0, 0, widthImg, heightImg - diff))
|
|
||||||
widthImg, heightImg = self.image.size
|
|
||||||
diff = delta
|
|
||||||
# right
|
|
||||||
while ImageStat.Stat(self.image.crop((widthImg - diff, 0, widthImg, heightImg))).var[0] < threshold\
|
|
||||||
and diff < widthImg:
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
# print "Right crop: %s"%diff
|
|
||||||
self.image = self.image.crop((0, 0, widthImg - diff, heightImg))
|
|
||||||
# print "New size: %sx%s"%(self.image.size[0],self.image.size[1])
|
|
||||||
return self.image
|
|
||||||
|
|
||||||
# def addProgressbar(self, file_number, files_totalnumber, size, howoften):
|
|
||||||
# if file_number // howoften != float(file_number) / howoften:
|
|
||||||
# return self.image
|
|
||||||
# white = (255, 255, 255)
|
|
||||||
# black = (0, 0, 0)
|
|
||||||
# widthDev, heightDev = size
|
|
||||||
# widthImg, heightImg = self.image.size
|
|
||||||
# pastePt = (
|
|
||||||
# max(0, (widthDev - widthImg) / 2),
|
|
||||||
# max(0, (heightDev - heightImg) / 2)
|
|
||||||
# )
|
|
||||||
# imageBg = Image.new('RGB', size, white)
|
|
||||||
# imageBg.paste(self.image, pastePt)
|
|
||||||
# self.image = imageBg
|
|
||||||
# widthImg, heightImg = self.image.size
|
|
||||||
# draw = ImageDraw.Draw(self.image)
|
|
||||||
# #Black rectangle
|
|
||||||
# draw.rectangle([(0, heightImg - 3), (widthImg, heightImg)], outline=black, fill=black)
|
|
||||||
# #White rectangle
|
|
||||||
# draw.rectangle([(widthImg * file_number / files_totalnumber, heightImg - 3), (widthImg - 1, heightImg)],
|
|
||||||
# outline=black, fill=white)
|
|
||||||
# #Making notches
|
|
||||||
# for i in range(1, 10):
|
|
||||||
# if i <= (10 * file_number / files_totalnumber):
|
|
||||||
# notch_colour = white # White
|
|
||||||
# else:
|
|
||||||
# notch_colour = black # Black
|
|
||||||
# draw.line([(widthImg * float(i) / 10, heightImg - 3), (widthImg * float(i) / 10, heightImg)],
|
|
||||||
# fill=notch_colour)
|
|
||||||
# #The 50%
|
|
||||||
# if i == 5:
|
|
||||||
# draw.rectangle([(widthImg / 2 - 1, heightImg - 5), (widthImg / 2 + 1, heightImg)],
|
|
||||||
# outline=black, fill=notch_colour)
|
|
||||||
# return self.image
|
|
||||||
#
|
|
||||||
# def frameImage(self):
|
|
||||||
# foreground = tuple(self.palette[:3])
|
|
||||||
# background = tuple(self.palette[-3:])
|
|
||||||
# widthDev, heightDev = self.size
|
|
||||||
# widthImg, heightImg = self.image.size
|
|
||||||
# pastePt = (
|
|
||||||
# max(0, (widthDev - widthImg) / 2),
|
|
||||||
# max(0, (heightDev - heightImg) / 2)
|
|
||||||
# )
|
|
||||||
# corner1 = (
|
|
||||||
# pastePt[0] - 1,
|
|
||||||
# pastePt[1] - 1
|
|
||||||
# )
|
|
||||||
# corner2 = (
|
|
||||||
# pastePt[0] + widthImg + 1,
|
|
||||||
# pastePt[1] + heightImg + 1
|
|
||||||
# )
|
|
||||||
# imageBg = Image.new(self.image.mode, self.size, background)
|
|
||||||
# imageBg.paste(self.image, pastePt)
|
|
||||||
# draw = ImageDraw.Draw(imageBg)
|
|
||||||
# draw.rectangle([corner1, corner2], outline=foreground)
|
|
||||||
# self.image = imageBg
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
|
||||||
#
|
|
||||||
# This is a python script. You need a Python interpreter to run it.
|
|
||||||
# For example, ActiveState Python, which exists for windows.
|
|
||||||
#
|
|
||||||
# This script strips the penultimate record from a Mobipocket file.
|
|
||||||
# This is useful because the current KindleGen add a compressed copy
|
|
||||||
# of the source files used in this record, making the ebook produced
|
|
||||||
# about twice as big as it needs to be.
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# This is free and unencumbered software released into the public domain.
|
|
||||||
#
|
|
||||||
# Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
||||||
# distribute this software, either in source code form or as a compiled
|
|
||||||
# binary, for any purpose, commercial or non-commercial, and by any
|
|
||||||
# means.
|
|
||||||
#
|
|
||||||
# In jurisdictions that recognize copyright laws, the author or authors
|
|
||||||
# of this software dedicate any and all copyright interest in the
|
|
||||||
# software to the public domain. We make this dedication for the benefit
|
|
||||||
# of the public at large and to the detriment of our heirs and
|
|
||||||
# successors. We intend this dedication to be an overt act of
|
|
||||||
# relinquishment in perpetuity of all present and future rights to this
|
|
||||||
# software under copyright law.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
||||||
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
||||||
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
||||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
# OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
#
|
|
||||||
# For more information, please refer to <http://unlicense.org/>
|
|
||||||
#
|
|
||||||
# Written by Paul Durrant, 2010-2011, paul@durrant.co.uk, pdurrant on mobileread.com
|
|
||||||
# With enhancements by Kevin Hendricks, KevinH on mobileread.com
|
|
||||||
#
|
|
||||||
# Changelog
|
|
||||||
# 1.00 - Initial version
|
|
||||||
# 1.10 - Added an option to output the stripped data
|
|
||||||
# 1.20 - Added check for source files section (thanks Piquan)
|
|
||||||
# 1.30 - Added prelim Support for K8 style mobis
|
|
||||||
# 1.31 - removed the SRCS section but kept a 0 size entry for it
|
|
||||||
# 1.32 - removes the SRCS section and its entry, now updates metadata 121 if needed
|
|
||||||
# 1.33 - now uses and modifies mobiheader SRCS and CNT
|
|
||||||
# 1.34 - added credit for Kevin Hendricks
|
|
||||||
# 1.35 - fixed bug when more than one compilation (SRCS/CMET) records
|
|
||||||
|
|
||||||
__version__ = '1.35'
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import struct
|
|
||||||
import binascii
|
|
||||||
|
|
||||||
class Unbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
def write(self, data):
|
|
||||||
self.stream.write(data)
|
|
||||||
self.stream.flush()
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
|
|
||||||
class StripException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SectionStripper:
|
|
||||||
def loadSection(self, section):
|
|
||||||
if (section + 1 == self.num_sections):
|
|
||||||
endoff = len(self.data_file)
|
|
||||||
else:
|
|
||||||
endoff = self.sections[section + 1][0]
|
|
||||||
off = self.sections[section][0]
|
|
||||||
return self.data_file[off:endoff]
|
|
||||||
|
|
||||||
def patch(self, off, new):
|
|
||||||
self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
|
|
||||||
|
|
||||||
def strip(self, off, len):
|
|
||||||
self.data_file = self.data_file[:off] + self.data_file[off+len:]
|
|
||||||
|
|
||||||
def patchSection(self, section, new, in_off = 0):
|
|
||||||
if (section + 1 == self.num_sections):
|
|
||||||
endoff = len(self.data_file)
|
|
||||||
else:
|
|
||||||
endoff = self.sections[section + 1][0]
|
|
||||||
off = self.sections[section][0]
|
|
||||||
assert off + in_off + len(new) <= endoff
|
|
||||||
self.patch(off + in_off, new)
|
|
||||||
|
|
||||||
def updateEXTH121(self, srcs_secnum, srcs_cnt, mobiheader):
|
|
||||||
mobi_length, = struct.unpack('>L',mobiheader[0x14:0x18])
|
|
||||||
exth_flag, = struct.unpack('>L', mobiheader[0x80:0x84])
|
|
||||||
exth = 'NONE'
|
|
||||||
try:
|
|
||||||
if exth_flag & 0x40:
|
|
||||||
exth = mobiheader[16 + mobi_length:]
|
|
||||||
if (len(exth) >= 4) and (exth[:4] == 'EXTH'):
|
|
||||||
nitems, = struct.unpack('>I', exth[8:12])
|
|
||||||
pos = 12
|
|
||||||
for i in xrange(nitems):
|
|
||||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
|
||||||
# print type, size
|
|
||||||
if type == 121:
|
|
||||||
boundaryptr, =struct.unpack('>L',exth[pos+8: pos + size])
|
|
||||||
if srcs_secnum <= boundaryptr:
|
|
||||||
boundaryptr -= srcs_cnt
|
|
||||||
prefix = mobiheader[0:16 + mobi_length + pos + 8]
|
|
||||||
suffix = mobiheader[16 + mobi_length + pos + 8 + 4:]
|
|
||||||
nval = struct.pack('>L',boundaryptr)
|
|
||||||
mobiheader = prefix + nval + suffix
|
|
||||||
pos += size
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return mobiheader
|
|
||||||
|
|
||||||
def __init__(self, datain):
|
|
||||||
if datain[0x3C:0x3C+8] != 'BOOKMOBI':
|
|
||||||
raise StripException("invalid file format")
|
|
||||||
self.num_sections, = struct.unpack('>H', datain[76:78])
|
|
||||||
|
|
||||||
# get mobiheader and check SRCS section number and count
|
|
||||||
offset0, = struct.unpack_from('>L', datain, 78)
|
|
||||||
offset1, = struct.unpack_from('>L', datain, 86)
|
|
||||||
mobiheader = datain[offset0:offset1]
|
|
||||||
srcs_secnum, srcs_cnt = struct.unpack_from('>2L', mobiheader, 0xe0)
|
|
||||||
if srcs_secnum == 0xffffffff or srcs_cnt == 0:
|
|
||||||
raise StripException("File doesn't contain the sources section.")
|
|
||||||
|
|
||||||
print "Found SRCS section number %d, and count %d" % (srcs_secnum, srcs_cnt)
|
|
||||||
# find its offset and length
|
|
||||||
next = srcs_secnum + srcs_cnt
|
|
||||||
srcs_offset, flgval = struct.unpack_from('>2L', datain, 78+(srcs_secnum*8))
|
|
||||||
next_offset, flgval = struct.unpack_from('>2L', datain, 78+(next*8))
|
|
||||||
srcs_length = next_offset - srcs_offset
|
|
||||||
if datain[srcs_offset:srcs_offset+4] != 'SRCS':
|
|
||||||
raise StripException("SRCS section num does not point to SRCS.")
|
|
||||||
print " beginning at offset %0x and ending at offset %0x" % (srcs_offset, srcs_length)
|
|
||||||
|
|
||||||
# it appears bytes 68-71 always contain (2*num_sections) + 1
|
|
||||||
# this is not documented anyplace at all but it appears to be some sort of next
|
|
||||||
# available unique_id used to identify specific sections in the palm db
|
|
||||||
self.data_file = datain[:68] + struct.pack('>L',((self.num_sections-srcs_cnt)*2+1))
|
|
||||||
self.data_file += datain[72:76]
|
|
||||||
|
|
||||||
# write out the number of sections reduced by srtcs_cnt
|
|
||||||
self.data_file = self.data_file + struct.pack('>H',self.num_sections-srcs_cnt)
|
|
||||||
|
|
||||||
# we are going to remove srcs_cnt SRCS sections so the offset of every entry in the table
|
|
||||||
# up to the srcs secnum must begin 8 bytes earlier per section removed (each table entry is 8 )
|
|
||||||
delta = -8 * srcs_cnt
|
|
||||||
for i in xrange(srcs_secnum):
|
|
||||||
offset, flgval = struct.unpack_from('>2L', datain, 78+(i*8))
|
|
||||||
offset += delta
|
|
||||||
self.data_file += struct.pack('>L',offset) + struct.pack('>L',flgval)
|
|
||||||
|
|
||||||
# for every record after the srcs_cnt SRCS records we must start it
|
|
||||||
# earlier by 8*srcs_cnt + the length of the srcs sections themselves)
|
|
||||||
delta = delta - srcs_length
|
|
||||||
for i in xrange(srcs_secnum+srcs_cnt,self.num_sections):
|
|
||||||
offset, flgval = struct.unpack_from('>2L', datain, 78+(i*8))
|
|
||||||
offset += delta
|
|
||||||
flgval = 2 * (i - srcs_cnt)
|
|
||||||
self.data_file += struct.pack('>L',offset) + struct.pack('>L',flgval)
|
|
||||||
|
|
||||||
# now pad it out to begin right at the first offset
|
|
||||||
# typically this is 2 bytes of nulls
|
|
||||||
first_offset, flgval = struct.unpack_from('>2L', self.data_file, 78)
|
|
||||||
self.data_file += '\0' * (first_offset - len(self.data_file))
|
|
||||||
|
|
||||||
# now finally add on every thing up to the original src_offset
|
|
||||||
self.data_file += datain[offset0: srcs_offset]
|
|
||||||
|
|
||||||
# and everything afterwards
|
|
||||||
self.data_file += datain[srcs_offset+srcs_length:]
|
|
||||||
|
|
||||||
#store away the SRCS section in case the user wants it output
|
|
||||||
self.stripped_data_header = datain[srcs_offset:srcs_offset+16]
|
|
||||||
self.stripped_data = datain[srcs_offset+16:srcs_offset+srcs_length]
|
|
||||||
|
|
||||||
# update the number of sections count
|
|
||||||
self.num_section = self.num_sections - srcs_cnt
|
|
||||||
|
|
||||||
# update the srcs_secnum and srcs_cnt in the mobiheader
|
|
||||||
offset0, flgval0 = struct.unpack_from('>2L', self.data_file, 78)
|
|
||||||
offset1, flgval1 = struct.unpack_from('>2L', self.data_file, 86)
|
|
||||||
mobiheader = self.data_file[offset0:offset1]
|
|
||||||
mobiheader = mobiheader[:0xe0]+ struct.pack('>L', 0xffffffff) + struct.pack('>L', 0) + mobiheader[0xe8:]
|
|
||||||
|
|
||||||
# if K8 mobi, handle metadata 121 in old mobiheader
|
|
||||||
mobiheader = self.updateEXTH121(srcs_secnum, srcs_cnt, mobiheader)
|
|
||||||
self.data_file = self.data_file[0:offset0] + mobiheader + self.data_file[offset1:]
|
|
||||||
print "done"
|
|
||||||
|
|
||||||
def getResult(self):
|
|
||||||
return self.data_file
|
|
||||||
|
|
||||||
def getStrippedData(self):
|
|
||||||
return self.stripped_data
|
|
||||||
|
|
||||||
def getHeader(self):
|
|
||||||
return self.stripped_data_header
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
infile = argv[0]
|
|
||||||
outfile = argv[1]
|
|
||||||
data_file = file(infile, 'rb').read()
|
|
||||||
try:
|
|
||||||
strippedFile = SectionStripper(data_file)
|
|
||||||
file(outfile, 'wb').write(strippedFile.getResult())
|
|
||||||
print "Header Bytes: " + binascii.b2a_hex(strippedFile.getHeader())
|
|
||||||
if len(argv)==3:
|
|
||||||
file(argv[2], 'wb').write(strippedFile.getStrippedData())
|
|
||||||
except StripException, e:
|
|
||||||
print "Error: %s" % e
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
|
||||||
print ('KindleStrip v%(__version__)s. '
|
|
||||||
'Written 2010-2012 by Paul Durrant and Kevin Hendricks.' % globals())
|
|
||||||
if len(sys.argv)<3 or len(sys.argv)>4:
|
|
||||||
print "Strips the Sources record from Mobipocket ebooks"
|
|
||||||
print "For ebooks generated using KindleGen 1.1 and later that add the source"
|
|
||||||
print "Usage:"
|
|
||||||
print " %s <infile> <outfile> <strippeddatafile>" % sys.argv[0]
|
|
||||||
print "<strippeddatafile> is optional."
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
main(sys.argv[1:])
|
|
||||||
sys.exit(0)
|
|
||||||
@@ -0,0 +1,868 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
## Form generated from reading UI file 'KCC.ui'
|
||||||
|
##
|
||||||
|
## Created by: Qt User Interface Compiler version 6.9.3
|
||||||
|
##
|
||||||
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||||
|
QMetaObject, QObject, QPoint, QRect,
|
||||||
|
QSize, QTime, QUrl, Qt)
|
||||||
|
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||||
|
QFont, QFontDatabase, QGradient, QIcon,
|
||||||
|
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||||
|
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||||
|
from PySide6.QtWidgets import (QAbstractItemView, QApplication, QCheckBox, QComboBox,
|
||||||
|
QGridLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||||
|
QListWidget, QListWidgetItem, QMainWindow, QProgressBar,
|
||||||
|
QPushButton, QSizePolicy, QSlider, QSpinBox,
|
||||||
|
QStatusBar, QWidget)
|
||||||
|
from . import KCC_rc
|
||||||
|
|
||||||
|
class Ui_mainWindow(object):
|
||||||
|
def setupUi(self, mainWindow):
|
||||||
|
if not mainWindow.objectName():
|
||||||
|
mainWindow.setObjectName(u"mainWindow")
|
||||||
|
mainWindow.resize(566, 671)
|
||||||
|
icon = QIcon()
|
||||||
|
icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
mainWindow.setWindowIcon(icon)
|
||||||
|
self.centralWidget = QWidget(mainWindow)
|
||||||
|
self.centralWidget.setObjectName(u"centralWidget")
|
||||||
|
self.gridLayout = QGridLayout(self.centralWidget)
|
||||||
|
self.gridLayout.setObjectName(u"gridLayout")
|
||||||
|
self.gridLayout.setContentsMargins(-1, -1, -1, 5)
|
||||||
|
self.croppingWidget = QWidget(self.centralWidget)
|
||||||
|
self.croppingWidget.setObjectName(u"croppingWidget")
|
||||||
|
self.croppingWidget.setVisible(False)
|
||||||
|
self.gridLayout_5 = QGridLayout(self.croppingWidget)
|
||||||
|
self.gridLayout_5.setObjectName(u"gridLayout_5")
|
||||||
|
self.gridLayout_5.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.croppingPowerSlider = QSlider(self.croppingWidget)
|
||||||
|
self.croppingPowerSlider.setObjectName(u"croppingPowerSlider")
|
||||||
|
self.croppingPowerSlider.setMaximum(300)
|
||||||
|
self.croppingPowerSlider.setSingleStep(1)
|
||||||
|
self.croppingPowerSlider.setOrientation(Qt.Orientation.Horizontal)
|
||||||
|
|
||||||
|
self.gridLayout_5.addWidget(self.croppingPowerSlider, 0, 1, 1, 1)
|
||||||
|
|
||||||
|
self.preserveMarginBox = QSpinBox(self.croppingWidget)
|
||||||
|
self.preserveMarginBox.setObjectName(u"preserveMarginBox")
|
||||||
|
sizePolicy = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||||
|
sizePolicy.setHorizontalStretch(0)
|
||||||
|
sizePolicy.setVerticalStretch(0)
|
||||||
|
sizePolicy.setHeightForWidth(self.preserveMarginBox.sizePolicy().hasHeightForWidth())
|
||||||
|
self.preserveMarginBox.setSizePolicy(sizePolicy)
|
||||||
|
self.preserveMarginBox.setMaximum(99)
|
||||||
|
self.preserveMarginBox.setSingleStep(5)
|
||||||
|
self.preserveMarginBox.setValue(0)
|
||||||
|
|
||||||
|
self.gridLayout_5.addWidget(self.preserveMarginBox, 1, 1, 1, 1)
|
||||||
|
|
||||||
|
self.preserveMarginLabel = QLabel(self.croppingWidget)
|
||||||
|
self.preserveMarginLabel.setObjectName(u"preserveMarginLabel")
|
||||||
|
|
||||||
|
self.gridLayout_5.addWidget(self.preserveMarginLabel, 1, 0, 1, 1)
|
||||||
|
|
||||||
|
self.croppingPowerLabel = QLabel(self.croppingWidget)
|
||||||
|
self.croppingPowerLabel.setObjectName(u"croppingPowerLabel")
|
||||||
|
|
||||||
|
self.gridLayout_5.addWidget(self.croppingPowerLabel, 0, 0, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
|
self.gridLayout.addWidget(self.croppingWidget, 9, 0, 1, 2)
|
||||||
|
|
||||||
|
self.progressBar = QProgressBar(self.centralWidget)
|
||||||
|
self.progressBar.setObjectName(u"progressBar")
|
||||||
|
self.progressBar.setMinimumSize(QSize(0, 30))
|
||||||
|
font = QFont()
|
||||||
|
font.setBold(True)
|
||||||
|
self.progressBar.setFont(font)
|
||||||
|
self.progressBar.setVisible(False)
|
||||||
|
self.progressBar.setAlignment(Qt.AlignmentFlag.AlignJustify|Qt.AlignmentFlag.AlignVCenter)
|
||||||
|
|
||||||
|
self.gridLayout.addWidget(self.progressBar, 1, 0, 1, 2)
|
||||||
|
|
||||||
|
self.jpegQualityWidget = QWidget(self.centralWidget)
|
||||||
|
self.jpegQualityWidget.setObjectName(u"jpegQualityWidget")
|
||||||
|
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
|
||||||
|
sizePolicy1.setHorizontalStretch(0)
|
||||||
|
sizePolicy1.setVerticalStretch(0)
|
||||||
|
sizePolicy1.setHeightForWidth(self.jpegQualityWidget.sizePolicy().hasHeightForWidth())
|
||||||
|
self.jpegQualityWidget.setSizePolicy(sizePolicy1)
|
||||||
|
self.jpegQualityWidget.setVisible(False)
|
||||||
|
self.horizontalLayout_12 = QHBoxLayout(self.jpegQualityWidget)
|
||||||
|
self.horizontalLayout_12.setObjectName(u"horizontalLayout_12")
|
||||||
|
self.horizontalLayout_12.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.jpegQualityLabel = QLabel(self.jpegQualityWidget)
|
||||||
|
self.jpegQualityLabel.setObjectName(u"jpegQualityLabel")
|
||||||
|
|
||||||
|
self.horizontalLayout_12.addWidget(self.jpegQualityLabel)
|
||||||
|
|
||||||
|
self.jpegQualitySpinBox = QSpinBox(self.jpegQualityWidget)
|
||||||
|
self.jpegQualitySpinBox.setObjectName(u"jpegQualitySpinBox")
|
||||||
|
self.jpegQualitySpinBox.setMaximum(95)
|
||||||
|
self.jpegQualitySpinBox.setSingleStep(5)
|
||||||
|
self.jpegQualitySpinBox.setValue(85)
|
||||||
|
|
||||||
|
self.horizontalLayout_12.addWidget(self.jpegQualitySpinBox)
|
||||||
|
|
||||||
|
|
||||||
|
self.gridLayout.addWidget(self.jpegQualityWidget, 10, 0, 1, 1)
|
||||||
|
|
||||||
|
self.chunkSizeWidget = QWidget(self.centralWidget)
|
||||||
|
self.chunkSizeWidget.setObjectName(u"chunkSizeWidget")
|
||||||
|
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
|
||||||
|
sizePolicy2.setHorizontalStretch(0)
|
||||||
|
sizePolicy2.setVerticalStretch(0)
|
||||||
|
sizePolicy2.setHeightForWidth(self.chunkSizeWidget.sizePolicy().hasHeightForWidth())
|
||||||
|
self.chunkSizeWidget.setSizePolicy(sizePolicy2)
|
||||||
|
self.chunkSizeWidget.setVisible(False)
|
||||||
|
self.horizontalLayout_4 = QHBoxLayout(self.chunkSizeWidget)
|
||||||
|
self.horizontalLayout_4.setSpacing(0)
|
||||||
|
self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
|
||||||
|
self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.chunkSizeLabel = QLabel(self.chunkSizeWidget)
|
||||||
|
self.chunkSizeLabel.setObjectName(u"chunkSizeLabel")
|
||||||
|
sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
|
||||||
|
sizePolicy3.setHorizontalStretch(0)
|
||||||
|
sizePolicy3.setVerticalStretch(0)
|
||||||
|
sizePolicy3.setHeightForWidth(self.chunkSizeLabel.sizePolicy().hasHeightForWidth())
|
||||||
|
self.chunkSizeLabel.setSizePolicy(sizePolicy3)
|
||||||
|
|
||||||
|
self.horizontalLayout_4.addWidget(self.chunkSizeLabel)
|
||||||
|
|
||||||
|
self.chunkSizeBox = QSpinBox(self.chunkSizeWidget)
|
||||||
|
self.chunkSizeBox.setObjectName(u"chunkSizeBox")
|
||||||
|
self.chunkSizeBox.setMinimum(50)
|
||||||
|
self.chunkSizeBox.setMaximum(600)
|
||||||
|
self.chunkSizeBox.setValue(400)
|
||||||
|
|
||||||
|
self.horizontalLayout_4.addWidget(self.chunkSizeBox)
|
||||||
|
|
||||||
|
self.chunkSizeWarnLabel = QLabel(self.chunkSizeWidget)
|
||||||
|
self.chunkSizeWarnLabel.setObjectName(u"chunkSizeWarnLabel")
|
||||||
|
sizePolicy3.setHeightForWidth(self.chunkSizeWarnLabel.sizePolicy().hasHeightForWidth())
|
||||||
|
self.chunkSizeWarnLabel.setSizePolicy(sizePolicy3)
|
||||||
|
|
||||||
|
self.horizontalLayout_4.addWidget(self.chunkSizeWarnLabel)
|
||||||
|
|
||||||
|
|
||||||
|
self.gridLayout.addWidget(self.chunkSizeWidget, 6, 0, 1, 1)
|
||||||
|
|
||||||
|
self.jobList = QListWidget(self.centralWidget)
|
||||||
|
self.jobList.setObjectName(u"jobList")
|
||||||
|
self.jobList.setMinimumSize(QSize(0, 150))
|
||||||
|
self.jobList.setStyleSheet(u"")
|
||||||
|
self.jobList.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
|
||||||
|
self.jobList.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
||||||
|
self.jobList.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
||||||
|
|
||||||
|
self.gridLayout.addWidget(self.jobList, 2, 0, 1, 2)
|
||||||
|
|
||||||
|
self.toolWidget = QWidget(self.centralWidget)
|
||||||
|
self.toolWidget.setObjectName(u"toolWidget")
|
||||||
|
self.gridLayout_6 = QGridLayout(self.toolWidget)
|
||||||
|
self.gridLayout_6.setObjectName(u"gridLayout_6")
|
||||||
|
self.gridLayout_6.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.editorButton = QPushButton(self.toolWidget)
|
||||||
|
self.editorButton.setObjectName(u"editorButton")
|
||||||
|
self.editorButton.setMinimumSize(QSize(0, 30))
|
||||||
|
icon1 = QIcon()
|
||||||
|
icon1.addFile(u":/Other/icons/editor.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.editorButton.setIcon(icon1)
|
||||||
|
|
||||||
|
self.gridLayout_6.addWidget(self.editorButton, 0, 0, 1, 1)
|
||||||
|
|
||||||
|
self.kofiButton = QPushButton(self.toolWidget)
|
||||||
|
self.kofiButton.setObjectName(u"kofiButton")
|
||||||
|
self.kofiButton.setMinimumSize(QSize(0, 30))
|
||||||
|
icon2 = QIcon()
|
||||||
|
icon2.addFile(u":/Brand/icons/kofi_symbol.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.kofiButton.setIcon(icon2)
|
||||||
|
self.kofiButton.setIconSize(QSize(19, 16))
|
||||||
|
|
||||||
|
self.gridLayout_6.addWidget(self.kofiButton, 0, 1, 1, 1)
|
||||||
|
|
||||||
|
self.wikiButton = QPushButton(self.toolWidget)
|
||||||
|
self.wikiButton.setObjectName(u"wikiButton")
|
||||||
|
self.wikiButton.setMinimumSize(QSize(0, 30))
|
||||||
|
icon3 = QIcon()
|
||||||
|
icon3.addFile(u":/Other/icons/wiki.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.wikiButton.setIcon(icon3)
|
||||||
|
|
||||||
|
self.gridLayout_6.addWidget(self.wikiButton, 0, 2, 1, 1)
|
||||||
|
|
||||||
|
self.youtubeButton = QPushButton(self.toolWidget)
|
||||||
|
self.youtubeButton.setObjectName(u"youtubeButton")
|
||||||
|
self.youtubeButton.setMinimumSize(QSize(0, 30))
|
||||||
|
|
||||||
|
self.gridLayout_6.addWidget(self.youtubeButton, 1, 0, 1, 1)
|
||||||
|
|
||||||
|
self.humbleButton = QPushButton(self.toolWidget)
|
||||||
|
self.humbleButton.setObjectName(u"humbleButton")
|
||||||
|
self.humbleButton.setMinimumSize(QSize(0, 30))
|
||||||
|
icon4 = QIcon()
|
||||||
|
icon4.addFile(u":/Brand/icons/Humble_H-Red.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.humbleButton.setIcon(icon4)
|
||||||
|
|
||||||
|
self.gridLayout_6.addWidget(self.humbleButton, 1, 1, 1, 1)
|
||||||
|
|
||||||
|
self.discordButton = QPushButton(self.toolWidget)
|
||||||
|
self.discordButton.setObjectName(u"discordButton")
|
||||||
|
self.discordButton.setMinimumSize(QSize(0, 30))
|
||||||
|
|
||||||
|
self.gridLayout_6.addWidget(self.discordButton, 1, 2, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
|
self.gridLayout.addWidget(self.toolWidget, 0, 0, 1, 2)
|
||||||
|
|
||||||
|
self.optionWidget = QWidget(self.centralWidget)
|
||||||
|
self.optionWidget.setObjectName(u"optionWidget")
|
||||||
|
self.gridLayout_2 = QGridLayout(self.optionWidget)
|
||||||
|
self.gridLayout_2.setObjectName(u"gridLayout_2")
|
||||||
|
self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.noRotateBox = QCheckBox(self.optionWidget)
|
||||||
|
self.noRotateBox.setObjectName(u"noRotateBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1)
|
||||||
|
|
||||||
|
self.maximizeStrips = QCheckBox(self.optionWidget)
|
||||||
|
self.maximizeStrips.setObjectName(u"maximizeStrips")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
|
||||||
|
|
||||||
|
self.rotateBox = QCheckBox(self.optionWidget)
|
||||||
|
self.rotateBox.setObjectName(u"rotateBox")
|
||||||
|
self.rotateBox.setTristate(True)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1)
|
||||||
|
|
||||||
|
self.pngLegacyBox = QCheckBox(self.optionWidget)
|
||||||
|
self.pngLegacyBox.setObjectName(u"pngLegacyBox")
|
||||||
|
self.pngLegacyBox.setEnabled(False)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.pngLegacyBox, 11, 0, 1, 1)
|
||||||
|
|
||||||
|
self.interPanelCropBox = QCheckBox(self.optionWidget)
|
||||||
|
self.interPanelCropBox.setObjectName(u"interPanelCropBox")
|
||||||
|
self.interPanelCropBox.setTristate(True)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1)
|
||||||
|
|
||||||
|
self.titleEdit = QLineEdit(self.optionWidget)
|
||||||
|
self.titleEdit.setObjectName(u"titleEdit")
|
||||||
|
sizePolicy2.setHeightForWidth(self.titleEdit.sizePolicy().hasHeightForWidth())
|
||||||
|
self.titleEdit.setSizePolicy(sizePolicy2)
|
||||||
|
self.titleEdit.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
|
||||||
|
self.titleEdit.setClearButtonEnabled(False)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.titleEdit, 0, 0, 1, 1)
|
||||||
|
|
||||||
|
self.authorEdit = QLineEdit(self.optionWidget)
|
||||||
|
self.authorEdit.setObjectName(u"authorEdit")
|
||||||
|
sizePolicy2.setHeightForWidth(self.authorEdit.sizePolicy().hasHeightForWidth())
|
||||||
|
self.authorEdit.setSizePolicy(sizePolicy2)
|
||||||
|
self.authorEdit.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
|
||||||
|
self.authorEdit.setClearButtonEnabled(False)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.authorEdit, 0, 1, 1, 1)
|
||||||
|
|
||||||
|
self.webtoonBox = QCheckBox(self.optionWidget)
|
||||||
|
self.webtoonBox.setObjectName(u"webtoonBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
|
||||||
|
|
||||||
|
self.fileFusionBox = QCheckBox(self.optionWidget)
|
||||||
|
self.fileFusionBox.setObjectName(u"fileFusionBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.fileFusionBox, 6, 0, 1, 1)
|
||||||
|
|
||||||
|
self.deleteBox = QCheckBox(self.optionWidget)
|
||||||
|
self.deleteBox.setObjectName(u"deleteBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.deleteBox, 5, 1, 1, 1)
|
||||||
|
|
||||||
|
self.gammaBox = QCheckBox(self.optionWidget)
|
||||||
|
self.gammaBox.setObjectName(u"gammaBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
|
||||||
|
|
||||||
|
self.noQuantizeBox = QCheckBox(self.optionWidget)
|
||||||
|
self.noQuantizeBox.setObjectName(u"noQuantizeBox")
|
||||||
|
self.noQuantizeBox.setEnabled(False)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.noQuantizeBox, 10, 2, 1, 1)
|
||||||
|
|
||||||
|
self.eraseRainbowBox = QCheckBox(self.optionWidget)
|
||||||
|
self.eraseRainbowBox.setObjectName(u"eraseRainbowBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.eraseRainbowBox, 7, 2, 1, 1)
|
||||||
|
|
||||||
|
self.coverFillBox = QCheckBox(self.optionWidget)
|
||||||
|
self.coverFillBox.setObjectName(u"coverFillBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.coverFillBox, 9, 1, 1, 1)
|
||||||
|
|
||||||
|
self.rotateRightBox = QCheckBox(self.optionWidget)
|
||||||
|
self.rotateRightBox.setObjectName(u"rotateRightBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.rotateRightBox, 10, 1, 1, 1)
|
||||||
|
|
||||||
|
self.mangaBox = QCheckBox(self.optionWidget)
|
||||||
|
self.mangaBox.setObjectName(u"mangaBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
|
||||||
|
|
||||||
|
self.spreadShiftBox = QCheckBox(self.optionWidget)
|
||||||
|
self.spreadShiftBox.setObjectName(u"spreadShiftBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.spreadShiftBox, 5, 0, 1, 1)
|
||||||
|
|
||||||
|
self.croppingBox = QCheckBox(self.optionWidget)
|
||||||
|
self.croppingBox.setObjectName(u"croppingBox")
|
||||||
|
self.croppingBox.setTristate(True)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
|
||||||
|
|
||||||
|
self.jpegQualityBox = QCheckBox(self.optionWidget)
|
||||||
|
self.jpegQualityBox.setObjectName(u"jpegQualityBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.jpegQualityBox, 8, 0, 1, 1)
|
||||||
|
|
||||||
|
self.outputSplit = QCheckBox(self.optionWidget)
|
||||||
|
self.outputSplit.setObjectName(u"outputSplit")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
|
||||||
|
|
||||||
|
self.metadataTitleBox = QCheckBox(self.optionWidget)
|
||||||
|
self.metadataTitleBox.setObjectName(u"metadataTitleBox")
|
||||||
|
self.metadataTitleBox.setTristate(True)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.metadataTitleBox, 7, 0, 1, 1)
|
||||||
|
|
||||||
|
self.noSmartCoverCropBox = QCheckBox(self.optionWidget)
|
||||||
|
self.noSmartCoverCropBox.setObjectName(u"noSmartCoverCropBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.noSmartCoverCropBox, 11, 1, 1, 1)
|
||||||
|
|
||||||
|
self.rotateFirstBox = QCheckBox(self.optionWidget)
|
||||||
|
self.rotateFirstBox.setObjectName(u"rotateFirstBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.rotateFirstBox, 8, 1, 1, 1)
|
||||||
|
|
||||||
|
self.mozJpegBox = QCheckBox(self.optionWidget)
|
||||||
|
self.mozJpegBox.setObjectName(u"mozJpegBox")
|
||||||
|
self.mozJpegBox.setTristate(True)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1)
|
||||||
|
|
||||||
|
self.autoLevelBox = QCheckBox(self.optionWidget)
|
||||||
|
self.autoLevelBox.setObjectName(u"autoLevelBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.autoLevelBox, 8, 2, 1, 1)
|
||||||
|
|
||||||
|
self.forcePngRgbBox = QCheckBox(self.optionWidget)
|
||||||
|
self.forcePngRgbBox.setObjectName(u"forcePngRgbBox")
|
||||||
|
self.forcePngRgbBox.setEnabled(False)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.forcePngRgbBox, 11, 2, 1, 1)
|
||||||
|
|
||||||
|
self.upscaleBox = QCheckBox(self.optionWidget)
|
||||||
|
self.upscaleBox.setObjectName(u"upscaleBox")
|
||||||
|
self.upscaleBox.setTristate(True)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.upscaleBox, 2, 1, 1, 1)
|
||||||
|
|
||||||
|
self.borderBox = QCheckBox(self.optionWidget)
|
||||||
|
self.borderBox.setObjectName(u"borderBox")
|
||||||
|
self.borderBox.setTristate(True)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
|
||||||
|
|
||||||
|
self.qualityBox = QCheckBox(self.optionWidget)
|
||||||
|
self.qualityBox.setObjectName(u"qualityBox")
|
||||||
|
self.qualityBox.setTristate(True)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.qualityBox, 1, 2, 1, 1)
|
||||||
|
|
||||||
|
self.pdfExtractBox = QCheckBox(self.optionWidget)
|
||||||
|
self.pdfExtractBox.setObjectName(u"pdfExtractBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.pdfExtractBox, 9, 0, 1, 1)
|
||||||
|
|
||||||
|
self.colorBox = QCheckBox(self.optionWidget)
|
||||||
|
self.colorBox.setObjectName(u"colorBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.colorBox, 3, 2, 1, 1)
|
||||||
|
|
||||||
|
self.pdfWidthBox = QCheckBox(self.optionWidget)
|
||||||
|
self.pdfWidthBox.setObjectName(u"pdfWidthBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.pdfWidthBox, 10, 0, 1, 1)
|
||||||
|
|
||||||
|
self.disableProcessingBox = QCheckBox(self.optionWidget)
|
||||||
|
self.disableProcessingBox.setObjectName(u"disableProcessingBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.disableProcessingBox, 5, 2, 1, 1)
|
||||||
|
|
||||||
|
self.outputFolderWidget = QWidget(self.optionWidget)
|
||||||
|
self.outputFolderWidget.setObjectName(u"outputFolderWidget")
|
||||||
|
self.horizontalLayout_3 = QHBoxLayout(self.outputFolderWidget)
|
||||||
|
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
|
||||||
|
self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.defaultOutputFolderBox = QCheckBox(self.outputFolderWidget)
|
||||||
|
self.defaultOutputFolderBox.setObjectName(u"defaultOutputFolderBox")
|
||||||
|
sizePolicy.setHeightForWidth(self.defaultOutputFolderBox.sizePolicy().hasHeightForWidth())
|
||||||
|
self.defaultOutputFolderBox.setSizePolicy(sizePolicy)
|
||||||
|
self.defaultOutputFolderBox.setTristate(True)
|
||||||
|
|
||||||
|
self.horizontalLayout_3.addWidget(self.defaultOutputFolderBox)
|
||||||
|
|
||||||
|
self.defaultOutputFolderButton = QPushButton(self.outputFolderWidget)
|
||||||
|
self.defaultOutputFolderButton.setObjectName(u"defaultOutputFolderButton")
|
||||||
|
self.defaultOutputFolderButton.setMinimumSize(QSize(0, 30))
|
||||||
|
icon5 = QIcon()
|
||||||
|
icon5.addFile(u":/Other/icons/folder_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.defaultOutputFolderButton.setIcon(icon5)
|
||||||
|
|
||||||
|
self.horizontalLayout_3.addWidget(self.defaultOutputFolderButton)
|
||||||
|
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.outputFolderWidget, 0, 2, 1, 1)
|
||||||
|
|
||||||
|
self.chunkSizeCheckBox = QCheckBox(self.optionWidget)
|
||||||
|
self.chunkSizeCheckBox.setObjectName(u"chunkSizeCheckBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 1, 1)
|
||||||
|
|
||||||
|
self.autocontrastBox = QCheckBox(self.optionWidget)
|
||||||
|
self.autocontrastBox.setObjectName(u"autocontrastBox")
|
||||||
|
self.autocontrastBox.setTristate(True)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.autocontrastBox, 9, 2, 1, 1)
|
||||||
|
|
||||||
|
self.webpBox = QCheckBox(self.optionWidget)
|
||||||
|
self.webpBox.setObjectName(u"webpBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.webpBox, 12, 0, 1, 1)
|
||||||
|
|
||||||
|
self.tempDirBox = QCheckBox(self.optionWidget)
|
||||||
|
self.tempDirBox.setObjectName(u"tempDirBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.tempDirBox, 12, 1, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
|
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
|
||||||
|
|
||||||
|
self.buttonWidget = QWidget(self.centralWidget)
|
||||||
|
self.buttonWidget.setObjectName(u"buttonWidget")
|
||||||
|
sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
||||||
|
sizePolicy4.setHorizontalStretch(0)
|
||||||
|
sizePolicy4.setVerticalStretch(0)
|
||||||
|
sizePolicy4.setHeightForWidth(self.buttonWidget.sizePolicy().hasHeightForWidth())
|
||||||
|
self.buttonWidget.setSizePolicy(sizePolicy4)
|
||||||
|
self.gridLayout_4 = QGridLayout(self.buttonWidget)
|
||||||
|
self.gridLayout_4.setObjectName(u"gridLayout_4")
|
||||||
|
self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.convertButton = QPushButton(self.buttonWidget)
|
||||||
|
self.convertButton.setObjectName(u"convertButton")
|
||||||
|
self.convertButton.setMinimumSize(QSize(0, 30))
|
||||||
|
self.convertButton.setFont(font)
|
||||||
|
icon6 = QIcon()
|
||||||
|
icon6.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.convertButton.setIcon(icon6)
|
||||||
|
|
||||||
|
self.gridLayout_4.addWidget(self.convertButton, 1, 3, 1, 1)
|
||||||
|
|
||||||
|
self.clearButton = QPushButton(self.buttonWidget)
|
||||||
|
self.clearButton.setObjectName(u"clearButton")
|
||||||
|
self.clearButton.setMinimumSize(QSize(0, 30))
|
||||||
|
icon7 = QIcon()
|
||||||
|
icon7.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.clearButton.setIcon(icon7)
|
||||||
|
|
||||||
|
self.gridLayout_4.addWidget(self.clearButton, 0, 3, 1, 1)
|
||||||
|
|
||||||
|
self.deviceBox = QComboBox(self.buttonWidget)
|
||||||
|
self.deviceBox.setObjectName(u"deviceBox")
|
||||||
|
self.deviceBox.setMinimumSize(QSize(0, 28))
|
||||||
|
|
||||||
|
self.gridLayout_4.addWidget(self.deviceBox, 1, 1, 1, 1)
|
||||||
|
|
||||||
|
self.fileButton = QPushButton(self.buttonWidget)
|
||||||
|
self.fileButton.setObjectName(u"fileButton")
|
||||||
|
self.fileButton.setMinimumSize(QSize(0, 30))
|
||||||
|
icon8 = QIcon()
|
||||||
|
icon8.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.fileButton.setIcon(icon8)
|
||||||
|
|
||||||
|
self.gridLayout_4.addWidget(self.fileButton, 0, 1, 1, 1)
|
||||||
|
|
||||||
|
self.directoryButton = QPushButton(self.buttonWidget)
|
||||||
|
self.directoryButton.setObjectName(u"directoryButton")
|
||||||
|
sizePolicy5 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)
|
||||||
|
sizePolicy5.setHorizontalStretch(0)
|
||||||
|
sizePolicy5.setVerticalStretch(0)
|
||||||
|
sizePolicy5.setHeightForWidth(self.directoryButton.sizePolicy().hasHeightForWidth())
|
||||||
|
self.directoryButton.setSizePolicy(sizePolicy5)
|
||||||
|
self.directoryButton.setIcon(icon5)
|
||||||
|
|
||||||
|
self.gridLayout_4.addWidget(self.directoryButton, 0, 4, 1, 1)
|
||||||
|
|
||||||
|
self.formatBox = QComboBox(self.buttonWidget)
|
||||||
|
self.formatBox.setObjectName(u"formatBox")
|
||||||
|
self.formatBox.setMinimumSize(QSize(0, 28))
|
||||||
|
|
||||||
|
self.gridLayout_4.addWidget(self.formatBox, 1, 4, 1, 1)
|
||||||
|
|
||||||
|
self.clearButton.raise_()
|
||||||
|
self.deviceBox.raise_()
|
||||||
|
self.convertButton.raise_()
|
||||||
|
self.fileButton.raise_()
|
||||||
|
self.directoryButton.raise_()
|
||||||
|
self.formatBox.raise_()
|
||||||
|
|
||||||
|
self.gridLayout.addWidget(self.buttonWidget, 3, 0, 1, 2)
|
||||||
|
|
||||||
|
self.gammaWidget = QWidget(self.centralWidget)
|
||||||
|
self.gammaWidget.setObjectName(u"gammaWidget")
|
||||||
|
self.gammaWidget.setVisible(False)
|
||||||
|
self.horizontalLayout_2 = QHBoxLayout(self.gammaWidget)
|
||||||
|
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
|
||||||
|
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.gammaLabel = QLabel(self.gammaWidget)
|
||||||
|
self.gammaLabel.setObjectName(u"gammaLabel")
|
||||||
|
|
||||||
|
self.horizontalLayout_2.addWidget(self.gammaLabel)
|
||||||
|
|
||||||
|
self.gammaSlider = QSlider(self.gammaWidget)
|
||||||
|
self.gammaSlider.setObjectName(u"gammaSlider")
|
||||||
|
self.gammaSlider.setMaximum(250)
|
||||||
|
self.gammaSlider.setSingleStep(5)
|
||||||
|
self.gammaSlider.setOrientation(Qt.Orientation.Horizontal)
|
||||||
|
|
||||||
|
self.horizontalLayout_2.addWidget(self.gammaSlider)
|
||||||
|
|
||||||
|
|
||||||
|
self.gridLayout.addWidget(self.gammaWidget, 7, 0, 1, 2)
|
||||||
|
|
||||||
|
self.customWidget = QWidget(self.centralWidget)
|
||||||
|
self.customWidget.setObjectName(u"customWidget")
|
||||||
|
self.customWidget.setVisible(False)
|
||||||
|
self.gridLayout_3 = QGridLayout(self.customWidget)
|
||||||
|
self.gridLayout_3.setObjectName(u"gridLayout_3")
|
||||||
|
self.gridLayout_3.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.hLabel = QLabel(self.customWidget)
|
||||||
|
self.hLabel.setObjectName(u"hLabel")
|
||||||
|
sizePolicy1.setHeightForWidth(self.hLabel.sizePolicy().hasHeightForWidth())
|
||||||
|
self.hLabel.setSizePolicy(sizePolicy1)
|
||||||
|
|
||||||
|
self.gridLayout_3.addWidget(self.hLabel, 0, 2, 1, 1)
|
||||||
|
|
||||||
|
self.widthBox = QSpinBox(self.customWidget)
|
||||||
|
self.widthBox.setObjectName(u"widthBox")
|
||||||
|
self.widthBox.setMaximum(6000)
|
||||||
|
|
||||||
|
self.gridLayout_3.addWidget(self.widthBox, 0, 1, 1, 1)
|
||||||
|
|
||||||
|
self.wLabel = QLabel(self.customWidget)
|
||||||
|
self.wLabel.setObjectName(u"wLabel")
|
||||||
|
sizePolicy1.setHeightForWidth(self.wLabel.sizePolicy().hasHeightForWidth())
|
||||||
|
self.wLabel.setSizePolicy(sizePolicy1)
|
||||||
|
|
||||||
|
self.gridLayout_3.addWidget(self.wLabel, 0, 0, 1, 1)
|
||||||
|
|
||||||
|
self.heightBox = QSpinBox(self.customWidget)
|
||||||
|
self.heightBox.setObjectName(u"heightBox")
|
||||||
|
self.heightBox.setMaximum(8000)
|
||||||
|
|
||||||
|
self.gridLayout_3.addWidget(self.heightBox, 0, 3, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
|
self.gridLayout.addWidget(self.customWidget, 8, 0, 1, 2)
|
||||||
|
|
||||||
|
mainWindow.setCentralWidget(self.centralWidget)
|
||||||
|
self.statusBar = QStatusBar(mainWindow)
|
||||||
|
self.statusBar.setObjectName(u"statusBar")
|
||||||
|
self.statusBar.setSizeGripEnabled(False)
|
||||||
|
mainWindow.setStatusBar(self.statusBar)
|
||||||
|
QWidget.setTabOrder(self.jobList, self.fileButton)
|
||||||
|
QWidget.setTabOrder(self.fileButton, self.clearButton)
|
||||||
|
QWidget.setTabOrder(self.clearButton, self.deviceBox)
|
||||||
|
QWidget.setTabOrder(self.deviceBox, self.widthBox)
|
||||||
|
QWidget.setTabOrder(self.widthBox, self.heightBox)
|
||||||
|
QWidget.setTabOrder(self.heightBox, self.convertButton)
|
||||||
|
QWidget.setTabOrder(self.convertButton, self.mangaBox)
|
||||||
|
QWidget.setTabOrder(self.mangaBox, self.rotateBox)
|
||||||
|
QWidget.setTabOrder(self.rotateBox, self.qualityBox)
|
||||||
|
QWidget.setTabOrder(self.qualityBox, self.webtoonBox)
|
||||||
|
QWidget.setTabOrder(self.webtoonBox, self.upscaleBox)
|
||||||
|
QWidget.setTabOrder(self.upscaleBox, self.gammaBox)
|
||||||
|
QWidget.setTabOrder(self.gammaBox, self.gammaSlider)
|
||||||
|
QWidget.setTabOrder(self.gammaSlider, self.borderBox)
|
||||||
|
QWidget.setTabOrder(self.borderBox, self.outputSplit)
|
||||||
|
QWidget.setTabOrder(self.outputSplit, self.colorBox)
|
||||||
|
QWidget.setTabOrder(self.colorBox, self.mozJpegBox)
|
||||||
|
QWidget.setTabOrder(self.mozJpegBox, self.maximizeStrips)
|
||||||
|
QWidget.setTabOrder(self.maximizeStrips, self.croppingBox)
|
||||||
|
QWidget.setTabOrder(self.croppingBox, self.croppingPowerSlider)
|
||||||
|
QWidget.setTabOrder(self.croppingPowerSlider, self.preserveMarginBox)
|
||||||
|
QWidget.setTabOrder(self.preserveMarginBox, self.spreadShiftBox)
|
||||||
|
QWidget.setTabOrder(self.spreadShiftBox, self.deleteBox)
|
||||||
|
QWidget.setTabOrder(self.deleteBox, self.disableProcessingBox)
|
||||||
|
QWidget.setTabOrder(self.disableProcessingBox, self.fileFusionBox)
|
||||||
|
QWidget.setTabOrder(self.fileFusionBox, self.noRotateBox)
|
||||||
|
QWidget.setTabOrder(self.noRotateBox, self.interPanelCropBox)
|
||||||
|
QWidget.setTabOrder(self.interPanelCropBox, self.metadataTitleBox)
|
||||||
|
QWidget.setTabOrder(self.metadataTitleBox, self.coverFillBox)
|
||||||
|
QWidget.setTabOrder(self.coverFillBox, self.chunkSizeCheckBox)
|
||||||
|
QWidget.setTabOrder(self.chunkSizeCheckBox, self.chunkSizeBox)
|
||||||
|
QWidget.setTabOrder(self.chunkSizeBox, self.eraseRainbowBox)
|
||||||
|
QWidget.setTabOrder(self.eraseRainbowBox, self.rotateFirstBox)
|
||||||
|
QWidget.setTabOrder(self.rotateFirstBox, self.autoLevelBox)
|
||||||
|
QWidget.setTabOrder(self.autoLevelBox, self.autocontrastBox)
|
||||||
|
QWidget.setTabOrder(self.autocontrastBox, self.editorButton)
|
||||||
|
QWidget.setTabOrder(self.editorButton, self.kofiButton)
|
||||||
|
QWidget.setTabOrder(self.kofiButton, self.wikiButton)
|
||||||
|
|
||||||
|
self.retranslateUi(mainWindow)
|
||||||
|
|
||||||
|
QMetaObject.connectSlotsByName(mainWindow)
|
||||||
|
# setupUi
|
||||||
|
|
||||||
|
def retranslateUi(self, mainWindow):
|
||||||
|
mainWindow.setWindowTitle(QCoreApplication.translate("mainWindow", u"Kindle Comic Converter", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.preserveMarginLabel.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>After calculating the cropping boundaries, "back up" a specified percentage amount.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.preserveMarginLabel.setText(QCoreApplication.translate("mainWindow", u"Preserve Margin %", None))
|
||||||
|
self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None))
|
||||||
|
self.jpegQualityLabel.setText(QCoreApplication.translate("mainWindow", u"JPEG Quality:", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.chunkSizeWidget.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Warning: chunk size greater than default may cause<br/>performance/battery issues, especially on older devices.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.chunkSizeLabel.setText(QCoreApplication.translate("mainWindow", u"Chunk size MB:", None))
|
||||||
|
self.chunkSizeWarnLabel.setText(QCoreApplication.translate("mainWindow", u"Greater than default may cause performance issues on older ereaders.", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.jobList.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Double click on source to open it in metadata editor.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.editorButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to edit directory.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.editorButton.setText(QCoreApplication.translate("mainWindow", u"Metadata Editor", None))
|
||||||
|
self.kofiButton.setText(QCoreApplication.translate("mainWindow", u"Support me on Ko-fi", None))
|
||||||
|
self.wikiButton.setText(QCoreApplication.translate("mainWindow", u"Wiki", None))
|
||||||
|
self.youtubeButton.setText(QCoreApplication.translate("mainWindow", u"YouTube", None))
|
||||||
|
self.humbleButton.setText(QCoreApplication.translate("mainWindow", u"Humble Bundle Referral", None))
|
||||||
|
self.discordButton.setText(QCoreApplication.translate("mainWindow", u"Discord", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.noRotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"Do not rotate double page spreads in spread splitter option.", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.maximizeStrips.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 1x4<br/></span>Keep format 1x4 panels strips.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 2x2<br/></span>Turn 1x4 strips to 2x2 to maximize screen usage.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.rotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Split<br/></span>Double page spreads will be cut into two separate pages.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Split and rotate<br/></span>Double page spreads will be displayed twice. First split and then rotated. </p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate<br/></span>Double page spreads will be rotated.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.pngLegacyBox.setToolTip(QCoreApplication.translate("mainWindow", u"Use a more compatible 8 bit PNG instead of 4 bit.", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.pngLegacyBox.setText(QCoreApplication.translate("mainWindow", u"PNG Legacy Mode", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.interPanelCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled<br/></span>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Horizontal<br/></span>Crop empty horizontal lines.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Both<br/></span>Crop empty horizontal and vertical lines.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.interPanelCropBox.setText(QCoreApplication.translate("mainWindow", u"Inter-panel crop", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.titleEdit.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Default Title</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.titleEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Title", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.webtoonBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.fileFusionBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Combines all selected files into a single file. (Helpful for combining chapters into volumes.)</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.fileFusionBox.setText(QCoreApplication.translate("mainWindow", u"File Fusion", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.deleteBox.setToolTip(QCoreApplication.translate("mainWindow", u"Delete input file(s) or directory. It's not recoverable!", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.deleteBox.setText(QCoreApplication.translate("mainWindow", u"Delete input", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.gammaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Set a custom gamma correction.</p><p>1.0 is default (disabled).<br/>< 1.0 makes the image brighter.<br/>> 1.0 makes the image darker. </p><p>1.8 was the default in KCC 9.1.0 and earlier.</p><p>Use if you want to make midtones darker.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.gammaBox.setText(QCoreApplication.translate("mainWindow", u"Custom gamma", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.noQuantizeBox.setToolTip(QCoreApplication.translate("mainWindow", u"Don't quantize PNG images to 16 colors (4 bit)\n"
|
||||||
|
"\n"
|
||||||
|
"This will double file size but preserve all 256 colors (8 bit).\n"
|
||||||
|
"\n"
|
||||||
|
"Eink only has 16 shades of gray so you probably don't want this.", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.noQuantizeBox.setText(QCoreApplication.translate("mainWindow", u"No Quantize", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.eraseRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Erase rainbow effect on color eink screen by attenuating interfering frequencies", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.eraseRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Rainbow eraser", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.coverFillBox.setToolTip(QCoreApplication.translate("mainWindow", u"Resize cover to exact device resolution by center-cropping to aspect ratio first.\n"
|
||||||
|
"May crop top/bottom or left/right depending on source aspect ratio. Not implemented for Kindle Scribe.", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.coverFillBox.setText(QCoreApplication.translate("mainWindow", u"Cover Fill", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.rotateRightBox.setToolTip(QCoreApplication.translate("mainWindow", u"Rotate 2 page spreads in opposite direction than normal.", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.rotateRightBox.setText(QCoreApplication.translate("mainWindow", u"Rotate Right", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.mangaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left (manga)", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.spreadShiftBox.setToolTip(QCoreApplication.translate("mainWindow", u"Shift first page to opposite side in landscape for two page spread alignment", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.spreadShiftBox.setText(QCoreApplication.translate("mainWindow", u"Spread shift", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.croppingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled</span></p><p>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Margins<br/></span>Margins</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Margins + page numbers<br/></span>Margins +page numbers</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.jpegQualityBox.setToolTip(QCoreApplication.translate("mainWindow", u"The JPEG quality, on a scale from 0 (worst) to 95 (best). \n"
|
||||||
|
"\n"
|
||||||
|
"Default is 85 for most devices besides Kindle Scribe and Colorsoft, which are 90.\n"
|
||||||
|
"\n"
|
||||||
|
"Higher values are larger and higher quality, and may resolve blank page issues.", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.jpegQualityBox.setText(QCoreApplication.translate("mainWindow", u"Custom JPEG Quality", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.outputSplit.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Automatic mode<br/></span>The output will be split automatically.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Checked - Volume mode<br/></span>Every subdirectory will be considered as a separate volume.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.metadataTitleBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Don't use metadata Title<br/></span>Write default title.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Add metadata Title to the default schema<br/></span>Write default title with Title from ComicInfo.xml or other embedded metadata.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Use metadata Title only<br/></span>Write Title from ComicInfo.xml or other embedded metadata.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.metadataTitleBox.setText(QCoreApplication.translate("mainWindow", u"Metadata Title", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.noSmartCoverCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"Disable attempt to crop main cover from wide image.", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.noSmartCoverCropBox.setText(QCoreApplication.translate("mainWindow", u"No Smart Cover Crop", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.rotateFirstBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>When the spread splitter option is partially checked,</p><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Rotate Last<br/></span>Put the rotated 2 page spread after the split spreads.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate First<br/></span>Put the rotated 2 page spread before the split spreads.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.rotateFirstBox.setText(QCoreApplication.translate("mainWindow", u"Rotate First", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.mozJpegBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - JPEG<br/></span>Use JPEG files</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - force PNG<br/></span>Create PNG files instead JPEG for black and white images</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - mozJpeg<br/></span>10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.mozJpegBox.setText(QCoreApplication.translate("mainWindow", u"JPEG/PNG/mozJpeg", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.autoLevelBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>By default, KCC maps the darkest pixel value to pure black (the black point.)</p><p>Extreme black point sets the black point to be the most common dark pixel value.</p><p>Useful when text is black but artwork is gray.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.autoLevelBox.setText(QCoreApplication.translate("mainWindow", u"Extreme Black Point", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.forcePngRgbBox.setToolTip(QCoreApplication.translate("mainWindow", u"Force full color images to be saved in lossless PNG format, dramatically increases the filesize.", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.forcePngRgbBox.setText(QCoreApplication.translate("mainWindow", u"Force PNG RGB", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.upscaleBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Nothing<br/></span>Images smaller than device resolution will not be resized.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Stretching<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be not preserved.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Upscaling<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be preserved.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.upscaleBox.setText(QCoreApplication.translate("mainWindow", u"Stretch/Upscale", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.borderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - White<br/></span>Margins will be untouched.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.qualityBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - 2 panels<br/></span>Zoom only the top and bottom of the page.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 4 high-quality panels<br/></span>Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.pdfExtractBox.setToolTip(QCoreApplication.translate("mainWindow", u"Use the PDF image extraction method from KCC 8 and earlier.\n"
|
||||||
|
"\n"
|
||||||
|
"Useful for really weird PDFs.", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.pdfExtractBox.setText(QCoreApplication.translate("mainWindow", u"PDF Legacy Extract", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.colorBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.pdfWidthBox.setToolTip(QCoreApplication.translate("mainWindow", u"Render vector PDFs to device width instead of height.\n"
|
||||||
|
"\n"
|
||||||
|
"Useful if you plan to crop a little off the top and bottom to fill screen.", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.pdfWidthBox.setText(QCoreApplication.translate("mainWindow", u"PDF Width Render", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.disableProcessingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Do not process any image, ignore profile and processing options.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.disableProcessingBox.setText(QCoreApplication.translate("mainWindow", u"Disable processing", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.defaultOutputFolderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - next to source<br/></span>Place output files next to source files</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - folder next to source<br/></span>Place output files in a folder next to source files</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Custom<br/></span>Place output files in custom directory specified by right button</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.defaultOutputFolderBox.setText(QCoreApplication.translate("mainWindow", u"Output Folder", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.defaultOutputFolderButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Use this to select the default output directory.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.defaultOutputFolderButton.setText("")
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.chunkSizeCheckBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:700; text-decoration: underline;\">Unchecked<br/></span>Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.</p><p><span style=\" font-weight:700; text-decoration: underline;\">Checked</span><br/>Output file size specified in "Chunk size MB" before split occurs.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.chunkSizeCheckBox.setText(QCoreApplication.translate("mainWindow", u"Chunk size", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.autocontrastBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - BW only<br/></span>Only autocontrast bw pages. Ignored for pages where near blacks or whites don't exist.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Disabled<br/></span>Disable autocontrast</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - BW and Color<br/></span>BW and color images will be autocontrasted. Ignored for pages where near blacks or whites don't exist.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.autocontrastBox.setText(QCoreApplication.translate("mainWindow", u"Custom Autocontrast", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.webpBox.setToolTip(QCoreApplication.translate("mainWindow", u"Replace JPG with lossy WebP and PNG with lossless WebP. This includes the JPG Quality.\n"
|
||||||
|
"\n"
|
||||||
|
"Ignored for Kindle EPUB/MOBI and all PDF.", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.webpBox.setText(QCoreApplication.translate("mainWindow", u"WebP (experimental)", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.tempDirBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Main Drive<br/></span>Use dedicated temporary directory on main OS drive.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Source File Drive<br/></span>Create temporary file directory on source file drive.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.tempDirBox.setText(QCoreApplication.translate("mainWindow", u"Temp Directory", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory for this list.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.convertButton.setText(QCoreApplication.translate("mainWindow", u"Convert", None))
|
||||||
|
self.clearButton.setText(QCoreApplication.translate("mainWindow", u"Clear list", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.deviceBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Target device.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.fileButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Add CBR, CBZ, CB7 or PDF file to queue.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.fileButton.setText(QCoreApplication.translate("mainWindow", u"Add input file(s)", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.directoryButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Add directory containing JPG, PNG or GIF files to queue.<br/><span style=\" font-weight:600;\">CBR, CBZ and CB7 files inside will not be processed!</span></p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.directoryButton.setText(QCoreApplication.translate("mainWindow", u"Add input folder(s)", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.formatBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Output format.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.hLabel.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.hLabel.setText(QCoreApplication.translate("mainWindow", u"Custom height:", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.widthBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.wLabel.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.wLabel.setText(QCoreApplication.translate("mainWindow", u"Custom width:", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.heightBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
# retranslateUi
|
||||||
|
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
## Form generated from reading UI file 'MetaEditor.ui'
|
||||||
|
##
|
||||||
|
## Created by: Qt User Interface Compiler version 6.9.3
|
||||||
|
##
|
||||||
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||||
|
QMetaObject, QObject, QPoint, QRect,
|
||||||
|
QSize, QTime, QUrl, Qt)
|
||||||
|
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||||
|
QFont, QFontDatabase, QGradient, QIcon,
|
||||||
|
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||||
|
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||||
|
from PySide6.QtWidgets import (QApplication, QDialog, QGridLayout, QHBoxLayout,
|
||||||
|
QLabel, QLineEdit, QPushButton, QSizePolicy,
|
||||||
|
QVBoxLayout, QWidget)
|
||||||
|
from . import KCC_rc
|
||||||
|
|
||||||
|
class Ui_editorDialog(object):
|
||||||
|
def setupUi(self, editorDialog):
|
||||||
|
if not editorDialog.objectName():
|
||||||
|
editorDialog.setObjectName(u"editorDialog")
|
||||||
|
editorDialog.resize(400, 260)
|
||||||
|
editorDialog.setMinimumSize(QSize(400, 260))
|
||||||
|
icon = QIcon()
|
||||||
|
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")
|
||||||
|
self.verticalLayout.setContentsMargins(-1, -1, -1, 5)
|
||||||
|
self.editorWidget = QWidget(editorDialog)
|
||||||
|
self.editorWidget.setObjectName(u"editorWidget")
|
||||||
|
self.gridLayout = QGridLayout(self.editorWidget)
|
||||||
|
self.gridLayout.setObjectName(u"gridLayout")
|
||||||
|
self.gridLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.label_1 = QLabel(self.editorWidget)
|
||||||
|
self.label_1.setObjectName(u"label_1")
|
||||||
|
|
||||||
|
self.gridLayout.addWidget(self.label_1, 0, 0, 1, 1)
|
||||||
|
|
||||||
|
self.seriesLine = QLineEdit(self.editorWidget)
|
||||||
|
self.seriesLine.setObjectName(u"seriesLine")
|
||||||
|
|
||||||
|
self.gridLayout.addWidget(self.seriesLine, 0, 1, 1, 1)
|
||||||
|
|
||||||
|
self.label_2 = QLabel(self.editorWidget)
|
||||||
|
self.label_2.setObjectName(u"label_2")
|
||||||
|
|
||||||
|
self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1)
|
||||||
|
|
||||||
|
self.volumeLine = QLineEdit(self.editorWidget)
|
||||||
|
self.volumeLine.setObjectName(u"volumeLine")
|
||||||
|
|
||||||
|
self.gridLayout.addWidget(self.volumeLine, 1, 1, 1, 1)
|
||||||
|
|
||||||
|
self.label_3 = QLabel(self.editorWidget)
|
||||||
|
self.label_3.setObjectName(u"label_3")
|
||||||
|
|
||||||
|
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, 3, 1, 1, 1)
|
||||||
|
|
||||||
|
self.label_4 = QLabel(self.editorWidget)
|
||||||
|
self.label_4.setObjectName(u"label_4")
|
||||||
|
|
||||||
|
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, 4, 1, 1, 1)
|
||||||
|
|
||||||
|
self.label_5 = QLabel(self.editorWidget)
|
||||||
|
self.label_5.setObjectName(u"label_5")
|
||||||
|
|
||||||
|
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, 5, 1, 1, 1)
|
||||||
|
|
||||||
|
self.label_6 = QLabel(self.editorWidget)
|
||||||
|
self.label_6.setObjectName(u"label_6")
|
||||||
|
|
||||||
|
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, 6, 1, 1, 1)
|
||||||
|
|
||||||
|
self.label_7 = QLabel(self.editorWidget)
|
||||||
|
self.label_7.setObjectName(u"label_7")
|
||||||
|
|
||||||
|
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, 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)
|
||||||
|
|
||||||
|
self.optionWidget = QWidget(editorDialog)
|
||||||
|
self.optionWidget.setObjectName(u"optionWidget")
|
||||||
|
self.horizontalLayout = QHBoxLayout(self.optionWidget)
|
||||||
|
self.horizontalLayout.setObjectName(u"horizontalLayout")
|
||||||
|
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.statusLabel = QLabel(self.optionWidget)
|
||||||
|
self.statusLabel.setObjectName(u"statusLabel")
|
||||||
|
sizePolicy = QSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding)
|
||||||
|
sizePolicy.setHorizontalStretch(0)
|
||||||
|
sizePolicy.setVerticalStretch(0)
|
||||||
|
sizePolicy.setHeightForWidth(self.statusLabel.sizePolicy().hasHeightForWidth())
|
||||||
|
self.statusLabel.setSizePolicy(sizePolicy)
|
||||||
|
|
||||||
|
self.horizontalLayout.addWidget(self.statusLabel)
|
||||||
|
|
||||||
|
self.okButton = QPushButton(self.optionWidget)
|
||||||
|
self.okButton.setObjectName(u"okButton")
|
||||||
|
self.okButton.setMinimumSize(QSize(0, 30))
|
||||||
|
icon1 = QIcon()
|
||||||
|
icon1.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.okButton.setIcon(icon1)
|
||||||
|
|
||||||
|
self.horizontalLayout.addWidget(self.okButton)
|
||||||
|
|
||||||
|
self.cancelButton = QPushButton(self.optionWidget)
|
||||||
|
self.cancelButton.setObjectName(u"cancelButton")
|
||||||
|
self.cancelButton.setMinimumSize(QSize(0, 30))
|
||||||
|
icon2 = QIcon()
|
||||||
|
icon2.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.cancelButton.setIcon(icon2)
|
||||||
|
|
||||||
|
self.horizontalLayout.addWidget(self.cancelButton)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
QMetaObject.connectSlotsByName(editorDialog)
|
||||||
|
# setupUi
|
||||||
|
|
||||||
|
def retranslateUi(self, editorDialog):
|
||||||
|
editorDialog.setWindowTitle(QCoreApplication.translate("editorDialog", u"Metadata editor", None))
|
||||||
|
self.label_1.setText(QCoreApplication.translate("editorDialog", u"Series:", None))
|
||||||
|
self.label_2.setText(QCoreApplication.translate("editorDialog", u"Volume:", None))
|
||||||
|
self.label_3.setText(QCoreApplication.translate("editorDialog", u"Number:", None))
|
||||||
|
self.label_4.setText(QCoreApplication.translate("editorDialog", u"Writer:", None))
|
||||||
|
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))
|
||||||
|
# retranslateUi
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
__version__ = '10.1.2'
|
||||||
|
__license__ = 'ISC'
|
||||||
|
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
|
||||||
|
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and/or distribute this software for
|
||||||
|
# any purpose with or without fee is hereby granted, provided that the
|
||||||
|
# above copyright notice and this permission notice appear in all
|
||||||
|
# copies.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||||
|
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
||||||
|
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
||||||
|
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
|
||||||
|
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||||
|
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
#
|
||||||
|
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
from shutil import rmtree
|
||||||
|
from multiprocessing import Pool
|
||||||
|
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):
|
||||||
|
if output:
|
||||||
|
mergeWorkerOutput.append(output)
|
||||||
|
mergeWorkerPool.terminate()
|
||||||
|
if GUI:
|
||||||
|
GUI.progressBarTick.emit('tick')
|
||||||
|
if not GUI.conversionAlive:
|
||||||
|
mergeWorkerPool.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
def mergeDirectory(work):
|
||||||
|
try:
|
||||||
|
directory = work[0]
|
||||||
|
images = []
|
||||||
|
imagesValid = []
|
||||||
|
sizes = []
|
||||||
|
targetHeight = 0
|
||||||
|
dot_clean(directory)
|
||||||
|
for root, _, files in walkLevel(directory, 0):
|
||||||
|
for name in files:
|
||||||
|
if getImageFileName(name) is not None:
|
||||||
|
i = Image.open(os.path.join(root, name))
|
||||||
|
images.append([os.path.join(root, name), i.size[0], i.size[1]])
|
||||||
|
sizes.append(i.size[0])
|
||||||
|
if len(images) > 0:
|
||||||
|
targetWidth = max(set(sizes), key=sizes.count)
|
||||||
|
for i in images:
|
||||||
|
targetHeight += i[2]
|
||||||
|
imagesValid.append(i[0])
|
||||||
|
# Silently drop directories that contain too many images
|
||||||
|
# 131072 = GIMP_MAX_IMAGE_SIZE / 4
|
||||||
|
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:
|
||||||
|
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')
|
||||||
|
except Exception:
|
||||||
|
return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2])
|
||||||
|
|
||||||
|
|
||||||
|
def detectSolid(img):
|
||||||
|
return not ImageChops.invert(img).getbbox() or not img.getbbox()
|
||||||
|
|
||||||
|
|
||||||
|
def splitImageTick(output):
|
||||||
|
if output:
|
||||||
|
splitWorkerOutput.append(output)
|
||||||
|
splitWorkerPool.terminate()
|
||||||
|
if GUI:
|
||||||
|
GUI.progressBarTick.emit('tick')
|
||||||
|
if not GUI.conversionAlive:
|
||||||
|
splitWorkerPool.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnboundLocalVariable
|
||||||
|
def splitImage(work):
|
||||||
|
try:
|
||||||
|
path = work[0]
|
||||||
|
name = work[1]
|
||||||
|
opt = work[2]
|
||||||
|
filePath = os.path.join(path, name)
|
||||||
|
Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
|
||||||
|
Image.MAX_IMAGE_PIXELS = 1000000000
|
||||||
|
imgOrg = Image.open(filePath).convert('RGB')
|
||||||
|
# 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:
|
||||||
|
drawImg = Image.open(filePath).convert(mode='RGBA')
|
||||||
|
draw = ImageDraw.Draw(drawImg)
|
||||||
|
|
||||||
|
# Find panels
|
||||||
|
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((h_pad, yWork, widthImg - h_pad, yWork + v_pad))
|
||||||
|
solid = detectSolid(tmpImg)
|
||||||
|
if not solid and not panelDetected:
|
||||||
|
panelDetected = True
|
||||||
|
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
|
||||||
|
# skip short panel at start
|
||||||
|
if panelY1 < v_pad * 2 and panelY2 - panelY1 < v_pad * 2:
|
||||||
|
continue
|
||||||
|
panels.append((panelY1, panelY2, panelY2 - panelY1))
|
||||||
|
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:
|
||||||
|
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:
|
||||||
|
# split super long panels with overlap
|
||||||
|
parts = math.ceil(panel[2] / opt.height)
|
||||||
|
diff = panel[2] // parts
|
||||||
|
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:
|
||||||
|
if pageLeft - panel[2] > 0:
|
||||||
|
pageLeft -= panel[2]
|
||||||
|
currentPage.append(panelNumber)
|
||||||
|
panelNumber += 1
|
||||||
|
else:
|
||||||
|
if len(currentPage) > 0:
|
||||||
|
pages.append(currentPage)
|
||||||
|
pageLeft = opt.height - panel[2]
|
||||||
|
currentPage = [panelNumber]
|
||||||
|
panelNumber += 1
|
||||||
|
if len(currentPage) > 0:
|
||||||
|
pages.append(currentPage)
|
||||||
|
|
||||||
|
# Create pages
|
||||||
|
pageNumber = 1
|
||||||
|
for page in pages:
|
||||||
|
pageHeight = 0
|
||||||
|
targetHeight = 0
|
||||||
|
for panel in page:
|
||||||
|
pageHeight += panelsProcessed[panel][2]
|
||||||
|
if pageHeight > 15:
|
||||||
|
newPage = Image.new('RGB', (widthImg, pageHeight))
|
||||||
|
for panel in page:
|
||||||
|
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).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, job_progress='', qtgui=None):
|
||||||
|
global args, GUI, splitWorkerPool, splitWorkerOutput, mergeWorkerPool, mergeWorkerOutput
|
||||||
|
parser = ArgumentParser(prog="kcc-c2p", usage="kcc-c2p [options] [input]", add_help=False)
|
||||||
|
|
||||||
|
mandatory_options = parser.add_argument_group("MANDATORY")
|
||||||
|
main_options = parser.add_argument_group("MAIN")
|
||||||
|
other_options = parser.add_argument_group("OTHER")
|
||||||
|
mandatory_options.add_argument("input", action="extend", nargs="*", default=None,
|
||||||
|
help="Full path to comic folder(s) to be processed. Separate multiple inputs"
|
||||||
|
" 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,
|
||||||
|
help="Combine every directory into a single image before splitting")
|
||||||
|
other_options.add_argument("-d", "--debug", action="store_true", dest="debug", default=False,
|
||||||
|
help="Create debug file for every split image")
|
||||||
|
other_options.add_argument("-h", "--help", action="help",
|
||||||
|
help="Show this help message and exit")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
if qtgui:
|
||||||
|
GUI = qtgui
|
||||||
|
else:
|
||||||
|
GUI = None
|
||||||
|
if not argv or args.input == []:
|
||||||
|
parser.print_help()
|
||||||
|
return 1
|
||||||
|
if args.height > 0:
|
||||||
|
for sourceDir in args.input:
|
||||||
|
targetDir = sourceDir
|
||||||
|
if os.path.isdir(sourceDir):
|
||||||
|
work = []
|
||||||
|
pagenumber = 1
|
||||||
|
splitWorkerOutput = []
|
||||||
|
splitWorkerPool = Pool(maxtasksperchild=10)
|
||||||
|
if args.merge:
|
||||||
|
print(f"{job_progress}Merging images...")
|
||||||
|
directoryNumer = 1
|
||||||
|
mergeWork = []
|
||||||
|
mergeWorkerOutput = []
|
||||||
|
mergeWorkerPool = Pool(maxtasksperchild=10)
|
||||||
|
mergeWork.append([targetDir])
|
||||||
|
for root, dirs, files in os.walk(targetDir, False):
|
||||||
|
dirs, files = walkSort(dirs, files)
|
||||||
|
for directory in dirs:
|
||||||
|
directoryNumer += 1
|
||||||
|
mergeWork.append([os.path.join(root, directory)])
|
||||||
|
if GUI:
|
||||||
|
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)
|
||||||
|
mergeWorkerPool.close()
|
||||||
|
mergeWorkerPool.join()
|
||||||
|
if GUI and not GUI.conversionAlive:
|
||||||
|
rmtree(targetDir, True)
|
||||||
|
raise UserWarning("Conversion interrupted.")
|
||||||
|
if len(mergeWorkerOutput) > 0:
|
||||||
|
rmtree(targetDir, True)
|
||||||
|
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0],
|
||||||
|
mergeWorkerOutput[0][1])
|
||||||
|
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:
|
||||||
|
pagenumber += 1
|
||||||
|
work.append([root, name, args])
|
||||||
|
else:
|
||||||
|
os.remove(os.path.join(root, name))
|
||||||
|
if GUI:
|
||||||
|
GUI.progressBarTick.emit(f'{job_progress}Splitting images')
|
||||||
|
GUI.progressBarTick.emit(str(pagenumber))
|
||||||
|
GUI.progressBarTick.emit('tick')
|
||||||
|
if len(work) > 0:
|
||||||
|
for i in work:
|
||||||
|
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.")
|
||||||
|
if len(splitWorkerOutput) > 0:
|
||||||
|
rmtree(targetDir, True)
|
||||||
|
raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0],
|
||||||
|
splitWorkerOutput[0][1])
|
||||||
|
else:
|
||||||
|
rmtree(targetDir, True)
|
||||||
|
raise UserWarning("C2P: Source directory is empty.")
|
||||||
|
else:
|
||||||
|
raise UserWarning("Provided input is not a directory.")
|
||||||
|
else:
|
||||||
|
raise UserWarning("Target height is not set.")
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
|
||||||
|
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and/or distribute this software for
|
||||||
|
# any purpose with or without fee is hereby granted, provided that the
|
||||||
|
# above copyright notice and this permission notice appear in all
|
||||||
|
# copies.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||||
|
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
||||||
|
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
||||||
|
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
|
||||||
|
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||||
|
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
#
|
||||||
|
|
||||||
|
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 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:
|
||||||
|
def __init__(self, filepath):
|
||||||
|
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 = [
|
||||||
|
[SEVENZIP, 'l', '-y', '-p1', self.basename],
|
||||||
|
]
|
||||||
|
|
||||||
|
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
||||||
|
extraction_commands.append(
|
||||||
|
['unrar', 'l', '-y', '-p1', self.basename],
|
||||||
|
)
|
||||||
|
|
||||||
|
for cmd in extraction_commands:
|
||||||
|
try:
|
||||||
|
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()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except CalledProcessError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise OSError(EXTRACTION_ERROR)
|
||||||
|
|
||||||
|
def extract(self, targetdir):
|
||||||
|
if not os.path.isdir(targetdir):
|
||||||
|
raise OSError('Target directory doesn\'t exist.')
|
||||||
|
|
||||||
|
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.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.basename, '-D', '-f', '-o', targetdir]
|
||||||
|
)
|
||||||
|
|
||||||
|
extraction_commands.reverse()
|
||||||
|
|
||||||
|
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
||||||
|
extraction_commands.append(
|
||||||
|
['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.basename, targetdir]
|
||||||
|
)
|
||||||
|
|
||||||
|
for cmd in extraction_commands:
|
||||||
|
try:
|
||||||
|
subprocess_run(cmd, capture_output=True, check=True, cwd=self.dirname)
|
||||||
|
return targetdir
|
||||||
|
except FileNotFoundError:
|
||||||
|
missing.append(cmd[0])
|
||||||
|
except CalledProcessError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
raise OSError(f'Extraction failed, install <a href="https://github.com/ciromattia/kcc#7-zip">specialized extraction software.</a> ')
|
||||||
|
else:
|
||||||
|
raise OSError(EXTRACTION_ERROR)
|
||||||
|
|
||||||
|
def addFile(self, sourcefile):
|
||||||
|
if self.type in ['RAR', 'RAR5']:
|
||||||
|
raise NotImplementedError
|
||||||
|
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([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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Based on initial version of DualMetaFix. Copyright (C) 2013 Kevin Hendricks
|
||||||
|
# Changes for KCC Copyright (C) 2014-2019 Pawel Jastrzebski <pawelj@iosphe.re>
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import mmap
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
|
class DualMetaFixException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# palm database offset constants
|
||||||
|
number_of_pdb_records = 76
|
||||||
|
first_pdb_record = 78
|
||||||
|
|
||||||
|
# important rec0 offsets
|
||||||
|
mobi_header_base = 16
|
||||||
|
mobi_header_length = 20
|
||||||
|
mobi_version = 36
|
||||||
|
title_offset = 84
|
||||||
|
|
||||||
|
|
||||||
|
def getint(data, ofs, sz='L'):
|
||||||
|
i, = struct.unpack_from('>' + sz, data, ofs)
|
||||||
|
return i
|
||||||
|
|
||||||
|
|
||||||
|
def writeint(data, ofs, n, slen='L'):
|
||||||
|
if slen == 'L':
|
||||||
|
return data[:ofs] + struct.pack('>L', n) + data[ofs + 4:]
|
||||||
|
else:
|
||||||
|
return data[:ofs] + struct.pack('>H', n) + data[ofs + 2:]
|
||||||
|
|
||||||
|
|
||||||
|
def getsecaddr(datain, secno):
|
||||||
|
nsec = getint(datain, number_of_pdb_records, 'H')
|
||||||
|
if (secno < 0) | (secno >= nsec):
|
||||||
|
emsg = 'requested section number %d out of range (nsec=%d)' % (secno, nsec)
|
||||||
|
raise DualMetaFixException(emsg)
|
||||||
|
secstart = getint(datain, first_pdb_record + secno * 8)
|
||||||
|
if secno == nsec - 1:
|
||||||
|
secend = len(datain)
|
||||||
|
else:
|
||||||
|
secend = getint(datain, first_pdb_record + (secno + 1) * 8)
|
||||||
|
return secstart, secend
|
||||||
|
|
||||||
|
|
||||||
|
def readsection(datain, secno):
|
||||||
|
secstart, secend = getsecaddr(datain, secno)
|
||||||
|
return datain[secstart:secend]
|
||||||
|
|
||||||
|
|
||||||
|
# overwrite section - must be exact same length as original
|
||||||
|
def replacesection(datain, secno, secdata):
|
||||||
|
secstart, secend = getsecaddr(datain, secno)
|
||||||
|
seclen = secend - secstart
|
||||||
|
if len(secdata) != seclen:
|
||||||
|
raise DualMetaFixException('section length change in replacesection')
|
||||||
|
datain[secstart:secstart + seclen] = secdata
|
||||||
|
|
||||||
|
|
||||||
|
def get_exth_params(rec0):
|
||||||
|
ebase = mobi_header_base + getint(rec0, mobi_header_length)
|
||||||
|
if rec0[ebase:ebase + 4] != b'EXTH':
|
||||||
|
raise DualMetaFixException('EXTH tag not found where expected')
|
||||||
|
elen = getint(rec0, ebase + 4)
|
||||||
|
enum = getint(rec0, ebase + 8)
|
||||||
|
rlen = len(rec0)
|
||||||
|
return ebase, elen, enum, rlen
|
||||||
|
|
||||||
|
|
||||||
|
def add_exth(rec0, exth_num, exth_bytes):
|
||||||
|
ebase, elen, enum, rlen = get_exth_params(rec0)
|
||||||
|
newrecsize = 8 + len(exth_bytes)
|
||||||
|
newrec0 = rec0[0:ebase + 4] + struct.pack('>L', elen + newrecsize) + struct.pack('>L', enum + 1) + \
|
||||||
|
struct.pack('>L', exth_num) + struct.pack('>L', newrecsize) + exth_bytes + rec0[ebase + 12:]
|
||||||
|
newrec0 = writeint(newrec0, title_offset, getint(newrec0, title_offset) + newrecsize)
|
||||||
|
# keep constant record length by removing newrecsize null bytes from end
|
||||||
|
sectail = newrec0[-newrecsize:]
|
||||||
|
if sectail != b'\0' * newrecsize:
|
||||||
|
raise DualMetaFixException('add_exth: trimmed non-null bytes at end of section')
|
||||||
|
newrec0 = newrec0[0:rlen]
|
||||||
|
return newrec0
|
||||||
|
|
||||||
|
|
||||||
|
def read_exth(rec0, exth_num):
|
||||||
|
exth_values = []
|
||||||
|
ebase, elen, enum, rlen = get_exth_params(rec0)
|
||||||
|
ebase += 12
|
||||||
|
while enum > 0:
|
||||||
|
exth_id = getint(rec0, ebase)
|
||||||
|
if exth_id == exth_num:
|
||||||
|
# We might have multiple exths, so build a list.
|
||||||
|
exth_values.append(rec0[ebase + 8:ebase + getint(rec0, ebase + 4)])
|
||||||
|
enum -= 1
|
||||||
|
ebase = ebase + getint(rec0, ebase + 4)
|
||||||
|
return exth_values
|
||||||
|
|
||||||
|
|
||||||
|
def del_exth(rec0, exth_num):
|
||||||
|
ebase, elen, enum, rlen = get_exth_params(rec0)
|
||||||
|
ebase_idx = ebase + 12
|
||||||
|
enum_idx = 0
|
||||||
|
while enum_idx < enum:
|
||||||
|
exth_id = getint(rec0, ebase_idx)
|
||||||
|
exth_size = getint(rec0, ebase_idx + 4)
|
||||||
|
if exth_id == exth_num:
|
||||||
|
newrec0 = rec0
|
||||||
|
newrec0 = writeint(newrec0, title_offset, getint(newrec0, title_offset) - exth_size)
|
||||||
|
newrec0 = newrec0[:ebase_idx] + newrec0[ebase_idx + exth_size:]
|
||||||
|
newrec0 = newrec0[0:ebase + 4] + struct.pack('>L', elen - exth_size) + \
|
||||||
|
struct.pack('>L', enum - 1) + newrec0[ebase + 12:]
|
||||||
|
newrec0 += b'\0' * exth_size
|
||||||
|
if rlen != len(newrec0):
|
||||||
|
raise DualMetaFixException('del_exth: incorrect section size change')
|
||||||
|
return newrec0
|
||||||
|
enum_idx += 1
|
||||||
|
ebase_idx = ebase_idx + exth_size
|
||||||
|
return rec0
|
||||||
|
|
||||||
|
|
||||||
|
class DualMobiMetaFix:
|
||||||
|
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)
|
||||||
|
self.datain_rec0 = readsection(self.datain, 0)
|
||||||
|
|
||||||
|
# in the first mobi header
|
||||||
|
# add 501 to "EBOK", add 113 as asin
|
||||||
|
rec0 = self.datain_rec0
|
||||||
|
rec0 = del_exth(rec0, 501)
|
||||||
|
rec0 = del_exth(rec0, 113)
|
||||||
|
rec0 = add_exth(rec0, 501, cdetype)
|
||||||
|
rec0 = add_exth(rec0, 113, asin)
|
||||||
|
replacesection(self.datain, 0, rec0)
|
||||||
|
|
||||||
|
ver = getint(self.datain_rec0, mobi_version)
|
||||||
|
self.combo = (ver != 8)
|
||||||
|
if not self.combo:
|
||||||
|
return
|
||||||
|
|
||||||
|
exth121 = read_exth(self.datain_rec0, 121)
|
||||||
|
if len(exth121) == 0:
|
||||||
|
self.combo = False
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# only pay attention to first exth121
|
||||||
|
# (there should only be one)
|
||||||
|
datain_kf8, = struct.unpack_from('>L', exth121[0], 0)
|
||||||
|
if datain_kf8 == 0xffffffff:
|
||||||
|
self.combo = False
|
||||||
|
return
|
||||||
|
self.datain_kfrec0 = readsection(self.datain, datain_kf8)
|
||||||
|
|
||||||
|
# in the second header
|
||||||
|
# add 501 to "EBOK", add 113 as asin
|
||||||
|
rec0 = self.datain_kfrec0
|
||||||
|
rec0 = del_exth(rec0, 501)
|
||||||
|
rec0 = del_exth(rec0, 113)
|
||||||
|
rec0 = add_exth(rec0, 501, cdetype)
|
||||||
|
rec0 = add_exth(rec0, 113, asin)
|
||||||
|
replacesection(self.datain, datain_kf8, rec0)
|
||||||
|
|
||||||
|
self.datain.flush()
|
||||||
|
self.datain.close()
|
||||||
@@ -0,0 +1,674 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2010 Alex Yatskov
|
||||||
|
# Copyright (C) 2011 Stanislav (proDOOMman) Kosolapov <prodoomman@gmail.com>
|
||||||
|
# Copyright (c) 2016 Alberto Planas <aplanas@gmail.com>
|
||||||
|
# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
|
||||||
|
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# 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, 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
|
||||||
|
from .shared import get_contain_resolution
|
||||||
|
|
||||||
|
AUTO_CROP_THRESHOLD = 0.015
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileData:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
Palette4 = [
|
||||||
|
0x00, 0x00, 0x00,
|
||||||
|
0x55, 0x55, 0x55,
|
||||||
|
0xaa, 0xaa, 0xaa,
|
||||||
|
0xff, 0xff, 0xff
|
||||||
|
]
|
||||||
|
|
||||||
|
Palette15 = [
|
||||||
|
0x00, 0x00, 0x00,
|
||||||
|
0x11, 0x11, 0x11,
|
||||||
|
0x22, 0x22, 0x22,
|
||||||
|
0x33, 0x33, 0x33,
|
||||||
|
0x44, 0x44, 0x44,
|
||||||
|
0x55, 0x55, 0x55,
|
||||||
|
0x66, 0x66, 0x66,
|
||||||
|
0x77, 0x77, 0x77,
|
||||||
|
0x88, 0x88, 0x88,
|
||||||
|
0x99, 0x99, 0x99,
|
||||||
|
0xaa, 0xaa, 0xaa,
|
||||||
|
0xbb, 0xbb, 0xbb,
|
||||||
|
0xcc, 0xcc, 0xcc,
|
||||||
|
0xdd, 0xdd, 0xdd,
|
||||||
|
0xff, 0xff, 0xff,
|
||||||
|
]
|
||||||
|
|
||||||
|
Palette16 = [
|
||||||
|
0x00, 0x00, 0x00,
|
||||||
|
0x11, 0x11, 0x11,
|
||||||
|
0x22, 0x22, 0x22,
|
||||||
|
0x33, 0x33, 0x33,
|
||||||
|
0x44, 0x44, 0x44,
|
||||||
|
0x55, 0x55, 0x55,
|
||||||
|
0x66, 0x66, 0x66,
|
||||||
|
0x77, 0x77, 0x77,
|
||||||
|
0x88, 0x88, 0x88,
|
||||||
|
0x99, 0x99, 0x99,
|
||||||
|
0xaa, 0xaa, 0xaa,
|
||||||
|
0xbb, 0xbb, 0xbb,
|
||||||
|
0xcc, 0xcc, 0xcc,
|
||||||
|
0xdd, 0xdd, 0xdd,
|
||||||
|
0xee, 0xee, 0xee,
|
||||||
|
0xff, 0xff, 0xff,
|
||||||
|
]
|
||||||
|
|
||||||
|
PalleteNull = [
|
||||||
|
]
|
||||||
|
|
||||||
|
ProfilesKindleEBOK = {
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfilesKindlePDOC = {
|
||||||
|
'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", (1264, 1680), Palette16, 1.0),
|
||||||
|
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
|
||||||
|
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
|
||||||
|
'KPW6': ("Kindle Paperwhite 6", (1272, 1696), 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", (1272, 1696), Palette16, 1.0),
|
||||||
|
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
|
||||||
|
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfilesKindle = {
|
||||||
|
**ProfilesKindleEBOK,
|
||||||
|
**ProfilesKindlePDOC
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfilesKobo = {
|
||||||
|
'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,
|
||||||
|
**ProfilesRemarkable,
|
||||||
|
'OTHER': ("Other", (0, 0), Palette16, 1.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ComicPageParser:
|
||||||
|
def __init__(self, source, options):
|
||||||
|
Image.MAX_IMAGE_PIXELS = int(2048 * 2048 * 2048 // 4 // 3)
|
||||||
|
self.opt = options
|
||||||
|
self.source = source
|
||||||
|
self.size = self.opt.profileData[1]
|
||||||
|
self.payload = []
|
||||||
|
|
||||||
|
# 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'):
|
||||||
|
Image.Resampling = Image
|
||||||
|
self.splitCheck()
|
||||||
|
|
||||||
|
def getImageHistogram(self, image):
|
||||||
|
histogram = image.histogram()
|
||||||
|
if histogram[0] == 0:
|
||||||
|
return -1
|
||||||
|
elif histogram[255] == 0:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def splitCheck(self):
|
||||||
|
width, height = self.image.size
|
||||||
|
dstwidth, dstheight = self.size
|
||||||
|
if self.opt.maximizestrips:
|
||||||
|
leftbox = (0, 0, int(width / 2), height)
|
||||||
|
rightbox = (int(width / 2), 0, width, height)
|
||||||
|
if self.opt.righttoleft:
|
||||||
|
pageone = self.image.crop(rightbox)
|
||||||
|
pagetwo = self.image.crop(leftbox)
|
||||||
|
else:
|
||||||
|
pageone = self.image.crop(leftbox)
|
||||||
|
pagetwo = self.image.crop(rightbox)
|
||||||
|
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.fill])
|
||||||
|
elif self.opt.webtoon:
|
||||||
|
self.payload.append(['N', self.source, self.image, self.fill])
|
||||||
|
# rotate only TODO dead code?
|
||||||
|
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth and self.opt.splitter == 1:
|
||||||
|
spread = self.image
|
||||||
|
if not self.opt.norotate:
|
||||||
|
if not self.opt.rotateright:
|
||||||
|
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
||||||
|
else:
|
||||||
|
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
|
||||||
|
self.payload.append(['R', self.source, spread, self.fill])
|
||||||
|
# elif wide enough to split
|
||||||
|
elif (width > height) != (dstwidth > dstheight) and width / height > 1.16:
|
||||||
|
# if (split) or (split and rotate)
|
||||||
|
if self.opt.splitter != 1 and width / height < 1.75:
|
||||||
|
if width > height:
|
||||||
|
leftbox = (0, 0, int(width / 2), height)
|
||||||
|
rightbox = (int(width / 2), 0, width, height)
|
||||||
|
else:
|
||||||
|
leftbox = (0, 0, width, int(height / 2))
|
||||||
|
rightbox = (0, int(height / 2), width, height)
|
||||||
|
if self.opt.righttoleft:
|
||||||
|
pageone = self.image.crop(rightbox)
|
||||||
|
pagetwo = self.image.crop(leftbox)
|
||||||
|
else:
|
||||||
|
pageone = self.image.crop(leftbox)
|
||||||
|
pagetwo = self.image.crop(rightbox)
|
||||||
|
self.payload.append(['S1', self.source, pageone, self.fill])
|
||||||
|
self.payload.append(['S2', self.source, pagetwo, self.fill])
|
||||||
|
|
||||||
|
# if (rotate) or (split and rotate)
|
||||||
|
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= 1.75):
|
||||||
|
spread = self.image
|
||||||
|
if not self.opt.norotate:
|
||||||
|
if not self.opt.rotateright:
|
||||||
|
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
||||||
|
else:
|
||||||
|
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.fill])
|
||||||
|
|
||||||
|
def fillCheck(self):
|
||||||
|
if self.opt.bordersColor:
|
||||||
|
return self.opt.bordersColor
|
||||||
|
else:
|
||||||
|
bw = self.image.convert('L').point(lambda x: 0 if x < 128 else 255, '1')
|
||||||
|
imageBoxA = bw.getbbox()
|
||||||
|
imageBoxB = ImageChops.invert(bw).getbbox()
|
||||||
|
if imageBoxA is None or imageBoxB is None:
|
||||||
|
surfaceB, surfaceW = 0, 0
|
||||||
|
diff = 0
|
||||||
|
else:
|
||||||
|
surfaceB = (imageBoxA[2] - imageBoxA[0]) * (imageBoxA[3] - imageBoxA[1])
|
||||||
|
surfaceW = (imageBoxB[2] - imageBoxB[0]) * (imageBoxB[3] - imageBoxB[1])
|
||||||
|
diff = ((max(surfaceB, surfaceW) - min(surfaceB, surfaceW)) / min(surfaceB, surfaceW)) * 100
|
||||||
|
if diff > 0.5:
|
||||||
|
if surfaceW < surfaceB:
|
||||||
|
return 'white'
|
||||||
|
elif surfaceW > surfaceB:
|
||||||
|
return 'black'
|
||||||
|
else:
|
||||||
|
fill = 0
|
||||||
|
startY = 0
|
||||||
|
while startY < bw.size[1]:
|
||||||
|
if startY + 5 > bw.size[1]:
|
||||||
|
startY = bw.size[1] - 5
|
||||||
|
fill += self.getImageHistogram(bw.crop((0, startY, bw.size[0], startY + 5)))
|
||||||
|
startY += 5
|
||||||
|
startX = 0
|
||||||
|
while startX < bw.size[0]:
|
||||||
|
if startX + 5 > bw.size[0]:
|
||||||
|
startX = bw.size[0] - 5
|
||||||
|
fill += self.getImageHistogram(bw.crop((startX, 0, startX + 5, bw.size[1])))
|
||||||
|
startX += 5
|
||||||
|
if fill > 0:
|
||||||
|
return 'black'
|
||||||
|
else:
|
||||||
|
return 'white'
|
||||||
|
|
||||||
|
|
||||||
|
class ComicPage:
|
||||||
|
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.original_color_mode = image.mode
|
||||||
|
# TODO: color check earlier
|
||||||
|
self.image = image.convert("RGB")
|
||||||
|
self.color = self.colorCheck()
|
||||||
|
self.colorOutput = self.color and self.opt.forcecolor
|
||||||
|
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.targetPathOrder = '-kcc-x'
|
||||||
|
elif 'R' in mode:
|
||||||
|
self.targetPathOrder = '-kcc-a' if options.rotatefirst else '-kcc-d'
|
||||||
|
if not options.norotate:
|
||||||
|
self.rotated = True
|
||||||
|
elif 'S1' in mode:
|
||||||
|
self.targetPathOrder = '-kcc-b'
|
||||||
|
elif 'S2' in mode:
|
||||||
|
self.targetPathOrder = '-kcc-c'
|
||||||
|
# backwards compatibility for Pillow >9.1.0
|
||||||
|
if not hasattr(Image, 'Resampling'):
|
||||||
|
Image.Resampling = Image
|
||||||
|
|
||||||
|
def colorCheck(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 self.rotated:
|
||||||
|
flags.append('Rotated')
|
||||||
|
if self.fill != 'white':
|
||||||
|
flags.append('BlackBackground')
|
||||||
|
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:
|
||||||
|
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 save_with_codec(self, image, targetPath):
|
||||||
|
if self.opt.forcepng and (not self.colorOutput or self.opt.force_png_rgb):
|
||||||
|
image.info.pop('transparency', None)
|
||||||
|
if self.opt.webp_output:
|
||||||
|
targetPath += '.webp'
|
||||||
|
image.save(targetPath, 'WEBP', lossless=True, quality=self.opt.jpegquality)
|
||||||
|
elif self.opt.kindle_azw3:
|
||||||
|
targetPath += '.gif'
|
||||||
|
image.save(targetPath, 'GIF', optimize=1, interlace=False)
|
||||||
|
else:
|
||||||
|
targetPath += '.png'
|
||||||
|
image.save(targetPath, 'PNG', optimize=1)
|
||||||
|
else:
|
||||||
|
if self.opt.webp_output:
|
||||||
|
targetPath += '.webp'
|
||||||
|
image.save(targetPath, 'WEBP', quality=self.opt.jpegquality)
|
||||||
|
elif self.opt.mozjpeg:
|
||||||
|
targetPath += '.jpg'
|
||||||
|
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:
|
||||||
|
targetPath += '.jpg'
|
||||||
|
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:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
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):
|
||||||
|
# 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.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):
|
||||||
|
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.kfx:
|
||||||
|
ratio_kfx = self.opt.kfx_resolution[1] / self.opt.kfx_resolution[0]
|
||||||
|
contain_size = get_contain_resolution(self.image, self.size)
|
||||||
|
if abs(ratio_image - ratio_kfx) < AUTO_CROP_THRESHOLD:
|
||||||
|
if contain_size[0] > self.opt.kfx_resolution[0] or contain_size[1] > self.opt.kfx_resolution[1]:
|
||||||
|
self.image = ImageOps.fit(self.image, self.opt.kfx_resolution, method=method)
|
||||||
|
else:
|
||||||
|
self.image = ImageOps.pad(self.image, self.opt.kfx_resolution, method=method, color=self.fill)
|
||||||
|
else:
|
||||||
|
self.image = ImageOps.pad(self.image, self.opt.kfx_resolution, method=method, color=self.fill)
|
||||||
|
elif self.opt.stretch:
|
||||||
|
self.image = self.image.resize(self.size, method)
|
||||||
|
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
|
||||||
|
pass
|
||||||
|
else: # if image bigger than device resolution or smaller with upscaling
|
||||||
|
if self.opt.profile == 'KDX' and abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD * 3:
|
||||||
|
self.image = ImageOps.fit(self.image, self.size, method=method)
|
||||||
|
elif abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
|
||||||
|
self.image = ImageOps.fit(self.image, self.size, method=method)
|
||||||
|
elif (self.opt.format in ('CBZ', 'PDF')) and not self.opt.white_borders:
|
||||||
|
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
|
||||||
|
else:
|
||||||
|
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]:
|
||||||
|
return Image.Resampling.BICUBIC
|
||||||
|
else:
|
||||||
|
return Image.Resampling.LANCZOS
|
||||||
|
|
||||||
|
def maybeCrop(self, box, minimum):
|
||||||
|
w, h = self.image.size
|
||||||
|
left, upper, right, lower = box
|
||||||
|
if self.opt.preservemargin:
|
||||||
|
ratio = 1 - self.opt.preservemargin / 100
|
||||||
|
box = left * ratio, upper * ratio, right + (w - right) * (1 - ratio), lower + (h - lower) * (1 - ratio)
|
||||||
|
box_area = (box[2] - box[0]) * (box[3] - box[1])
|
||||||
|
image_area = self.image.size[0] * self.image.size[1]
|
||||||
|
if (box_area / image_area) >= minimum:
|
||||||
|
self.image = self.image.crop(box)
|
||||||
|
|
||||||
|
def cropPageNumber(self, power, 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):
|
||||||
|
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, opt):
|
||||||
|
self.options = opt
|
||||||
|
self.source = source
|
||||||
|
self.image = Image.open(source)
|
||||||
|
self.smartcover = False
|
||||||
|
# backwards compatibility for Pillow >9.1.0
|
||||||
|
if not hasattr(Image, 'Resampling'):
|
||||||
|
Image.Resampling = Image
|
||||||
|
self.process()
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
self.image = self.image.convert('RGB')
|
||||||
|
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
|
||||||
|
if not self.options.forcecolor:
|
||||||
|
self.image = self.image.convert('L')
|
||||||
|
if not self.options.nosmartcovercrop:
|
||||||
|
self.crop_main_cover()
|
||||||
|
|
||||||
|
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:
|
||||||
|
self.smartcover = True
|
||||||
|
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.83:
|
||||||
|
self.smartcover = True
|
||||||
|
if self.options.righttoleft:
|
||||||
|
self.image = self.image.crop((w * .19, 0, w * .575, h))
|
||||||
|
else:
|
||||||
|
self.image = self.image.crop((w * .425, 0, .81 * w, h))
|
||||||
|
elif w / h > 1.7:
|
||||||
|
self.smartcover = True
|
||||||
|
if self.options.righttoleft:
|
||||||
|
self.image = self.image.crop((w * .2, 0, w * .583, h))
|
||||||
|
else:
|
||||||
|
self.image = self.image.crop((w * .417, 0, .8 * w, h))
|
||||||
|
elif w / h > 1.34:
|
||||||
|
self.smartcover = True
|
||||||
|
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))
|
||||||
|
elif w / h > 1.0:
|
||||||
|
self.smartcover = True
|
||||||
|
if self.options.righttoleft:
|
||||||
|
self.image = self.image.crop((w * .36, 0, w, h))
|
||||||
|
else:
|
||||||
|
self.image = self.image.crop((w, 0, .64 * w, h))
|
||||||
|
|
||||||
|
def save_to_folder(self, target, tomeid, len_tomes=0):
|
||||||
|
try:
|
||||||
|
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 = 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=self.options.jpegquality)
|
||||||
|
except IOError:
|
||||||
|
raise RuntimeError('Failed to upload cover.')
|
||||||
@@ -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 []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and/or distribute this software for
|
||||||
|
# any purpose with or without fee is hereby granted, provided that the
|
||||||
|
# above copyright notice and this permission notice appear in all
|
||||||
|
# copies.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||||
|
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
||||||
|
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
||||||
|
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
|
||||||
|
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||||
|
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
from . import image
|
||||||
|
|
||||||
|
|
||||||
|
class Kindle:
|
||||||
|
def __init__(self, profile):
|
||||||
|
self.profile = profile
|
||||||
|
self.path = self.findDevice()
|
||||||
|
if self.path:
|
||||||
|
self.coverSupport = self.checkThumbnails()
|
||||||
|
else:
|
||||||
|
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]):
|
||||||
|
if os.path.isdir(os.path.join(drive[1], 'system')) and \
|
||||||
|
os.path.isdir(os.path.join(drive[1], 'documents')):
|
||||||
|
return drive[1]
|
||||||
|
return False
|
||||||
|
|
||||||
|
def checkThumbnails(self):
|
||||||
|
if os.path.isdir(os.path.join(self.path, 'system', 'thumbnails')):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and/or distribute this software for
|
||||||
|
# any purpose with or without fee is hereby granted, provided that the
|
||||||
|
# above copyright notice and this permission notice appear in all
|
||||||
|
# copies.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||||
|
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
||||||
|
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
||||||
|
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
|
||||||
|
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||||
|
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import os
|
||||||
|
from xml.dom.minidom import parse, Document
|
||||||
|
from tempfile import mkdtemp
|
||||||
|
from shutil import rmtree
|
||||||
|
from xml.sax.saxutils import unescape
|
||||||
|
from . import comicarchive
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataParser:
|
||||||
|
def __init__(self, source):
|
||||||
|
self.source = source
|
||||||
|
self.data = {'Series': '',
|
||||||
|
'Volume': '',
|
||||||
|
'Number': '',
|
||||||
|
'Writers': [],
|
||||||
|
'Pencillers': [],
|
||||||
|
'Inkers': [],
|
||||||
|
'Colorists': [],
|
||||||
|
'Summary': '',
|
||||||
|
'Bookmarks': [],
|
||||||
|
'Title': ''}
|
||||||
|
self.rawdata = None
|
||||||
|
self.format = None
|
||||||
|
if self.source.endswith('.xml') and os.path.exists(self.source):
|
||||||
|
self.rawdata = parse(self.source)
|
||||||
|
elif not self.source.endswith('.xml'):
|
||||||
|
try:
|
||||||
|
cbx = comicarchive.ComicArchive(self.source)
|
||||||
|
self.rawdata = cbx.extractMetadata()
|
||||||
|
self.format = cbx.type
|
||||||
|
except OSError as e:
|
||||||
|
raise UserWarning(e)
|
||||||
|
if self.rawdata:
|
||||||
|
self.parseXML()
|
||||||
|
|
||||||
|
def parseXML(self):
|
||||||
|
if len(self.rawdata.getElementsByTagName('Series')) != 0:
|
||||||
|
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'] = unescape(self.rawdata.getElementsByTagName('Summary')[0].firstChild.nodeValue)
|
||||||
|
if len(self.rawdata.getElementsByTagName('Title')) != 0:
|
||||||
|
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(unescape(person))
|
||||||
|
self.data[field + 's'] = list(set(self.data[field + 's']))
|
||||||
|
self.data[field + 's'].sort()
|
||||||
|
if len(self.rawdata.getElementsByTagName('Page')) != 0:
|
||||||
|
for page in self.rawdata.getElementsByTagName('Page'):
|
||||||
|
if 'Bookmark' in page.attributes and 'Image' in page.attributes:
|
||||||
|
self.data['Bookmarks'].append((int(page.attributes['Image'].value),
|
||||||
|
page.attributes['Bookmark'].value))
|
||||||
|
|
||||||
|
def saveXML(self):
|
||||||
|
if self.rawdata:
|
||||||
|
root = self.rawdata.getElementsByTagName('ComicInfo')[0]
|
||||||
|
for row in (['Series', self.data['Series']], ['Volume', self.data['Volume']],
|
||||||
|
['Number', self.data['Number']], ['Writer', ', '.join(self.data['Writers'])],
|
||||||
|
['Penciller', ', '.join(self.data['Pencillers'])], ['Inker', ', '.join(self.data['Inkers'])],
|
||||||
|
['Colorist', ', '.join(self.data['Colorists'])], ['Summary', self.data['Summary']],
|
||||||
|
['Title', self.data['Title']]):
|
||||||
|
if self.rawdata.getElementsByTagName(row[0]):
|
||||||
|
node = self.rawdata.getElementsByTagName(row[0])[0]
|
||||||
|
if row[1]:
|
||||||
|
node.firstChild.replaceWholeText(row[1])
|
||||||
|
else:
|
||||||
|
root.removeChild(node)
|
||||||
|
elif row[1]:
|
||||||
|
main = self.rawdata.createElement(row[0])
|
||||||
|
root.appendChild(main)
|
||||||
|
text = self.rawdata.createTextNode(row[1])
|
||||||
|
main.appendChild(text)
|
||||||
|
else:
|
||||||
|
doc = Document()
|
||||||
|
root = doc.createElement('ComicInfo')
|
||||||
|
root.setAttribute('xmlns:xsd', 'http://www.w3.org/2001/XMLSchema')
|
||||||
|
root.setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance')
|
||||||
|
doc.appendChild(root)
|
||||||
|
for row in (['Series', self.data['Series']], ['Volume', self.data['Volume']],
|
||||||
|
['Number', self.data['Number']], ['Writer', ', '.join(self.data['Writers'])],
|
||||||
|
['Penciller', ', '.join(self.data['Pencillers'])], ['Inker', ', '.join(self.data['Inkers'])],
|
||||||
|
['Colorist', ', '.join(self.data['Colorists'])], ['Summary', self.data['Summary']],
|
||||||
|
['Title', self.data['Title']]):
|
||||||
|
if row[1]:
|
||||||
|
main = doc.createElement(row[0])
|
||||||
|
root.appendChild(main)
|
||||||
|
text = doc.createTextNode(row[1])
|
||||||
|
main.appendChild(text)
|
||||||
|
self.rawdata = doc
|
||||||
|
if self.source.endswith('.xml'):
|
||||||
|
with open(self.source, 'w', encoding='utf-8') as f:
|
||||||
|
self.rawdata.writexml(f, encoding='utf-8')
|
||||||
|
else:
|
||||||
|
workdir = mkdtemp('', 'KCC-')
|
||||||
|
tmpXML = os.path.join(workdir, 'ComicInfo.xml')
|
||||||
|
with open(tmpXML, 'w', encoding='utf-8') as f:
|
||||||
|
self.rawdata.writexml(f, encoding='utf-8')
|
||||||
|
try:
|
||||||
|
cbx = comicarchive.ComicArchive(self.source)
|
||||||
|
cbx.addFile(tmpXML)
|
||||||
|
except OSError as e:
|
||||||
|
raise UserWarning(e)
|
||||||
|
rmtree(workdir, True)
|
||||||
@@ -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
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
# Copyright (c) 2012 Ciro Mattia Gonano <ciromattia@gmail.com>
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
|
||||||
|
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
|
||||||
#
|
#
|
||||||
# Based upon the code snippet by Ned Batchelder
|
# Based upon the code snippet by Ned Batchelder
|
||||||
# (http://nedbatchelder.com/blog/200712/extracting_jpgs_from_pdfs.html)
|
# (http://nedbatchelder.com/blog/200712/extracting_jpgs_from_pdfs.html)
|
||||||
@@ -18,56 +21,55 @@
|
|||||||
# PERFORMANCE OF THIS SOFTWARE.
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
#
|
#
|
||||||
|
|
||||||
__license__ = 'ISC'
|
|
||||||
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
# skip stray images a few pixels in size in some PDFs
|
||||||
|
# typical images are many thousands in length
|
||||||
|
# https://github.com/ciromattia/kcc/pull/546
|
||||||
|
STRAY_IMAGE_LENGTH_THRESHOLD = 300
|
||||||
|
|
||||||
|
|
||||||
class PdfJpgExtract:
|
class PdfJpgExtract:
|
||||||
def __init__(self, origFileName):
|
def __init__(self, fname, fullPath):
|
||||||
self.origFileName = origFileName
|
self.fname = fname
|
||||||
self.filename = os.path.splitext(origFileName)
|
self.path = fullPath
|
||||||
self.path = self.filename[0]
|
|
||||||
|
|
||||||
def getPath(self):
|
def getPath(self):
|
||||||
return self.path
|
return self.path
|
||||||
|
|
||||||
def extract(self):
|
def extract(self):
|
||||||
pdf = file(self.origFileName, "rb").read()
|
pdf = open(self.fname, "rb").read()
|
||||||
|
startmark = b"\xff\xd8"
|
||||||
startmark = "\xff\xd8"
|
|
||||||
startfix = 0
|
startfix = 0
|
||||||
endmark = "\xff\xd9"
|
endmark = b"\xff\xd9"
|
||||||
endfix = 2
|
endfix = 2
|
||||||
i = 0
|
i = 0
|
||||||
|
|
||||||
njpg = 0
|
njpg = 0
|
||||||
os.makedirs(self.path)
|
|
||||||
while True:
|
while True:
|
||||||
istream = pdf.find("stream", i)
|
istream = pdf.find(b"stream", i)
|
||||||
if istream < 0:
|
if istream < 0:
|
||||||
break
|
break
|
||||||
istart = pdf.find(startmark, istream, istream + 20)
|
istart = pdf.find(startmark, istream, istream + 20)
|
||||||
if istart < 0:
|
if istart < 0:
|
||||||
i = istream + 20
|
i = istream + 20
|
||||||
continue
|
continue
|
||||||
iend = pdf.find("endstream", istart)
|
iend = pdf.find(b"endstream", istart)
|
||||||
if iend < 0:
|
if iend < 0:
|
||||||
raise Exception("Didn't find end of stream!")
|
raise Exception("Didn't find end of stream!")
|
||||||
iend = pdf.find(endmark, iend - 20)
|
iend = pdf.find(endmark, iend - 20)
|
||||||
if iend < 0:
|
if iend < 0:
|
||||||
raise Exception("Didn't find end of JPG!")
|
raise Exception("Didn't find end of JPG!")
|
||||||
|
|
||||||
istart += startfix
|
istart += startfix
|
||||||
iend += endfix
|
iend += endfix
|
||||||
print "JPG %d from %d to %d" % (njpg, istart, iend)
|
i = iend
|
||||||
|
|
||||||
|
if iend - istart < STRAY_IMAGE_LENGTH_THRESHOLD:
|
||||||
|
continue
|
||||||
|
|
||||||
jpg = pdf[istart:iend]
|
jpg = pdf[istart:iend]
|
||||||
jpgfile = file(self.path + "/jpg%d.jpg" % njpg, "wb")
|
jpgfile = open(os.path.join(self.path, "jpg%d.jpg" % njpg), "wb")
|
||||||
jpgfile.write(jpg)
|
jpgfile.write(jpg)
|
||||||
jpgfile.close()
|
jpgfile.close()
|
||||||
|
|
||||||
njpg += 1
|
njpg += 1
|
||||||
i = iend
|
|
||||||
return self.path
|
return njpg
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
|
||||||
|
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and/or distribute this software for
|
||||||
|
# any purpose with or without fee is hereby granted, provided that the
|
||||||
|
# above copyright notice and this permission notice appear in all
|
||||||
|
# copies.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||||
|
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
||||||
|
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
||||||
|
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
|
||||||
|
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||||
|
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
import subprocess
|
||||||
|
from packaging.version import Version
|
||||||
|
from re import split
|
||||||
|
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)
|
||||||
|
self.reset()
|
||||||
|
self.strict = False
|
||||||
|
self.convert_charrefs = True
|
||||||
|
self.fed = []
|
||||||
|
|
||||||
|
def handle_data(self, d):
|
||||||
|
self.fed.append(d)
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
return ''.join(self.fed)
|
||||||
|
|
||||||
|
def error(self, message):
|
||||||
|
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()
|
||||||
|
return [name, ext]
|
||||||
|
|
||||||
|
def get_contain_resolution(image, size):
|
||||||
|
'''same code as Pillow ImageOps.contain()'''
|
||||||
|
im_ratio = image.width / image.height
|
||||||
|
dest_ratio = size[0] / size[1]
|
||||||
|
|
||||||
|
if im_ratio != dest_ratio:
|
||||||
|
if im_ratio > dest_ratio:
|
||||||
|
new_height = round(image.height / image.width * size[0])
|
||||||
|
if new_height != size[1]:
|
||||||
|
size = (size[0], new_height)
|
||||||
|
else:
|
||||||
|
new_width = round(image.width / image.height * size[1])
|
||||||
|
if new_width != size[0]:
|
||||||
|
size = (new_width, size[1])
|
||||||
|
|
||||||
|
return size
|
||||||
|
|
||||||
|
|
||||||
|
def walkSort(dirnames, filenames):
|
||||||
|
convert = lambda text: int(text) if text.isdigit() else text
|
||||||
|
alphanum_key = lambda key: [convert(c) for c in split('([0-9]+)', key)]
|
||||||
|
dirnames.sort(key=lambda name: alphanum_key(name.lower()))
|
||||||
|
filenames.sort(key=lambda name: alphanum_key(name.lower()))
|
||||||
|
return dirnames, filenames
|
||||||
|
|
||||||
|
|
||||||
|
def walkLevel(some_dir, level=1):
|
||||||
|
some_dir = some_dir.rstrip(os.path.sep)
|
||||||
|
assert os.path.isdir(some_dir)
|
||||||
|
num_sep = some_dir.count(os.path.sep)
|
||||||
|
for root, dirs, files in os.walk(some_dir):
|
||||||
|
dirs, files = walkSort(dirs, files)
|
||||||
|
yield root, dirs, files
|
||||||
|
num_sep_this = root.count(os.path.sep)
|
||||||
|
if num_sep + level <= num_sep_this:
|
||||||
|
del dirs[:]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def sanitizeTrace(traceback):
|
||||||
|
return ''.join(format_tb(traceback))\
|
||||||
|
.replace('C:/projects/kcc/', '')\
|
||||||
|
.replace('c:/projects/kcc/', '')\
|
||||||
|
.replace('C:/python37-x64/', '')\
|
||||||
|
.replace('c:/python37-x64/', '')\
|
||||||
|
.replace('C:\\projects\\kcc\\', '')\
|
||||||
|
.replace('c:\\projects\\kcc\\', '')\
|
||||||
|
.replace('C:\\python37-x64\\', '')\
|
||||||
|
.replace('c:\\python37-x64\\', '')
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
def dependencyCheck(level):
|
||||||
|
missing = []
|
||||||
|
if level > 2:
|
||||||
|
try:
|
||||||
|
from PySide6.QtCore import qVersion as qtVersion
|
||||||
|
if Version('6.0.0') > Version(qtVersion()):
|
||||||
|
missing.append('PySide 6.0.0')
|
||||||
|
except ImportError:
|
||||||
|
missing.append('PySide 6.0.0+')
|
||||||
|
try:
|
||||||
|
import raven
|
||||||
|
except ImportError:
|
||||||
|
missing.append('raven 6.0.0+')
|
||||||
|
if level > 1:
|
||||||
|
try:
|
||||||
|
from psutil import __version__ as psutilVersion
|
||||||
|
if Version('5.0.0') > Version(psutilVersion):
|
||||||
|
missing.append('psutil 5.0.0+')
|
||||||
|
except ImportError:
|
||||||
|
missing.append('psutil 5.0.0+')
|
||||||
|
try:
|
||||||
|
from types import ModuleType
|
||||||
|
from slugify import __version__ as slugifyVersion
|
||||||
|
if isinstance(slugifyVersion, ModuleType):
|
||||||
|
slugifyVersion = slugifyVersion.__version__
|
||||||
|
if Version('1.2.1') > Version(slugifyVersion):
|
||||||
|
missing.append('python-slugify 1.2.1+')
|
||||||
|
except ImportError:
|
||||||
|
missing.append('python-slugify 1.2.1+')
|
||||||
|
try:
|
||||||
|
from PIL import __version__ as pillowVersion
|
||||||
|
if Version('8.3.0') > Version(pillowVersion):
|
||||||
|
missing.append('Pillow 8.3.0+')
|
||||||
|
except ImportError:
|
||||||
|
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)
|
||||||
|
|
||||||
|
def subprocess_run(command, **kwargs):
|
||||||
|
if (os.name == 'nt'):
|
||||||
|
kwargs.setdefault('creationflags', subprocess.CREATE_NO_WINDOW)
|
||||||
|
return subprocess.run(command, **kwargs)
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
|
||||||
|
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and/or distribute this software for
|
||||||
|
# any purpose with or without fee is hereby granted, provided that the
|
||||||
|
# above copyright notice and this permission notice appear in all
|
||||||
|
# copies.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||||
|
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
||||||
|
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
||||||
|
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
|
||||||
|
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||||
|
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from . import __version__
|
||||||
|
from .shared import dependencyCheck
|
||||||
|
|
||||||
|
|
||||||
|
def start():
|
||||||
|
dependencyCheck(3)
|
||||||
|
from . import KCC_gui
|
||||||
|
os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] = "1"
|
||||||
|
KCCAplication = KCC_gui.QApplicationMessaging(sys.argv)
|
||||||
|
if KCCAplication.isRunning():
|
||||||
|
for i in range(1, len(sys.argv)):
|
||||||
|
KCCAplication.sendMessage(sys.argv[i])
|
||||||
|
else:
|
||||||
|
KCCAplication.sendMessage('ARISE')
|
||||||
|
else:
|
||||||
|
KCCWindow = KCC_gui.QMainWindowKCC()
|
||||||
|
KCCUI = KCC_gui.KCCGUI(KCCAplication, KCCWindow)
|
||||||
|
for i in range(1, len(sys.argv)):
|
||||||
|
KCCUI.handleMessage(sys.argv[i])
|
||||||
|
sys.exit(KCCAplication.exec_())
|
||||||
|
|
||||||
|
|
||||||
|
def startC2E():
|
||||||
|
dependencyCheck(2)
|
||||||
|
from .comic2ebook import main
|
||||||
|
print('comic2ebook v' + __version__ + ' - Written by Ciro Mattia Gonano and Pawel Jastrzebski.')
|
||||||
|
sys.exit(main(sys.argv[1:]))
|
||||||
|
|
||||||
|
|
||||||
|
def startC2P():
|
||||||
|
dependencyCheck(1)
|
||||||
|
from .comic2panel import main
|
||||||
|
print('comic2panel v' + __version__ + ' - Written by Ciro Mattia Gonano and Pawel Jastrzebski.')
|
||||||
|
sys.exit(main(sys.argv[1:]))
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
PySide6<6.10
|
||||||
|
Pillow>=11.3.0
|
||||||
|
psutil>=5.9.5
|
||||||
|
requests>=2.31.0
|
||||||
|
python-slugify>=1.2.1,<9.0.0
|
||||||
|
raven>=6.0.0
|
||||||
|
packaging>=23.2
|
||||||
|
mozjpeg-lossless-optimization>=1.2.0
|
||||||
|
natsort>=8.4.0
|
||||||
|
distro>=1.8.0
|
||||||
|
numpy>=1.22.4
|
||||||
|
PyMuPDF>=1.18.0
|
||||||
@@ -1,87 +1,165 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
py2app/cx_Freeze build script for KCC.
|
pip/pyinstaller build script for KCC.
|
||||||
|
|
||||||
Will automatically ensure that all build prerequisites are available via ez_setup
|
Install as Python package:
|
||||||
|
python3 setup.py install
|
||||||
|
|
||||||
Usage (Mac OS X):
|
Create EXE/APP:
|
||||||
python setup.py py2app
|
python3 setup.py build_binary
|
||||||
|
python3 setup.py build_c2e
|
||||||
Usage (Windows):
|
python3 setup.py build_c2p
|
||||||
python setup.py build
|
|
||||||
"""
|
"""
|
||||||
from ez_setup import use_setuptools
|
|
||||||
use_setuptools()
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
import setuptools
|
||||||
|
from kindlecomicconverter import __version__
|
||||||
|
|
||||||
NAME = "KindleComicConverter"
|
NAME = 'KindleComicConverter'
|
||||||
VERSION = "2.10"
|
MAIN = 'kcc.py'
|
||||||
MAIN = "kcc.py"
|
VERSION = __version__
|
||||||
|
|
||||||
includefiles = ['README.md', 'MANIFEST.in', 'LICENSE.txt', 'comic2ebook.ico', 'comic2ebook.icns']
|
|
||||||
includes = []
|
|
||||||
excludes = []
|
|
||||||
|
|
||||||
if sys.platform == "darwin":
|
# noinspection PyUnresolvedReferences
|
||||||
from setuptools import setup
|
class BuildBinaryCommand(setuptools.Command):
|
||||||
extra_options = dict(
|
description = 'build binary release'
|
||||||
setup_requires=['py2app'],
|
user_options = []
|
||||||
app=[MAIN],
|
|
||||||
options=dict(
|
|
||||||
py2app=dict(
|
|
||||||
argv_emulation=True,
|
|
||||||
iconfile='comic2ebook.icns',
|
|
||||||
plist=dict(
|
|
||||||
CFBundleName=NAME,
|
|
||||||
CFBundleShortVersionString=VERSION,
|
|
||||||
CFBundleGetInfoString=NAME + " " + VERSION + ", written 2012-2013 by Ciro Mattia Gonano",
|
|
||||||
CFBundleExecutable=NAME,
|
|
||||||
CFBundleIdentifier='com.github.ciromattia.kcc',
|
|
||||||
CFBundleSignature='dplt'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif sys.platform == 'win32':
|
|
||||||
from cx_Freeze import setup, Executable
|
|
||||||
base = "Win32GUI"
|
|
||||||
extra_options = dict(
|
|
||||||
options={"build_exe": {"include_files": includefiles, 'excludes': excludes, 'compressed': True}},
|
|
||||||
executables=[Executable(MAIN,
|
|
||||||
base=base,
|
|
||||||
icon="comic2ebook.ico",
|
|
||||||
copyDependentFiles=True,
|
|
||||||
appendScriptToExe=True,
|
|
||||||
appendScriptToLibrary=False,
|
|
||||||
compress=True)])
|
|
||||||
else:
|
|
||||||
extra_options = dict(
|
|
||||||
scripts=[MAIN],
|
|
||||||
)
|
|
||||||
|
|
||||||
setup(
|
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 "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
|
||||||
|
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':
|
||||||
|
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(
|
||||||
|
'pyinstaller --hidden-import=_cffi_backend --hidden-import=queue -y -F -i icons/comic2ebook.ico -n kcc_linux_' + VERSION + ' kcc.py')
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# 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,
|
name=NAME,
|
||||||
version=VERSION,
|
version=VERSION,
|
||||||
author="Ciro Mattia Gonano",
|
author='Ciro Mattia Gonano, Pawel Jastrzebski',
|
||||||
author_email="ciromattia@gmail.com",
|
author_email='ciromattia@gmail.com, pawelj@iosphe.re',
|
||||||
description="A tool to convert comics (CBR/CBZ/PDFs/image folders) to Mobipocket.",
|
description='Comic and Manga converter for e-book readers.',
|
||||||
license="ISC License (ISCL)",
|
license='ISC License (ISCL)',
|
||||||
keywords="kindle comic mobipocket mobi cbz cbr manga",
|
keywords=['kindle', 'kobo', 'comic', 'manga', 'mobi', 'epub', 'cbz'],
|
||||||
url="http://github.com/ciromattia/kcc",
|
url='http://github.com/ciromattia/kcc',
|
||||||
classifiers=[
|
entry_points={
|
||||||
'Development Status :: 4 - Beta'
|
'console_scripts': [
|
||||||
'License :: OSI Approved :: ISC License (ISCL)',
|
'kcc-c2e = kindlecomicconverter.startup:startC2E',
|
||||||
'Environment :: Console',
|
'kcc-c2p = kindlecomicconverter.startup:startC2P',
|
||||||
'Environment :: MacOS X',
|
|
||||||
'Environment :: Win32 (MS Windows)',
|
|
||||||
'Environment :: X11 Applications',
|
|
||||||
'Intended Audience :: End Users/Desktop',
|
|
||||||
'Operating System :: OS Independent',
|
|
||||||
'Programming Language :: Python',
|
|
||||||
'Programming Language :: Python :: 2.7',
|
|
||||||
'Topic :: Multimedia :: Graphics :: Graphics Conversion',
|
|
||||||
'Topic :: Utilities'
|
|
||||||
],
|
],
|
||||||
packages=['kcc'],
|
'gui_scripts': [
|
||||||
**extra_options
|
'kcc = kindlecomicconverter.startup:start',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
packages=['kindlecomicconverter'],
|
||||||
|
install_requires=[
|
||||||
|
'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',
|
||||||
|
'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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
"""
|
|
||||||
cx_freeze build script for Windows KCC No-GUI release.
|
|
||||||
|
|
||||||
Usage (Windows):
|
|
||||||
python setup_console.py build
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
from cx_Freeze import setup, Executable
|
|
||||||
sys.path.insert(0, 'kcc')
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name = "KindleComicConverter",
|
|
||||||
version = "2.10",
|
|
||||||
author = "Ciro Mattia Gonano",
|
|
||||||
author_email = "ciromattia@gmail.com",
|
|
||||||
description = "A tool to convert comics (CBR/CBZ/PDFs/image folders) to MOBI.",
|
|
||||||
license= "ISC License (ISCL)",
|
|
||||||
keywords= "kindle comic mobipocket mobi cbz cbr manga",
|
|
||||||
url = "http://github.com/ciromattia/kcc",
|
|
||||||
executables = [Executable("kcc/comic2ebook.py", compress=True, copyDependentFiles=True, appendScriptToExe=True, appendScriptToLibrary=False),
|
|
||||||
Executable("kcc/kindlestrip.py", compress=True, copyDependentFiles=True, appendScriptToExe=True, appendScriptToLibrary=False)]
|
|
||||||
)
|
|
||||||