1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-16 22:18:51 +00:00

Compare commits

...

55 Commits
3.5 ... 3.6.2

Author SHA1 Message Date
Paweł Jastrzębski
e835502837 Version Bump 2013-12-07 20:08:20 +01:00
Paweł Jastrzębski
5bcdc78725 Fixed Panel View bugs 2013-12-07 14:50:37 +01:00
Paweł Jastrzębski
acb4dfad8f Fixex PNG hotfix 2013-12-07 09:52:20 +01:00
Paweł Jastrzębski
7f5de29174 Version Bump 2013-12-06 23:33:04 +01:00
Paweł Jastrzębski
11007402cd Fixed PNG output (sigh!) 2013-12-06 23:27:58 +01:00
Paweł Jastrzębski
0cf92fc48f Fixed psutil detection 2013-12-06 17:58:39 +01:00
Paweł Jastrzębski
953942ca00 Fixed tray icon owner 2013-12-06 17:42:11 +01:00
Paweł Jastrzębski
c46ca8b507 Updated README + Minor tweaks 2013-12-06 17:18:20 +01:00
Paweł Jastrzębski
48d3bee225 Upscaling is now default on Kindle Fire HD/HDX models 2013-12-05 19:49:29 +01:00
Paweł Jastrzębski
3b0e5cc309 Panel View support overhaul - Round 2 2013-12-05 15:41:47 +01:00
Paweł Jastrzębski
e5be31f9d5 TrayIcon: Clicking it now properly unminimize window 2013-12-04 19:04:54 +01:00
Paweł Jastrzębski
17ea85c31f Overhauled Panel View support 2013-12-04 18:32:32 +01:00
Paweł Jastrzębski
572e1422bf Gamma auto mode is now even more automatic 2013-12-04 18:30:19 +01:00
Ciro Mattia Gonano
af263073b5 Update README.md 2013-12-04 11:42:45 +01:00
Ciro Mattia Gonano
7facf2d620 Re-add text link for bitcoin 2013-12-04 11:42:26 +01:00
Ciro Mattia Gonano
eef3ff434b Added BountySource link and reformatted Donations section 2013-12-04 11:08:04 +01:00
Ciro Mattia Gonano
c680cfd5c5 Update README.md 2013-11-27 12:40:01 +01:00
Paweł Jastrzębski
3e8469611d Updated README 2013-11-25 10:41:30 +01:00
Paweł Jastrzębski
39ab475156 Updated README 2013-11-25 10:38:20 +01:00
Ciro Mattia Gonano
636de67a17 Update README.md 2013-11-25 10:14:56 +01:00
Ciro Mattia Gonano
d80c18f652 Add OS X 10.7 build download link 2013-11-25 10:14:23 +01:00
Paweł Jastrzębski
557bd2bbbf Added separate resolution for Kindle DX/DXG CBZ output (close #71) 2013-11-19 08:46:13 +01:00
Paweł Jastrzębski
ddd223c2ec Code cleanup 2013-11-13 11:27:26 +01:00
Paweł Jastrzębski
50f5b600b1 OS specific tweaks to status bar style 2013-11-12 14:46:32 +01:00
Paweł Jastrzębski
d94df8390a Added status bar with links 2013-11-12 14:32:38 +01:00
Paweł Jastrzębski
8b33331929 Disabled systray icon on OSX (close #70) 2013-11-12 13:18:27 +01:00
Paweł Jastrzębski
86a9dde1eb Added simple tray icon (close #69) 2013-11-12 12:13:57 +01:00
Ciro Mattia Gonano
33dec77063 Merge remote-tracking branch 'origin/master' 2013-11-11 11:52:04 +01:00
Ciro Mattia Gonano
fe06e2fa19 Version bump 2013-11-11 11:51:46 +01:00
Paweł Jastrzębski
7b5e3eaafd Updated README 2013-11-11 11:30:00 +01:00
Paweł Jastrzębski
0a30f1ffb9 Implemented OSX PATH change to Windows code too + minor tweaks 2013-11-09 20:09:34 +01:00
Paweł Jastrzębski
8687604d26 Tweak for Windows development environment 2013-11-08 18:19:58 +01:00
Ciro Mattia Gonano
0a9fd6c439 Merge branch 'master' of github.com:ciromattia/kcc 2013-11-08 17:21:42 +01:00
Paweł Jastrzębski
c95a9395de Optimization of ProgressThread 2013-11-08 17:13:41 +01:00
Paweł Jastrzębski
77066d7a9f Optimization of ProgressThread 2013-11-08 17:06:06 +01:00
Paweł Jastrzębski
c8e5b7de9a Implemented new method to detect border color in non-webtoon comics 2013-11-08 16:55:43 +01:00
Ciro Mattia Gonano
3e11a88a7c Bundle 7za and unrar for OSX too. 2013-11-08 15:32:22 +01:00
Paweł Jastrzębski
a7e4968836 GUI tweaks 2013-11-08 15:11:33 +01:00
Paweł Jastrzębski
6d9e2d3c03 Added Linux build script 2013-11-07 22:53:28 +01:00
Paweł Jastrzębski
0789e7a353 Updated README 2013-11-07 13:53:37 +01:00
Ciro Mattia Gonano
ff97a85552 KCC available from 10.6+ 2013-11-07 12:57:39 +01:00
Ciro Mattia Gonano
33cfd92cef Remove 10.8 limit 2013-11-07 12:14:03 +01:00
Paweł Jastrzębski
58513ef59f Updated Inno Setup script 2013-11-07 07:58:03 +01:00
Paweł Jastrzębski
6056e3e767 Updated OSX setup 2013-11-06 18:44:14 +01:00
Paweł Jastrzębski
1b1ed7c4ab Improved error messages about missing dependencies 2013-11-06 13:49:12 +01:00
Paweł Jastrzębski
5b44e4bddd Moved to psutil Popen 2013-11-06 11:41:19 +01:00
Paweł Jastrzębski
54592969a4 Optimized imports 2013-11-06 11:14:01 +01:00
Paweł Jastrzębski
38007ab3d5 Number of KindleGen threads is now dynamic 2013-11-06 11:08:14 +01:00
Paweł Jastrzębski
bdd10c7617 Tweaked KindleGen/KindleUnpack multiprocessing 2013-11-05 16:11:14 +01:00
Paweł Jastrzębski
c0f4bc021a Tweaked ComicRack metadata parser 2013-11-04 20:08:54 +01:00
Paweł Jastrzębski
34d6af93a6 Refactored KindleGen/KindleUnpack handling 2013-11-04 17:07:10 +01:00
Paweł Jastrzębski
0df481dabb Added ComicRack metadata parser 2013-11-03 09:29:13 +01:00
Paweł Jastrzębski
55c5b91411 README update 2013-10-31 14:04:06 +01:00
Paweł Jastrzębski
be745f4602 README update 2013-10-31 13:50:40 +01:00
Paweł Jastrzębski
8bf5ad0f12 Fixed headers 2013-10-30 11:50:32 +01:00
18 changed files with 867 additions and 425 deletions

8
.gitignore vendored
View File

@@ -2,11 +2,11 @@
*.cbz *.cbz
*.cbr *.cbr
.idea .idea
.DS_Store
Thumbs.db
build build
dist dist
Output Output
test
solaio
kindlegen* kindlegen*
UnRAR*
7za*
.DS_Store
Thumbs.db

View File

@@ -7,19 +7,19 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>420</width> <width>420</width>
<height>380</height> <height>397</height>
</rect> </rect>
</property> </property>
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>420</width> <width>420</width>
<height>380</height> <height>397</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>420</width> <width>420</width>
<height>380</height> <height>397</height>
</size> </size>
</property> </property>
<property name="font"> <property name="font">
@@ -27,9 +27,6 @@
<pointsize>9</pointsize> <pointsize>9</pointsize>
</font> </font>
</property> </property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="windowTitle"> <property name="windowTitle">
<string>Kindle Comic Converter</string> <string>Kindle Comic Converter</string>
</property> </property>
@@ -546,9 +543,6 @@ p, li { white-space: pre-wrap; }
<family>DejaVu Sans</family> <family>DejaVu Sans</family>
</font> </font>
</property> </property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;When converting color images setting this option to 1.0 &lt;span style=&quot; font-weight:600;&quot;&gt;might&lt;/span&gt; improve readability.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text"> <property name="text">
<string>Gamma: Auto</string> <string>Gamma: Auto</string>
</property> </property>
@@ -570,9 +564,6 @@ p, li { white-space: pre-wrap; }
<property name="focusPolicy"> <property name="focusPolicy">
<enum>Qt::ClickFocus</enum> <enum>Qt::ClickFocus</enum>
</property> </property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;When converting color images setting this option to 1.0 &lt;span style=&quot; font-weight:600;&quot;&gt;might&lt;/span&gt; improve readability.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="maximum"> <property name="maximum">
<number>500</number> <number>500</number>
</property> </property>
@@ -785,6 +776,17 @@ p, li { white-space: pre-wrap; }
<zorder>OptionsExpert</zorder> <zorder>OptionsExpert</zorder>
<zorder>ProgressBar</zorder> <zorder>ProgressBar</zorder>
</widget> </widget>
<widget class="QStatusBar" name="statusBar">
<property name="font">
<font>
<family>DejaVu Sans</family>
<pointsize>8</pointsize>
</font>
</property>
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
</widget>
<action name="ActionBasic"> <action name="ActionBasic">
<property name="checkable"> <property name="checkable">
<bool>true</bool> <bool>true</bool>

View File

@@ -7,19 +7,19 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>420</width> <width>420</width>
<height>380</height> <height>397</height>
</rect> </rect>
</property> </property>
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>420</width> <width>420</width>
<height>380</height> <height>397</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>420</width> <width>420</width>
<height>380</height> <height>397</height>
</size> </size>
</property> </property>
<property name="font"> <property name="font">
@@ -27,9 +27,6 @@
<pointsize>9</pointsize> <pointsize>9</pointsize>
</font> </font>
</property> </property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="windowTitle"> <property name="windowTitle">
<string>Kindle Comic Converter</string> <string>Kindle Comic Converter</string>
</property> </property>
@@ -546,9 +543,6 @@
<bold>false</bold> <bold>false</bold>
</font> </font>
</property> </property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:12pt;&quot;&gt;When converting color images setting this option to 1.0 &lt;/span&gt;&lt;span style=&quot; font-size:12pt; font-weight:600;&quot;&gt;might&lt;/span&gt;&lt;span style=&quot; font-size:12pt;&quot;&gt; improve readability.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text"> <property name="text">
<string>Gamma: Auto</string> <string>Gamma: Auto</string>
</property> </property>
@@ -570,9 +564,6 @@
<property name="focusPolicy"> <property name="focusPolicy">
<enum>Qt::ClickFocus</enum> <enum>Qt::ClickFocus</enum>
</property> </property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:12pt;&quot;&gt;When converting color images setting this option to 1.0 &lt;/span&gt;&lt;span style=&quot; font-size:12pt; font-weight:600;&quot;&gt;might&lt;/span&gt;&lt;span style=&quot; font-size:12pt;&quot;&gt; improve readability.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="maximum"> <property name="maximum">
<number>500</number> <number>500</number>
</property> </property>
@@ -797,6 +788,17 @@
<zorder>OptionsAdvancedGamma</zorder> <zorder>OptionsAdvancedGamma</zorder>
<zorder>OptionsExpert</zorder> <zorder>OptionsExpert</zorder>
</widget> </widget>
<widget class="QStatusBar" name="statusBar">
<property name="font">
<font>
<family>Aharoni</family>
<pointsize>8</pointsize>
</font>
</property>
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
</widget>
<action name="ActionBasic"> <action name="ActionBasic">
<property name="checkable"> <property name="checkable">
<bool>true</bool> <bool>true</bool>

26
KCC.ui
View File

@@ -7,19 +7,19 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>420</width> <width>420</width>
<height>380</height> <height>397</height>
</rect> </rect>
</property> </property>
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>420</width> <width>420</width>
<height>380</height> <height>397</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>420</width> <width>420</width>
<height>380</height> <height>397</height>
</size> </size>
</property> </property>
<property name="font"> <property name="font">
@@ -27,9 +27,6 @@
<pointsize>9</pointsize> <pointsize>9</pointsize>
</font> </font>
</property> </property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="windowTitle"> <property name="windowTitle">
<string>Kindle Comic Converter</string> <string>Kindle Comic Converter</string>
</property> </property>
@@ -472,9 +469,6 @@ p, li { white-space: pre-wrap; }
<height>40</height> <height>40</height>
</rect> </rect>
</property> </property>
<property name="toolTip">
<string>When converting color images setting this option to 1.0 MIGHT improve readability.</string>
</property>
<property name="text"> <property name="text">
<string>Gamma: Auto</string> <string>Gamma: Auto</string>
</property> </property>
@@ -491,9 +485,6 @@ p, li { white-space: pre-wrap; }
<property name="focusPolicy"> <property name="focusPolicy">
<enum>Qt::ClickFocus</enum> <enum>Qt::ClickFocus</enum>
</property> </property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;When converting color images setting this option to 1.0 &lt;span style=&quot; font-weight:600;&quot;&gt;might&lt;/span&gt; improve readability.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="maximum"> <property name="maximum">
<number>500</number> <number>500</number>
</property> </property>
@@ -674,6 +665,17 @@ p, li { white-space: pre-wrap; }
<zorder>OptionsExpert</zorder> <zorder>OptionsExpert</zorder>
<zorder>ProgressBar</zorder> <zorder>ProgressBar</zorder>
</widget> </widget>
<widget class="QStatusBar" name="statusBar">
<property name="font">
<font>
<family>MS Shell Dlg 2</family>
<pointsize>8</pointsize>
</font>
</property>
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
</widget>
<action name="ActionBasic"> <action name="ActionBasic">
<property name="checkable"> <property name="checkable">
<bool>true</bool> <bool>true</bool>

View File

@@ -1,6 +1,6 @@
# KCC # KCC
**KindleComicConverter** is a Python app to convert comic files or folders to ePub or Panel View MOBI. **Kindle Comic Converter** is a Python app to convert comic files or folders to ePub or Panel View MOBI.
It was initally developed for Kindle but since v2.2 it outputs valid ePub 2.0 so _**despite its name, KCC is 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 to EPUB converter that every e-reader owner can happily use**_. actually a comic to EPUB converter that every e-reader owner can happily use**_.
It can also optionally optimize images by applying a number of transformations. It can also optionally optimize images by applying a number of transformations.
@@ -8,21 +8,23 @@ It can also optionally optimize images by applying a number of transformations.
### 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 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'll 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 ;)
### Issues / new features / donations
If you have some problems using KCC please [file an issue here](https://github.com/ciromattia/kcc/issues/new).
If you can fix an open issue, fork & make a pull request.
If you want more chances an issue is fixes or your wanted feature added, consider [placing a bounty](https://www.bountysource.com/trackers/65571-ciromattia-kcc)!
### Donations
If you find **KCC** valuable you can consider donating to the authors: If you find **KCC** valuable you can consider donating to the authors:
* Ciro Mattia Gonano: [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=D8WNYNPBGDAS2) [![Flattr this](http://api.flattr.com/button/flattr-badge-large.png)](http://flattr.com/thing/2260449/ciromattiakcc-on-GitHub)
* Ciro Mattia Gonano [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=D8WNYNPBGDAS2) * Paweł Jastrzębski: [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YTTJ4LK2JDHPS) [![1W15wwqsfd7wbaZ6wvSJf1LW1bz6q5L8b](http://s30.postimg.org/6z3kwvdlp/BC_Rnd.png)](bitcoin:1W15wwqsfd7wbaZ6wvSJf1LW1bz6q5L8b?label=KCC) [1W15wwqsfd7wbaZ6wvSJf1LW1bz6q5L8b](bitcoin:1W15wwqsfd7wbaZ6wvSJf1LW1bz6q5L8b?label=KCC)
* Paweł Jastrzębski [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YTTJ4LK2JDHPS)
## BINARY RELEASES ## BINARY RELEASES
You can find the latest released binary at the following links: You can find the latest released binary at the following links:
- **Win64:** [http://kcc.vulturis.eu/Win64/](http://kcc.vulturis.eu/Win64/) - **Windows:** [http://kcc.vulturis.eu/Windows/](http://kcc.vulturis.eu/Windows/)
- **Win32:** [http://kcc.vulturis.eu/Win32/](http://kcc.vulturis.eu/Win32/) - **Linux:** [http://kcc.vulturis.eu/Linux/](http://kcc.vulturis.eu/Linux/)
- **OS X:** [http://kcc.vulturis.eu/OSX/](http://kcc.vulturis.eu/OSX/) - **OS X (10.8 or later):** [http://kcc.vulturis.eu/OSX/](http://kcc.vulturis.eu/OSX/)
- **Linux:** Just download sourcecode and launch: `python kcc.py` - **OS X (10.7 or earlier):** Soon™
## 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 file types:
@@ -40,9 +42,10 @@ You can find the latest released binary at the following links:
### For compiling/running from source: ### 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. - Python 2.7 - Included in MacOS and Linux, follow the [official documentation](http://www.python.org/getit/windows/) to install on Windows.
- PyQt4 - Please refer to official documentation for installing into your system. - [PyQt4](http://www.riverbankcomputing.co.uk/software/pyqt/download) - Please refer to official documentation for installing into your system.
- [Pillow](http://pypi.python.org/pypi/Pillow/) 2.2.1+ - For comic optimizations. Please refer to official documentation for installing into your system. - [Pillow](http://pypi.python.org/pypi/Pillow/) 2.2.1+ - For comic optimizations. Please refer to official documentation for installing into your system.
- **To build OS X release a modified QT is required:** [Patch](https://github.com/ciromattia/kcc/blob/master/other/QT-4.8.5-QListWidget.patch) - [Psutil](https://code.google.com/p/psutil/) - Please refer to official documentation for installing into your system.
- **To build OS X release you need a modified QT:** [patch](https://github.com/ciromattia/kcc/blob/master/other/QT-4.8.5-QListWidget.patch)
## USAGE ## USAGE
@@ -51,11 +54,17 @@ You can find the latest released binary at the following links:
* Read tooltip of _High/Ultra quality_ option. There are many important informations there. * Read tooltip of _High/Ultra quality_ option. There are many important informations there.
* When converting images smaller than device resolution remember to enable upscaling. * When converting images smaller than device resolution remember to enable upscaling.
* Panel View (auto zooming every part of page) can be disabled directly on Kindle. There is no KCC option to do that. * Panel View (auto zooming every part of page) can be disabled directly on Kindle. There is no KCC option to do that.
* If you're converting color images and the end result is not satisfactory, experiment with gamma correction option (check 1.0 setting first).
* Check our [wiki](https://github.com/ciromattia/kcc/wiki/Other-devices) for a list of tested Non-Kindle E-Readers. * Check our [wiki](https://github.com/ciromattia/kcc/wiki/Other-devices) for a list of tested Non-Kindle E-Readers.
* The first image found will be set as the comic's cover. * The first image found will be set as the comic's cover.
* All files/directories will be added to EPUB in alphabetical order. * All files/directories will be added to EPUB in alphabetical order.
* Output MOBI file should be uploaded via USB. Other methods might corrupt it. * Using high/ultra quality output option with Kindle Fire HD/HDX in most cases is just waste of space.
* ComicRack metadata will be parsed only if they are saved in *ComicInfo.xml* file.
### Calibre:
* Calibre can be used to upload files created by KCC.
* Uploading KCC output with Calibre will remove *Personal* tag from cover.
* **Don't convert files created by KCC with Calibre!** Any conversion process will corrupt the file!
* Don't use Calibre reader to preview files created by KCC. It can't parse them correctly.
### GUI ### GUI
@@ -147,30 +156,30 @@ The app relies and includes the following scripts/binaries:
* [Kindle Fire HDX 8.9"](http://kcc.vulturis.eu/Samples/Ubunchu!-KFHDX8.mobi) * [Kindle Fire HDX 8.9"](http://kcc.vulturis.eu/Samples/Ubunchu!-KFHDX8.mobi)
## CHANGELOG ## CHANGELOG
####1.00 ####1.0
* Initial version * Initial version
####1.10 ####1.1
* Added support for CBZ/CBR files in comic2ebook.py * Added support for CBZ/CBR files in comic2ebook.py
####1.11 ####1.1.1
* Added support for CBZ/CBR files in KindleComicConverter * Added support for CBZ/CBR files in Kindle Comic Converter
####1.20 ####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! * 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 ####1.3
* Fixed an issue in OPF generation for device resolution * Fixed an issue in OPF generation for device resolution
* Reworked options system (call with -h option to get the inline help) * Reworked options system (call with -h option to get the inline help)
####1.40 ####1.4
* Added some options for controlling image optimization * Added some options for controlling image optimization
* Further optimization (ImageOps, page numbering cut, autocontrast) * Further optimization (ImageOps, page numbering cut, autocontrast)
####1.41 ####1.4.1
* Fixed a serious bug on resizing when img ratio was bigger than device one * Fixed a serious bug on resizing when img ratio was bigger than device one
####1.50 ####1.5
* Added subfolder support for multiple chapters. * Added subfolder support for multiple chapters.
####2.0 ####2.0
@@ -179,7 +188,7 @@ The app relies and includes the following scripts/binaries:
####2.1 ####2.1
* Added basic error reporting * Added basic error reporting
#### 2.2: ####2.2:
* Added (valid!) ePub 2.0 output * Added (valid!) ePub 2.0 output
* Rename .zip files to .cbz to avoid overwriting * Rename .zip files to .cbz to avoid overwriting
@@ -284,6 +293,24 @@ The app relies and includes the following scripts/binaries:
* Improved multiprocessing speed * Improved multiprocessing speed
* GUI tweaks and minor bug fixes * GUI tweaks and minor bug fixes
####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.6.1:
* Fixed PNG output
####3.6.2:
* Fixed previous PNG output fix
* Fixed Panel View anomalies
## COPYRIGHT ## COPYRIGHT
Copyright (c) 2012-2013 Ciro Mattia Gonano and Paweł Jastrzębski. Copyright (c) 2012-2013 Ciro Mattia Gonano and Paweł Jastrzębski.

View File

@@ -1,5 +1,5 @@
#define MyAppName "Kindle Comic Converter" #define MyAppName "Kindle Comic Converter"
#define MyAppVersion "3.5" #define MyAppVersion "3.6.2"
#define MyAppPublisher "Ciro Mattia Gonano, Paweł Jastrzębski" #define MyAppPublisher "Ciro Mattia Gonano, Paweł Jastrzębski"
#define MyAppURL "http://kcc.vulturis.eu/" #define MyAppURL "http://kcc.vulturis.eu/"
#define MyAppExeName "KCC.exe" #define MyAppExeName "KCC.exe"
@@ -12,6 +12,7 @@ AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL} AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL} AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL} AppUpdatesURL={#MyAppURL}
AppCopyright=Copyright (C) 2012-2013 Ciro Mattia Gonano and Paweł Jastrzębski
DefaultDirName={pf}\{#MyAppName} DefaultDirName={pf}\{#MyAppName}
DefaultGroupName={#MyAppName} DefaultGroupName={#MyAppName}
AllowNoIcons=yes AllowNoIcons=yes
@@ -28,6 +29,7 @@ UninstallDisplayName={#MyAppName}
UninstallDisplayIcon={app}\{#MyAppExeName} UninstallDisplayIcon={app}\{#MyAppExeName}
ChangesAssociations=True ChangesAssociations=True
InfoAfterFile=other\InstallWarning.rtf InfoAfterFile=other\InstallWarning.rtf
SignTool=SignTool /d $q{#MyAppName}$q /du $q{#MyAppURL}$q $f
[Languages] [Languages]
Name: "english"; MessagesFile: "compiler:Default.isl" Name: "english"; MessagesFile: "compiler:Default.isl"
@@ -63,6 +65,7 @@ Source: "build\exe.win-amd64-2.7\select.pyd"; DestDir: "{app}"; Flags: ignorever
Source: "build\exe.win-amd64-2.7\sip.pyd"; DestDir: "{app}"; Flags: ignoreversion; Check: Is64BitInstallMode Source: "build\exe.win-amd64-2.7\sip.pyd"; DestDir: "{app}"; Flags: ignoreversion; Check: Is64BitInstallMode
Source: "build\exe.win-amd64-2.7\SSLEAY32.dll"; DestDir: "{app}"; Flags: ignoreversion; Check: Is64BitInstallMode Source: "build\exe.win-amd64-2.7\SSLEAY32.dll"; DestDir: "{app}"; Flags: ignoreversion; Check: Is64BitInstallMode
Source: "build\exe.win-amd64-2.7\unicodedata.pyd"; DestDir: "{app}"; Flags: ignoreversion; Check: Is64BitInstallMode Source: "build\exe.win-amd64-2.7\unicodedata.pyd"; DestDir: "{app}"; Flags: ignoreversion; Check: Is64BitInstallMode
Source: "build\exe.win-amd64-2.7\_psutil_mswindows.pyd"; DestDir: "{app}"; Flags: ignoreversion; Check: Is64BitInstallMode
; x86 files ; x86 files
Source: "build\exe.win32-2.7\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion solidbreak; Check: not Is64BitInstallMode Source: "build\exe.win32-2.7\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion solidbreak; Check: not Is64BitInstallMode
Source: "build\exe.win32-2.7\_ctypes.pyd"; DestDir: "{app}"; Flags: ignoreversion; Check: not Is64BitInstallMode Source: "build\exe.win32-2.7\_ctypes.pyd"; DestDir: "{app}"; Flags: ignoreversion; Check: not Is64BitInstallMode
@@ -87,6 +90,7 @@ Source: "build\exe.win32-2.7\select.pyd"; DestDir: "{app}"; Flags: ignoreversion
Source: "build\exe.win32-2.7\sip.pyd"; DestDir: "{app}"; Flags: ignoreversion; Check: not Is64BitInstallMode Source: "build\exe.win32-2.7\sip.pyd"; DestDir: "{app}"; Flags: ignoreversion; Check: not Is64BitInstallMode
Source: "build\exe.win32-2.7\SSLEAY32.dll"; DestDir: "{app}"; Flags: ignoreversion; Check: not Is64BitInstallMode Source: "build\exe.win32-2.7\SSLEAY32.dll"; DestDir: "{app}"; Flags: ignoreversion; Check: not Is64BitInstallMode
Source: "build\exe.win32-2.7\unicodedata.pyd"; DestDir: "{app}"; Flags: ignoreversion; Check: not Is64BitInstallMode Source: "build\exe.win32-2.7\unicodedata.pyd"; DestDir: "{app}"; Flags: ignoreversion; Check: not Is64BitInstallMode
Source: "build\exe.win32-2.7\_psutil_mswindows.pyd"; DestDir: "{app}"; Flags: ignoreversion; Check: not Is64BitInstallMode
; Common files ; Common files
Source: "LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion solidbreak Source: "LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion solidbreak
Source: "other\Additional-LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion Source: "other\Additional-LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion

45
kcc.py
View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python2
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2012-2013 Ciro Mattia Gonano <ciromattia@gmail.com> # Copyright (c) 2012-2013 Ciro Mattia Gonano <ciromattia@gmail.com>
@@ -18,7 +18,7 @@
# 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__ = '3.5' __version__ = '3.6.2'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@vulturis.eu>' __copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@vulturis.eu>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
@@ -30,22 +30,28 @@ try:
from PyQt4 import QtCore, QtGui, QtNetwork from PyQt4 import QtCore, QtGui, QtNetwork
except ImportError: except ImportError:
print "ERROR: PyQT4 is not installed!" print "ERROR: PyQT4 is not installed!"
if sys.platform.startswith('linux'):
import Tkinter
import tkMessageBox
importRoot = Tkinter.Tk()
importRoot.withdraw()
tkMessageBox.showerror("KCC - Error", "PyQT4 is not installed!")
exit(1) exit(1)
from kcc import KCC_gui from kcc import KCC_gui
from multiprocessing import freeze_support from multiprocessing import freeze_support
# OS specific PATH variable workarounds
if sys.platform.startswith('darwin'): if sys.platform.startswith('darwin'):
# Workaround Finder-launched app PATH evaluation if 'RESOURCEPATH' in os.environ:
os.environ['PATH'] = '/usr/local/bin:' + os.environ['PATH'] os.environ['PATH'] = os.environ['RESOURCEPATH'] + ':' + os.environ['PATH']
from kcc import KCC_ui_osx as KCC_ui else:
elif sys.platform.startswith('linux'): os.environ['PATH'] = os.path.dirname(os.path.abspath(__file__)) + '/other/:' + os.environ['PATH']
from kcc import KCC_ui_linux as KCC_ui elif sys.platform.startswith('win'):
else:
# Workaround for Windows file association mechanism
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
os.chdir(os.path.dirname(os.path.abspath(sys.executable))) os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
else: else:
os.environ['PATH'] = os.path.dirname(os.path.abspath(__file__)) + '/other/;' + os.environ['PATH']
os.chdir(os.path.dirname(os.path.abspath(__file__))) os.chdir(os.path.dirname(os.path.abspath(__file__)))
from kcc import KCC_ui
# Implementing detection of already running KCC instance and forwarding argv to it # Implementing detection of already running KCC instance and forwarding argv to it
@@ -89,10 +95,10 @@ class QApplicationMessaging(QtGui.QApplication):
return False return False
freeze_support() freeze_support()
APP = QApplicationMessaging(sys.argv) KCCAplication = QApplicationMessaging(sys.argv)
if APP.isRunning(): if KCCAplication.isRunning():
if len(sys.argv) > 1: if len(sys.argv) > 1:
APP.sendMessage(sys.argv[1].decode(sys.getfilesystemencoding())) KCCAplication.sendMessage(sys.argv[1].decode(sys.getfilesystemencoding()))
sys.exit(0) sys.exit(0)
else: else:
messageBox = QtGui.QMessageBox() messageBox = QtGui.QMessageBox()
@@ -101,13 +107,8 @@ if APP.isRunning():
messageBox.setWindowIcon(icon) messageBox.setWindowIcon(icon)
QtGui.QMessageBox.critical(messageBox, 'KCC - Error', 'KCC is already running!', QtGui.QMessageBox.Ok) QtGui.QMessageBox.critical(messageBox, 'KCC - Error', 'KCC is already running!', QtGui.QMessageBox.Ok)
sys.exit(1) sys.exit(1)
KCC = QtGui.QMainWindow() KCCWindow = QtGui.QMainWindow()
UI = KCC_ui.Ui_KCC() KCCUI = KCC_gui.KCCGUI(KCCAplication, KCCWindow)
UI.setupUi(KCC)
GUI = KCC_gui.Ui_KCC(UI, KCC, APP)
KCC.setWindowTitle("Kindle Comic Converter " + __version__)
KCC.show()
KCC.raise_()
if len(sys.argv) > 1: if len(sys.argv) > 1:
GUI.handleMessage(sys.argv[1].decode(sys.getfilesystemencoding())) KCCUI.handleMessage(sys.argv[1].decode(sys.getfilesystemencoding()))
sys.exit(APP.exec_()) sys.exit(KCCAplication.exec_())

View File

@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2012-2013 Ciro Mattia Gonano <ciromattia@gmail.com> # Copyright (c) 2012-2013 Ciro Mattia Gonano <ciromattia@gmail.com>
@@ -18,29 +17,47 @@
# 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__ = '3.5' __version__ = '3.6.2'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@vulturis.eu>' __copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@vulturis.eu>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os import os
import sys import sys
import shutil
import traceback import traceback
import urllib2 import urllib2
import time import socket
import comic2ebook import comic2ebook
import kindlesplit import kindlesplit
import socket from string import split
import string from time import sleep
from KCC_rc_web import WebContent from shutil import move
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from SocketServer import ThreadingMixIn from SocketServer import ThreadingMixIn
from image import ProfileData from image import ProfileData
from subprocess import call, Popen, STDOUT, PIPE from subprocess import STDOUT, PIPE
from PyQt4 import QtGui, QtCore from PyQt4 import QtGui, QtCore
from xml.dom.minidom import parse from xml.dom.minidom import parse
from HTMLParser import HTMLParser from HTMLParser import HTMLParser
from KCC_rc_web import WebContent
try:
#noinspection PyUnresolvedReferences
from psutil import TOTAL_PHYMEM, Popen
except ImportError:
print "ERROR: Psutil is not installed!"
if sys.platform.startswith('linux'):
import Tkinter
import tkMessageBox
importRoot = Tkinter.Tk()
importRoot.withdraw()
tkMessageBox.showerror("KCC - Error", "Psutil is not installed!")
exit(1)
if sys.platform.startswith('darwin'):
import KCC_ui_osx as KCC_ui
elif sys.platform.startswith('linux'):
import KCC_ui_linux as KCC_ui
else:
import KCC_ui
class Icons: class Icons:
@@ -64,6 +81,9 @@ class Icons:
self.error = QtGui.QIcon() self.error = QtGui.QIcon()
self.error.addPixmap(QtGui.QPixmap(":/Status/icons/error.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.error.addPixmap(QtGui.QPixmap(":/Status/icons/error.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.programIcon = QtGui.QIcon()
self.programIcon.addPixmap(QtGui.QPixmap(":/Icon/icons/comic2ebook.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
class HTMLStripper(HTMLParser): class HTMLStripper(HTMLParser):
def __init__(self): def __init__(self):
@@ -102,16 +122,16 @@ class WebServerHandler(BaseHTTPRequestHandler):
self.wfile.write('<!DOCTYPE html>\n' self.wfile.write('<!DOCTYPE html>\n'
'<html lang="en">\n' '<html lang="en">\n'
'<head><meta charset="utf-8">\n' '<head><meta charset="utf-8">\n'
'<link href="' + GUIMain.webContent.favicon + '" rel="icon" type="image/x-icon" />\n' '<link href="' + GUI.webContent.favicon + '" rel="icon" type="image/x-icon" />\n'
'<title>KindleComicConverter</title>\n' '<title>Kindle Comic Converter</title>\n'
'</head>\n' '</head>\n'
'<body>\n' '<body>\n'
'<div style="text-align: center; font-size:25px">\n' '<div style="text-align: center; font-size:25px">\n'
'<p style="font-size:50px">- <img style="vertical-align: middle" ' '<p style="font-size:50px">- <img style="vertical-align: middle" '
'alt="KCC Logo" src="' + GUIMain.webContent.logo + '" /> -</p>\n') 'alt="KCC Logo" src="' + GUI.webContent.logo + '" /> -</p>\n')
if len(GUIMain.completedWork) > 0 and not GUIMain.conversionAlive: if len(GUI.completedWork) > 0 and not GUI.conversionAlive:
for key in sorted(GUIMain.completedWork.iterkeys()): for key in sorted(GUI.completedWork.iterkeys()):
self.wfile.write('<p><a href="' + key + '">' + string.split(key, '.')[0] + '</a></p>\n') self.wfile.write('<p><a href="' + key + '">' + split(key, '.')[0] + '</a></p>\n')
else: else:
self.wfile.write('<p style="font-weight: bold">No downloads are available.<br/>' self.wfile.write('<p style="font-weight: bold">No downloads are available.<br/>'
'Convert some files and refresh this page.</p>\n') 'Convert some files and refresh this page.</p>\n')
@@ -119,7 +139,7 @@ class WebServerHandler(BaseHTTPRequestHandler):
'</body>\n' '</body>\n'
'</html>\n') '</html>\n')
elif sendReply: elif sendReply:
outputFile = GUIMain.completedWork[urllib2.unquote(self.path[1:])].decode('utf-8') outputFile = GUI.completedWork[urllib2.unquote(self.path[1:])].decode('utf-8')
fp = open(outputFile, 'rb') fp = open(outputFile, 'rb')
self.send_response(200) self.send_response(200)
self.send_header('Content-type', mimetype) self.send_header('Content-type', mimetype)
@@ -194,27 +214,133 @@ class VersionThread(QtCore.QThread):
'Changelog</a>)', 'warning') 'Changelog</a>)', 'warning')
# noinspection PyBroadException class ProgressThread(QtCore.QThread):
class WorkerThread(QtCore.QThread):
def __init__(self): def __init__(self):
QtCore.QThread.__init__(self) QtCore.QThread.__init__(self)
self.running = False
self.content = None
self.progress = 0
def __del__(self):
self.wait()
def run(self):
self.running = True
while self.running:
sleep(1)
if self.content:
self.emit(QtCore.SIGNAL("addMessage"), self.content + self.progress * '.', 'info', True)
self.progress += 1
if self.progress == 4:
self.progress = 0
def stop(self):
self.running = False
class WorkerSignals(QtCore.QObject):
result = QtCore.pyqtSignal(list)
class KindleGenThread(QtCore.QRunnable):
def __init__(self, batch):
super(KindleGenThread, self).__init__()
self.signals = WorkerSignals()
self.work = batch
def run(self):
kindlegenErrorCode = 0
kindlegenError = ''
try:
if os.path.getsize(self.work) < 367001600:
output = Popen('kindlegen -locale en "' + self.work.encode(sys.getfilesystemencoding()) + '"',
stdout=PIPE, stderr=STDOUT, shell=True)
for line in output.stdout:
# ERROR: Generic error
if "Error(" in line:
kindlegenErrorCode = 1
kindlegenError = line
# ERROR: EPUB too big
if ":E23026:" in line:
kindlegenErrorCode = 23026
if kindlegenErrorCode > 0:
break
else:
# ERROR: EPUB too big
kindlegenErrorCode = 23026
self.signals.result.emit([kindlegenErrorCode, kindlegenError, self.work])
except StandardError:
# ERROR: Unknown generic error
kindlegenErrorCode = 1
self.signals.result.emit([kindlegenErrorCode, kindlegenError, self.work])
class KindleUnpackThread(QtCore.QRunnable):
def __init__(self, batch):
super(KindleUnpackThread, self).__init__()
self.signals = WorkerSignals()
self.work = batch
def run(self):
item = self.work[0]
profile = self.work[1]
os.remove(item)
mobiPath = item.replace('.epub', '.mobi')
move(mobiPath, mobiPath + '_toclean')
try:
# MOBI file produced by KindleGen is hybrid. KF8 + M7 + Source header
# KindleSplit is removing redundant data as we need only KF8 part for new Kindle models
if profile in ['K345', 'KHD', 'KF', 'KFHD', 'KFHD8', 'KFHDX', 'KFHDX8', 'KFA']:
newKindle = True
else:
newKindle = False
mobisplit = kindlesplit.mobi_split(mobiPath + '_toclean', newKindle)
open(mobiPath, 'wb').write(mobisplit.getResult())
self.signals.result.emit([True])
except StandardError:
self.signals.result.emit([False])
class WorkerThread(QtCore.QThread):
#noinspection PyArgumentList
def __init__(self):
QtCore.QThread.__init__(self)
self.pool = QtCore.QThreadPool()
self.conversionAlive = False self.conversionAlive = False
self.errors = False self.errors = False
self.kindlegenErrorCode = 0 self.kindlegenErrorCode = [0]
self.kindlegenError = None self.workerOutput = []
# Let's make sure that we don't fill the memory
availableMemory = TOTAL_PHYMEM/1000000000
if availableMemory <= 2:
self.threadNumber = 1
elif 2 < availableMemory <= 4:
self.threadNumber = 2
else:
self.threadNumber = 4
# Let's make sure that we don't use too many threads
if self.threadNumber > QtCore.QThread.idealThreadCount():
self.threadNumber = QtCore.QThread.idealThreadCount()
def __del__(self): def __del__(self):
self.wait() self.wait()
def sync(self): def sync(self):
self.conversionAlive = GUIMain.conversionAlive self.conversionAlive = GUI.conversionAlive
def clean(self): def clean(self):
GUIMain.needClean = True GUI.progress.content = ''
GUI.progress.stop()
GUI.needClean = True
self.emit(QtCore.SIGNAL("hideProgressBar")) self.emit(QtCore.SIGNAL("hideProgressBar"))
self.emit(QtCore.SIGNAL("addMessage"), '<b>Conversion interrupted.</b>', 'error') self.emit(QtCore.SIGNAL("addMessage"), '<b>Conversion interrupted.</b>', 'error')
self.emit(QtCore.SIGNAL("addTrayMessage"), 'Conversion interrupted.', 'Critical')
self.emit(QtCore.SIGNAL("modeConvert"), True) self.emit(QtCore.SIGNAL("modeConvert"), True)
def addResult(self, output):
self.emit(QtCore.SIGNAL("progressBarTick"))
self.workerOutput.append(output)
def run(self): def run(self):
self.emit(QtCore.SIGNAL("modeConvert"), False) self.emit(QtCore.SIGNAL("modeConvert"), False)
profile = ProfileData.ProfileLabels[str(GUI.DeviceBox.currentText())] profile = ProfileData.ProfileLabels[str(GUI.DeviceBox.currentText())]
@@ -228,7 +354,10 @@ class WorkerThread(QtCore.QThread):
argv.append("--quality=1") argv.append("--quality=1")
elif GUI.QualityBox.checkState() == 2: elif GUI.QualityBox.checkState() == 2:
argv.append("--quality=2") argv.append("--quality=2")
if GUIMain.currentMode > 1: if GUI.currentMode == 1:
if profile in ['KFHD', 'KFHD8', 'KFHDX', 'KFHDX8']:
argv.append("--upscale")
if GUI.currentMode > 1:
if GUI.ProcessingBox.isChecked(): if GUI.ProcessingBox.isChecked():
argv.append("--noprocessing") argv.append("--noprocessing")
if GUI.NoRotateBox.isChecked(): if GUI.NoRotateBox.isChecked():
@@ -245,13 +374,14 @@ class WorkerThread(QtCore.QThread):
argv.append("--forcepng") argv.append("--forcepng")
if GUI.WebtoonBox.isChecked(): if GUI.WebtoonBox.isChecked():
argv.append("--webtoon") argv.append("--webtoon")
if float(GUIMain.GammaValue) > 0.09: if float(GUI.GammaValue) > 0.09:
argv.append("--gamma=" + GUIMain.GammaValue) # noinspection PyTypeChecker
argv.append("--gamma=" + GUI.GammaValue)
if str(GUI.FormatBox.currentText()) == 'CBZ': if str(GUI.FormatBox.currentText()) == 'CBZ':
argv.append("--cbz-output") argv.append("--cbz-output")
if str(GUI.FormatBox.currentText()) == 'MOBI': if str(GUI.FormatBox.currentText()) == 'MOBI':
argv.append("--batchsplit") argv.append("--batchsplit")
if GUIMain.currentMode > 2: if GUI.currentMode > 2:
argv.append("--customwidth=" + str(GUI.customWidth.text())) argv.append("--customwidth=" + str(GUI.customWidth.text()))
argv.append("--customheight=" + str(GUI.customHeight.text())) argv.append("--customheight=" + str(GUI.customHeight.text()))
if GUI.ColorBox.isChecked(): if GUI.ColorBox.isChecked():
@@ -262,16 +392,18 @@ class WorkerThread(QtCore.QThread):
currentJobs.append(unicode(GUI.JobList.item(i).text())) currentJobs.append(unicode(GUI.JobList.item(i).text()))
GUI.JobList.clear() GUI.JobList.clear()
for job in currentJobs: for job in currentJobs:
time.sleep(0.5) sleep(0.5)
if not self.conversionAlive: if not self.conversionAlive:
self.clean() self.clean()
return return
self.errors = False self.errors = False
self.emit(QtCore.SIGNAL("addMessage"), '<b>Source:</b> ' + job, 'info') self.emit(QtCore.SIGNAL("addMessage"), '<b>Source:</b> ' + job, 'info')
if str(GUI.FormatBox.currentText()) == 'CBZ': if str(GUI.FormatBox.currentText()) == 'CBZ':
self.emit(QtCore.SIGNAL("addMessage"), 'Creating CBZ file...', 'info') self.emit(QtCore.SIGNAL("addMessage"), 'Creating CBZ files', 'info')
GUI.progress.content = 'Creating CBZ files'
else: else:
self.emit(QtCore.SIGNAL("addMessage"), 'Creating EPUB file...', 'info') self.emit(QtCore.SIGNAL("addMessage"), 'Creating EPUB files', 'info')
GUI.progress.content = 'Creating EPUB files'
jobargv = list(argv) jobargv = list(argv)
jobargv.append(job) jobargv.append(job)
try: try:
@@ -282,15 +414,19 @@ class WorkerThread(QtCore.QThread):
self.clean() self.clean()
return return
else: else:
GUI.progress.content = ''
self.errors = True self.errors = True
self.emit(QtCore.SIGNAL("addMessage"), str(warn), 'warning') self.emit(QtCore.SIGNAL("addMessage"), str(warn), 'warning')
self.emit(QtCore.SIGNAL("addMessage"), 'Failed to create output file!', 'warning') self.emit(QtCore.SIGNAL("addMessage"), 'Failed to create output file!', 'warning')
self.emit(QtCore.SIGNAL("addTrayMessage"), 'Failed to create output file!', 'Critical')
except Exception as err: except Exception as err:
GUI.progress.content = ''
self.errors = True self.errors = True
type_, value_, traceback_ = sys.exc_info() type_, value_, traceback_ = sys.exc_info()
self.emit(QtCore.SIGNAL("showDialog"), "Error during conversion %s:\n\n%s\n\nTraceback:\n%s" self.emit(QtCore.SIGNAL("showDialog"), "Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
% (jobargv[-1], str(err), traceback.format_tb(traceback_))) % (jobargv[-1], str(err), traceback.format_tb(traceback_)))
self.emit(QtCore.SIGNAL("addMessage"), 'Failed to create EPUB!', 'error') self.emit(QtCore.SIGNAL("addMessage"), 'Failed to create EPUB!', 'error')
self.emit(QtCore.SIGNAL("addTrayMessage"), 'Failed to create EPUB!', 'Critical')
if not self.conversionAlive: if not self.conversionAlive:
for item in outputPath: for item in outputPath:
if os.path.exists(item): if os.path.exists(item):
@@ -298,117 +434,133 @@ class WorkerThread(QtCore.QThread):
self.clean() self.clean()
return return
if not self.errors: if not self.errors:
GUI.progress.content = ''
if str(GUI.FormatBox.currentText()) == 'CBZ': if str(GUI.FormatBox.currentText()) == 'CBZ':
self.emit(QtCore.SIGNAL("addMessage"), 'Creating CBZ file... <b>Done!</b>', 'info', True) self.emit(QtCore.SIGNAL("addMessage"), 'Creating CBZ files... <b>Done!</b>', 'info', True)
else: else:
self.emit(QtCore.SIGNAL("addMessage"), 'Creating EPUB file... <b>Done!</b>', 'info', True) self.emit(QtCore.SIGNAL("addMessage"), 'Creating EPUB files... <b>Done!</b>', 'info', True)
if str(GUI.FormatBox.currentText()) == 'MOBI': if str(GUI.FormatBox.currentText()) == 'MOBI':
tomeNumber = 0
self.emit(QtCore.SIGNAL("progressBarTick"), 'status', 'Creating MOBI files') self.emit(QtCore.SIGNAL("progressBarTick"), 'status', 'Creating MOBI files')
self.emit(QtCore.SIGNAL("progressBarTick"), len(outputPath)*2) self.emit(QtCore.SIGNAL("progressBarTick"), len(outputPath)*2+1)
self.emit(QtCore.SIGNAL("progressBarTick"))
self.emit(QtCore.SIGNAL("addMessage"), 'Creating MOBI files', 'info')
GUI.progress.content = 'Creating MOBI files'
self.workerOutput = []
# Number of KindleGen threads depends on the size of RAM
self.pool.setMaxThreadCount(self.threadNumber)
for item in outputPath: for item in outputPath:
tomeNumber += 1 worker = KindleGenThread(item)
if len(outputPath) > 1: worker.signals.result.connect(self.addResult)
self.emit(QtCore.SIGNAL("addMessage"), 'Creating MOBI file (' + str(tomeNumber) self.pool.start(worker)
+ '/' + str(len(outputPath)) + ')...', 'info') self.pool.waitForDone()
else: sleep(0.5)
self.emit(QtCore.SIGNAL("addMessage"), 'Creating MOBI file...', 'info') self.kindlegenErrorCode = [0]
self.emit(QtCore.SIGNAL("progressBarTick")) for errors in self.workerOutput:
try: if errors[0] != 0:
self.kindlegenErrorCode = 0 self.kindlegenErrorCode = errors
if os.path.getsize(item) < 367001600: break
output = Popen('kindlegen -locale en "' + item.encode(sys.getfilesystemencoding()) + if not self.conversionAlive:
'"', stdout=PIPE, stderr=STDOUT, shell=True) for item in outputPath:
for line in output.stdout: if os.path.exists(item):
# ERROR: Generic error os.remove(item)
if "Error(" in line:
self.kindlegenErrorCode = 1
self.kindlegenError = line
# ERROR: EPUB too big
if ":E23026:" in line:
self.kindlegenErrorCode = 23026
if self.kindlegenErrorCode > 0:
break
else:
# ERROR: EPUB too big
self.kindlegenErrorCode = 23026
except:
# ERROR: Unknown generic error
self.kindlegenErrorCode = 1
continue
if not self.conversionAlive:
for item in outputPath:
if os.path.exists(item):
os.remove(item)
if os.path.exists(item.replace('.epub', '.mobi')):
os.remove(item.replace('.epub', '.mobi'))
self.clean()
return
if self.kindlegenErrorCode == 0:
if len(outputPath) > 1:
self.emit(QtCore.SIGNAL("addMessage"), 'Creating MOBI file (' + str(tomeNumber) + '/'
+ str(len(outputPath)) + ')... <b>Done!</b>',
'info', True)
else:
self.emit(QtCore.SIGNAL("addMessage"), 'Creating MOBI file... <b>Done!</b>', 'info',
True)
self.emit(QtCore.SIGNAL("addMessage"), 'Cleaning MOBI file...', 'info')
self.emit(QtCore.SIGNAL("progressBarTick"))
os.remove(item)
mobiPath = item.replace('.epub', '.mobi')
shutil.move(mobiPath, mobiPath + '_toclean')
try:
# MOBI file produced by KindleGen is hybrid. KF8 + M7 + Source header
# KindleSplit is removing redundant data as we need only KF8 part for new Kindle models
if profile in ['K345', 'KHD', 'KF', 'KFHD', 'KFHD8', 'KFHDX8', 'KFA']:
newKindle = True
else:
newKindle = False
mobisplit = kindlesplit.mobi_split(mobiPath + '_toclean', newKindle)
open(mobiPath, 'wb').write(mobisplit.getResult())
except Exception:
self.errors = True
if not self.errors:
os.remove(mobiPath + '_toclean')
self.emit(QtCore.SIGNAL("addMessage"), 'Cleaning MOBI file... <b>Done!</b>', 'info',
True)
GUIMain.completedWork[os.path.basename(mobiPath).encode('utf-8')] = \
mobiPath.encode('utf-8')
else:
os.remove(mobiPath + '_toclean')
os.remove(mobiPath)
self.emit(QtCore.SIGNAL("addMessage"),
'KindleUnpack failed to clean MOBI file!', 'error')
else:
epubSize = (os.path.getsize(item))/1024/1024
os.remove(item)
if os.path.exists(item.replace('.epub', '.mobi')): if os.path.exists(item.replace('.epub', '.mobi')):
os.remove(item.replace('.epub', '.mobi')) os.remove(item.replace('.epub', '.mobi'))
self.emit(QtCore.SIGNAL("addMessage"), 'KindleGen failed to create MOBI!', 'error') self.clean()
if self.kindlegenErrorCode == 1 and self.kindlegenError: return
self.emit(QtCore.SIGNAL("showDialog"), "KindleGen error:\n\n" + self.kindlegenError) if self.kindlegenErrorCode[0] == 0:
if self.kindlegenErrorCode == 23026: GUI.progress.content = ''
self.emit(QtCore.SIGNAL("addMessage"), 'Created EPUB file was too big.', self.emit(QtCore.SIGNAL("addMessage"), 'Creating MOBI files... <b>Done!</b>', 'info', True)
'error') self.emit(QtCore.SIGNAL("addMessage"), 'Cleaning MOBI files', 'info')
self.emit(QtCore.SIGNAL("addMessage"), 'EPUB file: ' + str(epubSize) + 'MB.' GUI.progress.content = 'Cleaning MOBI files'
' Supported size: ~300MB.', 'error') self.workerOutput = []
# Multithreading KindleUnpack in current form is a waste of resources.
# Unless we higly optimise KindleUnpack or drop 32bit support this will not change.
self.pool.setMaxThreadCount(1)
for item in outputPath:
worker = KindleUnpackThread([item, profile])
worker.signals.result.connect(self.addResult)
self.pool.start(worker)
self.pool.waitForDone()
sleep(0.5)
for success in self.workerOutput:
if not success:
self.errors = True
break
if not self.errors:
for item in outputPath:
GUI.progress.content = ''
mobiPath = item.replace('.epub', '.mobi')
os.remove(mobiPath + '_toclean')
GUI.completedWork[os.path.basename(mobiPath).encode('utf-8')] = \
mobiPath.encode('utf-8')
self.emit(QtCore.SIGNAL("addMessage"), 'Cleaning MOBI files... <b>Done!</b>', 'info',
True)
else:
GUI.progress.content = ''
for item in outputPath:
mobiPath = item.replace('.epub', '.mobi')
if os.path.exists(mobiPath):
os.remove(mobiPath)
if os.path.exists(mobiPath + '_toclean'):
os.remove(mobiPath + '_toclean')
self.emit(QtCore.SIGNAL("addMessage"), 'KindleUnpack failed to clean MOBI file!', 'error')
self.emit(QtCore.SIGNAL("addTrayMessage"), 'KindleUnpack failed to clean MOBI file!',
'Critical')
else:
GUI.progress.content = ''
epubSize = (os.path.getsize(self.kindlegenErrorCode[2]))/1024/1024
for item in outputPath:
if os.path.exists(item):
os.remove(item)
if os.path.exists(item.replace('.epub', '.mobi')):
os.remove(item.replace('.epub', '.mobi'))
self.emit(QtCore.SIGNAL("addMessage"), 'KindleGen failed to create MOBI!', 'error')
self.emit(QtCore.SIGNAL("addTrayMessage"), 'KindleGen failed to create MOBI!', 'Critical')
if self.kindlegenErrorCode[0] == 1 and self.kindlegenErrorCode[1] != '':
self.emit(QtCore.SIGNAL("showDialog"), "KindleGen error:\n\n" +
self.self.kindlegenErrorCode[1])
if self.kindlegenErrorCode[0] == 23026:
self.emit(QtCore.SIGNAL("addMessage"), 'Created EPUB file was too big.',
'error')
self.emit(QtCore.SIGNAL("addMessage"), 'EPUB file: ' + str(epubSize) + 'MB.'
' Supported size: ~300MB.', 'error')
else: else:
for item in outputPath: for item in outputPath:
GUIMain.completedWork[os.path.basename(item).encode('utf-8')] = item.encode('utf-8') GUI.completedWork[os.path.basename(item).encode('utf-8')] = item.encode('utf-8')
GUI.progress.content = ''
GUI.progress.stop()
self.emit(QtCore.SIGNAL("hideProgressBar")) self.emit(QtCore.SIGNAL("hideProgressBar"))
GUIMain.needClean = True GUI.needClean = True
self.emit(QtCore.SIGNAL("addMessage"), '<b>All jobs completed.</b>', 'info') self.emit(QtCore.SIGNAL("addMessage"), '<b>All jobs completed.</b>', 'info')
self.emit(QtCore.SIGNAL("addTrayMessage"), 'All jobs completed.', 'Information')
self.emit(QtCore.SIGNAL("modeConvert"), True) self.emit(QtCore.SIGNAL("modeConvert"), True)
# noinspection PyBroadException class SystemTrayIcon(QtGui.QSystemTrayIcon):
class Ui_KCC(object): def __init__(self):
if not sys.platform.startswith('darwin') and self.isSystemTrayAvailable():
QtGui.QSystemTrayIcon.__init__(self, GUI.icons.programIcon, MW)
self.activated.connect(self.catchClicks)
def catchClicks(self):
MW.showNormal()
MW.raise_()
MW.activateWindow()
def addTrayMessage(self, message, icon):
if not sys.platform.startswith('darwin'):
icon = eval('QtGui.QSystemTrayIcon.' + icon)
if self.supportsMessages() and not MW.isActiveWindow():
self.showMessage('Kindle Comic Converter', message, icon)
class KCCGUI(KCC_ui.Ui_KCC):
def selectDir(self): def selectDir(self):
if self.needClean: if self.needClean:
self.needClean = False self.needClean = False
GUI.JobList.clear() GUI.JobList.clear()
# Dirty, dirty way but OS native QFileDialogs don't support directory multiselect # Dirty, dirty way but OS native QFileDialogs don't support directory multiselect
dirDialog = QtGui.QFileDialog(MainWindow, 'Select directory', self.lastPath) dirDialog = QtGui.QFileDialog(MW, 'Select directory', self.lastPath)
dirDialog.setFileMode(dirDialog.Directory) dirDialog.setFileMode(dirDialog.Directory)
dirDialog.setOption(dirDialog.ShowDirsOnly, True) dirDialog.setOption(dirDialog.ShowDirsOnly, True)
dirDialog.setOption(dirDialog.DontUseNativeDialog, True) dirDialog.setOption(dirDialog.DontUseNativeDialog, True)
@@ -428,6 +580,7 @@ class Ui_KCC(object):
dname = dname.replace('/', '\\') dname = dname.replace('/', '\\')
self.lastPath = os.path.abspath(os.path.join(unicode(dname), os.pardir)) self.lastPath = os.path.abspath(os.path.join(unicode(dname), os.pardir))
GUI.JobList.addItem(dname) GUI.JobList.addItem(dname)
MW.setFocus()
def selectFile(self): def selectFile(self):
if self.needClean: if self.needClean:
@@ -435,17 +588,17 @@ class Ui_KCC(object):
GUI.JobList.clear() GUI.JobList.clear()
if self.UnRAR: if self.UnRAR:
if self.sevenza: if self.sevenza:
fnames = QtGui.QFileDialog.getOpenFileNames(MainWindow, 'Select file', self.lastPath, fnames = QtGui.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
'*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.pdf') '*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.pdf')
else: else:
fnames = QtGui.QFileDialog.getOpenFileNames(MainWindow, 'Select file', self.lastPath, fnames = QtGui.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
'*.cbz *.cbr *.zip *.rar *.pdf') '*.cbz *.cbr *.zip *.rar *.pdf')
else: else:
if self.sevenza: if self.sevenza:
fnames = QtGui.QFileDialog.getOpenFileNames(MainWindow, 'Select file', self.lastPath, fnames = QtGui.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
'*.cbz *.cb7 *.zip *.7z *.pdf') '*.cbz *.cb7 *.zip *.7z *.pdf')
else: else:
fnames = QtGui.QFileDialog.getOpenFileNames(MainWindow, 'Select file', self.lastPath, fnames = QtGui.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
'*.cbz *.zip *.pdf') '*.cbz *.zip *.pdf')
for fname in fnames: for fname in fnames:
if unicode(fname) != "": if unicode(fname) != "":
@@ -457,9 +610,14 @@ class Ui_KCC(object):
def modeBasic(self): def modeBasic(self):
self.currentMode = 1 self.currentMode = 1
MainWindow.setMinimumSize(QtCore.QSize(420, 270)) if sys.platform.startswith('darwin'):
MainWindow.setMaximumSize(QtCore.QSize(420, 270)) MW.setMinimumSize(QtCore.QSize(420, 291))
MainWindow.resize(420, 270) MW.setMaximumSize(QtCore.QSize(420, 291))
MW.resize(420, 291)
else:
MW.setMinimumSize(QtCore.QSize(420, 287))
MW.setMaximumSize(QtCore.QSize(420, 287))
MW.resize(420, 287)
GUI.BasicModeButton.setStyleSheet('font-weight:Bold;') GUI.BasicModeButton.setStyleSheet('font-weight:Bold;')
GUI.AdvModeButton.setStyleSheet('font-weight:Normal;') GUI.AdvModeButton.setStyleSheet('font-weight:Normal;')
GUI.FormatBox.setCurrentIndex(0) GUI.FormatBox.setCurrentIndex(0)
@@ -478,9 +636,9 @@ class Ui_KCC(object):
def modeAdvanced(self): def modeAdvanced(self):
self.currentMode = 2 self.currentMode = 2
MainWindow.setMinimumSize(QtCore.QSize(420, 345)) MW.setMinimumSize(QtCore.QSize(420, 365))
MainWindow.setMaximumSize(QtCore.QSize(420, 345)) MW.setMaximumSize(QtCore.QSize(420, 365))
MainWindow.resize(420, 345) MW.resize(420, 365)
GUI.BasicModeButton.setStyleSheet('font-weight:Normal;') GUI.BasicModeButton.setStyleSheet('font-weight:Normal;')
GUI.AdvModeButton.setStyleSheet('font-weight:Bold;') GUI.AdvModeButton.setStyleSheet('font-weight:Bold;')
GUI.FormatBox.setEnabled(True) GUI.FormatBox.setEnabled(True)
@@ -495,9 +653,9 @@ class Ui_KCC(object):
def modeExpert(self, KFA=False): def modeExpert(self, KFA=False):
self.modeAdvanced() self.modeAdvanced()
self.currentMode = 3 self.currentMode = 3
MainWindow.setMinimumSize(QtCore.QSize(420, 380)) MW.setMinimumSize(QtCore.QSize(420, 397))
MainWindow.setMaximumSize(QtCore.QSize(420, 380)) MW.setMaximumSize(QtCore.QSize(420, 397))
MainWindow.resize(420, 380) MW.resize(420, 397)
GUI.OptionsExpert.setEnabled(True) GUI.OptionsExpert.setEnabled(True)
if KFA: if KFA:
GUI.ColorBox.setChecked(True) GUI.ColorBox.setChecked(True)
@@ -561,7 +719,6 @@ class Ui_KCC(object):
GUI.QualityBox.setChecked(False) GUI.QualityBox.setChecked(False)
GUI.MangaBox.setEnabled(False) GUI.MangaBox.setEnabled(False)
GUI.MangaBox.setChecked(False) GUI.MangaBox.setChecked(False)
self.addMessage('If images will be too dark after conversion: Set <i>Gamma</i> to 1.0.', 'info')
else: else:
if not GUI.ProcessingBox.isChecked(): if not GUI.ProcessingBox.isChecked():
GUI.NoRotateBox.setEnabled(True) GUI.NoRotateBox.setEnabled(True)
@@ -634,9 +791,12 @@ class Ui_KCC(object):
GUI.QualityBox.setChecked(False) GUI.QualityBox.setChecked(False)
GUI.QualityBox.setEnabled(False) GUI.QualityBox.setEnabled(False)
self.QualityBoxDisabled = True self.QualityBoxDisabled = True
if value in [4, 5, 6, 7]:
if GUI.UpscaleBox.isEnabled():
GUI.UpscaleBox.setChecked(True)
else: else:
if not GUI.WebtoonBox.isChecked() and not GUI.ProcessingBox.isChecked() \ if not GUI.WebtoonBox.isChecked() and not GUI.ProcessingBox.isChecked() \
and str(GUI.FormatBox.currentText()) != 'CBZ': and str(GUI.FormatBox.currentText()) != 'CBZ' and value not in [9, 11, 12, 13]:
GUI.QualityBox.setEnabled(True) GUI.QualityBox.setEnabled(True)
self.QualityBoxDisabled = False self.QualityBoxDisabled = False
@@ -675,7 +835,7 @@ class Ui_KCC(object):
GUI.JobList.scrollToBottom() GUI.JobList.scrollToBottom()
def showDialog(self, message): def showDialog(self, message):
QtGui.QMessageBox.critical(MainWindow, 'KCC - Error', message, QtGui.QMessageBox.Ok) QtGui.QMessageBox.critical(MW, 'KCC - Error', message, QtGui.QMessageBox.Ok)
def updateProgressbar(self, new=False, status=False): def updateProgressbar(self, new=False, status=False):
if new == "status": if new == "status":
@@ -694,6 +854,7 @@ class Ui_KCC(object):
self.conversionAlive = False self.conversionAlive = False
self.worker.sync() self.worker.sync()
else: else:
self.progress.start()
if self.needClean: if self.needClean:
self.needClean = False self.needClean = False
GUI.JobList.clear() GUI.JobList.clear()
@@ -743,8 +904,8 @@ class Ui_KCC(object):
self.settings.sync() self.settings.sync()
def handleMessage(self, message): def handleMessage(self, message):
MainWindow.raise_() MW.raise_()
MainWindow.activateWindow() MW.activateWindow()
if not self.conversionAlive: if not self.conversionAlive:
if self.needClean: if self.needClean:
self.needClean = False self.needClean = False
@@ -768,16 +929,18 @@ class Ui_KCC(object):
else: else:
self.addMessage('This file type is unsupported!', 'error') self.addMessage('This file type is unsupported!', 'error')
def __init__(self, UI, KCC, APP): def __init__(self, KCCAplication, KCCWindow):
global GUI, GUIMain, MainWindow global APP, MW, GUI
GUI = UI APP = KCCAplication
GUIMain = self MW = KCCWindow
MainWindow = KCC GUI = self
self.setupUi(MW)
# User settings will be reverted to default ones if were created in one of the following versions # User settings will be reverted to default ones if were created in one of the following versions
# Empty string cover all versions before this system was implemented # Empty string cover all versions before this system was implemented
purgeSettingsVersions = [''] purgeSettingsVersions = ['']
self.icons = Icons() self.icons = Icons()
self.webContent = WebContent() self.webContent = WebContent()
self.tray = SystemTrayIcon()
self.settings = QtCore.QSettings('KindleComicConverter', 'KindleComicConverter') self.settings = QtCore.QSettings('KindleComicConverter', 'KindleComicConverter')
self.settingsVersion = self.settings.value('settingsVersion', '', type=str) self.settingsVersion = self.settings.value('settingsVersion', '', type=str)
if self.settingsVersion in purgeSettingsVersions: if self.settingsVersion in purgeSettingsVersions:
@@ -793,6 +956,7 @@ class Ui_KCC(object):
self.worker = WorkerThread() self.worker = WorkerThread()
self.versionCheck = VersionThread() self.versionCheck = VersionThread()
self.contentServer = WebServerThread() self.contentServer = WebServerThread()
self.progress = ProgressThread()
self.conversionAlive = False self.conversionAlive = False
self.needClean = True self.needClean = True
self.QualityBoxDisabled = False self.QualityBoxDisabled = False
@@ -800,17 +964,38 @@ class Ui_KCC(object):
self.completedWork = {} self.completedWork = {}
if sys.platform.startswith('darwin'): if sys.platform.startswith('darwin'):
self.listFontSize = 11 self.listFontSize = 11
self.statusBarFontSize = 10
self.statusBarStyle = 'QLabel{padding-top:5px;padding-bottom:5px;border-top:2px solid #C2C7CB}'
elif sys.platform.startswith('linux'): elif sys.platform.startswith('linux'):
self.listFontSize = 8 self.listFontSize = 8
self.statusBarFontSize = 8
self.statusBarStyle = 'QLabel{padding-top:5px;padding-bottom:3px;border-top:2px solid #C2C7CB}'
self.tray.show()
else: else:
self.listFontSize = 9 self.listFontSize = 9
self.statusBarFontSize = 8
self.statusBarStyle = 'QLabel{padding-top:3px;padding-bottom:3px;border-top:2px solid #C2C7CB}'
self.tray.show()
statusBarLabel = QtGui.QLabel('<b><a href="http://kcc.vulturis.eu/">HOMEPAGE</a> - <a href="https://github.com/'
'ciromattia/kcc/blob/master/README.md#issues--new-features--donations">DONATE</a>'
' - <a href="https://github.com/ciromattia/kcc/blob/master/README.md#kcc">README<'
'/a> - <a href="https://github.com/ciromattia/kcc/wiki">WIKI</a></b>')
statusBarLabel.setAlignment(QtCore.Qt.AlignCenter)
statusBarLabel.setStyleSheet(self.statusBarStyle)
statusBarLabel.setOpenExternalLinks(True)
statusBarLabelFont = QtGui.QFont()
statusBarLabelFont.setPointSize(self.statusBarFontSize)
statusBarLabel.setFont(statusBarLabelFont)
GUI.statusBar.addPermanentWidget(statusBarLabel, 1)
self.addMessage('<b>Welcome!</b>', 'info') self.addMessage('<b>Welcome!</b>', 'info')
self.addMessage('<b>Remember:</b> All options have additional informations in tooltips.', 'info') self.addMessage('<b>Remember:</b> All options have additional informations in tooltips.', 'info')
if self.firstStart: if self.firstStart:
self.addMessage('Since you are using <b>KCC</b> for first time please see few ' self.addMessage('Since you are using <b>KCC</b> for first time please see few '
'<a href="https://github.com/ciromattia/kcc#important-tips">important tips</a>.', 'info') '<a href="https://github.com/ciromattia/kcc#important-tips">important tips</a>.', 'info')
if call('kindlegen -locale en', stdout=PIPE, stderr=STDOUT, shell=True) == 0: kindleGenExitCode = Popen('kindlegen -locale en', stdout=PIPE, stderr=STDOUT, shell=True)
if kindleGenExitCode.wait() == 0:
self.KindleGen = True self.KindleGen = True
formats = ['MOBI', 'EPUB', 'CBZ'] formats = ['MOBI', 'EPUB', 'CBZ']
versionCheck = Popen('kindlegen -locale en', stdout=PIPE, stderr=STDOUT, shell=True) versionCheck = Popen('kindlegen -locale en', stdout=PIPE, stderr=STDOUT, shell=True)
@@ -828,14 +1013,16 @@ class Ui_KCC(object):
formats = ['EPUB', 'CBZ'] formats = ['EPUB', 'CBZ']
self.addMessage('Cannot find <a href="http://www.amazon.com/gp/feature.html?ie=UTF8&docId=' self.addMessage('Cannot find <a href="http://www.amazon.com/gp/feature.html?ie=UTF8&docId='
'1000765211">kindlegen</a> in PATH! MOBI creation will be disabled.', 'warning') '1000765211">kindlegen</a> in PATH! MOBI creation will be disabled.', 'warning')
rarExitCode = call('unrar', stdout=PIPE, stderr=STDOUT, shell=True) rarExitCode = Popen('unrar', stdout=PIPE, stderr=STDOUT, shell=True)
rarExitCode = rarExitCode.wait()
if rarExitCode == 0 or rarExitCode == 7: if rarExitCode == 0 or rarExitCode == 7:
self.UnRAR = True self.UnRAR = True
else: else:
self.UnRAR = False self.UnRAR = False
self.addMessage('Cannot find <a href="http://www.rarlab.com/rar_add.htm">UnRAR</a>!' self.addMessage('Cannot find <a href="http://www.rarlab.com/rar_add.htm">UnRAR</a>!'
' Processing of CBR/RAR files will be disabled.', 'warning') ' Processing of CBR/RAR files will be disabled.', 'warning')
sevenzaExitCode = call('7za', stdout=PIPE, stderr=STDOUT, shell=True) sevenzaExitCode = Popen('7za', stdout=PIPE, stderr=STDOUT, shell=True)
sevenzaExitCode = sevenzaExitCode.wait()
if sevenzaExitCode == 0 or sevenzaExitCode == 7: if sevenzaExitCode == 0 or sevenzaExitCode == 7:
self.sevenza = True self.sevenza = True
else: else:
@@ -856,14 +1043,16 @@ class Ui_KCC(object):
GUI.ProcessingBox.stateChanged.connect(self.toggleProcessingBox) GUI.ProcessingBox.stateChanged.connect(self.toggleProcessingBox)
GUI.DeviceBox.activated.connect(self.changeDevice) GUI.DeviceBox.activated.connect(self.changeDevice)
GUI.FormatBox.activated.connect(self.changeFormat) GUI.FormatBox.activated.connect(self.changeFormat)
KCC.connect(self.worker, QtCore.SIGNAL("progressBarTick"), self.updateProgressbar) MW.connect(self.worker, QtCore.SIGNAL("progressBarTick"), self.updateProgressbar)
KCC.connect(self.worker, QtCore.SIGNAL("modeConvert"), self.modeConvert) MW.connect(self.worker, QtCore.SIGNAL("modeConvert"), self.modeConvert)
KCC.connect(self.worker, QtCore.SIGNAL("addMessage"), self.addMessage) MW.connect(self.worker, QtCore.SIGNAL("addMessage"), self.addMessage)
KCC.connect(self.worker, QtCore.SIGNAL("showDialog"), self.showDialog) MW.connect(self.worker, QtCore.SIGNAL("addTrayMessage"), self.tray.addTrayMessage)
KCC.connect(self.worker, QtCore.SIGNAL("hideProgressBar"), self.hideProgressBar) MW.connect(self.worker, QtCore.SIGNAL("showDialog"), self.showDialog)
KCC.connect(self.versionCheck, QtCore.SIGNAL("addMessage"), self.addMessage) MW.connect(self.worker, QtCore.SIGNAL("hideProgressBar"), self.hideProgressBar)
KCC.connect(self.contentServer, QtCore.SIGNAL("addMessage"), self.addMessage) MW.connect(self.versionCheck, QtCore.SIGNAL("addMessage"), self.addMessage)
KCC.closeEvent = self.saveSettings MW.connect(self.contentServer, QtCore.SIGNAL("addMessage"), self.addMessage)
MW.connect(self.progress, QtCore.SIGNAL("addMessage"), self.addMessage)
MW.closeEvent = self.saveSettings
for f in formats: for f in formats:
GUI.FormatBox.addItem(eval('self.icons.' + f + 'Format'), f) GUI.FormatBox.addItem(eval('self.icons.' + f + 'Format'), f)
@@ -907,3 +1096,6 @@ class Ui_KCC(object):
self.contentServer.start() self.contentServer.start()
self.hideProgressBar() self.hideProgressBar()
self.worker.sync() self.worker.sync()
MW.setWindowTitle("Kindle Comic Converter " + __version__)
MW.show()
MW.raise_()

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'KCC.ui' # Form implementation generated from reading ui file 'KCC.ui'
# #
# Created: Sat Oct 12 11:28:00 2013 # Created: Wed Dec 04 18:28:13 2013
# by: PyQt4 UI code generator 4.10.3 # by: PyQt4 UI code generator 4.10.3
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@@ -26,13 +26,12 @@ except AttributeError:
class Ui_KCC(object): class Ui_KCC(object):
def setupUi(self, KCC): def setupUi(self, KCC):
KCC.setObjectName(_fromUtf8("KCC")) KCC.setObjectName(_fromUtf8("KCC"))
KCC.resize(420, 380) KCC.resize(420, 397)
KCC.setMinimumSize(QtCore.QSize(420, 380)) KCC.setMinimumSize(QtCore.QSize(420, 397))
KCC.setMaximumSize(QtCore.QSize(420, 380)) KCC.setMaximumSize(QtCore.QSize(420, 397))
font = QtGui.QFont() font = QtGui.QFont()
font.setPointSize(9) font.setPointSize(9)
KCC.setFont(font) KCC.setFont(font)
KCC.setFocusPolicy(QtCore.Qt.NoFocus)
icon = QtGui.QIcon() icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(_fromUtf8(":/Icon/icons/comic2ebook.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) icon.addPixmap(QtGui.QPixmap(_fromUtf8(":/Icon/icons/comic2ebook.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
KCC.setWindowIcon(icon) KCC.setWindowIcon(icon)
@@ -247,6 +246,14 @@ class Ui_KCC(object):
self.customHeight.setObjectName(_fromUtf8("customHeight")) self.customHeight.setObjectName(_fromUtf8("customHeight"))
self.gridLayout_2.addWidget(self.customHeight, 0, 3, 1, 1) self.gridLayout_2.addWidget(self.customHeight, 0, 3, 1, 1)
KCC.setCentralWidget(self.Form) KCC.setCentralWidget(self.Form)
self.statusBar = QtGui.QStatusBar(KCC)
font = QtGui.QFont()
font.setFamily(_fromUtf8("MS Shell Dlg 2"))
font.setPointSize(8)
self.statusBar.setFont(font)
self.statusBar.setSizeGripEnabled(False)
self.statusBar.setObjectName(_fromUtf8("statusBar"))
KCC.setStatusBar(self.statusBar)
self.ActionBasic = QtGui.QAction(KCC) self.ActionBasic = QtGui.QAction(KCC)
self.ActionBasic.setCheckable(True) self.ActionBasic.setCheckable(True)
self.ActionBasic.setChecked(False) self.ActionBasic.setChecked(False)
@@ -298,9 +305,7 @@ class Ui_KCC(object):
self.RotateBox.setText(_translate("KCC", "Horizontal mode", None)) self.RotateBox.setText(_translate("KCC", "Horizontal mode", None))
self.BasicModeButton.setText(_translate("KCC", "Basic", None)) self.BasicModeButton.setText(_translate("KCC", "Basic", None))
self.AdvModeButton.setText(_translate("KCC", "Advanced", None)) self.AdvModeButton.setText(_translate("KCC", "Advanced", None))
self.GammaLabel.setToolTip(_translate("KCC", "When converting color images setting this option to 1.0 MIGHT improve readability.", None))
self.GammaLabel.setText(_translate("KCC", "Gamma: Auto", None)) self.GammaLabel.setText(_translate("KCC", "Gamma: Auto", None))
self.GammaSlider.setToolTip(_translate("KCC", "<html><head/><body><p>When converting color images setting this option to 1.0 <span style=\" font-weight:600;\">might</span> improve readability.</p></body></html>", None))
self.ColorBox.setToolTip(_translate("KCC", "Do not convert images to grayscale.", None)) self.ColorBox.setToolTip(_translate("KCC", "Do not convert images to grayscale.", None))
self.ColorBox.setText(_translate("KCC", "Color mode", None)) self.ColorBox.setText(_translate("KCC", "Color mode", None))
self.wLabel.setToolTip(_translate("KCC", "Resolution of target device.", None)) self.wLabel.setToolTip(_translate("KCC", "Resolution of target device.", None))

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'KCC-Linux.ui' # Form implementation generated from reading ui file 'KCC-Linux.ui'
# #
# Created: Sat Oct 12 11:28:11 2013 # Created: Wed Dec 04 18:27:52 2013
# by: PyQt4 UI code generator 4.10.3 # by: PyQt4 UI code generator 4.10.3
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@@ -26,13 +26,12 @@ except AttributeError:
class Ui_KCC(object): class Ui_KCC(object):
def setupUi(self, KCC): def setupUi(self, KCC):
KCC.setObjectName(_fromUtf8("KCC")) KCC.setObjectName(_fromUtf8("KCC"))
KCC.resize(420, 380) KCC.resize(420, 397)
KCC.setMinimumSize(QtCore.QSize(420, 380)) KCC.setMinimumSize(QtCore.QSize(420, 397))
KCC.setMaximumSize(QtCore.QSize(420, 380)) KCC.setMaximumSize(QtCore.QSize(420, 397))
font = QtGui.QFont() font = QtGui.QFont()
font.setPointSize(9) font.setPointSize(9)
KCC.setFont(font) KCC.setFont(font)
KCC.setFocusPolicy(QtCore.Qt.NoFocus)
icon = QtGui.QIcon() icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(_fromUtf8(":/Icon/icons/comic2ebook.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) icon.addPixmap(QtGui.QPixmap(_fromUtf8(":/Icon/icons/comic2ebook.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
KCC.setWindowIcon(icon) KCC.setWindowIcon(icon)
@@ -317,6 +316,14 @@ class Ui_KCC(object):
self.customHeight.setObjectName(_fromUtf8("customHeight")) self.customHeight.setObjectName(_fromUtf8("customHeight"))
self.gridLayout_2.addWidget(self.customHeight, 0, 3, 1, 1) self.gridLayout_2.addWidget(self.customHeight, 0, 3, 1, 1)
KCC.setCentralWidget(self.Form) KCC.setCentralWidget(self.Form)
self.statusBar = QtGui.QStatusBar(KCC)
font = QtGui.QFont()
font.setFamily(_fromUtf8("DejaVu Sans"))
font.setPointSize(8)
self.statusBar.setFont(font)
self.statusBar.setSizeGripEnabled(False)
self.statusBar.setObjectName(_fromUtf8("statusBar"))
KCC.setStatusBar(self.statusBar)
self.ActionBasic = QtGui.QAction(KCC) self.ActionBasic = QtGui.QAction(KCC)
self.ActionBasic.setCheckable(True) self.ActionBasic.setCheckable(True)
self.ActionBasic.setChecked(False) self.ActionBasic.setChecked(False)
@@ -367,9 +374,7 @@ class Ui_KCC(object):
self.RotateBox.setText(_translate("KCC", "Horizontal mode", None)) self.RotateBox.setText(_translate("KCC", "Horizontal mode", None))
self.BasicModeButton.setText(_translate("KCC", "Basic", None)) self.BasicModeButton.setText(_translate("KCC", "Basic", None))
self.AdvModeButton.setText(_translate("KCC", "Advanced", None)) self.AdvModeButton.setText(_translate("KCC", "Advanced", None))
self.GammaLabel.setToolTip(_translate("KCC", "<html><head/><body><p>When converting color images setting this option to 1.0 <span style=\" font-weight:600;\">might</span> improve readability.</p></body></html>", None))
self.GammaLabel.setText(_translate("KCC", "Gamma: Auto", None)) self.GammaLabel.setText(_translate("KCC", "Gamma: Auto", None))
self.GammaSlider.setToolTip(_translate("KCC", "<html><head/><body><p>When converting color images setting this option to 1.0 <span style=\" font-weight:600;\">might</span> improve readability.</p></body></html>", None))
self.ColorBox.setToolTip(_translate("KCC", "Do not convert images to grayscale.", None)) self.ColorBox.setToolTip(_translate("KCC", "Do not convert images to grayscale.", None))
self.ColorBox.setText(_translate("KCC", "Color mode", None)) self.ColorBox.setText(_translate("KCC", "Color mode", None))
self.wLabel.setToolTip(_translate("KCC", "Resolution of target device.", None)) self.wLabel.setToolTip(_translate("KCC", "Resolution of target device.", None))

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'KCC-OSX.ui' # Form implementation generated from reading ui file 'KCC-OSX.ui'
# #
# Created: Sat Oct 12 11:28:19 2013 # Created: Wed Dec 04 18:28:04 2013
# by: PyQt4 UI code generator 4.10.3 # by: PyQt4 UI code generator 4.10.3
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@@ -26,13 +26,12 @@ except AttributeError:
class Ui_KCC(object): class Ui_KCC(object):
def setupUi(self, KCC): def setupUi(self, KCC):
KCC.setObjectName(_fromUtf8("KCC")) KCC.setObjectName(_fromUtf8("KCC"))
KCC.resize(420, 380) KCC.resize(420, 397)
KCC.setMinimumSize(QtCore.QSize(420, 380)) KCC.setMinimumSize(QtCore.QSize(420, 397))
KCC.setMaximumSize(QtCore.QSize(420, 380)) KCC.setMaximumSize(QtCore.QSize(420, 397))
font = QtGui.QFont() font = QtGui.QFont()
font.setPointSize(9) font.setPointSize(9)
KCC.setFont(font) KCC.setFont(font)
KCC.setFocusPolicy(QtCore.Qt.NoFocus)
icon = QtGui.QIcon() icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(_fromUtf8(":/Icon/icons/comic2ebook.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off) icon.addPixmap(QtGui.QPixmap(_fromUtf8(":/Icon/icons/comic2ebook.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
KCC.setWindowIcon(icon) KCC.setWindowIcon(icon)
@@ -340,6 +339,14 @@ class Ui_KCC(object):
self.customHeight.setObjectName(_fromUtf8("customHeight")) self.customHeight.setObjectName(_fromUtf8("customHeight"))
self.gridLayout_2.addWidget(self.customHeight, 0, 3, 1, 1) self.gridLayout_2.addWidget(self.customHeight, 0, 3, 1, 1)
KCC.setCentralWidget(self.Form) KCC.setCentralWidget(self.Form)
self.statusBar = QtGui.QStatusBar(KCC)
font = QtGui.QFont()
font.setFamily(_fromUtf8("Aharoni"))
font.setPointSize(8)
self.statusBar.setFont(font)
self.statusBar.setSizeGripEnabled(False)
self.statusBar.setObjectName(_fromUtf8("statusBar"))
KCC.setStatusBar(self.statusBar)
self.ActionBasic = QtGui.QAction(KCC) self.ActionBasic = QtGui.QAction(KCC)
self.ActionBasic.setCheckable(True) self.ActionBasic.setCheckable(True)
self.ActionBasic.setChecked(False) self.ActionBasic.setChecked(False)
@@ -385,9 +392,7 @@ class Ui_KCC(object):
self.RotateBox.setText(_translate("KCC", "Horizontal mode", None)) self.RotateBox.setText(_translate("KCC", "Horizontal mode", None))
self.BasicModeButton.setText(_translate("KCC", "Basic", None)) self.BasicModeButton.setText(_translate("KCC", "Basic", None))
self.AdvModeButton.setText(_translate("KCC", "Advanced", None)) self.AdvModeButton.setText(_translate("KCC", "Advanced", None))
self.GammaLabel.setToolTip(_translate("KCC", "<html><head/><body><p><span style=\" font-size:12pt;\">When converting color images setting this option to 1.0 </span><span style=\" font-size:12pt; font-weight:600;\">might</span><span style=\" font-size:12pt;\"> improve readability.</span></p></body></html>", None))
self.GammaLabel.setText(_translate("KCC", "Gamma: Auto", None)) self.GammaLabel.setText(_translate("KCC", "Gamma: Auto", None))
self.GammaSlider.setToolTip(_translate("KCC", "<html><head/><body><p><span style=\" font-size:12pt;\">When converting color images setting this option to 1.0 </span><span style=\" font-size:12pt; font-weight:600;\">might</span><span style=\" font-size:12pt;\"> improve readability.</span></p></body></html>", None))
self.ColorBox.setToolTip(_translate("KCC", "<html><head/><body><p><span style=\" font-size:12pt;\">Do not convert images to grayscale.</span></p></body></html>", None)) self.ColorBox.setToolTip(_translate("KCC", "<html><head/><body><p><span style=\" font-size:12pt;\">Do not convert images to grayscale.</span></p></body></html>", None))
self.ColorBox.setText(_translate("KCC", "Color mode", None)) self.ColorBox.setText(_translate("KCC", "Color mode", None))
self.wLabel.setToolTip(_translate("KCC", "<html><head/><body><p><span style=\" font-size:12pt;\">Resolution of target device.</span></p></body></html>", None)) self.wLabel.setToolTip(_translate("KCC", "<html><head/><body><p><span style=\" font-size:12pt;\">Resolution of target device.</span></p></body></html>", None))

View File

@@ -1,4 +1,4 @@
__version__ = '3.5' __version__ = '3.6.2'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@vulturis.eu>' __copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@vulturis.eu>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'

View File

@@ -23,7 +23,21 @@ import os
import zipfile import zipfile
import rarfile import rarfile
import locale import locale
from subprocess import Popen, STDOUT, PIPE from sys import platform
from subprocess import STDOUT, PIPE
try:
#noinspection PyUnresolvedReferences
from psutil import Popen
except ImportError:
print "ERROR: Psutil is not installed!"
if platform.startswith('linux'):
import Tkinter
import tkMessageBox
importRoot = Tkinter.Tk()
importRoot.withdraw()
tkMessageBox.showerror("KCC - Error", "Psutil is not installed!")
exit(1)
from shutil import move
class CBxArchive: class CBxArchive:
@@ -91,9 +105,10 @@ class CBxArchive:
elif self.compressor == '7z': elif self.compressor == '7z':
self.extractCB7(targetdir) self.extractCB7(targetdir)
adir = os.listdir(targetdir) adir = os.listdir(targetdir)
if 'ComicInfo.xml' in adir:
adir.remove('ComicInfo.xml')
if len(adir) == 1 and os.path.isdir(os.path.join(targetdir, adir[0])): 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])): for f in os.listdir(os.path.join(targetdir, adir[0])):
shutil.move(os.path.join(targetdir, adir[0], f), targetdir) move(os.path.join(targetdir, adir[0], f), targetdir)
os.rmdir(os.path.join(targetdir, adir[0])) os.rmdir(os.path.join(targetdir, adir[0]))
return targetdir return targetdir

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python2
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2012-2013 Ciro Mattia Gonano <ciromattia@gmail.com> # Copyright (c) 2012-2013 Ciro Mattia Gonano <ciromattia@gmail.com>
@@ -18,20 +18,23 @@
# 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__ = '3.5' __version__ = '3.6.2'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@vulturis.eu>' __copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@vulturis.eu>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os import os
import sys import sys
import tempfile
import re import re
import stat import stat
import string import string
import unicodedata
from tempfile import mkdtemp
from shutil import move, copyfile, copytree, rmtree, make_archive from shutil import move, copyfile, copytree, rmtree, make_archive
from optparse import OptionParser, OptionGroup from optparse import OptionParser, OptionGroup
from multiprocessing import Pool, freeze_support from multiprocessing import Pool, freeze_support
from xml.dom.minidom import parse
from uuid import uuid4
try: try:
from PyQt4 import QtCore from PyQt4 import QtCore
except ImportError: except ImportError:
@@ -102,9 +105,9 @@ def buildHTML(path, imgfile):
elif noHorizontalPV and not noVerticalPV: elif noHorizontalPV and not noVerticalPV:
if rotatedPage: if rotatedPage:
if options.righttoleft: if options.righttoleft:
order = [2, 1]
else:
order = [1, 2] order = [1, 2]
else:
order = [2, 1]
else: else:
order = [1, 2] order = [1, 2]
boxes = ["BoxT", "BoxB"] boxes = ["BoxT", "BoxB"]
@@ -126,35 +129,48 @@ def buildHTML(path, imgfile):
"}'></a></div>\n"]) "}'></a></div>\n"])
if options.quality == 2: if options.quality == 2:
imgfilepv = string.split(imgfile, ".") imgfilepv = string.split(imgfile, ".")
imgfilepv[0] = imgfilepv[0].split("_kccx")[0].replace("_kccnh", "").replace("_kccnv", "") imgfilepv[0] = imgfilepv[0].split("_kccxl")[0].replace("_kccnh", "").replace("_kccnv", "")
imgfilepv[0] += "_kcchq" imgfilepv[0] += "_kcchq"
imgfilepv = string.join(imgfilepv, ".") imgfilepv = string.join(imgfilepv, ".")
else: else:
imgfilepv = imgfile imgfilepv = imgfile
if "_kccx" in filename[0]: if "_kccxl" in filename[0]:
xy = string.split(filename[0], "_kccx")[1] borders = filename[0].split('_kccxl')[1]
x = string.split(xy, "_kccy")[0].lstrip("0") borders = re.findall('[0-9]{1,6}', borders)
y = string.split(xy, "_kccy")[1].lstrip("0") xl = borders[0].lstrip("0")
if x != "": yu = borders[1].lstrip("0")
x = "-" + str(float(x)/100) + "%" xr = borders[2].lstrip("0")
yd = borders[3].lstrip("0")
if xl != "":
xl = "-" + str(float(xl)/100) + "%"
else: else:
x = "0%" xl = "0%"
if y != "": if xr != "":
y = "-" + str(float(y)/100) + "%" xr = "-" + str(float(xr)/100) + "%"
else: else:
y = "0%" xr = "0%"
if yu != "":
yu = "-" + str(float(yu)/100) + "%"
else:
yu = "0%"
if yd != "":
yd = "-" + str(float(yd)/100) + "%"
else:
yd = "0%"
else: else:
x = "0%" xl = "0%"
y = "0%" yu = "0%"
boxStyles = {"BoxTL": "left:" + x + ";top:" + y + ";", xr = "0%"
"BoxTR": "right:" + x + ";top:" + y + ";", yd = "0%"
"BoxBL": "left:" + x + ";bottom:" + y + ";", boxStyles = {"BoxTL": "left:" + xl + ";top:" + yu + ";",
"BoxBR": "right:" + x + ";bottom:" + y + ";", "BoxTR": "right:" + xr + ";top:" + yu + ";",
"BoxT": "left:-25%;top:" + y + ";", "BoxBL": "left:" + xl + ";bottom:" + yd + ";",
"BoxB": "left:-25%;bottom:" + y + ";", "BoxBR": "right:" + xr + ";bottom:" + yd + ";",
"BoxL": "left:" + x + ";top:-25%;", "BoxT": "left:-25%;top:" + yu + ";",
"BoxR": "right:" + x + ";top:-25%;", "BoxB": "left:-25%;bottom:" + yd + ";",
"BoxC": "right:-25%;top:-25%;" "BoxL": "left:" + xl + ";top:-25%;",
"BoxR": "right:" + xr + ";top:-25%;",
"BoxC": "left:-25%;top:-25%;"
} }
for box in boxes: for box in boxes:
f.writelines(["<div id=\"" + box + "-Panel-Parent\" class=\"target-mag-parent\"><div id=\"", f.writelines(["<div id=\"" + box + "-Panel-Parent\" class=\"target-mag-parent\"><div id=\"",
@@ -168,7 +184,6 @@ def buildHTML(path, imgfile):
def buildNCX(dstdir, title, chapters): def buildNCX(dstdir, title, chapters):
from uuid import uuid4
options.uuid = str(uuid4()) options.uuid = str(uuid4())
options.uuid = options.uuid.encode('utf-8') options.uuid = options.uuid.encode('utf-8')
ncxfile = os.path.join(dstdir, 'OEBPS', 'toc.ncx') ncxfile = os.path.join(dstdir, 'OEBPS', 'toc.ncx')
@@ -216,9 +231,10 @@ def buildOPF(dstdir, title, filelist, cover=None):
"xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n", "xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n",
"<dc:title>", title.encode('utf-8'), "</dc:title>\n", "<dc:title>", title.encode('utf-8'), "</dc:title>\n",
"<dc:language>en-US</dc:language>\n", "<dc:language>en-US</dc:language>\n",
"<dc:identifier id=\"BookID\" opf:scheme=\"UUID\">", options.uuid, "</dc:identifier>\n", "<dc:identifier id=\"BookID\" opf:scheme=\"UUID\">", options.uuid, "</dc:identifier>\n"])
"<dc:Creator>KCC</dc:Creator>\n", for author in options.authors:
"<meta name=\"generator\" content=\"KindleComicConverter-" + __version__ + "\"/>\n", f.writelines(["<dc:Creator>", author.encode('utf-8'), "</dc:Creator>\n"])
f.writelines(["<meta name=\"generator\" content=\"KindleComicConverter-" + __version__ + "\"/>\n",
"<meta name=\"RegionMagnification\" content=\"true\"/>\n", "<meta name=\"RegionMagnification\" content=\"true\"/>\n",
"<meta name=\"region-mag\" content=\"true\"/>\n", "<meta name=\"region-mag\" content=\"true\"/>\n",
"<meta name=\"cover\" content=\"cover\"/>\n", "<meta name=\"cover\" content=\"cover\"/>\n",
@@ -292,17 +308,24 @@ def getImageFileName(imgfile):
return filename return filename
def applyImgOptimization(img, opt, overrideQuality=5): def applyImgOptimization(img, opt, hqImage=None):
img.getImageFill(opt.webtoon) if not img.fill:
img.getImageFill(opt.webtoon)
if not opt.webtoon: if not opt.webtoon:
img.cropWhiteSpace(10.0) img.cropWhiteSpace(10.0)
if opt.cutpagenumbers and not opt.webtoon: if opt.cutpagenumbers and not opt.webtoon:
img.cutPageNumber() img.cutPageNumber()
img.optimizeImage(opt.gamma) img.optimizeImage(opt.gamma)
if overrideQuality != 5: if hqImage:
img.resizeImage(opt.upscale, opt.stretch, opt.bordersColor, overrideQuality) img.resizeImage(opt.upscale, opt.stretch, opt.bordersColor, 0)
img.calculateBorder(hqImage, True)
else: else:
img.resizeImage(opt.upscale, opt.stretch, opt.bordersColor, opt.quality) img.resizeImage(opt.upscale, opt.stretch, opt.bordersColor, opt.quality)
if opt.panelview:
if opt.quality == 0:
img.calculateBorder(img)
elif opt.quality == 1:
img.calculateBorder(img, True)
if opt.forcepng and not opt.forcecolor: if opt.forcepng and not opt.forcecolor:
img.quantizeImage() img.quantizeImage()
@@ -374,21 +397,21 @@ def fileImgProcess(work):
applyImgOptimization(img1, opt) applyImgOptimization(img1, opt)
img1.saveToDir(dirpath, opt.forcepng, opt.forcecolor, wipe) img1.saveToDir(dirpath, opt.forcepng, opt.forcecolor, wipe)
if opt.quality == 2: if opt.quality == 2:
img3 = image.ComicPage(split[0], opt.profileData) img0b = image.ComicPage(split[0], opt.profileData, img0.fill)
applyImgOptimization(img3, opt, 0) applyImgOptimization(img0b, opt, img0)
img3.saveToDir(dirpath, opt.forcepng, opt.forcecolor, True) img0b.saveToDir(dirpath, opt.forcepng, opt.forcecolor, True)
img4 = image.ComicPage(split[1], opt.profileData) img1b = image.ComicPage(split[1], opt.profileData, img1.fill)
applyImgOptimization(img4, opt, 0) applyImgOptimization(img1b, opt, img1)
img4.saveToDir(dirpath, opt.forcepng, opt.forcecolor, True) img1b.saveToDir(dirpath, opt.forcepng, opt.forcecolor, True)
else: else:
applyImgOptimization(img, opt) applyImgOptimization(img, opt)
img.saveToDir(dirpath, opt.forcepng, opt.forcecolor, wipe) img.saveToDir(dirpath, opt.forcepng, opt.forcecolor, wipe)
if opt.quality == 2: if opt.quality == 2:
img2 = image.ComicPage(os.path.join(dirpath, afile), opt.profileData) img2 = image.ComicPage(os.path.join(dirpath, afile), opt.profileData, img.fill)
if img.rotated: if img.rotated:
img2.image = img2.image.rotate(90) img2.image = img2.image.rotate(90)
img2.rotated = True img2.rotated = True
applyImgOptimization(img2, opt, 0) applyImgOptimization(img2, opt, img)
img2.saveToDir(dirpath, opt.forcepng, opt.forcecolor, True) img2.saveToDir(dirpath, opt.forcepng, opt.forcecolor, True)
except StandardError: except StandardError:
return str(sys.exc_info()[1]) return str(sys.exc_info()[1])
@@ -535,7 +558,7 @@ def getWorkFolder(afile):
if len(afile) > 240: if len(afile) > 240:
raise UserWarning("Path is too long.") raise UserWarning("Path is too long.")
if os.path.isdir(afile): if os.path.isdir(afile):
workdir = tempfile.mkdtemp('', 'KCC-TMP-') workdir = mkdtemp('', 'KCC-TMP-')
try: try:
os.rmdir(workdir) # needed for copytree() fails if dst already exists os.rmdir(workdir) # needed for copytree() fails if dst already exists
fullPath = os.path.join(workdir, 'OEBPS', 'Images') fullPath = os.path.join(workdir, 'OEBPS', 'Images')
@@ -554,7 +577,7 @@ def getWorkFolder(afile):
rmtree(path, True) rmtree(path, True)
raise UserWarning("Failed to extract images.") raise UserWarning("Failed to extract images.")
else: else:
workdir = tempfile.mkdtemp('', 'KCC-TMP-') workdir = mkdtemp('', 'KCC-TMP-')
cbx = cbxarchive.CBxArchive(afile) cbx = cbxarchive.CBxArchive(afile)
if cbx.isCbxFile(): if cbx.isCbxFile():
try: try:
@@ -573,9 +596,59 @@ def getWorkFolder(afile):
return path return path
def checkComicInfo(path, originalPath):
xmlPath = os.path.join(path, 'ComicInfo.xml')
options.authors = ['KCC']
titleSuffix = ''
if options.title == 'defaulttitle':
defaultTitle = True
if os.path.isdir(originalPath):
options.title = os.path.basename(originalPath)
else:
options.title = os.path.splitext(os.path.basename(originalPath))[0]
else:
defaultTitle = False
if os.path.exists(xmlPath):
try:
xml = parse(xmlPath)
except StandardError:
os.remove(xmlPath)
return
options.authors = []
if defaultTitle:
if len(xml.getElementsByTagName('Series')) != 0:
options.title = xml.getElementsByTagName('Series')[0].firstChild.nodeValue
if len(xml.getElementsByTagName('Volume')) != 0:
titleSuffix += ' V' + xml.getElementsByTagName('Volume')[0].firstChild.nodeValue
if len(xml.getElementsByTagName('Number')) != 0:
titleSuffix += ' #' + xml.getElementsByTagName('Number')[0].firstChild.nodeValue
options.title += titleSuffix
if len(xml.getElementsByTagName('Writer')) != 0:
authorsTemp = string.split(xml.getElementsByTagName('Writer')[0].firstChild.nodeValue, ', ')
for author in authorsTemp:
options.authors.append(author)
if len(xml.getElementsByTagName('Penciller')) != 0:
authorsTemp = string.split(xml.getElementsByTagName('Penciller')[0].firstChild.nodeValue, ', ')
for author in authorsTemp:
options.authors.append(author)
if len(xml.getElementsByTagName('Inker')) != 0:
authorsTemp = string.split(xml.getElementsByTagName('Inker')[0].firstChild.nodeValue, ', ')
for author in authorsTemp:
options.authors.append(author)
if len(xml.getElementsByTagName('Colorist')) != 0:
authorsTemp = string.split(xml.getElementsByTagName('Colorist')[0].firstChild.nodeValue, ', ')
for author in authorsTemp:
options.authors.append(author)
if len(options.authors) > 0:
options.authors = list(set(options.authors))
options.authors.sort()
else:
options.authors = ['KCC']
os.remove(xmlPath)
def slugify(value): def slugify(value):
# Normalizes string, converts to lowercase, removes non-alpha characters and converts spaces to hyphens. # Normalizes string, converts to lowercase, removes non-alpha characters and converts spaces to hyphens.
import unicodedata
if isinstance(value, str): if isinstance(value, str):
#noinspection PyArgumentList #noinspection PyArgumentList
value = unicodedata.normalize('NFKD', unicode(value, 'latin1')).encode('ascii', 'ignore') value = unicodedata.normalize('NFKD', unicode(value, 'latin1')).encode('ascii', 'ignore')
@@ -631,7 +704,7 @@ def getDirectorySize(start_path='.'):
def createNewTome(): def createNewTome():
tomePathRoot = tempfile.mkdtemp('', 'KCC-TMP-') tomePathRoot = mkdtemp('', 'KCC-TMP-')
tomePath = os.path.join(tomePathRoot, 'OEBPS', 'Images') tomePath = os.path.join(tomePathRoot, 'OEBPS', 'Images')
os.makedirs(tomePath) os.makedirs(tomePath)
return tomePath, tomePathRoot return tomePath, tomePathRoot
@@ -853,6 +926,7 @@ def main(argv=None, qtGUI=None):
parser.print_help() parser.print_help()
return return
path = getWorkFolder(args[0]) path = getWorkFolder(args[0])
checkComicInfo(path + "/OEBPS/Images/", args[0])
if options.webtoon: if options.webtoon:
if GUI: if GUI:
GUI.emit(QtCore.SIGNAL("progressBarTick"), 'status', 'Splitting images') GUI.emit(QtCore.SIGNAL("progressBarTick"), 'status', 'Splitting images')
@@ -879,19 +953,13 @@ def main(argv=None, qtGUI=None):
GUI.emit(QtCore.SIGNAL("progressBarTick"), 'status', 'Compressing CBZ files') GUI.emit(QtCore.SIGNAL("progressBarTick"), 'status', 'Compressing CBZ files')
else: else:
GUI.emit(QtCore.SIGNAL("progressBarTick"), 'status', 'Compressing EPUB files') GUI.emit(QtCore.SIGNAL("progressBarTick"), 'status', 'Compressing EPUB files')
GUI.emit(QtCore.SIGNAL("progressBarTick"), len(tomes)) GUI.emit(QtCore.SIGNAL("progressBarTick"), len(tomes) + 1)
GUI.emit(QtCore.SIGNAL("progressBarTick"))
options.baseTitle = options.title
for tome in tomes: for tome in tomes:
if GUI:
GUI.emit(QtCore.SIGNAL("progressBarTick"))
if os.path.isdir(args[0]):
barePath = os.path.basename(args[0])
else:
barePath = os.path.splitext(os.path.basename(args[0]))[0]
if len(tomes) > 1: if len(tomes) > 1:
tomeNumber += 1 tomeNumber += 1
options.title = barePath + ' ' + str(tomeNumber) options.title = options.baseTitle + ' [' + str(tomeNumber) + '/' + str(len(tomes)) + ']'
elif options.title == 'defaulttitle':
options.title = barePath
if options.cbzoutput: if options.cbzoutput:
# if CBZ output wanted, compress all images and return filepath # if CBZ output wanted, compress all images and return filepath
print "\nCreating CBZ file..." print "\nCreating CBZ file..."
@@ -911,6 +979,8 @@ def main(argv=None, qtGUI=None):
make_archive(tome + '_comic', 'zip', tome) make_archive(tome + '_comic', 'zip', tome)
move(tome + '_comic.zip', filepath[-1]) move(tome + '_comic.zip', filepath[-1])
rmtree(tome, True) rmtree(tome, True)
if GUI:
GUI.emit(QtCore.SIGNAL("progressBarTick"))
return filepath return filepath
@@ -968,6 +1038,9 @@ def checkOptions():
if options.profile == 'KFA' and (options.customwidth == 0 or options.customheight == 0): if options.profile == 'KFA' and (options.customwidth == 0 or options.customheight == 0):
print "ERROR: Kindle for Android profile require --customwidth and --customheight options!" print "ERROR: Kindle for Android profile require --customwidth and --customheight options!"
sys.exit(1) sys.exit(1)
# CBZ files on Kindle DX/DXG support higher resolution
if options.profile == 'KDX' and options.cbzoutput:
options.customheight = 1200
# Override profile data # Override profile data
if options.customwidth != 0 or options.customheight != 0: if options.customwidth != 0 or options.customheight != 0:
X = image.ProfileData.Profiles[options.profile][1][0] X = image.ProfileData.Profiles[options.profile][1][0]

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python2
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2012-2013 Ciro Mattia Gonano <ciromattia@gmail.com> # Copyright (c) 2012-2013 Ciro Mattia Gonano <ciromattia@gmail.com>
@@ -18,7 +18,7 @@
# 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__ = '3.5' __version__ = '3.6.2'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@vulturis.eu>' __copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@vulturis.eu>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
@@ -33,9 +33,25 @@ try:
from PIL import Image, ImageStat from PIL import Image, ImageStat
if tuple(map(int, ('2.2.1'.split(".")))) > tuple(map(int, (Image.PILLOW_VERSION.split(".")))): if tuple(map(int, ('2.2.1'.split(".")))) > tuple(map(int, (Image.PILLOW_VERSION.split(".")))):
print "ERROR: Pillow 2.2.1 or newer is required!" print "ERROR: Pillow 2.2.1 or newer is required!"
if sys.platform.startswith('linux'):
#noinspection PyUnresolvedReferences
import Tkinter
#noinspection PyUnresolvedReferences
import tkMessageBox
importRoot = Tkinter.Tk()
importRoot.withdraw()
tkMessageBox.showerror("KCC - Error", "Pillow 2.2.1 or newer is required!")
exit(1) exit(1)
except ImportError: except ImportError:
print "ERROR: Pillow is not installed!" print "ERROR: Pillow is not installed!"
if sys.platform.startswith('linux'):
#noinspection PyUnresolvedReferences
import Tkinter
#noinspection PyUnresolvedReferences
import tkMessageBox
importRoot = Tkinter.Tk()
importRoot.withdraw()
tkMessageBox.showerror("KCC - Error", "Pillow 2.2.1 or newer is required!")
exit(1) exit(1)
try: try:
from PyQt4 import QtCore from PyQt4 import QtCore

View File

@@ -21,14 +21,31 @@ __copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jas
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os import os
from sys import platform
try: try:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
from PIL import Image, ImageOps, ImageStat, ImageChops from PIL import Image, ImageOps, ImageStat, ImageChops
if tuple(map(int, ('2.2.1'.split(".")))) > tuple(map(int, (Image.PILLOW_VERSION.split(".")))): if tuple(map(int, ('2.2.1'.split(".")))) > tuple(map(int, (Image.PILLOW_VERSION.split(".")))):
print "ERROR: Pillow 2.2.1 or newer is required!" print "ERROR: Pillow 2.2.1 or newer is required!"
if platform.startswith('linux'):
#noinspection PyUnresolvedReferences
import Tkinter
#noinspection PyUnresolvedReferences
import tkMessageBox
importRoot = Tkinter.Tk()
importRoot.withdraw()
tkMessageBox.showerror("KCC - Error", "Pillow 2.2.1 or newer is required!")
exit(1) exit(1)
except ImportError: except ImportError:
print "ERROR: Pillow is not installed!" print "ERROR: Pillow is not installed!"
if platform.startswith('linux'):
#noinspection PyUnresolvedReferences
import Tkinter
#noinspection PyUnresolvedReferences
import tkMessageBox
importRoot = Tkinter.Tk()
importRoot.withdraw()
tkMessageBox.showerror("KCC - Error", "Pillow 2.2.1 or newer is required!")
exit(1) exit(1)
@@ -133,7 +150,7 @@ class ProfileData:
class ComicPage: class ComicPage:
def __init__(self, source, device): def __init__(self, source, device, fill=None):
try: try:
self.profile_label, self.size, self.palette, self.gamma, self.panelviewsize = device self.profile_label, self.size, self.palette, self.gamma, self.panelviewsize = device
except KeyError: except KeyError:
@@ -163,7 +180,10 @@ class ComicPage:
self.border = None self.border = None
self.noHPV = None self.noHPV = None
self.noVPV = None self.noVPV = None
self.fill = None if fill:
self.fill = fill
else:
self.fill = None
def saveToDir(self, targetdir, forcepng, color, wipe): def saveToDir(self, targetdir, forcepng, color, wipe):
try: try:
@@ -181,7 +201,8 @@ class ComicPage:
if self.noVPV: if self.noVPV:
suffix += "_kccnv" suffix += "_kccnv"
if self.border: if self.border:
suffix += "_kccx" + str(self.border[0]) + "_kccy" + str(self.border[1]) suffix += "_kccxl" + str(self.border[0]) + "_kccyu" + str(self.border[1]) + "_kccxr" +\
str(self.border[2]) + "_kccyd" + str(self.border[3])
if forcepng: if forcepng:
self.image.save(os.path.join(targetdir, os.path.splitext(self.filename)[0] + suffix + ".png"), "PNG", self.image.save(os.path.join(targetdir, os.path.splitext(self.filename)[0] + suffix + ".png"), "PNG",
optimize=1) optimize=1)
@@ -194,6 +215,8 @@ class ComicPage:
def optimizeImage(self, gamma): def optimizeImage(self, gamma):
if gamma < 0.1: if gamma < 0.1:
gamma = self.gamma gamma = self.gamma
if self.gamma != 1.0 and self.isImageColor(self.image):
gamma = 1.0
if gamma == 1.0: if gamma == 1.0:
self.image = ImageOps.autocontrast(self.image) self.image = ImageOps.autocontrast(self.image)
else: else:
@@ -210,58 +233,61 @@ class ComicPage:
# Quantize is deprecated but new function call it internally anyway... # Quantize is deprecated but new function call it internally anyway...
self.image = self.image.quantize(palette=palImg) self.image = self.image.quantize(palette=palImg)
def calculateBorderPercent(self, x, img, isWidth):
if isWidth:
return int(round(float(x)/float(img.image.size[0]), 4) * 10000 * 1.5)
else:
return int(round(float(x)/float(img.image.size[1]), 4) * 10000 * 1.5)
def calculateBorder(self, sourceImage, isHQ=False):
if self.fill == 'white':
# This code trigger only when sourceImage is already saved. So we can break color quantization.
if sourceImage.image.mode == 'P':
sourceImage.image = sourceImage.image.convert('RGB')
border = ImageChops.invert(sourceImage.image).getbbox()
else:
border = sourceImage.image.getbbox()
if border is not None:
if isHQ:
multiplier = 1.0
else:
multiplier = 1.5
self.border = [self.calculateBorderPercent(border[0], sourceImage, True),
self.calculateBorderPercent(border[1], sourceImage, False),
self.calculateBorderPercent((sourceImage.image.size[0] - border[2]), sourceImage, True),
self.calculateBorderPercent((sourceImage.image.size[1] - border[3]), sourceImage, False)]
if int((border[2] - border[0]) * multiplier) < self.size[0]:
self.noHPV = True
if int((border[3] - border[1]) * multiplier) < self.size[1]:
self.noVPV = True
else:
self.border = [0, 0, 0, 0]
self.noHPV = True
self.noVPV = True
def resizeImage(self, upscale=False, stretch=False, bordersColor=None, qualityMode=0): def resizeImage(self, upscale=False, stretch=False, bordersColor=None, qualityMode=0):
# High-quality downscaling filter
method = Image.ANTIALIAS
if bordersColor: if bordersColor:
fill = bordersColor fill = bordersColor
else: else:
fill = self.fill fill = self.fill
# Set target size
if qualityMode == 0: if qualityMode == 0:
size = (self.size[0], self.size[1]) size = (self.size[0], self.size[1])
generateBorder = True
elif qualityMode == 1:
size = (self.panelviewsize[0], self.panelviewsize[1])
generateBorder = True
else: else:
size = (self.panelviewsize[0], self.panelviewsize[1]) size = (self.panelviewsize[0], self.panelviewsize[1])
generateBorder = False # If image is smaller than device resolution and upscale is off - Just expand it by adding margins
# If image is smaller than screen and upscale is off - Just expand it if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1] and not upscale:
if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]: borderw = (self.size[0] - self.image.size[0]) / 2
if not upscale: borderh = (self.size[1] - self.image.size[1]) / 2
borderw = (self.size[0] - self.image.size[0]) / 2 self.image = ImageOps.expand(self.image, border=(borderw, borderh), fill=fill)
borderh = (self.size[1] - self.image.size[1]) / 2 return self.image
self.image = ImageOps.expand(self.image, border=(borderw, borderh), fill=fill)
if generateBorder:
if (self.image.size[0]-(2*borderw))*1.5 < self.size[0]:
self.noHPV = True
if (self.image.size[1]-(2*borderh))*1.5 < self.size[1]:
self.noVPV = True
self.border = [int(round(float(borderw)/float(self.image.size[0])*100, 2)*100*1.5),
int(round(float(borderh)/float(self.image.size[1])*100, 2)*100*1.5)]
return self.image
else:
# Cubic spline interpolation in a 4x4 environment
method = Image.BICUBIC
# If stretching is on - Resize without other considerations # If stretching is on - Resize without other considerations
if stretch: if stretch:
if self.image.size[0] <= size[0] and self.image.size[1] <= size[1]:
method = Image.BICUBIC
else:
method = Image.ANTIALIAS
self.image = self.image.resize(size, method) self.image = self.image.resize(size, method)
if generateBorder:
if fill == 'white':
border = ImageOps.invert(self.image).getbbox()
else:
border = self.image.getbbox()
if border is not None:
if (border[2]-border[0])*1.5 < self.size[0]:
self.noHPV = True
if (border[3]-border[1])*1.5 < self.size[1]:
self.noVPV = True
self.border = [int(round(float(border[0])/float(self.image.size[0])*100, 2)*100*1.5),
int(round(float(border[1])/float(self.image.size[1])*100, 2)*100*1.5)]
else:
self.border = [0, 0]
self.noHPV = True
self.noVPV = True
return self.image return self.image
# Otherwise - Upscale/Downscale # Otherwise - Upscale/Downscale
ratioDev = float(self.size[0]) / float(self.size[1]) ratioDev = float(self.size[0]) / float(self.size[1])
@@ -271,23 +297,11 @@ class ComicPage:
elif (float(self.image.size[0]) / float(self.image.size[1])) > ratioDev: elif (float(self.image.size[0]) / float(self.image.size[1])) > ratioDev:
diff = int(self.image.size[0] / ratioDev) - self.image.size[1] 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.expand(self.image, border=(0, diff / 2), fill=fill)
if self.image.size[0] <= size[0] and self.image.size[1] <= size[1]:
method = Image.BICUBIC
else:
method = Image.ANTIALIAS
self.image = ImageOps.fit(self.image, size, method=method, centering=(0.5, 0.5)) self.image = ImageOps.fit(self.image, size, method=method, centering=(0.5, 0.5))
if generateBorder:
if fill == 'white':
border = ImageOps.invert(self.image).getbbox()
else:
border = self.image.getbbox()
if border is not None:
if (border[2]-border[0])*1.5 < self.size[0]:
self.noHPV = True
if (border[3]-border[1])*1.5 < self.size[1]:
self.noVPV = True
self.border = [int(round(float(border[0])/float(self.image.size[0])*100, 2)*100*1.5),
int(round(float(border[1])/float(self.image.size[1])*100, 2)*100*1.5)]
else:
self.border = [0, 0]
self.noHPV = True
self.noVPV = True
return self.image return self.image
def splitPage(self, targetdir, righttoleft=False, rotate=False): def splitPage(self, targetdir, righttoleft=False, rotate=False):
@@ -419,7 +433,7 @@ class ComicPage:
self.image = self.image.crop((0, 0, widthImg - diff, heightImg)) self.image = self.image.crop((0, 0, widthImg - diff, heightImg))
return self.image return self.image
def getImageHistogram(self, image): def getImageHistogram(self, image, new=True):
histogram = image.histogram() histogram = image.histogram()
RBGW = [] RBGW = []
pixelCount = 0 pixelCount = 0
@@ -428,37 +442,100 @@ class ComicPage:
RBGW.append(histogram[i] + histogram[256 + i] + histogram[512 + i]) RBGW.append(histogram[i] + histogram[256 + i] + histogram[512 + i])
white = 0 white = 0
black = 0 black = 0
for i in range(245, 256): for i in range(251, 256):
white += RBGW[i] white += RBGW[i]
for i in range(11): for i in range(5):
black += RBGW[i] black += RBGW[i]
if black > white and black > pixelCount*0.5: if new:
return True if black > 0 and white == 0:
return 1
elif white > 0 and black == 0:
return -1
else:
return False
else: else:
return False if black > white and black > pixelCount*0.5:
return True
else:
return False
def getImageFill(self, isWebToon): def getImageFill(self, isWebToon):
fill = 0 if isWebToon:
if isWebToon or self.rotated: fill = 0
fill += self.getImageHistogram(self.image.crop((0, 0, self.image.size[0], 5))) fill += self.getImageHistogram(self.image.crop((0, 0, self.image.size[0], 5)), False)
fill += self.getImageHistogram(self.image.crop((0, self.image.size[1]-5, self.image.size[0], fill += self.getImageHistogram(self.image.crop((0, self.image.size[1]-5, self.image.size[0],
self.image.size[1]))) self.image.size[1])), False)
else: if fill == 2:
fill += self.getImageHistogram(self.image.crop((0, 0, 5, self.image.size[1]))) self.fill = 'black'
fill += self.getImageHistogram(self.image.crop((self.image.size[0]-5, 0, self.image.size[0], elif fill == 0:
self.image.size[1]))) self.fill = 'white'
if fill == 2: else:
self.fill = 'black' fill = 0
elif fill == 0: fill += self.getImageHistogram(self.image.crop((0, 0, 5, 5)), False)
self.fill = 'white' fill += self.getImageHistogram(self.image.crop((self.image.size[0]-5, 0, self.image.size[0], 5)), False)
fill += self.getImageHistogram(self.image.crop((0, self.image.size[1]-5, 5, self.image.size[1])), False)
fill += self.getImageHistogram(self.image.crop((self.image.size[0]-5, self.image.size[1]-5,
self.image.size[0], self.image.size[1])), False)
if fill > 1:
self.fill = 'black'
else:
self.fill = 'white'
else: else:
fill = 0 fill = 0
fill += self.getImageHistogram(self.image.crop((0, 0, 5, 5))) # Search fom horizontal solid lines
fill += self.getImageHistogram(self.image.crop((self.image.size[0]-5, 0, self.image.size[0], 5))) startY = 0
fill += self.getImageHistogram(self.image.crop((0, self.image.size[1]-5, 5, self.image.size[1]))) stopY = 3
fill += self.getImageHistogram(self.image.crop((self.image.size[0]-5, self.image.size[1]-5, searching = True
self.image.size[0], self.image.size[1]))) while stopY <= self.image.size[1]:
if fill > 1: checkSolid = self.getImageHistogram(self.image.crop((0, startY, self.image.size[0], stopY)))
if checkSolid:
fill += checkSolid
startY = stopY + 1
stopY = startY + 3
if stopY > self.image.size[1] and searching:
startY = self.image.size[1] - 3
stopY = self.image.size[1]
searching = False
# Search fom vertical solid lines
startX = 0
stopX = 3
searching = True
while stopX <= self.image.size[0]:
checkSolid = self.getImageHistogram(self.image.crop((startX, 0, stopX, self.image.size[1])))
if checkSolid:
fill += checkSolid
startX = stopX + 1
stopX = startX + 3
if stopX > self.image.size[0] and searching:
startX = self.image.size[0] - 3
stopX = self.image.size[0]
searching = False
if fill > 0:
self.fill = 'black' self.fill = 'black'
else: else:
self.fill = 'white' self.fill = 'white'
def isImageColor(self, image):
v = ImageStat.Stat(image).var
isMonochromatic = reduce(lambda x, y: x and y < 0.005, v, True)
if isMonochromatic:
# Monochromatic
return False
else:
if len(v) == 3:
maxmin = abs(max(v) - min(v))
if maxmin > 1000:
# Color
return True
elif maxmin > 100:
# Probably color
return True
else:
# Grayscale
return False
elif len(v) == 1:
# Black and white
return False
else:
# Detection failed
return False

View File

@@ -1,3 +1,4 @@
#!/usr/bin/env python2
""" """
cx_Freeze build script for KCC. cx_Freeze build script for KCC.
@@ -10,7 +11,7 @@ Usage (Windows):
from sys import platform from sys import platform
NAME = "KindleComicConverter" NAME = "KindleComicConverter"
VERSION = "3.5" VERSION = "3.6.2"
MAIN = "kcc.py" MAIN = "kcc.py"
if platform == "darwin": if platform == "darwin":
@@ -23,10 +24,11 @@ if platform == "darwin":
argv_emulation=True, argv_emulation=True,
iconfile='icons/comic2ebook.icns', iconfile='icons/comic2ebook.icns',
includes=['PIL', 'sip', 'PyQt4', 'PyQt4.QtCore', 'PyQt4.QtGui', 'PyQt4.QtNetwork'], includes=['PIL', 'sip', 'PyQt4', 'PyQt4.QtCore', 'PyQt4.QtGui', 'PyQt4.QtNetwork'],
qt_plugins=[],
excludes=['PyQt4.QtDeclarative', 'PyQt4.QtDesigner', 'PyQt4.QtHelp', 'PyQt4.QtMultimedia', excludes=['PyQt4.QtDeclarative', 'PyQt4.QtDesigner', 'PyQt4.QtHelp', 'PyQt4.QtMultimedia',
'PyQt4.QtOpenGL', 'PyQt4.QtScript', 'PyQt4.QtScriptTools', 'PyQt4.QtSql', 'PyQt4.QtSvg', 'PyQt4.QtOpenGL', 'PyQt4.QtScript', 'PyQt4.QtScriptTools', 'PyQt4.QtSql', 'PyQt4.QtSvg',
'PyQt4.QtXmlPatterns', 'PyQt4.QtXml', 'PyQt4.QtWebKit', 'PyQt4.QtTest'], 'PyQt4.QtXmlPatterns', 'PyQt4.QtXml', 'PyQt4.QtWebKit', 'PyQt4.QtTest', 'Tkinter'],
resources=['LICENSE.txt', 'other/Additional-LICENSE.txt'], resources=['LICENSE.txt', 'other/Additional-LICENSE.txt', 'other/unrar', 'other/7za'],
plist=dict( plist=dict(
CFBundleName=NAME, CFBundleName=NAME,
CFBundleShortVersionString=VERSION, CFBundleShortVersionString=VERSION,
@@ -42,6 +44,10 @@ if platform == "darwin":
CFBundleTypeRole='Viewer', CFBundleTypeRole='Viewer',
) )
], ],
LSMinimumSystemVersion='10.6.0',
LSEnvironment=dict(
PATH='/usr/local/bin:/usr/bin:/bin'
),
NSHumanReadableCopyright='ISC License (ISCL)' NSHumanReadableCopyright='ISC License (ISCL)'
) )
) )
@@ -55,7 +61,8 @@ elif platform == "win32":
['other/UnRAR.exe', 'UnRAR.exe'], ['other/UnRAR.exe', 'UnRAR.exe'],
['other/7za.exe', '7za.exe'], ['other/7za.exe', '7za.exe'],
['other/Additional-LICENSE.txt', 'Additional-LICENSE.txt'] ['other/Additional-LICENSE.txt', 'Additional-LICENSE.txt']
], "compressed": True}}, ], "compressed": True,
"excludes": ['Tkinter']}},
executables=[Executable(MAIN, executables=[Executable(MAIN,
base=base, base=base,
targetName="KCC.exe", targetName="KCC.exe",
@@ -65,18 +72,10 @@ elif platform == "win32":
appendScriptToLibrary=False, appendScriptToLibrary=False,
compress=True)]) compress=True)])
else: else:
from cx_Freeze import setup, Executable print 'Please use setup.sh to build Linux package.'
extra_options = dict( exit()
options={"build_exe": {"include_files": ['LICENSE.txt',
['other/Additional-LICENSE.txt', 'Additional-LICENSE.txt']
], "compressed": True}},
executables=[Executable(MAIN,
icon="icons/comic2ebook.png",
copyDependentFiles=True,
appendScriptToExe=True,
appendScriptToLibrary=False,
compress=True)])
#noinspection PyUnboundLocalVariable
setup( setup(
name=NAME, name=NAME,
version=VERSION, version=VERSION,
@@ -89,3 +88,8 @@ setup(
packages=['kcc'], requires=['Pillow'], packages=['kcc'], requires=['Pillow'],
**extra_options **extra_options
) )
if platform == "darwin":
from os import chmod
chmod('dist/' + NAME + '.app/Contents/Resources/unrar', 0777)
chmod('dist/' + NAME + '.app/Contents/Resources/7za', 0777)

12
setup.sh Normal file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
# Linux Python package build script
VERSION="3.6.2"
cp kcc.py __main__.py
zip kcc.zip __main__.py kcc/*.py
echo "#!/usr/bin/env python2" > kcc-bin
cat kcc.zip >> kcc-bin
chmod +x kcc-bin
tar --xform s:^.*/:: --xform s/kcc-bin/kcc/ --xform s/comic2ebook/kcc/ -czf KindleComicConverter_linux_$VERSION.tar.gz kcc-bin LICENSE.txt icons/comic2ebook.png
rm __main__.py kcc.zip kcc-bin