1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-16 14:08:45 +00:00

Compare commits

..

85 Commits
2.7 ... 2.10

Author SHA1 Message Date
Paweł Jastrzębski
b068d82ccf Made split filenames more idiot-proof 2013-05-28 13:57:11 +02:00
Paweł Jastrzębski
43ca5ac5b9 py2exe fix 2013-05-28 13:28:09 +02:00
Ciro Mattia Gonano
18c3ab2340 2.10 release 2013-05-28 12:56:59 +02:00
Ciro Mattia Gonano
87eaba184e Update README 2013-05-28 12:21:58 +02:00
Ciro Mattia Gonano
97b5d8a2ee Merge branch 'master' of github.com:ciromattia/kcc 2013-05-28 12:18:58 +02:00
Ciro Mattia Gonano
7c3a762107 Add "1" if sanitized filename already exists (fixes #50) 2013-05-28 12:17:08 +02:00
Paweł Jastrzębski
0b90af77da Disabled cropping and page number cutting for blank pages (close #43) 2013-05-28 12:07:45 +02:00
Paweł Jastrzębski
28dcab8ee8 Multiprocessing - final final touches + Code cleanup (close #39) 2013-05-28 11:24:37 +02:00
Paweł Jastrzębski
6c468a5291 dirImgProcess - multiprocessing 2013-05-28 08:47:24 +02:00
Paweł Jastrzębski
d090d8c2e8 Refactoring of dirImgProcess 2013-05-27 20:06:47 +02:00
Paweł Jastrzębski
aba315866e Small tweaks 2013-05-27 18:54:39 +02:00
Paweł Jastrzębski
e981aa4520 Added some comments 2013-05-27 18:46:54 +02:00
Paweł Jastrzębski
e603622021 Removed redundant --panelviewhorizontal 2013-05-27 18:26:53 +02:00
Paweł Jastrzębski
3e007965b2 Fixed panel order for horizontal pages when --rotate is enabled 2013-05-27 15:21:16 +02:00
Paweł Jastrzębski
c0610360a3 Updated README (I hate Markdown) 2013-05-27 09:21:09 +02:00
Paweł Jastrzębski
ff8f6e073f Updated README 2013-05-27 09:18:28 +02:00
Paweł Jastrzębski
aadb5407d2 Updated README 2013-05-27 09:00:01 +02:00
Paweł Jastrzębski
2e4d5eb958 Updated README 2013-05-27 08:54:51 +02:00
Ciro Mattia Gonano
33fb13a66e Merge branch 'master' of github.com:ciromattia/kcc 2013-05-20 18:23:03 +02:00
Ciro Mattia Gonano
4dc69aa1c4 Add default value for saveToDir() last param 2013-05-20 18:22:35 +02:00
Paweł Jastrzębski
b3681a3ceb Fixed merge 2013-05-16 11:49:49 +02:00
Ciro Mattia Gonano
348dcc4275 Merge pull request #49 from ciromattia/HorizontalPanelView
Horizontal Panel View
2013-05-16 01:20:31 -07:00
Ciro Mattia Gonano
39e69119ac Merge branch 'master' into HorizontalPanelView
Conflicts:
	kcc/comic2ebook.py
2013-05-16 10:19:48 +02:00
Paweł Jastrzębski
8c57926978 Disabling landscape mode as it is broken in this mode 2013-05-07 22:16:33 +02:00
Paweł Jastrzębski
8b1965054f Added --panelviewhorizontal option 2013-05-07 11:03:44 +02:00
Paweł Jastrzębski
18993069e3 Quantization must be disabled. 2013-04-30 12:57:32 +02:00
Ciro Mattia Gonano
7e6c8cc768 Update README 2013-04-30 10:29:40 +02:00
Ciro Mattia Gonano
3ae44d2fcb Merge pull request #47 from ciromattia/KindleFire
Kindle Fire support
2013-04-30 01:27:30 -07:00
Ciro Mattia Gonano
a6eb3936e4 Don't force upscale on Kindle Fire (let users choose if they want it) 2013-04-30 10:27:04 +02:00
Paweł Jastrzębski
bb24d3ca30 Preliminary support for horizontal Panel View. 2013-04-29 22:38:24 +02:00
Paweł Jastrzębski
7e191c0be5 Experimental support of Kindle Fire - Tweaks 2013-04-29 17:51:14 +02:00
Paweł Jastrzębski
d9ea165bbb Experimental support of Kindle Fire 2013-04-29 17:19:06 +02:00
Ciro Mattia Gonano
148211a5c7 Update for 2.9 release. 2013-04-19 12:14:24 +02:00
Ciro Mattia Gonano
23e07f47f0 Merge pull request #46 from ciromattia/slugify
Filenames slugification
2013-04-19 02:27:54 -07:00
Ciro Mattia Gonano
724156c554 Small fixes 2013-04-12 01:36:51 +02:00
Ciro Mattia Gonano
b972e4c746 Remove Windows silly 'thumbs.db' too 2013-04-11 12:33:14 +02:00
Ciro Mattia Gonano
f0afa1fff2 Convert dot char to hyphen.
Removes UNIX-hidden files and dirs from the final archive (prevents .DS_Store and stuff)
2013-04-11 12:18:02 +02:00
Ciro Mattia Gonano
a36c05f0c5 Merge from master 2013-04-11 12:00:51 +02:00
Ciro Mattia Gonano
4f3a66b4eb Update README.md 2013-04-11 12:59:01 +03:00
Ciro Mattia Gonano
6369c7ea44 Update after merging of #44 2013-04-11 12:54:16 +03:00
Ciro Mattia Gonano
f1b8aff8d4 Merge pull request #44 from devernay/master
Support more input image formats: GIF, TIFF, ...
2013-04-11 02:50:37 -07:00
Ciro Mattia Gonano
be270aa797 Add number padding and lowering for file names (not directory) 2013-04-11 11:49:29 +02:00
Ciro Mattia Gonano
f33d355024 Filenames slugifications (#28, #31, #9, #8) 2013-04-11 10:34:33 +02:00
Ciro Mattia Gonano
6f913b026e rarfile updated to 2.6 2013-04-11 09:34:20 +02:00
Ciro Mattia Gonano
220b4e0954 Add an option to generate a CBZ skipping all the EPUB/Mobi stuff.
Prevent output files to overwrite the source (add _kcc if duplicate is detected)
Fixes #45
2013-04-10 12:29:31 +02:00
Frédéric Devernay
bac4a4fd86 support more input image formats 2013-04-04 14:09:21 +02:00
Paweł Jastrzębski
d923299230 Updated README 2013-03-21 09:02:17 +01:00
Ciro Mattia Gonano
a79ebcec86 Version bump 2013-03-21 01:28:27 +01:00
Ciro Mattia Gonano
69bbee7648 Merge pull request #41 from ciromattia/kcc-outputformat2
Change default file output format
2013-03-20 14:33:43 -07:00
Paweł Jastrzębski
57d7729d05 Decreasing quality
File size/quality ratio is too high. It is not worth it.
Sadly huffman table optimization build-in PIL is just simply broken.
2013-03-20 21:12:30 +01:00
Paweł Jastrzębski
25a1dcb72c Updated GUI 2013-03-20 16:34:04 +01:00
Paweł Jastrzębski
cff9b73b80 Force JPEG output 2013-03-20 16:20:17 +01:00
Paweł Jastrzębski
23336de5b5 Version bump 2013-03-20 10:58:27 +01:00
Ciro Mattia Gonano
d346ca0466 Update version and readme 2013-03-20 10:39:42 +01:00
Ciro Mattia Gonano
22fadf078e Optimize archive extraction for zip/rar files (closes #40) 2013-03-20 10:35:50 +01:00
Ciro Mattia Gonano
74acf85683 Merge pull request #36 from ciromattia/kcc-panelview
Panel View support
2013-03-20 02:14:17 -07:00
Ciro Mattia Gonano
713de03d1d Merge branch 'master' into kcc-panelview 2013-03-20 10:12:57 +01:00
Ciro Mattia Gonano
5817f52110 Update rarfile 2013-03-20 10:10:01 +01:00
Paweł Jastrzębski
dfc03aee38 Restoring more safe filesize limit 2013-03-18 14:13:53 +01:00
Paweł Jastrzębski
6bc8038068 Yet another refactoring of resizeImage
I hope all bugs are dead this time!
2013-03-17 12:52:33 +01:00
Paweł Jastrzębski
eb7d56c1b9 Force upscaling for spitted pages
Only when landscape mode is on.
Without it smaller splitted pages looks really bad on PW.
2013-03-17 10:45:46 +01:00
Paweł Jastrzębski
f5b515ef79 GUI update 2013-03-17 09:37:57 +01:00
Paweł Jastrzębski
943431346d MAJOR Landscape Mode improvement
No idea how this change will impact Non-Kindle devices but quality
increase of spitted pages is amazing.
2013-03-17 08:47:35 +01:00
Paweł Jastrzębski
a597173b71 Added high quality Panel View 2013-03-16 21:53:23 +01:00
Paweł Jastrzębski
c484cc8fff Code inspection 2013-03-16 20:05:27 +01:00
Paweł Jastrzębski
f195a4ccbf Preliminary Panel View support for K3/KNT 2013-03-16 19:51:23 +01:00
Paweł Jastrzębski
f55bb6dce6 Code cleaning 2013-03-16 13:33:13 +01:00
Paweł Jastrzębski
e7f49f8330 New profiles
Kindle Touch support Virtual Panel View.
Kindle Classic/NT is not.
2013-03-16 10:19:57 +01:00
Paweł Jastrzębski
cec9356fc2 Code cleaning 2013-03-16 10:10:19 +01:00
Paweł Jastrzębski
6d704b382d Fixed merge bug 2013-03-13 21:10:50 +01:00
Ciro Mattia Gonano
1c5f67111f Merge branch 'master' into kcc-fakepanelview 2013-03-13 00:38:20 +01:00
Ciro Mattia Gonano
660df4b370 Bump to 2.8-dev 2013-03-13 00:38:03 +01:00
Ciro Mattia Gonano
19dc5cc6f2 Merge branch 'master' into kcc-fakepanelview
Apply PEP-8 codestyle
2013-03-13 00:21:09 +01:00
Ciro Mattia Gonano
67baf90d04 Merge branch 'master' into kcc-fakepanelview
Apply PEP-8 codestyle
2013-03-13 00:20:38 +01:00
Paweł Jastrzębski
07bf41ea6c Made commands little shorter 2013-03-12 19:56:19 +01:00
Ciro Mattia Gonano
c2cfc9b8db Apply the correct ordering for image processing (closes #33, thanks to @devernay) 2013-03-12 16:54:14 +01:00
Ciro Mattia Gonano
7a7a2bc10b Don't set topmost as we don't want GUI to be ALWAYS on top 2013-03-12 16:47:11 +01:00
Paweł Jastrzębski
e3127ed516 Code cleanup 2013-03-12 14:00:52 +01:00
Paweł Jastrzębski
93a98db43e Disabled unused functions 2013-03-12 10:48:30 +01:00
Paweł Jastrzębski
750b55649a Draft of FakePanelView Landscape mode 2013-03-12 10:47:37 +01:00
Paweł Jastrzębski
ca5854a8bd Real Panel View don't split in half 2013-03-10 18:18:20 +01:00
Paweł Jastrzębski
712f728a5e Draft of FakePanelView mode #2 2013-03-10 17:29:06 +01:00
Paweł Jastrzębski
eb7766a5e5 Draft of FakePanelView mode 2013-03-10 11:58:52 +01:00
Paweł Jastrzębski
0cabbfde96 Additional tweaks of Landscape mode 2013-03-10 10:43:34 +01:00
Paweł Jastrzębski
c5983276e5 Added --fakepanelview option 2013-03-10 00:03:11 +01:00
10 changed files with 959 additions and 538 deletions

163
README.md
View File

@@ -1,26 +1,32 @@
# KCC
# KCC
`KCC` (a.k.a. `KindleComicConverter`) is a Python app to convert comic files or folders to ePub or Panel View MOBI.
It was initally developed for Kindle but as of version 2.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 2 epub converter that every ereader owner can happily use**_.
It also optimizes comic images by:
- Enhancing contrast.
- Cutting page numbering.
- Cropping white borders.
- Resizing larger images to device's native resolution.
- Quantizing images to device's palette.
It can also optionally optimize images by applying a number of transformations.
### 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.
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 ;)
## BINARY RELEASES
You can find the latest released binary at the following links:
- OS X: [https://dl.dropbox.com/u/16806101/KindleComicConverter_osx_2.7.zip](https://dl.dropbox.com/u/16806101/KindleComicConverter_osx_2.7.zip)
- Win64: [https://dl.dropbox.com/u/16806101/KindleComicConverter_win-amd64_2.7.zip](https://dl.dropbox.com/u/16806101/KindleComicConverter_win-amd64_2.7.zip)
- Win32: [http://pawelj.vulturis.eu/Shared/KindleComicConverter_win-x86_2.7.zip](http://pawelj.vulturis.eu/Shared/KindleComicConverter_win-x86_2.7.zip) *(thanks to [AcidWeb](https://github.com/AcidWeb))*
- OS X: [https://dl.dropbox.com/u/16806101/KindleComicConverter_osx_2.10.zip](https://dl.dropbox.com/u/16806101/KindleComicConverter_osx_2.10.zip)
- Win64: [https://dl.dropbox.com/u/16806101/KindleComicConverter_win-amd64_2.10.zip](https://dl.dropbox.com/u/16806101/KindleComicConverter_win-amd64_2.10.zip)
- Win32: [http://pawelj.vulturis.eu/Shared/KindleComicConverter_win-x86_2.10.zip](http://pawelj.vulturis.eu/Shared/KindleComicConverter_win-x86_2.10.zip)
- Linux: Just download sourcecode and launch `python kcc.py` *(Provided you have Python and Pillow installed)*
## AWKCC .NET GUI
![AWKCC](http://pawelj.vulturis.eu/Shared/CurrentAWKCC.png)
[All-in-one package for Windows users](http://www.mobileread.com/forums/showpost.php?p=2444957&postcount=3)
## INPUT FORMATS
`kcc` can understand and convert, at the moment, the following file types:
- PNG, JPG
- PNG, JPG, GIF, TIFF, BMP
- Folders
- CBZ, ZIP
- CBR, RAR *(With `unrar` executable)*
@@ -52,12 +58,14 @@ Options:
--version show program's version number and exit
-h, --help show this help message and exit
-p PROFILE, --profile=PROFILE
Device profile (Choose one among K1, K2, K3, K4, KDX, KDXG or KHD) [Default=KHD]
Device profile (Choose one among K1, K2, K3, K4NT, K4T, KDX, KDXG, KHD, KF, KFHD, KFHD8) [Default=KHD]
-t TITLE, --title=TITLE
Comic title [Default=filename]
-m, --manga-style Manga style (Right-to-left reading and splitting) [Default=False]
-c, --cbz-output Outputs a CBZ archive and does not generate EPUB
--nopanelviewhq Disable high quality Panel View [Default=False]
--noprocessing Do not apply image preprocessing (Page splitting and optimizations) [Default=True]
--nodithering Disable image quantization [Default=False]
--forcepng Create PNG files instead JPEG (For non-Kindle devices) [Default=False]
--gamma=GAMMA Apply gamma correction to linearize the image [Default=Auto]
--upscale Resize images smaller than device's resolution [Default=False]
--stretch Stretch images to device's resolution [Default=False]
@@ -66,7 +74,7 @@ Options:
--nosplitrotate Disable splitting and rotation [Default=False]
--nocutpagenumbers Do not try to cut page numbering on images [Default=True]
-o OUTPUT, --output=OUTPUT
Output generated EPUB to specified directory or file
Output generated file (EPUB or CBZ) to specified directory or file
-v, --verbose Verbose output [Default=False]
```
@@ -85,42 +93,95 @@ The app relies and includes the following scripts/binaries:
- `magic.py` from [python-magic](https://github.com/ahupp/python-magic) library
## CHANGELOG
- 1.00: Initial version
- 1.10: Added support for CBZ/CBR files in comic2ebook.py
- 1.11: Added support for CBZ/CBR files in KindleComicConverter
- 1.20: Comic optimizations! Split pages not target-oriented (landscape with portrait target or portrait
with landscape target), add palette and other image optimizations from Mangle.
WARNING: PIL is required for all image mangling!
- 1.30: Fixed an issue in OPF generation for device resolution
Reworked options system (call with -h option to get the inline help)
- 1.40: Added some options for controlling image optimization
Further optimization (ImageOps, page numbering cut, autocontrast)
- 1.41: Fixed a serious bug on resizing when img ratio was bigger than device one
- 1.50: Added subfolder support for multiple chapters.
- 2.0: GUI! AppleScript is gone and Tk is used to provide cross-platform GUI support.
- 2.1: Added basic error reporting
- 2.2: Added (valid!) ePub 2.0 output
Rename .zip files to .cbz to avoid overwriting
- 2.3: Fixed win32 ePub generation, folder handling, filenames with spaces and subfolders
- 2.4: Use temporary directory as workdir (fixes converting from external volumes and zipfiles renaming)
Fixed "add folders" from GUI.
- 2.5: Added --black-borders option to set added borders black when page's ratio is not the device's one (#11).
Fixes epub containing zipped itself (#10)
- 2.6: Added --rotate option to rotate landscape images instead of splitting them (#16, #24)
Added --output option to customize ePub output dir/file (#22)
Add rendition:layout and rendition:orientation ePub meta tags (supported by new kindlegen 2.8)
Fixed natural sorting for files (#18)
- 2.7: Lots of GUI improvements (#27, #13)
Added gamma support within --gamma option (defaults to profile-specified gamma) (#26, #27)
Added --nodithering option to prevent dithering optimizations (#27)
Epub margins support (#30)
Fixed no file added if file has no spaces on Windows (#25)
Gracefully exit if unrar missing (#15)
Do not call kindlegen if source epub is bigger than 320MB (#17)
Get filetype from magic number (#14)
PDF conversion works again
####1.00
* Initial version
####1.10
* Added support for CBZ/CBR files in comic2ebook.py
####1.11
* Added support for CBZ/CBR files in KindleComicConverter
####1.20
* Comic optimizations! Split pages not target-oriented (landscape with portrait target or portrait with landscape target), add palette and other image optimizations from Mangle. WARNING: PIL is required for all image mangling!
####1.30
* Fixed an issue in OPF generation for device resolution
* Reworked options system (call with -h option to get the inline help)
####1.40
* Added some options for controlling image optimization
* Further optimization (ImageOps, page numbering cut, autocontrast)
####1.41
* Fixed a serious bug on resizing when img ratio was bigger than device one
####1.50
* Added subfolder support for multiple chapters.
####2.0
* GUI! AppleScript is gone and Tk is used to provide cross-platform GUI support.
####2.1
* Added basic error reporting
#### 2.2:
* Added (valid!) ePub 2.0 output
* Rename .zip files to .cbz to avoid overwriting
####2.3
* Fixed win32 ePub generation, folder handling, filenames with spaces and subfolders
####2.4
* Use temporary directory as workdir (fixes converting from external volumes and zipfiles renaming)
* Fixed "add folders" from GUI.
####2.5
* Added --black-borders option to set added borders black when page's ratio is not the device's one (#11).
* Fixes epub containing zipped itself (#10)
####2.6
* Added --rotate option to rotate landscape images instead of splitting them (#16, #24)
* Added --output option to customize ePub output dir/file (#22)
* Add rendition:layout and rendition:orientation ePub meta tags (supported by new kindlegen 2.8)
* Fixed natural sorting for files (#18)
####2.7
* Lots of GUI improvements (#27, #13)
* Added gamma support within --gamma option (defaults to profile-specified gamma) (#26, #27)
* Added --nodithering option to prevent dithering optimizations (#27)
* Epub margins support (#30)
* Fixed no file added if file has no spaces on Windows (#25)
* Gracefully exit if unrar missing (#15)
* Do not call kindlegen if source epub is bigger than 320MB (#17)
* Get filetype from magic number (#14)
* PDF conversion works again
####2.8
* Updated rarfile library
* Panel View support + HQ support (#36) - new option: --nopanelviewhq
* Split profiles for K4NT and K4T
* Rewrite of Landscape Mode support (huge readability improvement for KPW)
* Upscale use now BILINEAR method
* Added generic CSS file
* Optimized archive extraction for zip/rar files (#40)
####2.9
* Added support for generating a plain CBZ (skipping all the EPUB/Mobi generation) (#45)
* Prevent output file overwriting the source one: if a duplicate name is detected, append _kcc to the name
* Rarfile library updated to 2.6
* Added GIF, TIFF and BMP to supported formats (#42)
* Filenames slugifications (#28, #31, #9, #8)
####2.10:
* Multiprocessing support
* Kindle Fire support (color ePub/Mobi)
* Panel View support for horizontal content
* Fixed panel order for horizontal pages when --rotate is enabled
* Disabled cropping and page number cutting for blank pages
* Fixed some slugify issues with specific file naming conventions (#50, #51)
## COPYRIGHT
Copyright (c) 2012-2013 Ciro Mattia Gonano with further contributions by Paweł Jastrzębski.
KCC is released under ISC LICENSE; see LICENSE.txt for further details.
Copyright (c) 2012-2013 Ciro Mattia Gonano and Paweł Jastrzębski.
KCC is released under ISC LICENSE; see LICENSE.txt for further details.

6
kcc.py
View File

@@ -16,7 +16,7 @@
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
#
__version__ = '2.7'
__version__ = '2.10'
__license__ = 'ISC'
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
__docformat__ = 'restructuredtext en'
@@ -24,13 +24,15 @@ __docformat__ = 'restructuredtext en'
from Tkinter import *
from kcc import gui
from sys import platform
from multiprocessing import freeze_support
import os
freeze_support()
root = Tk()
root.resizable(width=False, height=False)
root.config(padx=5, pady=5, takefocus=True)
root.title("Kindle Comic Converter v" + __version__)
root.wm_attributes("-topmost", 1)
#root.wm_attributes("-topmost", 1)
if platform == 'darwin':
os.environ['PATH'] = '/usr/local/bin:' + os.environ['PATH']
elif platform == 'win32':

View File

@@ -1,4 +1,4 @@
__version__ = '2.0'
__version__ = '2.10'
__license__ = 'ISC'
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
__docformat__ = 'restructuredtext en'

View File

@@ -38,6 +38,7 @@ class CBxArchive:
def extractCBZ(self, targetdir):
cbzFile = zipfile.ZipFile(self.origFileName)
filelist = []
for f in cbzFile.namelist():
if f.startswith('__MACOSX') or f.endswith('.DS_Store'):
pass # skip MacOS special files
@@ -47,10 +48,12 @@ class CBxArchive:
except:
pass # the dir exists so we are going to extract the images only.
else:
cbzFile.extract(f, targetdir)
filelist.append(f)
cbzFile.extractall(targetdir, filelist)
def extractCBR(self, targetdir):
cbrFile = rarfile.RarFile(self.origFileName)
filelist = []
for f in cbrFile.namelist():
if f.startswith('__MACOSX') or f.endswith('.DS_Store'):
pass # skip MacOS special files
@@ -60,9 +63,11 @@ class CBxArchive:
except:
pass # the dir exists so we are going to extract the images only.
else:
cbrFile.extract(f, targetdir)
filelist.append(f)
cbrFile.extractall(targetdir, filelist)
def extract(self, targetdir):
print "\n" + targetdir + "\n"
if self.compressor == 'rar':
self.extractCBR(targetdir)
elif self.compressor == 'zip':

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Ciro Mattia Gonano <ciromattia@gmail.com>
#
@@ -16,7 +17,7 @@
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
#
__version__ = '2.7'
__version__ = '2.10'
__license__ = 'ISC'
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
__docformat__ = 'restructuredtext en'
@@ -31,6 +32,7 @@ from shutil import copytree
from shutil import rmtree
from shutil import make_archive
from optparse import OptionParser
from multiprocessing import Pool, freeze_support
import image
import cbxarchive
import pdfjpgextract
@@ -39,6 +41,11 @@ import pdfjpgextract
def buildHTML(path, imgfile):
filename = getImageFileName(imgfile)
if filename is not None:
# All files marked with this sufix need horizontal Panel View.
if "_rotated" in str(filename):
rotate = True
else:
rotate = False
htmlpath = ''
postfix = ''
backref = 1
@@ -61,26 +68,71 @@ def buildHTML(path, imgfile):
"<title>", filename[0], "</title>\n",
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n",
"<link href=\"", "../" * (backref - 1),
"stylesheet.css\" type=\"text/css\" rel=\"stylesheet\"/>\n",
"<link href=\"", "../" * (backref - 1),
"page_styles.css\" type=\"text/css\" rel=\"stylesheet\"/>\n",
"style.css\" type=\"text/css\" rel=\"stylesheet\"/>\n",
"</head>\n",
"<body class=\"kcc\">\n",
"<div class=\"kcc1\"><img src=\"", "../" * backref, "Images/", postfix, imgfile, "\" alt=\"",
imgfile, "\" class=\"kcc2\"/></div>\n",
#"<div id=\"", filename[0], "-1\">\n",
#"<a class=\"app-amzn-magnify\" data-app-amzn-magnify='{\"targetId\":\"", filename[0],
#"-1-magTargetParent\", \"ordinal\":1}'></a>\n",
#"</div>\n",
#"<div id=\"", filename[0], "-1-magTargetParent\" class=\"target-mag-parent\">\n",
#"<div class=\"target-mag-lb\">\n",
#"</div>\n",
#"<div id=\"", filename[0], "-1-magTarget\" class=\"target-mag\">\n",
#"<img src=\"../" * backref, "Images/", postfix, imgfile, "\" alt=\"", imgfile, "\"/>\n",
#"</div></div>\n",
"</body>\n",
"</html>"
"<body>\n",
"<div class=\"fs\">\n",
"<div><img src=\"", "../" * backref, "Images/", postfix, imgfile, "\" alt=\"",
imgfile, "\" class=\"singlePage\"/></div>\n"
])
if options.panelview:
if rotate:
if options.righttoleft:
f.writelines(["<div id=\"BoxTL\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
"'{\"targetId\":\"BoxTL-Panel-Parent\", \"ordinal\":1}'></a></div>\n",
"<div id=\"BoxTR\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
"'{\"targetId\":\"BoxTR-Panel-Parent\", \"ordinal\":3}'></a></div>\n",
"<div id=\"BoxBL\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
"'{\"targetId\":\"BoxBL-Panel-Parent\", \"ordinal\":2}'></a></div>\n",
"<div id=\"BoxBR\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify="
"'{\"targetId\":\"BoxBR-Panel-Parent\", \"ordinal\":4}'></a></div>\n"
])
else:
f.writelines(["<div id=\"BoxTL\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
"'{\"targetId\":\"BoxTL-Panel-Parent\", \"ordinal\":2}'></a></div>\n",
"<div id=\"BoxTR\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
"'{\"targetId\":\"BoxTR-Panel-Parent\", \"ordinal\":4}'></a></div>\n",
"<div id=\"BoxBL\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
"'{\"targetId\":\"BoxBL-Panel-Parent\", \"ordinal\":1}'></a></div>\n",
"<div id=\"BoxBR\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify="
"'{\"targetId\":\"BoxBR-Panel-Parent\", \"ordinal\":3}'></a></div>\n"
])
else:
if options.righttoleft:
f.writelines(["<div id=\"BoxTL\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
"'{\"targetId\":\"BoxTL-Panel-Parent\", \"ordinal\":2}'></a></div>\n",
"<div id=\"BoxTR\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
"'{\"targetId\":\"BoxTR-Panel-Parent\", \"ordinal\":1}'></a></div>\n",
"<div id=\"BoxBL\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
"'{\"targetId\":\"BoxBL-Panel-Parent\", \"ordinal\":4}'></a></div>\n",
"<div id=\"BoxBR\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify="
"'{\"targetId\":\"BoxBR-Panel-Parent\", \"ordinal\":3}'></a></div>\n"
])
else:
f.writelines(["<div id=\"BoxTL\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
"'{\"targetId\":\"BoxTL-Panel-Parent\", \"ordinal\":1}'></a></div>\n",
"<div id=\"BoxTR\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
"'{\"targetId\":\"BoxTR-Panel-Parent\", \"ordinal\":2}'></a></div>\n",
"<div id=\"BoxBL\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
"'{\"targetId\":\"BoxBL-Panel-Parent\", \"ordinal\":3}'></a></div>\n",
"<div id=\"BoxBR\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify="
"'{\"targetId\":\"BoxBR-Panel-Parent\", \"ordinal\":4}'></a></div>\n"
])
f.writelines(["<div id=\"BoxTL-Panel-Parent\" class=\"target-mag-parent\"><div id=\"BoxTL-Panel\" class=\"",
"target-mag\"><img src=\"", "../" * backref, "Images/", postfix, imgfile, "\" alt=\"",
imgfile, "\"/></div></div>\n",
"<div id=\"BoxTR-Panel-Parent\" class=\"target-mag-parent\"><div id=\"BoxTR-Panel\" class=\"",
"target-mag\"><img src=\"", "../" * backref, "Images/", postfix, imgfile, "\" alt=\"",
imgfile, "\"/></div></div>\n",
"<div id=\"BoxBL-Panel-Parent\" class=\"target-mag-parent\"><div id=\"BoxBL-Panel\" class=\"",
"target-mag\"><img src=\"", "../" * backref, "Images/", postfix, imgfile, "\" alt=\"",
imgfile, "\"/></div></div>\n",
"<div id=\"BoxBR-Panel-Parent\" class=\"target-mag-parent\"><div id=\"BoxBR-Panel\" class=\"",
"target-mag\"><img src=\"", "../" * backref, "Images/", postfix, imgfile, "\" alt=\"",
imgfile, "\"/></div></div>\n"
])
f.writelines(["</div>\n</body>\n</html>"])
f.close()
return path, imgfile
@@ -106,7 +158,7 @@ def buildNCX(dstdir, title, chapters):
f = open(ncxfile, "w")
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
"<!DOCTYPE ncx PUBLIC \"-//NISO//DTD ncx 2005-1//EN\" ",
"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd\">\n",
"\"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd\">\n",
"<ncx version=\"2005-1\" xml:lang=\"en-US\" xmlns=\"http://www.daisy.org/z3986/2005/ncx/\">\n",
"<head>\n",
"<meta name=\"dtb:uid\" content=\"015ffaec-9340-42f8-b163-a0c5ab7d0611\"/>\n",
@@ -128,12 +180,12 @@ def buildNCX(dstdir, title, chapters):
return
def buildOPF(profile, dstdir, title, filelist, cover=None, righttoleft=False):
def buildOPF(profile, dstdir, title, filelist, cover=None):
opffile = os.path.join(dstdir, 'OEBPS', 'content.opf')
# read the first file resolution
profilelabel, deviceres, palette, gamma = image.ProfileData.Profiles[profile]
profilelabel, deviceres, palette, gamma, panelviewsize = image.ProfileData.Profiles[profile]
imgres = str(deviceres[0]) + "x" + str(deviceres[1])
if righttoleft:
if options.righttoleft:
writingmode = "horizontal-rl"
facing = "right"
facing1 = "right"
@@ -159,16 +211,19 @@ def buildOPF(profile, dstdir, title, filelist, cover=None, righttoleft=False):
"<meta name=\"book-type\" content=\"comic\"/>\n",
"<meta name=\"zero-gutter\" content=\"true\"/>\n",
"<meta name=\"zero-margin\" content=\"true\"/>\n",
"<meta name=\"fixed-layout\" content=\"true\"/>\n",
"<meta name=\"orientation-lock\" content=\"none\"/>\n",
"<meta name=\"original-resolution\" content=\"", imgres, "\"/>\n",
"<meta name=\"fixed-layout\" content=\"true\"/>\n"
])
if options.landscapemode:
f.writelines(["<meta name=\"rendition:orientation\" content=\"auto\"/>\n",
"<meta name=\"orientation-lock\" content=\"none\"/>\n"])
else:
f.writelines(["<meta name=\"rendition:orientation\" content=\"portrait\"/>\n",
"<meta name=\"orientation-lock\" content=\"portrait\"/>\n"])
f.writelines(["<meta name=\"original-resolution\" content=\"", imgres, "\"/>\n",
"<meta name=\"primary-writing-mode\" content=\"", writingmode, "\"/>\n",
"<meta name=\"rendition:layout\" content=\"pre-paginated\"/>\n",
"<meta name=\"rendition:orientation\" content=\"auto\"/>\n",
"</metadata>\n<manifest>\n<item id=\"ncx\" href=\"toc.ncx\" ",
"media-type=\"application/x-dtbncx+xml\"/>\n"
])
# set cover
"media-type=\"application/x-dtbncx+xml\"/>\n"])
if cover is not None:
filename = getImageFileName(cover.replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\'))
if '.png' == filename[1]:
@@ -191,36 +246,46 @@ def buildOPF(profile, dstdir, title, filelist, cover=None, righttoleft=False):
mt = 'image/jpeg'
f.write("<item id=\"img_" + uniqueid + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\""
+ mt + "\"/>\n")
if (options.profile == 'K4' or options.profile == 'KHD') and splitCount > 0:
if options.landscapemode and splitCount > 0:
splitCountUsed = 1
while splitCountUsed <= splitCount:
f.write("<item id=\"blank-page" + str(splitCountUsed) +
"\" href=\"Text/blank.html\" media-type=\"application/xhtml+xml\"/>\n")
splitCountUsed += 1
f.write("<item id=\"css\" href=\"Text/style.css\" media-type=\"text/css\"/>\n")
f.write("</manifest>\n<spine toc=\"ncx\">\n")
splitCountUsed = 1
for entry in reflist:
if entry.endswith("-1"):
if ((righttoleft and facing == 'left') or (not righttoleft and facing == 'right')) and \
(options.profile == 'K4' or options.profile == 'KHD'):
if entry.endswith("-kcca"):
# noinspection PyRedundantParentheses
if ((options.righttoleft and facing == 'left') or (not options.righttoleft and facing == 'right')) and\
options.landscapemode:
f.write("<itemref idref=\"blank-page" + str(splitCountUsed) + "\" properties=\"layout-blank\"/>\n")
splitCountUsed += 1
f.write("<itemref idref=\"page_" + entry + "\" properties=\"page-spread-" + facing1 + "\"/>\n")
elif entry.endswith("-2"):
f.write("<itemref idref=\"page_" + entry + "\" properties=\"page-spread-" + facing2 + "\"/>\n")
if righttoleft:
if options.landscapemode:
f.write("<itemref idref=\"page_" + entry + "\" properties=\"page-spread-" + facing1 + "\"/>\n")
else:
f.write("<itemref idref=\"page_" + entry + "\"/>\n")
elif entry.endswith("-kccb"):
if options.landscapemode:
f.write("<itemref idref=\"page_" + entry + "\" properties=\"page-spread-" + facing2 + "\"/>\n")
else:
f.write("<itemref idref=\"page_" + entry + "\"/>\n")
if options.righttoleft:
facing = "right"
else:
facing = "left"
else:
f.write("<itemref idref=\"page_" + entry + "\" properties=\"page-spread-" + facing + "\"/>\n")
if options.landscapemode:
f.write("<itemref idref=\"page_" + entry + "\" properties=\"page-spread-" + facing + "\"/>\n")
else:
f.write("<itemref idref=\"page_" + entry + "\"/>\n")
if facing == 'right':
facing = 'left'
else:
facing = 'right'
f.write("</spine>\n<guide>\n</guide>\n</package>\n")
f.close()
# finish with standard ePub folders
os.mkdir(os.path.join(dstdir, 'META-INF'))
f = open(os.path.join(dstdir, 'mimetype'), 'w')
f.write('application/epub+zip')
@@ -241,79 +306,84 @@ def getImageFileName(imgfile):
if filename[0].startswith('.') or\
(filename[1].lower() != '.png' and
filename[1].lower() != '.jpg' and
filename[1].lower() != '.gif' and
filename[1].lower() != '.tif' and
filename[1].lower() != '.tiff' and
filename[1].lower() != '.bmp' and
filename[1].lower() != '.jpeg'):
return None
return filename
def isInFilelist(filename, filelist):
filename = os.path.splitext(filename)
seen = False
for item in filelist:
if filename[0] == item[0]:
seen = True
return seen
def applyImgOptimization(img, isSplit=False, toRight=False):
img.optimizeImage(options.gamma)
def applyImgOptimization(img, isSplit, toRight, options):
img.cropWhiteSpace(10.0)
if options.cutpagenumbers:
img.cutPageNumber()
img.resizeImage(options.upscale, options.stretch, options.black_borders, isSplit, toRight)
if not options.notquantize:
img.resizeImage(options.upscale, options.stretch, options.black_borders, isSplit, toRight, options.landscapemode,
options.nopanelviewhq)
img.optimizeImage(options.gamma)
if options.forcepng:
img.quantizeImage()
def dirImgProcess(path):
global options, splitCount
if options.righttoleft:
facing = "right"
else:
facing = "left"
work = []
pagenumber = 0
pagenumbermodifier = 0
pool = Pool()
for (dirpath, dirnames, filenames) in os.walk(path):
for afile in filenames:
if getImageFileName(afile) is not None:
if options.verbose:
print "Optimizing " + afile + " for " + options.profile
else:
print ".",
img = image.ComicPage(os.path.join(dirpath, afile), options.profile)
if options.nosplitrotate:
split = None
else:
split = img.splitPage(dirpath, options.righttoleft, options.rotate)
if split is not None:
if options.verbose:
print "Splitted " + afile
if options.righttoleft:
toRight1 = False
toRight2 = True
else:
toRight1 = True
toRight2 = False
if options.righttoleft:
if facing == "left":
splitCount += 1
facing = "right"
else:
if facing == "right":
splitCount += 1
facing = "left"
img0 = image.ComicPage(split[0], options.profile)
applyImgOptimization(img0, True, toRight1)
img0.saveToDir(dirpath, options.notquantize)
img1 = image.ComicPage(split[1], options.profile)
applyImgOptimization(img1, True, toRight2)
img1.saveToDir(dirpath, options.notquantize)
else:
if facing == "right":
facing = "left"
else:
facing = "right"
applyImgOptimization(img)
img.saveToDir(dirpath, options.notquantize)
pagenumber += 1
work.append([afile, dirpath, pagenumber, options])
splitpages = pool.map(fileImgProcess, work)
pool.close()
pool.join()
splitpages = filter(None, splitpages)
splitpages.sort()
for page in splitpages:
if (page + pagenumbermodifier) % 2 == 0:
splitCount += 1
pagenumbermodifier += 1
pagenumbermodifier += 1
def fileImgProcess(work):
afile = work[0]
dirpath = work[1]
pagenumber = work[2]
options = work[3]
output = None
if options.verbose:
print "Optimizing " + afile + " for " + options.profile
else:
print ".",
img = image.ComicPage(os.path.join(dirpath, afile), options.profile)
if options.nosplitrotate:
split = None
else:
split = img.splitPage(dirpath, options.righttoleft, options.rotate)
if split is not None and split is not "R":
if options.verbose:
print "Splitted " + afile
if options.righttoleft:
toRight1 = False
toRight2 = True
else:
toRight1 = True
toRight2 = False
output = pagenumber
img0 = image.ComicPage(split[0], options.profile)
applyImgOptimization(img0, True, toRight1, options)
img0.saveToDir(dirpath, options.forcepng, options.forcecolor)
img1 = image.ComicPage(split[1], options.profile)
applyImgOptimization(img1, True, toRight2, options)
img1.saveToDir(dirpath, options.forcepng, options.forcecolor)
else:
applyImgOptimization(img, False, False, options)
img.saveToDir(dirpath, options.forcepng, options.forcecolor, split)
return output
def genEpubStruct(path):
@@ -321,48 +391,134 @@ def genEpubStruct(path):
filelist = []
chapterlist = []
cover = None
_, deviceres, _, _, panelviewsize = image.ProfileData.Profiles[options.profile]
sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
os.mkdir(os.path.join(path, 'OEBPS', 'Text'))
f = open(os.path.join(path, 'OEBPS', 'Text', 'page_styles.css'), 'w')
f = open(os.path.join(path, 'OEBPS', 'Text', 'style.css'), 'w')
# DON'T COMPRESS CSS. KINDLE WILL FAIL TO PARSE IT.
# Generic Panel View support + Margins fix for Non-Kindle devices.
f.writelines(["@page {\n",
" margin-bottom: 0;\n",
" margin-top: 0\n",
"}\n"])
f.close()
f = open(os.path.join(path, 'OEBPS', 'Text', 'stylesheet.css'), 'w')
f.writelines([".kcc {\n",
" display: block;\n",
" margin-bottom: 0;\n",
" margin-left: 0;\n",
" margin-right: 0;\n",
" margin-top: 0;\n",
" padding-bottom: 0;\n",
" padding-left: 0;\n",
" padding-right: 0;\n",
" padding-top: 0;\n",
" text-align: left\n",
"margin-bottom: 0;\n",
"margin-top: 0\n",
"}\n",
".kcc1 {\n",
" display: block;\n",
" text-align: center\n",
"body {\n",
"display: block;\n",
"margin-bottom: 0;\n",
"margin-left: 0;\n",
"margin-right: 0;\n",
"margin-top: 0;\n",
"padding-bottom: 0;\n",
"padding-left: 0;\n",
"padding-right: 0;\n",
"padding-top: 0;\n",
"text-align: left\n",
"}\n",
".kcc2 {\n",
" height: auto;\n",
" width: auto\n",
"}\n"])
"div.fs {\n",
"height: ", str(deviceres[1]), "px;\n",
"width: ", str(deviceres[0]), "px;\n",
"position: relative;\n",
"display: block;\n",
"text-align: center\n",
"}\n",
"div.fs a {\n",
"display: block;\n",
"width : 100%;\n",
"height: 100%;\n",
"}\n",
"div.fs div {\n",
"position: absolute;\n",
"}\n",
"img.singlePage {\n",
"position: absolute;\n",
"height: ", str(deviceres[1]), "px;\n",
"width: ", str(deviceres[0]), "px;\n",
"}\n",
"div.target-mag-parent {\n",
"width:100%;\n",
"height:100%;\n",
"display:none;\n",
"}\n",
"div.target-mag {\n",
"position: absolute;\n",
"display: block;\n",
"overflow: hidden;\n",
"}\n",
"div.target-mag img {\n",
"position: absolute;\n",
"height: ", str(panelviewsize[1]), "px;\n",
"width: ", str(panelviewsize[0]), "px;\n",
"}\n",
"#BoxTL {\n",
"top: 0;\n",
"left: 0;\n",
"height: 50%;\n",
"width: 50%;\n",
"}\n",
"#BoxTR {\n",
"top: 0;\n",
"right: 0;\n",
"height: 50%;\n",
"width: 50%;\n",
"}\n",
"#BoxBL {\n",
"bottom: 0;\n",
"left: 0;\n",
"height: 50%;\n",
"width: 50%;\n",
"}\n",
"#BoxBR {\n",
"bottom: 0;\n",
"right: 0;\n",
"height: 50%;\n",
"width: 50%;\n",
"}\n",
"#BoxTL-Panel {\n",
"top: 0;\n",
"left: 0;\n",
"height: 100%;\n",
"width: 100%;\n",
"}\n",
"#BoxTL-Panel img {\n",
"top: 0%;\n",
"left: 0%;\n",
"}\n",
"#BoxTR-Panel {\n",
"top: 0;\n",
"right: 0;\n",
"height: 100%;\n",
"width: 100%;\n",
"}\n",
"#BoxTR-Panel img {\n",
"top: 0%;\n",
"right: 0%;\n",
"}\n",
"#BoxBL-Panel {\n",
"bottom: 0;\n",
"left: 0;\n",
"height: 100%;\n",
"width: 100%;\n",
"}\n",
"#BoxBL-Panel img {\n",
"bottom: 0%;\n",
"left: 0%;\n",
"}\n",
"#BoxBR-Panel {\n",
"bottom: 0;\n",
"right: 0;\n",
"height: 100%;\n",
"width: 100%;\n",
"}\n",
"#BoxBR-Panel img {\n",
"bottom: 0%;\n",
"right: 0%;\n",
"}"
])
f.close()
for (dirpath, dirnames, filenames) in os.walk(os.path.join(path, 'OEBPS', 'Images')):
chapter = False
for afile in filenames:
filename = getImageFileName(afile)
if filename is not None:
# put credits at the end
if "credit" in afile.lower():
os.rename(os.path.join(dirpath, afile), os.path.join(dirpath, 'ZZZ999_' + afile))
afile = 'ZZZ999_' + afile
if "+" in afile.lower() or "#" in afile.lower():
newfilename = afile.replace('+', '_').replace('#', '_')
os.rename(os.path.join(dirpath, afile), os.path.join(dirpath, newfilename))
afile = newfilename
filelist.append(buildHTML(dirpath, afile))
if not chapter:
chapterlist.append((dirpath.replace('Images', 'Text'), filelist[-1][1]))
@@ -375,8 +531,8 @@ def genEpubStruct(path):
convert = lambda text: int(text) if text.isdigit() else text
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
filelist.sort(key=lambda name: (alphanum_key(name[0].lower()), alphanum_key(name[1].lower())))
buildOPF(options.profile, path, options.title, filelist, cover, options.righttoleft)
if (options.profile == 'K4' or options.profile == 'KHD') and splitCount > 0:
buildOPF(options.profile, path, options.title, filelist, cover)
if options.landscapemode and splitCount > 0:
filelist.append(buildBlankHTML(os.path.join(path, 'OEBPS', 'Text')))
@@ -409,14 +565,45 @@ def getWorkFolder(afile):
return path
def slugify(value):
"""
Normalizes string, converts to lowercase, removes non-alpha characters,
and converts spaces to hyphens.
"""
import unicodedata
value = unicodedata.normalize('NFKD', unicode(value, 'latin1')).encode('ascii', 'ignore')
value = re.sub('[^\w\s\.-]', '', value).strip().lower()
value = re.sub('[-\.\s]+', '-', value)
value = re.sub(r'([0-9]+)', r'00000\1', value)
value = re.sub(r'0*([0-9]{6,})', r'\1', value)
return value
def sanitizeTree(filetree):
for root, dirs, files in os.walk(filetree, False):
for name in files:
if name.startswith('.') or name.lower() == 'thumbs.db':
os.remove(os.path.join(root, name))
else:
splitname = os.path.splitext(name)
slugified = slugify(splitname[0])
while os.path.exists(os.path.join(root, slugified + splitname[1])):
slugified += "1"
os.rename(os.path.join(root, name), os.path.join(root, slugified + splitname[1]))
for name in dirs:
if name.startswith('.'):
os.remove(os.path.join(root, name))
else:
os.rename(os.path.join(root, name), os.path.join(root, slugify(name)))
def Copyright():
print ('comic2ebook v%(__version__)s. '
'Written 2012 by Ciro Mattia Gonano.' % globals())
'Written 2013 by Ciro Mattia Gonano and Pawel Jastrzebski.' % globals())
def Usage():
print "Generates HTML, NCX and OPF for a Comic ebook from a bunch of images."
print "Optimized for creating MOBI files to be read on Kindle Paperwhite."
print "Generates EPUB/CBZ comic ebook from a bunch of images."
parser.print_help()
@@ -425,15 +612,20 @@ def main(argv=None):
usage = "Usage: %prog [options] comic_file|comic_folder"
parser = OptionParser(usage=usage, version=__version__)
parser.add_option("-p", "--profile", action="store", dest="profile", default="KHD",
help="Device profile (Choose one among K1, K2, K3, K4, KDX, KDXG or KHD) [Default=KHD]")
help="Device profile (Choose one among K1, K2, K3, K4NT, K4T, KDX, KDXG, KHD, KF, KFHD, KFHD8) "
"[Default=KHD]")
parser.add_option("-t", "--title", action="store", dest="title", default="defaulttitle",
help="Comic title [Default=filename]")
parser.add_option("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
help="Manga style (Right-to-left reading and splitting) [Default=False]")
parser.add_option("-c", "--cbz-output", action="store_true", dest="cbzoutput", default=False,
help="Outputs a CBZ archive and does not generate EPUB")
parser.add_option("--nopanelviewhq", action="store_true", dest="nopanelviewhq", default=False,
help="Disable high quality Panel View [Default=False]")
parser.add_option("--noprocessing", action="store_false", dest="imgproc", default=True,
help="Do not apply image preprocessing (Page splitting and optimizations) [Default=True]")
parser.add_option("--nodithering", action="store_true", dest="notquantize", default=False,
help="Disable image quantization [Default=False]")
parser.add_option("--forcepng", action="store_true", dest="forcepng", default=False,
help="Create PNG files instead JPEG (For non-Kindle devices) [Default=False]")
parser.add_option("--gamma", type="float", dest="gamma", default="0.0",
help="Apply gamma correction to linearize the image [Default=Auto]")
parser.add_option("--upscale", action="store_true", dest="upscale", default=False,
@@ -450,10 +642,11 @@ def main(argv=None):
parser.add_option("--nocutpagenumbers", action="store_false", dest="cutpagenumbers", default=True,
help="Do not try to cut page numbering on images [Default=True]")
parser.add_option("-o", "--output", action="store", dest="output", default=None,
help="Output generated EPUB to specified directory or file")
help="Output generated file (EPUB or CBZ) to specified directory or file")
parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False,
help="Verbose output [Default=False]")
options, args = parser.parse_args(argv)
checkOptions()
if len(args) != 1:
parser.print_help()
return
@@ -464,32 +657,82 @@ def main(argv=None):
if options.imgproc:
print "Processing images..."
dirImgProcess(path + "/OEBPS/Images/")
print "\nCreating ePub structure..."
genEpubStruct(path)
# actually zip the ePub
if options.output is not None:
if options.output.endswith('.epub'):
epubpath = os.path.abspath(options.output)
elif os.path.isdir(args[0]):
epubpath = os.path.abspath(options.output) + "/" + os.path.basename(args[0]) + '.epub'
else:
epubpath = os.path.abspath(options.output) + "/" \
+ os.path.basename(os.path.splitext(args[0])[0]) + '.epub'
elif os.path.isdir(args[0]):
epubpath = args[0] + '.epub'
if options.cbzoutput:
# if CBZ output wanted, compress all images and return filepath
print "\nCreating CBZ file..."
filepath = getOutputFilename(args[0], options.output, '.cbz')
make_archive(path + '_comic', 'zip', path + '/OEBPS/Images')
else:
epubpath = os.path.splitext(args[0])[0] + '.epub'
make_archive(path + '_comic', 'zip', path)
move(path + '_comic.zip', epubpath)
print "\nCreating ePub structure..."
genEpubStruct(path)
# actually zip the ePub
filepath = getOutputFilename(args[0], options.output, '.epub')
make_archive(path + '_comic', 'zip', path)
move(path + '_comic.zip', filepath)
rmtree(path)
return epubpath
return filepath
def getOutputFilename(srcpath, wantedname, ext):
if not ext.startswith('.'):
ext = '.' + ext
if wantedname is not None:
if wantedname.endswith(ext):
filename = os.path.abspath(wantedname)
elif os.path.isdir(srcpath):
filename = os.path.abspath(options.output) + "/" + os.path.basename(srcpath) + ext
else:
filename = os.path.abspath(options.output) + "/" \
+ os.path.basename(os.path.splitext(srcpath)[0]) + ext
elif os.path.isdir(srcpath):
filename = srcpath + ext
else:
filename = os.path.splitext(srcpath)[0] + ext
if os.path.isfile(filename):
filename = os.path.splitext(filename)[0] + '_kcc' + ext
return filename
def checkOptions():
global options
# Landscape mode is only supported by Kindle Touch and Paperwhite.
if options.profile == 'K4T' or options.profile == 'KHD':
options.landscapemode = True
else:
options.landscapemode = False
# Older Kindle don't support Virtual Panel View. We providing them configuration that will fake that feature.
if options.profile == 'K3' or options.profile == 'K4NT':
# Real Panel View
options.panelview = True
else:
# Virtual Panel View
options.panelview = False
# Older Kindle don't need higher resolution files due lack of Panel View.
# Kindle Fire family have very high resolution. Bigger images are not needed.
if options.profile == 'K1' or options.profile == 'K2' or options.profile == 'KDX' or options.profile == 'KDXG'\
or options.profile == 'KF' or options.profile == 'KFHD' or options.profile == 'KFHD8':
options.nopanelviewhq = True
# Disabling grayscale conversion for Kindle Fire family.
# Forcing JPEG output. For now code can't provide color PNG files.
if options.profile == 'KF' or options.profile == 'KFHD' or options.profile == 'KFHD8':
options.forcecolor = True
options.forcepng = False
else:
options.forcecolor = False
# Mixing vertical and horizontal pages require real Panel View.
# Landscape mode don't work correcly without Virtual Panel View.
if options.rotate:
options.panelview = True
options.landscapemode = False
def getEpubPath():
global epub_path
return epub_path
if __name__ == "__main__":
freeze_support()
Copyright()
main(sys.argv[1:])
sys.exit(0)

View File

@@ -28,7 +28,6 @@ import comic2ebook
import kindlestrip
from image import ProfileData
from subprocess import call
from subprocess import check_call
import os
import shutil
import stat
@@ -94,29 +93,33 @@ class MainWindow:
self.options = {
'Aepub_only': IntVar(None, 0),
'Bmangastyle': IntVar(None, 0),
'Cimage_preprocess': IntVar(None, 0),
'Dnotquantize': IntVar(None, 0),
'Eimage_gamma': DoubleVar(None, 0.0),
'Fimage_upscale': IntVar(None, 0),
'Gimage_stretch': IntVar(None, 0),
'Hblack_borders': IntVar(None, 0),
'Irotate': IntVar(None, 0),
'Jnosplitrotate': IntVar(None, 0),
'Kcut_page_numbers': IntVar(None, 0)
'Bcbz_only': IntVar(None, 0),
'Cmangastyle': IntVar(None, 0),
'Dnopanelviewhq': IntVar(None, 0),
'Eimage_preprocess': IntVar(None, 0),
'Fforcepng': IntVar(None, 0),
'Gimage_gamma': DoubleVar(None, 0.0),
'Himage_upscale': IntVar(None, 0),
'Iimage_stretch': IntVar(None, 0),
'Jblack_borders': IntVar(None, 0),
'Krotate': IntVar(None, 0),
'Lnosplitrotate': IntVar(None, 0),
'Mcut_page_numbers': IntVar(None, 0)
}
self.optionlabels = {
'Aepub_only': "Generate EPUB only",
'Cimage_preprocess': "Disable image optimizations",
'Dnotquantize': "Disable image quantization",
'Jnosplitrotate': "Disable splitting and rotation",
'Irotate': "Rotate images instead splitting them",
'Kcut_page_numbers': "Disable page numbers cutting",
'Bmangastyle': "Manga mode",
'Eimage_gamma': "Custom gamma correction",
'Fimage_upscale': "Allow image upscaling",
'Gimage_stretch': "Stretch images",
'Hblack_borders': "Use black borders"
'Bcbz_only': "Generate CBZ only (skip EPUB/Mobi generation)",
'Cmangastyle': "Manga mode",
'Dnopanelviewhq': "Disable high quality Panel View",
'Eimage_preprocess': "Disable image optimizations",
'Fforcepng': "Create PNG files instead of JPEG",
'Gimage_gamma': "Custom gamma correction",
'Himage_upscale': "Allow image upscaling",
'Iimage_stretch': "Stretch images",
'Jblack_borders': "Use black borders (instead of white ones)",
'Krotate': "Rotate images (instead of splitting them)",
'Lnosplitrotate': "Disable both splitting and rotation",
'Mcut_page_numbers': "Disable page numbers cutting"
}
self.optionsButtons = {}
for key in sorted(self.options):
@@ -161,27 +164,31 @@ class MainWindow:
return
profilekey = ProfileData.ProfileLabels[self.profile.get()]
argv = ["-p", profilekey]
if self.options['Eimage_gamma'].get() != 0.0:
argv.append("--gamma")
argv.append(self.options['Eimage_gamma'].get())
if self.options['Cimage_preprocess'].get() == 1:
argv.append("--noprocessing")
if self.options['Dnotquantize'].get() == 1:
argv.append("--nodithering")
if self.options['Jnosplitrotate'].get() == 1:
argv.append("--nosplitrotate")
if self.options['Irotate'].get() == 1:
argv.append("--rotate")
if self.options['Kcut_page_numbers'].get() == 1:
argv.append("--nocutpagenumbers")
if self.options['Bmangastyle'].get() == 1:
if self.options['Bcbz_only'].get() == 1:
argv.append("-c")
if self.options['Cmangastyle'].get() == 1:
argv.append("-m")
if self.options['Fimage_upscale'].get() == 1:
if self.options['Dnopanelviewhq'].get() == 1:
argv.append("--nopanelviewhq")
if self.options['Eimage_preprocess'].get() == 1:
argv.append("--noprocessing")
if self.options['Fforcepng'].get() == 1:
argv.append("--forcepng")
if self.options['Gimage_gamma'].get() != 0.0:
argv.append("--gamma")
argv.append(self.options['Gimage_gamma'].get())
if self.options['Himage_upscale'].get() == 1:
argv.append("--upscale")
if self.options['Gimage_stretch'].get() == 1:
if self.options['Iimage_stretch'].get() == 1:
argv.append("--stretch")
if self.options['Hblack_borders'].get() == 1:
if self.options['Jblack_borders'].get() == 1:
argv.append("--blackborders")
if self.options['Krotate'].get() == 1:
argv.append("--rotate")
if self.options['Lnosplitrotate'].get() == 1:
argv.append("--nosplitrotate")
if self.options['Mcut_page_numbers'].get() == 1:
argv.append("--nocutpagenumbers")
errors = False
left_files = len(self.filelist)
filenum = 0
@@ -204,12 +211,12 @@ class MainWindow:
(subargv[-1], str(err), traceback.format_tb(traceback_)))
errors = True
continue
if self.options['Aepub_only'].get() == 0:
if self.options['Aepub_only'].get() == 0 and self.options['Bcbz_only'].get() == 0:
try:
if os.path.getsize(epub_path) > 335544320:
# do not call kindlegen if source is bigger than 320MB
if os.path.getsize(epub_path) > 314572800:
# do not call kindlegen if source is bigger than 300MB
tkMessageBox.showwarning('KindleGen Warning',
"ePub file %s is bigger than 320MB, not suitable for kindlegen" %
"ePub file %s is bigger than 300MB, not suitable for kindlegen" %
epub_path)
continue
retcode = call("kindlegen \"" + epub_path + "\"", shell=True)

View File

@@ -20,7 +20,7 @@ __copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
__docformat__ = 'restructuredtext en'
import os
from PIL import Image, ImageOps, ImageDraw, ImageStat
from PIL import Image, ImageOps, ImageStat, ImageChops
class ImageFlags:
@@ -77,23 +77,31 @@ class ProfileData:
]
Profiles = {
'K1': ("Kindle", (600, 800), Palette4, 1.8),
'K2': ("Kindle 2", (600, 800), Palette15, 1.8),
'K3': ("Kindle 3/Keyboard", (600, 800), Palette16, 1.8),
'K4': ("Kindle 4/NT/Touch", (600, 800), Palette16, 1.8),
'KHD': ("Kindle Paperwhite", (758, 1024), Palette16, 1.8),
'KDX': ("Kindle DX", (824, 1200), Palette15, 1.8),
'KDXG': ("Kindle DXG", (824, 1200), Palette16, 1.8)
'K1': ("Kindle 1", (600, 800), Palette4, 1.8, (900, 1200)),
'K2': ("Kindle 2", (600, 800), Palette15, 1.8, (900, 1200)),
'K3': ("Kindle Keyboard", (600, 800), Palette16, 1.8, (900, 1200)),
'K4NT': ("Kindle Non-Touch", (600, 800), Palette16, 1.8, (900, 1200)),
'K4T': ("Kindle Touch", (600, 800), Palette16, 1.8, (900, 1200)),
'KHD': ("Kindle Paperwhite", (758, 1024), Palette16, 1.8, (1137, 1536)),
'KDX': ("Kindle DX", (824, 1200), Palette15, 1.8, (1236, 1800)),
'KDXG': ("Kindle DXG", (824, 1200), Palette16, 1.8, (1236, 1800)),
'KF': ("Kindle Fire", (600, 1024), Palette16, 1.0, (900, 1536)),
'KFHD': ("Kindle Fire HD 7\"", (800, 1280), Palette16, 1.0, (1200, 1920)),
'KFHD8': ("Kindle Fire HD 8.9\"", (1200, 1920), Palette16, 1.0, (1800, 2880))
}
ProfileLabels = {
"Kindle": 'K1',
"Kindle 1": 'K1',
"Kindle 2": 'K2',
"Kindle 3/Keyboard": 'K3',
"Kindle 4/NT/Touch": 'K4',
"Kindle 4/Non-Touch": 'K4NT',
"Kindle 4/Touch": 'K4T',
"Kindle Paperwhite": 'KHD',
"Kindle DX": 'KDX',
"Kindle DXG": 'KDXG'
"Kindle DXG": 'KDXG',
"Kindle Fire": 'KF',
"Kindle Fire HD 7\"": 'KFHD',
"Kindle Fire HD 8.9\"": 'KFHD8'
}
@@ -101,7 +109,7 @@ class ComicPage:
def __init__(self, source, device):
try:
self.profile = device
self.profile_label, self.size, self.palette, self.gamma = ProfileData.Profiles[device]
self.profile_label, self.size, self.palette, self.gamma, self.panelviewsize = ProfileData.Profiles[device]
except KeyError:
raise RuntimeError('Unexpected output device %s' % device)
try:
@@ -111,15 +119,21 @@ class ComicPage:
raise RuntimeError('Cannot read image file %s' % source)
self.image = self.image.convert('RGB')
def saveToDir(self, targetdir, notquantize):
def saveToDir(self, targetdir, forcepng, color, sufix=None):
filename = os.path.basename(self.origFileName)
try:
self.image = self.image.convert('L') # convert to grayscale
os.remove(os.path.join(targetdir, filename))
if notquantize:
self.image.save(os.path.join(targetdir, os.path.splitext(filename)[0] + ".jpg"), "JPEG")
if not color:
self.image = self.image.convert('L') # convert to grayscale
# Sufix is used to recognise which files need horizontal Panel View.
if sufix == "R":
sufix = "_rotated"
else:
self.image.save(os.path.join(targetdir, os.path.splitext(filename)[0] + ".png"), "PNG")
sufix = ""
os.remove(os.path.join(targetdir, filename))
if forcepng:
self.image.save(os.path.join(targetdir, os.path.splitext(filename)[0] + sufix + ".png"), "PNG")
else:
self.image.save(os.path.join(targetdir, os.path.splitext(filename)[0] + sufix + ".jpg"), "JPEG")
except IOError as e:
raise RuntimeError('Cannot write image in directory %s: %s' % (targetdir, e))
@@ -132,6 +146,8 @@ class ComicPage:
self.image = ImageOps.autocontrast(Image.eval(self.image, lambda a: 255 * (a / 255.) ** gamma))
def quantizeImage(self):
self.image = self.image.convert('L') # convert to grayscale
self.image = self.image.convert("RGB") # convert back to RGB
colors = len(self.palette) / 3
if colors < 256:
self.palette += self.palette[:3] * (256 - colors)
@@ -139,46 +155,48 @@ class ComicPage:
palImg.putpalette(self.palette)
self.image = self.image.quantize(palette=palImg)
def resizeImage(self, upscale=False, stretch=False, black_borders=False, isSplit=False, toRight=False):
def resizeImage(self, upscale=False, stretch=False, black_borders=False, isSplit=False, toRight=False,
landscapeMode=False, noPanelViewHQ=False):
method = Image.ANTIALIAS
if black_borders:
fill = 'black'
else:
fill = 'white'
if noPanelViewHQ:
size = (self.size[0], self.size[1])
else:
size = (self.panelviewsize[0], self.panelviewsize[1])
if isSplit and landscapeMode:
upscale = True
if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
if not upscale:
if isSplit and (self.profile == 'K4' or self.profile == 'KHD'):
borderw = (self.size[0] - self.image.size[0])
borderh = (self.size[1] - self.image.size[1]) / 2
self.image = ImageOps.expand(self.image, border=(0, borderh), fill=fill)
tempImg = Image.new(self.image.mode, (self.image.size[0] + borderw, self.image.size[1]), fill)
if toRight:
tempImg.paste(self.image, (borderw, 0))
else:
tempImg.paste(self.image, (0, 0))
self.image = tempImg
else:
borderw = (self.size[0] - self.image.size[0]) / 2
borderh = (self.size[1] - self.image.size[1]) / 2
self.image = ImageOps.expand(self.image, border=(borderw, borderh), fill=fill)
borderw = (self.size[0] - self.image.size[0]) / 2
borderh = (self.size[1] - self.image.size[1]) / 2
self.image = ImageOps.expand(self.image, border=(borderw, borderh), fill=fill)
return self.image
else:
method = Image.NEAREST
method = Image.BILINEAR
if stretch: # if stretching call directly resize() without other considerations.
self.image = self.image.resize(self.size, method)
self.image = self.image.resize(size, method)
return self.image
ratioDev = float(self.size[0]) / float(self.size[1])
if (float(self.image.size[0]) / float(self.image.size[1])) < ratioDev:
diff = int(self.image.size[1] * ratioDev) - self.image.size[0]
if isSplit and (self.profile == 'K4' or self.profile == 'KHD'):
diff = 2
self.image = ImageOps.expand(self.image, border=(diff / 2, 0), fill=fill)
if isSplit and landscapeMode:
diff = int(self.image.size[1] * ratioDev) - self.image.size[0]
self.image = ImageOps.expand(self.image, border=(diff / 2, 0), fill=fill)
tempImg = Image.new(self.image.mode, (self.image.size[0] + diff, self.image.size[1]), fill)
if toRight:
tempImg.paste(self.image, (diff, 0))
else:
tempImg.paste(self.image, (0, 0))
self.image = tempImg
else:
diff = int(self.image.size[1] * ratioDev) - self.image.size[0]
self.image = ImageOps.expand(self.image, border=(diff / 2, 0), fill=fill)
elif (float(self.image.size[0]) / float(self.image.size[1])) > ratioDev:
diff = int(self.image.size[0] / ratioDev) - self.image.size[1]
self.image = ImageOps.expand(self.image, border=(0, diff / 2), fill=fill)
self.image = ImageOps.fit(self.image, self.size, method=method, centering=(0.5, 0.5))
self.image = ImageOps.fit(self.image, size, method=method, centering=(0.5, 0.5))
return self.image
def splitPage(self, targetdir, righttoleft=False, rotate=False):
@@ -189,7 +207,7 @@ class ComicPage:
if (width > height) != (dstwidth > dstheight):
if rotate:
self.image = self.image.rotate(90)
return None
return "R"
else:
if width > height:
# source is landscape, so split by the width
@@ -200,8 +218,8 @@ class ComicPage:
leftbox = (0, 0, width, height / 2)
rightbox = (0, height / 2, width, height)
filename = os.path.splitext(os.path.basename(self.origFileName))
fileone = targetdir + '/' + filename[0] + '-1' + filename[1]
filetwo = targetdir + '/' + filename[0] + '-2' + filename[1]
fileone = targetdir + '/' + filename[0] + '-kcca' + filename[1]
filetwo = targetdir + '/' + filename[0] + '-kccb' + filename[1]
try:
if righttoleft:
pageone = self.image.crop(rightbox)
@@ -218,152 +236,154 @@ class ComicPage:
else:
return None
def frameImage(self):
foreground = tuple(self.palette[:3])
background = tuple(self.palette[-3:])
widthDev, heightDev = self.size
widthImg, heightImg = self.image.size
pastePt = (
max(0, (widthDev - widthImg) / 2),
max(0, (heightDev - heightImg) / 2)
)
corner1 = (
pastePt[0] - 1,
pastePt[1] - 1
)
corner2 = (
pastePt[0] + widthImg + 1,
pastePt[1] + heightImg + 1
)
imageBg = Image.new(self.image.mode, self.size, background)
imageBg.paste(self.image, pastePt)
draw = ImageDraw.Draw(imageBg)
draw.rectangle([corner1, corner2], outline=foreground)
self.image = imageBg
def cutPageNumber(self):
widthImg, heightImg = self.image.size
delta = 2
diff = delta
fixedThreshold = 5
if ImageStat.Stat(self.image).var[0] < 2 * fixedThreshold:
return self.image
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] < fixedThreshold\
and diff < heightImg:
diff += delta
diff -= delta
pageNumberCut1 = diff
if diff < delta:
if ImageChops.invert(self.image).getbbox() is not None:
widthImg, heightImg = self.image.size
delta = 2
diff = delta
oldStat = ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0]
diff += delta
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] - oldStat > 0\
and diff < heightImg / 4:
fixedThreshold = 5
if ImageStat.Stat(self.image).var[0] < 2 * fixedThreshold:
return self.image
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] < fixedThreshold\
and diff < heightImg:
diff += delta
diff -= delta
pageNumberCut1 = diff
if diff < delta:
diff = delta
oldStat = ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0]
diff += delta
diff -= delta
pageNumberCut2 = diff
diff += delta
oldStat = ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg - pageNumberCut2))).var[0]
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg - pageNumberCut2))).var[0]\
< fixedThreshold + oldStat and diff < heightImg / 4:
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] - oldStat > 0\
and diff < heightImg / 4:
oldStat = ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0]
diff += delta
diff -= delta
pageNumberCut2 = diff
diff += delta
diff -= delta
pageNumberCut3 = diff
delta = 5
diff = delta
while ImageStat.Stat(self.image.crop((0, heightImg - pageNumberCut2, diff, heightImg))).var[0] < fixedThreshold\
and diff < widthImg:
diff += delta
diff -= delta
pageNumberX1 = diff
diff = delta
while ImageStat.Stat(self.image.crop((widthImg - diff, heightImg - pageNumberCut2,
widthImg, heightImg))).var[0] < fixedThreshold and diff < widthImg:
diff += delta
diff -= delta
pageNumberX2 = widthImg - diff
if pageNumberCut3 - pageNumberCut1 > 2 * delta\
and float(pageNumberX2 - pageNumberX1) / float(pageNumberCut2 - pageNumberCut1) <= 9.0\
and ImageStat.Stat(self.image.crop((0, heightImg - pageNumberCut3, widthImg, heightImg))).var[0]\
/ ImageStat.Stat(self.image).var[0] < 0.1\
and pageNumberCut3 < heightImg / 4 - delta:
diff = pageNumberCut3
else:
diff = pageNumberCut1
self.image = self.image.crop((0, 0, widthImg, heightImg - diff))
oldStat = ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg - pageNumberCut2))).var[0]
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg - pageNumberCut2))).var[0]\
< fixedThreshold + oldStat and diff < heightImg / 4:
diff += delta
diff -= delta
pageNumberCut3 = diff
delta = 5
diff = delta
while ImageStat.Stat(self.image.crop((0, heightImg - pageNumberCut2, diff, heightImg))).var[0] < fixedThreshold\
and diff < widthImg:
diff += delta
diff -= delta
pageNumberX1 = diff
diff = delta
while ImageStat.Stat(self.image.crop((widthImg - diff, heightImg - pageNumberCut2,
widthImg, heightImg))).var[0] < fixedThreshold and diff < widthImg:
diff += delta
diff -= delta
pageNumberX2 = widthImg - diff
if pageNumberCut3 - pageNumberCut1 > 2 * delta\
and float(pageNumberX2 - pageNumberX1) / float(pageNumberCut2 - pageNumberCut1) <= 9.0\
and ImageStat.Stat(self.image.crop((0, heightImg - pageNumberCut3, widthImg, heightImg))).var[0]\
/ ImageStat.Stat(self.image).var[0] < 0.1\
and pageNumberCut3 < heightImg / 4 - delta:
diff = pageNumberCut3
else:
diff = pageNumberCut1
self.image = self.image.crop((0, 0, widthImg, heightImg - diff))
return self.image
def cropWhiteSpace(self, threshold):
widthImg, heightImg = self.image.size
delta = 10
diff = delta
# top
while ImageStat.Stat(self.image.crop((0, 0, widthImg, diff))).var[0] < threshold and diff < heightImg:
diff += delta
diff -= delta
# print "Top crop: %s"%diff
self.image = self.image.crop((0, diff, widthImg, heightImg))
widthImg, heightImg = self.image.size
diff = delta
# left
while ImageStat.Stat(self.image.crop((0, 0, diff, heightImg))).var[0] < threshold and diff < widthImg:
diff += delta
diff -= delta
# print "Left crop: %s"%diff
self.image = self.image.crop((diff, 0, widthImg, heightImg))
widthImg, heightImg = self.image.size
diff = delta
# down
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] < threshold\
and diff < heightImg:
diff += delta
diff -= delta
# print "Down crop: %s"%diff
self.image = self.image.crop((0, 0, widthImg, heightImg - diff))
widthImg, heightImg = self.image.size
diff = delta
# right
while ImageStat.Stat(self.image.crop((widthImg - diff, 0, widthImg, heightImg))).var[0] < threshold\
and diff < widthImg:
diff += delta
diff -= delta
# print "Right crop: %s"%diff
self.image = self.image.crop((0, 0, widthImg - diff, heightImg))
# print "New size: %sx%s"%(self.image.size[0],self.image.size[1])
if ImageChops.invert(self.image).getbbox() is not None:
widthImg, heightImg = self.image.size
delta = 10
diff = delta
# top
while ImageStat.Stat(self.image.crop((0, 0, widthImg, diff))).var[0] < threshold and diff < heightImg:
diff += delta
diff -= delta
# print "Top crop: %s"%diff
self.image = self.image.crop((0, diff, widthImg, heightImg))
widthImg, heightImg = self.image.size
diff = delta
# left
while ImageStat.Stat(self.image.crop((0, 0, diff, heightImg))).var[0] < threshold and diff < widthImg:
diff += delta
diff -= delta
# print "Left crop: %s"%diff
self.image = self.image.crop((diff, 0, widthImg, heightImg))
widthImg, heightImg = self.image.size
diff = delta
# down
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] < threshold\
and diff < heightImg:
diff += delta
diff -= delta
# print "Down crop: %s"%diff
self.image = self.image.crop((0, 0, widthImg, heightImg - diff))
widthImg, heightImg = self.image.size
diff = delta
# right
while ImageStat.Stat(self.image.crop((widthImg - diff, 0, widthImg, heightImg))).var[0] < threshold\
and diff < widthImg:
diff += delta
diff -= delta
# print "Right crop: %s"%diff
self.image = self.image.crop((0, 0, widthImg - diff, heightImg))
# print "New size: %sx%s"%(self.image.size[0],self.image.size[1])
return self.image
def addProgressbar(self, file_number, files_totalnumber, size, howoften):
if file_number // howoften != float(file_number) / howoften:
return self.image
white = (255, 255, 255)
black = (0, 0, 0)
widthDev, heightDev = size
widthImg, heightImg = self.image.size
pastePt = (
max(0, (widthDev - widthImg) / 2),
max(0, (heightDev - heightImg) / 2)
)
imageBg = Image.new('RGB', size, white)
imageBg.paste(self.image, pastePt)
self.image = imageBg
widthImg, heightImg = self.image.size
draw = ImageDraw.Draw(self.image)
#Black rectangle
draw.rectangle([(0, heightImg - 3), (widthImg, heightImg)], outline=black, fill=black)
#White rectangle
draw.rectangle([(widthImg * file_number / files_totalnumber, heightImg - 3), (widthImg - 1, heightImg)],
outline=black, fill=white)
#Making notches
for i in range(1, 10):
if i <= (10 * file_number / files_totalnumber):
notch_colour = white # White
else:
notch_colour = black # Black
draw.line([(widthImg * float(i) / 10, heightImg - 3), (widthImg * float(i) / 10, heightImg)],
fill=notch_colour)
#The 50%
if i == 5:
draw.rectangle([(widthImg / 2 - 1, heightImg - 5), (widthImg / 2 + 1, heightImg)],
outline=black, fill=notch_colour)
return self.image
# def addProgressbar(self, file_number, files_totalnumber, size, howoften):
# if file_number // howoften != float(file_number) / howoften:
# return self.image
# white = (255, 255, 255)
# black = (0, 0, 0)
# widthDev, heightDev = size
# widthImg, heightImg = self.image.size
# pastePt = (
# max(0, (widthDev - widthImg) / 2),
# max(0, (heightDev - heightImg) / 2)
# )
# imageBg = Image.new('RGB', size, white)
# imageBg.paste(self.image, pastePt)
# self.image = imageBg
# widthImg, heightImg = self.image.size
# draw = ImageDraw.Draw(self.image)
# #Black rectangle
# draw.rectangle([(0, heightImg - 3), (widthImg, heightImg)], outline=black, fill=black)
# #White rectangle
# draw.rectangle([(widthImg * file_number / files_totalnumber, heightImg - 3), (widthImg - 1, heightImg)],
# outline=black, fill=white)
# #Making notches
# for i in range(1, 10):
# if i <= (10 * file_number / files_totalnumber):
# notch_colour = white # White
# else:
# notch_colour = black # Black
# draw.line([(widthImg * float(i) / 10, heightImg - 3), (widthImg * float(i) / 10, heightImg)],
# fill=notch_colour)
# #The 50%
# if i == 5:
# draw.rectangle([(widthImg / 2 - 1, heightImg - 5), (widthImg / 2 + 1, heightImg)],
# outline=black, fill=notch_colour)
# return self.image
#
# def frameImage(self):
# foreground = tuple(self.palette[:3])
# background = tuple(self.palette[-3:])
# widthDev, heightDev = self.size
# widthImg, heightImg = self.image.size
# pastePt = (
# max(0, (widthDev - widthImg) / 2),
# max(0, (heightDev - heightImg) / 2)
# )
# corner1 = (
# pastePt[0] - 1,
# pastePt[1] - 1
# )
# corner2 = (
# pastePt[0] + widthImg + 1,
# pastePt[1] + heightImg + 1
# )
# imageBg = Image.new(self.image.mode, self.size, background)
# imageBg.paste(self.image, pastePt)
# draw = ImageDraw.Draw(imageBg)
# draw.rectangle([corner1, corner2], outline=foreground)
# self.image = imageBg

View File

@@ -1,6 +1,6 @@
# rarfile.py
#
# Copyright (c) 2005-2012 Marko Kreen <markokr@gmail.com>
# Copyright (c) 2005-2013 Marko Kreen <markokr@gmail.com>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
@@ -17,7 +17,7 @@
r"""RAR archive reader.
This is Python module for Rar archive reading. The interface
is made as zipfile like as possible.
is made as :mod:`zipfile`-like as possible.
Basic logic:
- Parse archive structure with Python.
@@ -34,7 +34,17 @@ Example::
for f in rf.infolist():
print f.filename, f.file_size
if f.filename == 'README':
print rf.read(f)
print(rf.read(f))
Archive files can also be accessed via file-like object returned
by :meth:`RarFile.open`::
import rarfile
with rarfile.RarFile('archive.rar') as rf:
with rf.open('README') as f:
for ln in f:
print(ln.strip())
There are few module-level parameters to tune behaviour,
here they are with defaults, and reason to change it::
@@ -64,7 +74,7 @@ For more details, refer to source.
"""
__version__ = '2.5'
__version__ = '2.6'
# export only interesting items
__all__ = ['is_rarfile', 'RarInfo', 'RarFile', 'RarExtFile']
@@ -73,7 +83,7 @@ __all__ = ['is_rarfile', 'RarInfo', 'RarFile', 'RarExtFile']
## Imports and compat - support both Python 2.x and 3.x
##
import sys, os, struct
import sys, os, struct, errno
from struct import pack, unpack
from binascii import crc32
from tempfile import mkstemp
@@ -148,45 +158,45 @@ except ImportError:
## Module configuration. Can be tuned after importing.
##
# default fallback charset
#: default fallback charset
DEFAULT_CHARSET = "windows-1252"
# list of encodings to try, with fallback to DEFAULT_CHARSET if none succeed
#: list of encodings to try, with fallback to DEFAULT_CHARSET if none succeed
TRY_ENCODINGS = ('utf8', 'utf-16le')
# 'unrar', 'rar' or full path to either one
#: 'unrar', 'rar' or full path to either one
UNRAR_TOOL = "unrar"
# Command line args to use for opening file for reading.
#: Command line args to use for opening file for reading.
OPEN_ARGS = ('p', '-inul')
# Command line args to use for extracting file to disk.
#: Command line args to use for extracting file to disk.
EXTRACT_ARGS = ('x', '-y', '-idq')
# args for testrar()
#: args for testrar()
TEST_ARGS = ('t', '-idq')
# whether to speed up decompression by using tmp archive
#: whether to speed up decompression by using tmp archive
USE_EXTRACT_HACK = 1
# limit the filesize for tmp archive usage
#: limit the filesize for tmp archive usage
HACK_SIZE_LIMIT = 20*1024*1024
# whether to parse file/archive comments.
#: whether to parse file/archive comments.
NEED_COMMENTS = 1
# whether to convert comments to unicode strings
#: whether to convert comments to unicode strings
UNICODE_COMMENTS = 0
# When RAR is corrupt, stopping on bad header is better
# On unknown/misparsed RAR headers reporting is better
#: When RAR is corrupt, stopping on bad header is better
#: On unknown/misparsed RAR headers reporting is better
REPORT_BAD_HEADER = 0
# Convert RAR time tuple into datetime() object
#: Convert RAR time tuple into datetime() object
USE_DATETIME = 0
# Separator for path name components. RAR internally uses '\\'.
# Use '/' to be similar with zipfile.
#: Separator for path name components. RAR internally uses '\\'.
#: Use '/' to be similar with zipfile.
PATH_SEP = '\\'
##
@@ -320,6 +330,8 @@ class RarMemoryError(RarExecError):
"""Memory error"""
class RarCreateError(RarExecError):
"""Create error"""
class RarNoFilesError(RarExecError):
"""No files that match pattern were found"""
class RarUserBreak(RarExecError):
"""User stop"""
class RarUnknownError(RarExecError):
@@ -335,49 +347,57 @@ def is_rarfile(fn):
class RarInfo(object):
'''An entry in rar archive.
@ivar filename:
File name with relative path.
Default path separator is '/', to change set rarfile.PATH_SEP.
Always unicode string.
@ivar date_time:
Modification time, tuple of (year, month, day, hour, minute, second).
Or datetime() object if USE_DATETIME is set.
@ivar file_size:
Uncompressed size.
@ivar compress_size:
Compressed size.
@ivar compress_type:
Compression method: 0x30 - 0x35.
@ivar extract_version:
Minimal Rar version needed for decompressing.
@ivar host_os:
Host OS type, one of RAR_OS_* constants.
@ivar mode:
File attributes. May be either dos-style or unix-style, depending on host_os.
@ivar CRC:
CRC-32 of uncompressed file, unsigned int.
@ivar volume:
Volume nr, starting from 0.
@ivar volume_file:
Volume file name, where file starts.
@ivar type:
One of RAR_BLOCK_* types. Only entries with type==RAR_BLOCK_FILE are shown in .infolist().
@ivar flags:
For files, RAR_FILE_* bits.
@ivar comment:
File comment (unicode string or None).
r'''An entry in rar archive.
@ivar mtime:
Optional time field: Modification time, with float seconds.
Same as .date_time but with more precision.
@ivar ctime:
Optional time field: creation time, with float seconds.
@ivar atime:
Optional time field: last access time, with float seconds.
@ivar arctime:
Optional time field: archival time, with float seconds.
:mod:`zipfile`-compatible fields:
filename
File name with relative path.
Default path separator is '\\', to change set rarfile.PATH_SEP.
Always unicode string.
date_time
Modification time, tuple of (year, month, day, hour, minute, second).
Or datetime() object if USE_DATETIME is set.
file_size
Uncompressed size.
compress_size
Compressed size.
CRC
CRC-32 of uncompressed file, unsigned int.
comment
File comment. Byte string or None. Use UNICODE_COMMENTS
to get automatic decoding to unicode.
volume
Volume nr, starting from 0.
RAR-specific fields:
compress_type
Compression method: 0x30 - 0x35.
extract_version
Minimal Rar version needed for decompressing.
host_os
Host OS type, one of RAR_OS_* constants.
mode
File attributes. May be either dos-style or unix-style, depending on host_os.
volume_file
Volume file name, where file starts.
mtime
Optional time field: Modification time, with float seconds.
Same as .date_time but with more precision.
ctime
Optional time field: creation time, with float seconds.
atime
Optional time field: last access time, with float seconds.
arctime
Optional time field: archival time, with float seconds.
Internal fields:
type
One of RAR_BLOCK_* types. Only entries with type==RAR_BLOCK_FILE are shown in .infolist().
flags
For files, RAR_FILE_* bits.
'''
__slots__ = (
@@ -417,7 +437,7 @@ class RarInfo(object):
'header_offset',
'salt',
'volume_file',
)
)
def isdir(self):
'''Returns True if the entry is a directory.'''
@@ -431,19 +451,27 @@ class RarInfo(object):
class RarFile(object):
'''Parse RAR structure, provide access to files in archive.
@ivar comment:
Archive comment (unicode string or None).
'''
#: Archive comment. Byte string or None. Use UNICODE_COMMENTS
#: to get automatic decoding to unicode.
comment = None
def __init__(self, rarfile, mode="r", charset=None, info_callback=None, crc_check = True):
"""Open and parse a RAR archive.
@param rarfile: archive file name
@param mode: only 'r' is supported.
@param charset: fallback charset to use, if filenames are not already Unicode-enabled.
@param info_callback: debug callback, gets to see all archive entries.
@param crc_check: set to False to disable CRC checks
Parameters:
rarfile
archive file name
mode
only 'r' is supported.
charset
fallback charset to use, if filenames are not already Unicode-enabled.
info_callback
debug callback, gets to see all archive entries.
crc_check
set to False to disable CRC checks
"""
self.rarfile = rarfile
self.comment = None
@@ -455,6 +483,7 @@ class RarFile(object):
self._needs_password = False
self._password = None
self._crc_check = crc_check
self._vol_list = []
self._main = None
@@ -487,6 +516,14 @@ class RarFile(object):
'''Return RarInfo objects for all files/directories in archive.'''
return self._info_list
def volumelist(self):
'''Returns filenames of archive volumes.
In case of single-volume archive, the list contains
just the name of main archive file.
'''
return self._vol_list
def getinfo(self, fname):
'''Return RarInfo for file.'''
@@ -508,7 +545,8 @@ class RarFile(object):
raise NoRarEntry("No such file: "+fname)
def open(self, fname, mode = 'r', psw = None):
'''Return open file object, where the data can be read.
'''Returns file-like object (:class:`RarExtFile`),
from where the data can be read.
The object implements io.RawIOBase interface, so it can
be further wrapped with io.BufferedReader and io.TextIOWrapper.
@@ -520,9 +558,14 @@ class RarFile(object):
uncompressed files, on compressed files the seeking is implemented
by reading ahead and/or restarting the decompression.
@param fname: file name or RarInfo instance.
@param mode: must be 'r'
@param psw: password to use for extracting.
Parameters:
fname
file name or RarInfo instance.
mode
must be 'r'
psw
password to use for extracting.
'''
if mode != 'r':
@@ -569,8 +612,12 @@ class RarFile(object):
For longer files using .open() may be better idea.
@param fname: filename or RarInfo instance
@param psw: password to use for extracting.
Parameters:
fname
filename or RarInfo instance
psw
password to use for extracting.
"""
f = self.open(fname, 'r', psw)
@@ -591,9 +638,14 @@ class RarFile(object):
def extract(self, member, path=None, pwd=None):
"""Extract single file into current directory.
@param member: filename or RarInfo instance
@param path: optional destination path
@param pwd: optional password to use
Parameters:
member
filename or RarInfo instance
path
optional destination path
pwd
optional password to use
"""
if isinstance(member, RarInfo):
fname = member.filename
@@ -604,9 +656,14 @@ class RarFile(object):
def extractall(self, path=None, members=None, pwd=None):
"""Extract all files into current directory.
@param path: optional destination path
@param members: optional filename or RarInfo instance list to extract
@param pwd: optional password to use
Parameters:
path
optional destination path
members
optional filename or RarInfo instance list to extract
pwd
optional password to use
"""
fnlist = []
if members is not None:
@@ -691,6 +748,7 @@ class RarFile(object):
more_vols = 0
endarc = 0
volfile = self.rarfile
self._vol_list = [self.rarfile]
while 1:
if endarc:
h = None # don't read past ENDARC
@@ -705,6 +763,7 @@ class RarFile(object):
self._fd = fd
more_vols = 0
endarc = 0
self._vol_list.append(volfile)
continue
break
h.volume = volume
@@ -729,7 +788,7 @@ class RarFile(object):
# RAR 2.x does not write RAR_BLOCK_ENDARC
if h.flags & RAR_FILE_SPLIT_AFTER:
more_vols = 1
# RAR 2.x does not set RAR_MAIN_FIRSTVOLUME
# RAR 2.x does not set RAR_MAIN_FIRSTVOLUME
if volume == 0 and h.flags & RAR_FILE_SPLIT_BEFORE:
raise NeedFirstVolume("Need to start from first volume")
@@ -950,7 +1009,7 @@ class RarFile(object):
pos += S_COMMENT_HDR.size
data = hdata[pos : pos_next]
cmt = rar_decompress(ver, meth, data, declen, sflags,
crc, self._password)
crc, self._password)
if not self._crc_check:
h.comment = self._decode_comment(cmt)
elif crc32(cmt) & 0xFFFF == crc:
@@ -1073,7 +1132,7 @@ class RarFile(object):
# decompress
cmt = rar_decompress(inf.extract_version, inf.compress_type, data,
inf.file_size, inf.flags, inf.CRC, psw, inf.salt)
inf.file_size, inf.flags, inf.CRC, psw, inf.salt)
# check crc
if self._crc_check:
@@ -1208,7 +1267,7 @@ class UnicodeFilename:
class RarExtFile(RawIOBase):
"""Base class for 'file-like' object that RarFile.open() returns.
"""Base class for file-like object that :meth:`RarFile.open` returns.
Provides public methods and common crc checking.
@@ -1216,13 +1275,15 @@ class RarExtFile(RawIOBase):
- no short reads - .read() and .readinfo() read as much as requested.
- no internal buffer, use io.BufferedReader for that.
@ivar name:
filename of the archive entry.
If :mod:`io` module is available (Python 2.6+, 3.x), then this calls
will inherit from :class:`io.RawIOBase` class. This makes line-based
access available: :meth:`RarExtFile.readline` and ``for ln in f``.
"""
def __init__(self, rf, inf):
"""Fill common fields"""
#: Filename of the archive entry
name = None
def __init__(self, rf, inf):
RawIOBase.__init__(self)
# standard io.* properties
@@ -1323,7 +1384,13 @@ class RarExtFile(RawIOBase):
return self.inf.file_size - self.remain
def seek(self, ofs, whence = 0):
"""Seek in data."""
"""Seek in data.
On uncompressed files, the seeking works by actual
seeks so it's fast. On compresses files its slow
- forward seeking happends by reading ahead,
backwards by re-opening and decompressing from the start.
"""
# disable crc check when seeking
self.crc_check = 0
@@ -1372,8 +1439,17 @@ class RarExtFile(RawIOBase):
"""Returns True"""
return True
def writable(self):
"""Returns False.
Writing is not supported."""
return False
def seekable(self):
"""Returns True"""
"""Returns True.
Seeking is supported, although it's slow on compressed files.
"""
return True
def readall(self):
@@ -1668,7 +1744,7 @@ def rar_decompress(vers, meth, data, declen=0, flags=0, crc=0, psw=None, salt=No
date = 0
mode = 0x20
fhdr = S_FILE_HDR.pack(len(data), declen, RAR_OS_MSDOS, crc,
date, vers, meth, len(fname), mode)
date, vers, meth, len(fname), mode)
fhdr += fname
if flags & RAR_FILE_SALT:
if not salt:
@@ -1757,8 +1833,15 @@ def custom_popen(cmd):
creationflags = 0x08000000 # CREATE_NO_WINDOW
# run command
p = Popen(cmd, bufsize = 0, stdout = PIPE, stdin = PIPE, stderr = STDOUT,
creationflags = creationflags)
try:
p = Popen(cmd, bufsize = 0,
stdout = PIPE, stdin = PIPE, stderr = STDOUT,
creationflags = creationflags)
except OSError:
ex = sys.exc_info()[1]
if ex.errno == errno.ENOENT:
raise RarExecError("Unrar not installed? (rarfile.UNRAR_TOOL=%r)" % UNRAR_TOOL)
raise
return p
def check_returncode(p, out):
@@ -1770,9 +1853,9 @@ def check_returncode(p, out):
# map return code to exception class
errmap = [None,
RarWarning, RarFatalError, RarCRCError, RarLockedArchiveError,
RarWriteError, RarOpenError, RarUserError, RarMemoryError,
RarCreateError] # codes from rar.txt
RarWarning, RarFatalError, RarCRCError, RarLockedArchiveError,
RarWriteError, RarOpenError, RarUserError, RarMemoryError,
RarCreateError, RarNoFilesError] # codes from rar.txt
if code > 0 and code < len(errmap):
exc = errmap[code]
elif code == 255:

View File

@@ -15,7 +15,7 @@ use_setuptools()
import sys
NAME = "KindleComicConverter"
VERSION = "2.7"
VERSION = "2.10"
MAIN = "kcc.py"
includefiles = ['README.md', 'MANIFEST.in', 'LICENSE.txt', 'comic2ebook.ico', 'comic2ebook.icns']

View File

@@ -10,7 +10,7 @@ sys.path.insert(0, 'kcc')
setup(
name = "KindleComicConverter",
version = "2.7",
version = "2.10",
author = "Ciro Mattia Gonano",
author_email = "ciromattia@gmail.com",
description = "A tool to convert comics (CBR/CBZ/PDFs/image folders) to MOBI.",