mirror of
https://github.com/ciromattia/kcc
synced 2026-04-18 06:58:58 +00:00
Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b068d82ccf | ||
|
|
43ca5ac5b9 | ||
|
|
18c3ab2340 | ||
|
|
87eaba184e | ||
|
|
97b5d8a2ee | ||
|
|
7c3a762107 | ||
|
|
0b90af77da | ||
|
|
28dcab8ee8 | ||
|
|
6c468a5291 | ||
|
|
d090d8c2e8 | ||
|
|
aba315866e | ||
|
|
e981aa4520 | ||
|
|
e603622021 | ||
|
|
3e007965b2 | ||
|
|
c0610360a3 | ||
|
|
ff8f6e073f | ||
|
|
aadb5407d2 | ||
|
|
2e4d5eb958 | ||
|
|
33fb13a66e | ||
|
|
4dc69aa1c4 | ||
|
|
b3681a3ceb | ||
|
|
348dcc4275 | ||
|
|
39e69119ac | ||
|
|
8c57926978 | ||
|
|
8b1965054f | ||
|
|
18993069e3 | ||
|
|
7e6c8cc768 | ||
|
|
3ae44d2fcb | ||
|
|
a6eb3936e4 | ||
|
|
bb24d3ca30 | ||
|
|
7e191c0be5 | ||
|
|
d9ea165bbb | ||
|
|
148211a5c7 | ||
|
|
23e07f47f0 | ||
|
|
724156c554 | ||
|
|
b972e4c746 | ||
|
|
f0afa1fff2 | ||
|
|
a36c05f0c5 | ||
|
|
4f3a66b4eb | ||
|
|
6369c7ea44 | ||
|
|
f1b8aff8d4 | ||
|
|
be270aa797 | ||
|
|
f33d355024 | ||
|
|
6f913b026e | ||
|
|
220b4e0954 | ||
|
|
bac4a4fd86 | ||
|
|
d923299230 | ||
|
|
a79ebcec86 | ||
|
|
69bbee7648 | ||
|
|
57d7729d05 | ||
|
|
25a1dcb72c | ||
|
|
cff9b73b80 | ||
|
|
23336de5b5 | ||
|
|
d346ca0466 | ||
|
|
22fadf078e | ||
|
|
74acf85683 | ||
|
|
713de03d1d | ||
|
|
5817f52110 | ||
|
|
dfc03aee38 | ||
|
|
6bc8038068 | ||
|
|
eb7d56c1b9 | ||
|
|
f5b515ef79 | ||
|
|
943431346d | ||
|
|
a597173b71 | ||
|
|
c484cc8fff | ||
|
|
f195a4ccbf | ||
|
|
f55bb6dce6 | ||
|
|
e7f49f8330 | ||
|
|
cec9356fc2 | ||
|
|
6d704b382d | ||
|
|
1c5f67111f | ||
|
|
660df4b370 | ||
|
|
19dc5cc6f2 | ||
|
|
67baf90d04 | ||
|
|
07bf41ea6c | ||
|
|
c2cfc9b8db | ||
|
|
7a7a2bc10b | ||
|
|
e3127ed516 | ||
|
|
93a98db43e | ||
|
|
750b55649a | ||
|
|
ca5854a8bd | ||
|
|
712f728a5e | ||
|
|
eb7766a5e5 | ||
|
|
0cabbfde96 | ||
|
|
c5983276e5 |
161
README.md
161
README.md
@@ -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.
|
`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**_.
|
actually a comic 2 epub converter that every ereader owner can happily use**_.
|
||||||
|
|
||||||
It also optimizes comic images by:
|
It can also optionally optimize images by applying a number of transformations.
|
||||||
- Enhancing contrast.
|
|
||||||
- Cutting page numbering.
|
### A word of warning
|
||||||
- Cropping white borders.
|
**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.
|
||||||
- Resizing larger images to device's native resolution.
|
Amazon's tool is for comic _publishers_ and involves a lot of manual effort, while **KCC** is for comic _readers_.
|
||||||
- Quantizing images to device's palette.
|
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
|
## BINARY RELEASES
|
||||||
You can find the latest released binary at the following links:
|
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)
|
- 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.7.zip](https://dl.dropbox.com/u/16806101/KindleComicConverter_win-amd64_2.7.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.7.zip](http://pawelj.vulturis.eu/Shared/KindleComicConverter_win-x86_2.7.zip) *(thanks to [AcidWeb](https://github.com/AcidWeb))*
|
- 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)*
|
- Linux: Just download sourcecode and launch `python kcc.py` *(Provided you have Python and Pillow installed)*
|
||||||
|
|
||||||
|
## AWKCC .NET GUI
|
||||||
|

|
||||||
|
|
||||||
|
[All-in-one package for Windows users](http://www.mobileread.com/forums/showpost.php?p=2444957&postcount=3)
|
||||||
|
|
||||||
## 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:
|
||||||
- PNG, JPG
|
- PNG, JPG, GIF, TIFF, BMP
|
||||||
- Folders
|
- Folders
|
||||||
- CBZ, ZIP
|
- CBZ, ZIP
|
||||||
- CBR, RAR *(With `unrar` executable)*
|
- CBR, RAR *(With `unrar` executable)*
|
||||||
@@ -52,12 +58,14 @@ Options:
|
|||||||
--version show program's version number and exit
|
--version show program's version number and exit
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-p PROFILE, --profile=PROFILE
|
-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
|
-t TITLE, --title=TITLE
|
||||||
Comic title [Default=filename]
|
Comic title [Default=filename]
|
||||||
-m, --manga-style Manga style (Right-to-left reading and splitting) [Default=False]
|
-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]
|
--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]
|
--gamma=GAMMA Apply gamma correction to linearize the image [Default=Auto]
|
||||||
--upscale Resize images smaller than device's resolution [Default=False]
|
--upscale Resize images smaller than device's resolution [Default=False]
|
||||||
--stretch Stretch images to 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]
|
--nosplitrotate Disable splitting and rotation [Default=False]
|
||||||
--nocutpagenumbers Do not try to cut page numbering on images [Default=True]
|
--nocutpagenumbers Do not try to cut page numbering on images [Default=True]
|
||||||
-o OUTPUT, --output=OUTPUT
|
-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]
|
-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
|
- `magic.py` from [python-magic](https://github.com/ahupp/python-magic) library
|
||||||
|
|
||||||
## CHANGELOG
|
## CHANGELOG
|
||||||
- 1.00: Initial version
|
####1.00
|
||||||
- 1.10: Added support for CBZ/CBR files in comic2ebook.py
|
* Initial version
|
||||||
- 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
|
####1.10
|
||||||
with landscape target), add palette and other image optimizations from Mangle.
|
* Added support for CBZ/CBR files in comic2ebook.py
|
||||||
WARNING: PIL is required for all image mangling!
|
|
||||||
- 1.30: Fixed an issue in OPF generation for device resolution
|
####1.11
|
||||||
Reworked options system (call with -h option to get the inline help)
|
* Added support for CBZ/CBR files in KindleComicConverter
|
||||||
- 1.40: Added some options for controlling image optimization
|
|
||||||
Further optimization (ImageOps, page numbering cut, autocontrast)
|
####1.20
|
||||||
- 1.41: Fixed a serious bug on resizing when img ratio was bigger than device one
|
* 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.50: Added subfolder support for multiple chapters.
|
|
||||||
- 2.0: GUI! AppleScript is gone and Tk is used to provide cross-platform GUI support.
|
####1.30
|
||||||
- 2.1: Added basic error reporting
|
* Fixed an issue in OPF generation for device resolution
|
||||||
- 2.2: Added (valid!) ePub 2.0 output
|
* Reworked options system (call with -h option to get the inline help)
|
||||||
Rename .zip files to .cbz to avoid overwriting
|
|
||||||
- 2.3: Fixed win32 ePub generation, folder handling, filenames with spaces and subfolders
|
####1.40
|
||||||
- 2.4: Use temporary directory as workdir (fixes converting from external volumes and zipfiles renaming)
|
* Added some options for controlling image optimization
|
||||||
Fixed "add folders" from GUI.
|
* Further optimization (ImageOps, page numbering cut, autocontrast)
|
||||||
- 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)
|
####1.41
|
||||||
- 2.6: Added --rotate option to rotate landscape images instead of splitting them (#16, #24)
|
* Fixed a serious bug on resizing when img ratio was bigger than device one
|
||||||
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)
|
####1.50
|
||||||
Fixed natural sorting for files (#18)
|
* Added subfolder support for multiple chapters.
|
||||||
- 2.7: Lots of GUI improvements (#27, #13)
|
|
||||||
Added gamma support within --gamma option (defaults to profile-specified gamma) (#26, #27)
|
####2.0
|
||||||
Added --nodithering option to prevent dithering optimizations (#27)
|
* GUI! AppleScript is gone and Tk is used to provide cross-platform GUI support.
|
||||||
Epub margins support (#30)
|
|
||||||
Fixed no file added if file has no spaces on Windows (#25)
|
####2.1
|
||||||
Gracefully exit if unrar missing (#15)
|
* Added basic error reporting
|
||||||
Do not call kindlegen if source epub is bigger than 320MB (#17)
|
|
||||||
Get filetype from magic number (#14)
|
#### 2.2:
|
||||||
PDF conversion works again
|
* Added (valid!) ePub 2.0 output
|
||||||
|
* Rename .zip files to .cbz to avoid overwriting
|
||||||
|
|
||||||
|
####2.3
|
||||||
|
* Fixed win32 ePub generation, folder handling, filenames with spaces and subfolders
|
||||||
|
|
||||||
|
####2.4
|
||||||
|
* Use temporary directory as workdir (fixes converting from external volumes and zipfiles renaming)
|
||||||
|
* Fixed "add folders" from GUI.
|
||||||
|
|
||||||
|
####2.5
|
||||||
|
* Added --black-borders option to set added borders black when page's ratio is not the device's one (#11).
|
||||||
|
* Fixes epub containing zipped itself (#10)
|
||||||
|
|
||||||
|
####2.6
|
||||||
|
* Added --rotate option to rotate landscape images instead of splitting them (#16, #24)
|
||||||
|
* Added --output option to customize ePub output dir/file (#22)
|
||||||
|
* Add rendition:layout and rendition:orientation ePub meta tags (supported by new kindlegen 2.8)
|
||||||
|
* Fixed natural sorting for files (#18)
|
||||||
|
|
||||||
|
####2.7
|
||||||
|
* Lots of GUI improvements (#27, #13)
|
||||||
|
* Added gamma support within --gamma option (defaults to profile-specified gamma) (#26, #27)
|
||||||
|
* Added --nodithering option to prevent dithering optimizations (#27)
|
||||||
|
* Epub margins support (#30)
|
||||||
|
* Fixed no file added if file has no spaces on Windows (#25)
|
||||||
|
* Gracefully exit if unrar missing (#15)
|
||||||
|
* Do not call kindlegen if source epub is bigger than 320MB (#17)
|
||||||
|
* Get filetype from magic number (#14)
|
||||||
|
* PDF conversion works again
|
||||||
|
|
||||||
|
####2.8
|
||||||
|
* Updated rarfile library
|
||||||
|
* Panel View support + HQ support (#36) - new option: --nopanelviewhq
|
||||||
|
* Split profiles for K4NT and K4T
|
||||||
|
* Rewrite of Landscape Mode support (huge readability improvement for KPW)
|
||||||
|
* Upscale use now BILINEAR method
|
||||||
|
* Added generic CSS file
|
||||||
|
* Optimized archive extraction for zip/rar files (#40)
|
||||||
|
|
||||||
|
####2.9
|
||||||
|
* Added support for generating a plain CBZ (skipping all the EPUB/Mobi generation) (#45)
|
||||||
|
* Prevent output file overwriting the source one: if a duplicate name is detected, append _kcc to the name
|
||||||
|
* Rarfile library updated to 2.6
|
||||||
|
* Added GIF, TIFF and BMP to supported formats (#42)
|
||||||
|
* Filenames slugifications (#28, #31, #9, #8)
|
||||||
|
|
||||||
|
####2.10:
|
||||||
|
* Multiprocessing support
|
||||||
|
* Kindle Fire support (color ePub/Mobi)
|
||||||
|
* Panel View support for horizontal content
|
||||||
|
* Fixed panel order for horizontal pages when --rotate is enabled
|
||||||
|
* Disabled cropping and page number cutting for blank pages
|
||||||
|
* Fixed some slugify issues with specific file naming conventions (#50, #51)
|
||||||
|
|
||||||
## COPYRIGHT
|
## COPYRIGHT
|
||||||
|
|
||||||
Copyright (c) 2012-2013 Ciro Mattia Gonano with further contributions by Paweł Jastrzębski.
|
Copyright (c) 2012-2013 Ciro Mattia Gonano and Paweł Jastrzębski.
|
||||||
KCC is released under ISC LICENSE; see LICENSE.txt for further details.
|
KCC is released under ISC LICENSE; see LICENSE.txt for further details.
|
||||||
6
kcc.py
6
kcc.py
@@ -16,7 +16,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__ = '2.7'
|
__version__ = '2.10'
|
||||||
__license__ = 'ISC'
|
__license__ = 'ISC'
|
||||||
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
@@ -24,13 +24,15 @@ __docformat__ = 'restructuredtext en'
|
|||||||
from Tkinter import *
|
from Tkinter import *
|
||||||
from kcc import gui
|
from kcc import gui
|
||||||
from sys import platform
|
from sys import platform
|
||||||
|
from multiprocessing import freeze_support
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
freeze_support()
|
||||||
root = Tk()
|
root = Tk()
|
||||||
root.resizable(width=False, height=False)
|
root.resizable(width=False, height=False)
|
||||||
root.config(padx=5, pady=5, takefocus=True)
|
root.config(padx=5, pady=5, takefocus=True)
|
||||||
root.title("Kindle Comic Converter v" + __version__)
|
root.title("Kindle Comic Converter v" + __version__)
|
||||||
root.wm_attributes("-topmost", 1)
|
#root.wm_attributes("-topmost", 1)
|
||||||
if platform == 'darwin':
|
if platform == 'darwin':
|
||||||
os.environ['PATH'] = '/usr/local/bin:' + os.environ['PATH']
|
os.environ['PATH'] = '/usr/local/bin:' + os.environ['PATH']
|
||||||
elif platform == 'win32':
|
elif platform == 'win32':
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
__version__ = '2.0'
|
__version__ = '2.10'
|
||||||
__license__ = 'ISC'
|
__license__ = 'ISC'
|
||||||
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
@@ -38,6 +38,7 @@ class CBxArchive:
|
|||||||
|
|
||||||
def extractCBZ(self, targetdir):
|
def extractCBZ(self, targetdir):
|
||||||
cbzFile = zipfile.ZipFile(self.origFileName)
|
cbzFile = zipfile.ZipFile(self.origFileName)
|
||||||
|
filelist = []
|
||||||
for f in cbzFile.namelist():
|
for f in cbzFile.namelist():
|
||||||
if f.startswith('__MACOSX') or f.endswith('.DS_Store'):
|
if f.startswith('__MACOSX') or f.endswith('.DS_Store'):
|
||||||
pass # skip MacOS special files
|
pass # skip MacOS special files
|
||||||
@@ -47,10 +48,12 @@ class CBxArchive:
|
|||||||
except:
|
except:
|
||||||
pass # the dir exists so we are going to extract the images only.
|
pass # the dir exists so we are going to extract the images only.
|
||||||
else:
|
else:
|
||||||
cbzFile.extract(f, targetdir)
|
filelist.append(f)
|
||||||
|
cbzFile.extractall(targetdir, filelist)
|
||||||
|
|
||||||
def extractCBR(self, targetdir):
|
def extractCBR(self, targetdir):
|
||||||
cbrFile = rarfile.RarFile(self.origFileName)
|
cbrFile = rarfile.RarFile(self.origFileName)
|
||||||
|
filelist = []
|
||||||
for f in cbrFile.namelist():
|
for f in cbrFile.namelist():
|
||||||
if f.startswith('__MACOSX') or f.endswith('.DS_Store'):
|
if f.startswith('__MACOSX') or f.endswith('.DS_Store'):
|
||||||
pass # skip MacOS special files
|
pass # skip MacOS special files
|
||||||
@@ -60,9 +63,11 @@ class CBxArchive:
|
|||||||
except:
|
except:
|
||||||
pass # the dir exists so we are going to extract the images only.
|
pass # the dir exists so we are going to extract the images only.
|
||||||
else:
|
else:
|
||||||
cbrFile.extract(f, targetdir)
|
filelist.append(f)
|
||||||
|
cbrFile.extractall(targetdir, filelist)
|
||||||
|
|
||||||
def extract(self, targetdir):
|
def extract(self, targetdir):
|
||||||
|
print "\n" + targetdir + "\n"
|
||||||
if self.compressor == 'rar':
|
if self.compressor == 'rar':
|
||||||
self.extractCBR(targetdir)
|
self.extractCBR(targetdir)
|
||||||
elif self.compressor == 'zip':
|
elif self.compressor == 'zip':
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# Copyright (c) 2012 Ciro Mattia Gonano <ciromattia@gmail.com>
|
# 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
|
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
# PERFORMANCE OF THIS SOFTWARE.
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
#
|
#
|
||||||
__version__ = '2.7'
|
__version__ = '2.10'
|
||||||
__license__ = 'ISC'
|
__license__ = 'ISC'
|
||||||
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
@@ -31,6 +32,7 @@ from shutil import copytree
|
|||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
from shutil import make_archive
|
from shutil import make_archive
|
||||||
from optparse import OptionParser
|
from optparse import OptionParser
|
||||||
|
from multiprocessing import Pool, freeze_support
|
||||||
import image
|
import image
|
||||||
import cbxarchive
|
import cbxarchive
|
||||||
import pdfjpgextract
|
import pdfjpgextract
|
||||||
@@ -39,6 +41,11 @@ import pdfjpgextract
|
|||||||
def buildHTML(path, imgfile):
|
def buildHTML(path, imgfile):
|
||||||
filename = getImageFileName(imgfile)
|
filename = getImageFileName(imgfile)
|
||||||
if filename is not None:
|
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 = ''
|
htmlpath = ''
|
||||||
postfix = ''
|
postfix = ''
|
||||||
backref = 1
|
backref = 1
|
||||||
@@ -61,26 +68,71 @@ def buildHTML(path, imgfile):
|
|||||||
"<title>", filename[0], "</title>\n",
|
"<title>", filename[0], "</title>\n",
|
||||||
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n",
|
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n",
|
||||||
"<link href=\"", "../" * (backref - 1),
|
"<link href=\"", "../" * (backref - 1),
|
||||||
"stylesheet.css\" type=\"text/css\" rel=\"stylesheet\"/>\n",
|
"style.css\" type=\"text/css\" rel=\"stylesheet\"/>\n",
|
||||||
"<link href=\"", "../" * (backref - 1),
|
|
||||||
"page_styles.css\" type=\"text/css\" rel=\"stylesheet\"/>\n",
|
|
||||||
"</head>\n",
|
"</head>\n",
|
||||||
"<body class=\"kcc\">\n",
|
"<body>\n",
|
||||||
"<div class=\"kcc1\"><img src=\"", "../" * backref, "Images/", postfix, imgfile, "\" alt=\"",
|
"<div class=\"fs\">\n",
|
||||||
imgfile, "\" class=\"kcc2\"/></div>\n",
|
"<div><img src=\"", "../" * backref, "Images/", postfix, imgfile, "\" alt=\"",
|
||||||
#"<div id=\"", filename[0], "-1\">\n",
|
imgfile, "\" class=\"singlePage\"/></div>\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>"
|
|
||||||
])
|
])
|
||||||
|
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()
|
f.close()
|
||||||
return path, imgfile
|
return path, imgfile
|
||||||
|
|
||||||
@@ -106,7 +158,7 @@ def buildNCX(dstdir, title, chapters):
|
|||||||
f = open(ncxfile, "w")
|
f = open(ncxfile, "w")
|
||||||
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
||||||
"<!DOCTYPE ncx PUBLIC \"-//NISO//DTD ncx 2005-1//EN\" ",
|
"<!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",
|
"<ncx version=\"2005-1\" xml:lang=\"en-US\" xmlns=\"http://www.daisy.org/z3986/2005/ncx/\">\n",
|
||||||
"<head>\n",
|
"<head>\n",
|
||||||
"<meta name=\"dtb:uid\" content=\"015ffaec-9340-42f8-b163-a0c5ab7d0611\"/>\n",
|
"<meta name=\"dtb:uid\" content=\"015ffaec-9340-42f8-b163-a0c5ab7d0611\"/>\n",
|
||||||
@@ -128,12 +180,12 @@ def buildNCX(dstdir, title, chapters):
|
|||||||
return
|
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')
|
opffile = os.path.join(dstdir, 'OEBPS', 'content.opf')
|
||||||
# read the first file resolution
|
# 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])
|
imgres = str(deviceres[0]) + "x" + str(deviceres[1])
|
||||||
if righttoleft:
|
if options.righttoleft:
|
||||||
writingmode = "horizontal-rl"
|
writingmode = "horizontal-rl"
|
||||||
facing = "right"
|
facing = "right"
|
||||||
facing1 = "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=\"book-type\" content=\"comic\"/>\n",
|
||||||
"<meta name=\"zero-gutter\" content=\"true\"/>\n",
|
"<meta name=\"zero-gutter\" content=\"true\"/>\n",
|
||||||
"<meta name=\"zero-margin\" content=\"true\"/>\n",
|
"<meta name=\"zero-margin\" content=\"true\"/>\n",
|
||||||
"<meta name=\"fixed-layout\" content=\"true\"/>\n",
|
"<meta name=\"fixed-layout\" content=\"true\"/>\n"
|
||||||
"<meta name=\"orientation-lock\" content=\"none\"/>\n",
|
])
|
||||||
"<meta name=\"original-resolution\" content=\"", imgres, "\"/>\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=\"primary-writing-mode\" content=\"", writingmode, "\"/>\n",
|
||||||
"<meta name=\"rendition:layout\" content=\"pre-paginated\"/>\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\" ",
|
"</metadata>\n<manifest>\n<item id=\"ncx\" href=\"toc.ncx\" ",
|
||||||
"media-type=\"application/x-dtbncx+xml\"/>\n"
|
"media-type=\"application/x-dtbncx+xml\"/>\n"])
|
||||||
])
|
|
||||||
# set cover
|
|
||||||
if cover is not None:
|
if cover is not None:
|
||||||
filename = getImageFileName(cover.replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\'))
|
filename = getImageFileName(cover.replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\'))
|
||||||
if '.png' == filename[1]:
|
if '.png' == filename[1]:
|
||||||
@@ -191,36 +246,46 @@ def buildOPF(profile, dstdir, title, filelist, cover=None, righttoleft=False):
|
|||||||
mt = 'image/jpeg'
|
mt = 'image/jpeg'
|
||||||
f.write("<item id=\"img_" + uniqueid + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\""
|
f.write("<item id=\"img_" + uniqueid + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\""
|
||||||
+ mt + "\"/>\n")
|
+ mt + "\"/>\n")
|
||||||
if (options.profile == 'K4' or options.profile == 'KHD') and splitCount > 0:
|
if options.landscapemode and splitCount > 0:
|
||||||
splitCountUsed = 1
|
splitCountUsed = 1
|
||||||
while splitCountUsed <= splitCount:
|
while splitCountUsed <= splitCount:
|
||||||
f.write("<item id=\"blank-page" + str(splitCountUsed) +
|
f.write("<item id=\"blank-page" + str(splitCountUsed) +
|
||||||
"\" href=\"Text/blank.html\" media-type=\"application/xhtml+xml\"/>\n")
|
"\" href=\"Text/blank.html\" media-type=\"application/xhtml+xml\"/>\n")
|
||||||
splitCountUsed += 1
|
splitCountUsed += 1
|
||||||
|
f.write("<item id=\"css\" href=\"Text/style.css\" media-type=\"text/css\"/>\n")
|
||||||
f.write("</manifest>\n<spine toc=\"ncx\">\n")
|
f.write("</manifest>\n<spine toc=\"ncx\">\n")
|
||||||
splitCountUsed = 1
|
splitCountUsed = 1
|
||||||
for entry in reflist:
|
for entry in reflist:
|
||||||
if entry.endswith("-1"):
|
if entry.endswith("-kcca"):
|
||||||
if ((righttoleft and facing == 'left') or (not righttoleft and facing == 'right')) and \
|
# noinspection PyRedundantParentheses
|
||||||
(options.profile == 'K4' or options.profile == 'KHD'):
|
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")
|
f.write("<itemref idref=\"blank-page" + str(splitCountUsed) + "\" properties=\"layout-blank\"/>\n")
|
||||||
splitCountUsed += 1
|
splitCountUsed += 1
|
||||||
f.write("<itemref idref=\"page_" + entry + "\" properties=\"page-spread-" + facing1 + "\"/>\n")
|
if options.landscapemode:
|
||||||
elif entry.endswith("-2"):
|
f.write("<itemref idref=\"page_" + entry + "\" properties=\"page-spread-" + facing1 + "\"/>\n")
|
||||||
f.write("<itemref idref=\"page_" + entry + "\" properties=\"page-spread-" + facing2 + "\"/>\n")
|
else:
|
||||||
if righttoleft:
|
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"
|
facing = "right"
|
||||||
else:
|
else:
|
||||||
facing = "left"
|
facing = "left"
|
||||||
else:
|
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':
|
if facing == 'right':
|
||||||
facing = 'left'
|
facing = 'left'
|
||||||
else:
|
else:
|
||||||
facing = 'right'
|
facing = 'right'
|
||||||
f.write("</spine>\n<guide>\n</guide>\n</package>\n")
|
f.write("</spine>\n<guide>\n</guide>\n</package>\n")
|
||||||
f.close()
|
f.close()
|
||||||
# finish with standard ePub folders
|
|
||||||
os.mkdir(os.path.join(dstdir, 'META-INF'))
|
os.mkdir(os.path.join(dstdir, 'META-INF'))
|
||||||
f = open(os.path.join(dstdir, 'mimetype'), 'w')
|
f = open(os.path.join(dstdir, 'mimetype'), 'w')
|
||||||
f.write('application/epub+zip')
|
f.write('application/epub+zip')
|
||||||
@@ -241,79 +306,84 @@ def getImageFileName(imgfile):
|
|||||||
if filename[0].startswith('.') or\
|
if filename[0].startswith('.') or\
|
||||||
(filename[1].lower() != '.png' and
|
(filename[1].lower() != '.png' and
|
||||||
filename[1].lower() != '.jpg' 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'):
|
filename[1].lower() != '.jpeg'):
|
||||||
return None
|
return None
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
|
||||||
def isInFilelist(filename, filelist):
|
def applyImgOptimization(img, isSplit, toRight, options):
|
||||||
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)
|
|
||||||
img.cropWhiteSpace(10.0)
|
img.cropWhiteSpace(10.0)
|
||||||
if options.cutpagenumbers:
|
if options.cutpagenumbers:
|
||||||
img.cutPageNumber()
|
img.cutPageNumber()
|
||||||
img.resizeImage(options.upscale, options.stretch, options.black_borders, isSplit, toRight)
|
img.resizeImage(options.upscale, options.stretch, options.black_borders, isSplit, toRight, options.landscapemode,
|
||||||
if not options.notquantize:
|
options.nopanelviewhq)
|
||||||
|
img.optimizeImage(options.gamma)
|
||||||
|
if options.forcepng:
|
||||||
img.quantizeImage()
|
img.quantizeImage()
|
||||||
|
|
||||||
|
|
||||||
def dirImgProcess(path):
|
def dirImgProcess(path):
|
||||||
global options, splitCount
|
global options, splitCount
|
||||||
if options.righttoleft:
|
work = []
|
||||||
facing = "right"
|
pagenumber = 0
|
||||||
else:
|
pagenumbermodifier = 0
|
||||||
facing = "left"
|
pool = Pool()
|
||||||
|
|
||||||
for (dirpath, dirnames, filenames) in os.walk(path):
|
for (dirpath, dirnames, filenames) in os.walk(path):
|
||||||
for afile in filenames:
|
for afile in filenames:
|
||||||
if getImageFileName(afile) is not None:
|
if getImageFileName(afile) is not None:
|
||||||
if options.verbose:
|
pagenumber += 1
|
||||||
print "Optimizing " + afile + " for " + options.profile
|
work.append([afile, dirpath, pagenumber, options])
|
||||||
else:
|
splitpages = pool.map(fileImgProcess, work)
|
||||||
print ".",
|
pool.close()
|
||||||
img = image.ComicPage(os.path.join(dirpath, afile), options.profile)
|
pool.join()
|
||||||
if options.nosplitrotate:
|
splitpages = filter(None, splitpages)
|
||||||
split = None
|
splitpages.sort()
|
||||||
else:
|
for page in splitpages:
|
||||||
split = img.splitPage(dirpath, options.righttoleft, options.rotate)
|
if (page + pagenumbermodifier) % 2 == 0:
|
||||||
if split is not None:
|
splitCount += 1
|
||||||
if options.verbose:
|
pagenumbermodifier += 1
|
||||||
print "Splitted " + afile
|
pagenumbermodifier += 1
|
||||||
if options.righttoleft:
|
|
||||||
toRight1 = False
|
|
||||||
toRight2 = True
|
def fileImgProcess(work):
|
||||||
else:
|
afile = work[0]
|
||||||
toRight1 = True
|
dirpath = work[1]
|
||||||
toRight2 = False
|
pagenumber = work[2]
|
||||||
if options.righttoleft:
|
options = work[3]
|
||||||
if facing == "left":
|
output = None
|
||||||
splitCount += 1
|
if options.verbose:
|
||||||
facing = "right"
|
print "Optimizing " + afile + " for " + options.profile
|
||||||
else:
|
else:
|
||||||
if facing == "right":
|
print ".",
|
||||||
splitCount += 1
|
img = image.ComicPage(os.path.join(dirpath, afile), options.profile)
|
||||||
facing = "left"
|
if options.nosplitrotate:
|
||||||
img0 = image.ComicPage(split[0], options.profile)
|
split = None
|
||||||
applyImgOptimization(img0, True, toRight1)
|
else:
|
||||||
img0.saveToDir(dirpath, options.notquantize)
|
split = img.splitPage(dirpath, options.righttoleft, options.rotate)
|
||||||
img1 = image.ComicPage(split[1], options.profile)
|
if split is not None and split is not "R":
|
||||||
applyImgOptimization(img1, True, toRight2)
|
if options.verbose:
|
||||||
img1.saveToDir(dirpath, options.notquantize)
|
print "Splitted " + afile
|
||||||
else:
|
if options.righttoleft:
|
||||||
if facing == "right":
|
toRight1 = False
|
||||||
facing = "left"
|
toRight2 = True
|
||||||
else:
|
else:
|
||||||
facing = "right"
|
toRight1 = True
|
||||||
applyImgOptimization(img)
|
toRight2 = False
|
||||||
img.saveToDir(dirpath, options.notquantize)
|
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):
|
def genEpubStruct(path):
|
||||||
@@ -321,48 +391,134 @@ def genEpubStruct(path):
|
|||||||
filelist = []
|
filelist = []
|
||||||
chapterlist = []
|
chapterlist = []
|
||||||
cover = None
|
cover = None
|
||||||
|
_, deviceres, _, _, panelviewsize = image.ProfileData.Profiles[options.profile]
|
||||||
|
sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
|
||||||
os.mkdir(os.path.join(path, 'OEBPS', 'Text'))
|
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",
|
f.writelines(["@page {\n",
|
||||||
" margin-bottom: 0;\n",
|
"margin-bottom: 0;\n",
|
||||||
" margin-top: 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",
|
|
||||||
"}\n",
|
"}\n",
|
||||||
".kcc1 {\n",
|
"body {\n",
|
||||||
" display: block;\n",
|
"display: block;\n",
|
||||||
" text-align: center\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",
|
"}\n",
|
||||||
".kcc2 {\n",
|
"div.fs {\n",
|
||||||
" height: auto;\n",
|
"height: ", str(deviceres[1]), "px;\n",
|
||||||
" width: auto\n",
|
"width: ", str(deviceres[0]), "px;\n",
|
||||||
"}\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()
|
f.close()
|
||||||
for (dirpath, dirnames, filenames) in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
for (dirpath, dirnames, filenames) in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
||||||
chapter = False
|
chapter = False
|
||||||
for afile in filenames:
|
for afile in filenames:
|
||||||
filename = getImageFileName(afile)
|
filename = getImageFileName(afile)
|
||||||
if filename is not None:
|
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))
|
filelist.append(buildHTML(dirpath, afile))
|
||||||
if not chapter:
|
if not chapter:
|
||||||
chapterlist.append((dirpath.replace('Images', 'Text'), filelist[-1][1]))
|
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
|
convert = lambda text: int(text) if text.isdigit() else text
|
||||||
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
|
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())))
|
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)
|
buildOPF(options.profile, path, options.title, filelist, cover)
|
||||||
if (options.profile == 'K4' or options.profile == 'KHD') and splitCount > 0:
|
if options.landscapemode and splitCount > 0:
|
||||||
filelist.append(buildBlankHTML(os.path.join(path, 'OEBPS', 'Text')))
|
filelist.append(buildBlankHTML(os.path.join(path, 'OEBPS', 'Text')))
|
||||||
|
|
||||||
|
|
||||||
@@ -409,14 +565,45 @@ def getWorkFolder(afile):
|
|||||||
return path
|
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():
|
def Copyright():
|
||||||
print ('comic2ebook v%(__version__)s. '
|
print ('comic2ebook v%(__version__)s. '
|
||||||
'Written 2012 by Ciro Mattia Gonano.' % globals())
|
'Written 2013 by Ciro Mattia Gonano and Pawel Jastrzebski.' % globals())
|
||||||
|
|
||||||
|
|
||||||
def Usage():
|
def Usage():
|
||||||
print "Generates HTML, NCX and OPF for a Comic ebook from a bunch of images."
|
print "Generates EPUB/CBZ comic ebook from a bunch of images."
|
||||||
print "Optimized for creating MOBI files to be read on Kindle Paperwhite."
|
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
|
|
||||||
|
|
||||||
@@ -425,15 +612,20 @@ def main(argv=None):
|
|||||||
usage = "Usage: %prog [options] comic_file|comic_folder"
|
usage = "Usage: %prog [options] comic_file|comic_folder"
|
||||||
parser = OptionParser(usage=usage, version=__version__)
|
parser = OptionParser(usage=usage, version=__version__)
|
||||||
parser.add_option("-p", "--profile", action="store", dest="profile", default="KHD",
|
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",
|
parser.add_option("-t", "--title", action="store", dest="title", default="defaulttitle",
|
||||||
help="Comic title [Default=filename]")
|
help="Comic title [Default=filename]")
|
||||||
parser.add_option("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
|
parser.add_option("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
|
||||||
help="Manga style (Right-to-left reading and splitting) [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,
|
parser.add_option("--noprocessing", action="store_false", dest="imgproc", default=True,
|
||||||
help="Do not apply image preprocessing (Page splitting and optimizations) [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,
|
parser.add_option("--forcepng", action="store_true", dest="forcepng", default=False,
|
||||||
help="Disable image quantization [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",
|
parser.add_option("--gamma", type="float", dest="gamma", default="0.0",
|
||||||
help="Apply gamma correction to linearize the image [Default=Auto]")
|
help="Apply gamma correction to linearize the image [Default=Auto]")
|
||||||
parser.add_option("--upscale", action="store_true", dest="upscale", default=False,
|
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,
|
parser.add_option("--nocutpagenumbers", action="store_false", dest="cutpagenumbers", default=True,
|
||||||
help="Do not try to cut page numbering on images [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,
|
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,
|
parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False,
|
||||||
help="Verbose output [Default=False]")
|
help="Verbose output [Default=False]")
|
||||||
options, args = parser.parse_args(argv)
|
options, args = parser.parse_args(argv)
|
||||||
|
checkOptions()
|
||||||
if len(args) != 1:
|
if len(args) != 1:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
return
|
return
|
||||||
@@ -464,32 +657,82 @@ def main(argv=None):
|
|||||||
if options.imgproc:
|
if options.imgproc:
|
||||||
print "Processing images..."
|
print "Processing images..."
|
||||||
dirImgProcess(path + "/OEBPS/Images/")
|
dirImgProcess(path + "/OEBPS/Images/")
|
||||||
print "\nCreating ePub structure..."
|
if options.cbzoutput:
|
||||||
genEpubStruct(path)
|
# if CBZ output wanted, compress all images and return filepath
|
||||||
# actually zip the ePub
|
print "\nCreating CBZ file..."
|
||||||
if options.output is not None:
|
filepath = getOutputFilename(args[0], options.output, '.cbz')
|
||||||
if options.output.endswith('.epub'):
|
make_archive(path + '_comic', 'zip', path + '/OEBPS/Images')
|
||||||
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'
|
|
||||||
else:
|
else:
|
||||||
epubpath = os.path.splitext(args[0])[0] + '.epub'
|
print "\nCreating ePub structure..."
|
||||||
make_archive(path + '_comic', 'zip', path)
|
genEpubStruct(path)
|
||||||
move(path + '_comic.zip', epubpath)
|
# actually zip the ePub
|
||||||
|
filepath = getOutputFilename(args[0], options.output, '.epub')
|
||||||
|
make_archive(path + '_comic', 'zip', path)
|
||||||
|
move(path + '_comic.zip', filepath)
|
||||||
rmtree(path)
|
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():
|
def getEpubPath():
|
||||||
global epub_path
|
global epub_path
|
||||||
return epub_path
|
return epub_path
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
freeze_support()
|
||||||
Copyright()
|
Copyright()
|
||||||
main(sys.argv[1:])
|
main(sys.argv[1:])
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|||||||
91
kcc/gui.py
91
kcc/gui.py
@@ -28,7 +28,6 @@ import comic2ebook
|
|||||||
import kindlestrip
|
import kindlestrip
|
||||||
from image import ProfileData
|
from image import ProfileData
|
||||||
from subprocess import call
|
from subprocess import call
|
||||||
from subprocess import check_call
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
@@ -94,29 +93,33 @@ class MainWindow:
|
|||||||
|
|
||||||
self.options = {
|
self.options = {
|
||||||
'Aepub_only': IntVar(None, 0),
|
'Aepub_only': IntVar(None, 0),
|
||||||
'Bmangastyle': IntVar(None, 0),
|
'Bcbz_only': IntVar(None, 0),
|
||||||
'Cimage_preprocess': IntVar(None, 0),
|
'Cmangastyle': IntVar(None, 0),
|
||||||
'Dnotquantize': IntVar(None, 0),
|
'Dnopanelviewhq': IntVar(None, 0),
|
||||||
'Eimage_gamma': DoubleVar(None, 0.0),
|
'Eimage_preprocess': IntVar(None, 0),
|
||||||
'Fimage_upscale': IntVar(None, 0),
|
'Fforcepng': IntVar(None, 0),
|
||||||
'Gimage_stretch': IntVar(None, 0),
|
'Gimage_gamma': DoubleVar(None, 0.0),
|
||||||
'Hblack_borders': IntVar(None, 0),
|
'Himage_upscale': IntVar(None, 0),
|
||||||
'Irotate': IntVar(None, 0),
|
'Iimage_stretch': IntVar(None, 0),
|
||||||
'Jnosplitrotate': IntVar(None, 0),
|
'Jblack_borders': IntVar(None, 0),
|
||||||
'Kcut_page_numbers': IntVar(None, 0)
|
'Krotate': IntVar(None, 0),
|
||||||
|
'Lnosplitrotate': IntVar(None, 0),
|
||||||
|
'Mcut_page_numbers': IntVar(None, 0)
|
||||||
}
|
}
|
||||||
self.optionlabels = {
|
self.optionlabels = {
|
||||||
'Aepub_only': "Generate EPUB only",
|
'Aepub_only': "Generate EPUB only",
|
||||||
'Cimage_preprocess': "Disable image optimizations",
|
'Bcbz_only': "Generate CBZ only (skip EPUB/Mobi generation)",
|
||||||
'Dnotquantize': "Disable image quantization",
|
'Cmangastyle': "Manga mode",
|
||||||
'Jnosplitrotate': "Disable splitting and rotation",
|
'Dnopanelviewhq': "Disable high quality Panel View",
|
||||||
'Irotate': "Rotate images instead splitting them",
|
'Eimage_preprocess': "Disable image optimizations",
|
||||||
'Kcut_page_numbers': "Disable page numbers cutting",
|
'Fforcepng': "Create PNG files instead of JPEG",
|
||||||
'Bmangastyle': "Manga mode",
|
'Gimage_gamma': "Custom gamma correction",
|
||||||
'Eimage_gamma': "Custom gamma correction",
|
'Himage_upscale': "Allow image upscaling",
|
||||||
'Fimage_upscale': "Allow image upscaling",
|
'Iimage_stretch': "Stretch images",
|
||||||
'Gimage_stretch': "Stretch images",
|
'Jblack_borders': "Use black borders (instead of white ones)",
|
||||||
'Hblack_borders': "Use black borders"
|
'Krotate': "Rotate images (instead of splitting them)",
|
||||||
|
'Lnosplitrotate': "Disable both splitting and rotation",
|
||||||
|
'Mcut_page_numbers': "Disable page numbers cutting"
|
||||||
}
|
}
|
||||||
self.optionsButtons = {}
|
self.optionsButtons = {}
|
||||||
for key in sorted(self.options):
|
for key in sorted(self.options):
|
||||||
@@ -161,27 +164,31 @@ class MainWindow:
|
|||||||
return
|
return
|
||||||
profilekey = ProfileData.ProfileLabels[self.profile.get()]
|
profilekey = ProfileData.ProfileLabels[self.profile.get()]
|
||||||
argv = ["-p", profilekey]
|
argv = ["-p", profilekey]
|
||||||
if self.options['Eimage_gamma'].get() != 0.0:
|
if self.options['Bcbz_only'].get() == 1:
|
||||||
argv.append("--gamma")
|
argv.append("-c")
|
||||||
argv.append(self.options['Eimage_gamma'].get())
|
if self.options['Cmangastyle'].get() == 1:
|
||||||
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:
|
|
||||||
argv.append("-m")
|
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")
|
argv.append("--upscale")
|
||||||
if self.options['Gimage_stretch'].get() == 1:
|
if self.options['Iimage_stretch'].get() == 1:
|
||||||
argv.append("--stretch")
|
argv.append("--stretch")
|
||||||
if self.options['Hblack_borders'].get() == 1:
|
if self.options['Jblack_borders'].get() == 1:
|
||||||
argv.append("--blackborders")
|
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
|
errors = False
|
||||||
left_files = len(self.filelist)
|
left_files = len(self.filelist)
|
||||||
filenum = 0
|
filenum = 0
|
||||||
@@ -204,12 +211,12 @@ class MainWindow:
|
|||||||
(subargv[-1], str(err), traceback.format_tb(traceback_)))
|
(subargv[-1], str(err), traceback.format_tb(traceback_)))
|
||||||
errors = True
|
errors = True
|
||||||
continue
|
continue
|
||||||
if self.options['Aepub_only'].get() == 0:
|
if self.options['Aepub_only'].get() == 0 and self.options['Bcbz_only'].get() == 0:
|
||||||
try:
|
try:
|
||||||
if os.path.getsize(epub_path) > 335544320:
|
if os.path.getsize(epub_path) > 314572800:
|
||||||
# do not call kindlegen if source is bigger than 320MB
|
# do not call kindlegen if source is bigger than 300MB
|
||||||
tkMessageBox.showwarning('KindleGen Warning',
|
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)
|
epub_path)
|
||||||
continue
|
continue
|
||||||
retcode = call("kindlegen \"" + epub_path + "\"", shell=True)
|
retcode = call("kindlegen \"" + epub_path + "\"", shell=True)
|
||||||
|
|||||||
388
kcc/image.py
388
kcc/image.py
@@ -20,7 +20,7 @@ __copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from PIL import Image, ImageOps, ImageDraw, ImageStat
|
from PIL import Image, ImageOps, ImageStat, ImageChops
|
||||||
|
|
||||||
|
|
||||||
class ImageFlags:
|
class ImageFlags:
|
||||||
@@ -77,23 +77,31 @@ class ProfileData:
|
|||||||
]
|
]
|
||||||
|
|
||||||
Profiles = {
|
Profiles = {
|
||||||
'K1': ("Kindle", (600, 800), Palette4, 1.8),
|
'K1': ("Kindle 1", (600, 800), Palette4, 1.8, (900, 1200)),
|
||||||
'K2': ("Kindle 2", (600, 800), Palette15, 1.8),
|
'K2': ("Kindle 2", (600, 800), Palette15, 1.8, (900, 1200)),
|
||||||
'K3': ("Kindle 3/Keyboard", (600, 800), Palette16, 1.8),
|
'K3': ("Kindle Keyboard", (600, 800), Palette16, 1.8, (900, 1200)),
|
||||||
'K4': ("Kindle 4/NT/Touch", (600, 800), Palette16, 1.8),
|
'K4NT': ("Kindle Non-Touch", (600, 800), Palette16, 1.8, (900, 1200)),
|
||||||
'KHD': ("Kindle Paperwhite", (758, 1024), Palette16, 1.8),
|
'K4T': ("Kindle Touch", (600, 800), Palette16, 1.8, (900, 1200)),
|
||||||
'KDX': ("Kindle DX", (824, 1200), Palette15, 1.8),
|
'KHD': ("Kindle Paperwhite", (758, 1024), Palette16, 1.8, (1137, 1536)),
|
||||||
'KDXG': ("Kindle DXG", (824, 1200), Palette16, 1.8)
|
'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 = {
|
ProfileLabels = {
|
||||||
"Kindle": 'K1',
|
"Kindle 1": 'K1',
|
||||||
"Kindle 2": 'K2',
|
"Kindle 2": 'K2',
|
||||||
"Kindle 3/Keyboard": 'K3',
|
"Kindle 3/Keyboard": 'K3',
|
||||||
"Kindle 4/NT/Touch": 'K4',
|
"Kindle 4/Non-Touch": 'K4NT',
|
||||||
|
"Kindle 4/Touch": 'K4T',
|
||||||
"Kindle Paperwhite": 'KHD',
|
"Kindle Paperwhite": 'KHD',
|
||||||
"Kindle DX": 'KDX',
|
"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):
|
def __init__(self, source, device):
|
||||||
try:
|
try:
|
||||||
self.profile = device
|
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:
|
except KeyError:
|
||||||
raise RuntimeError('Unexpected output device %s' % device)
|
raise RuntimeError('Unexpected output device %s' % device)
|
||||||
try:
|
try:
|
||||||
@@ -111,15 +119,21 @@ class ComicPage:
|
|||||||
raise RuntimeError('Cannot read image file %s' % source)
|
raise RuntimeError('Cannot read image file %s' % source)
|
||||||
self.image = self.image.convert('RGB')
|
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)
|
filename = os.path.basename(self.origFileName)
|
||||||
try:
|
try:
|
||||||
self.image = self.image.convert('L') # convert to grayscale
|
if not color:
|
||||||
os.remove(os.path.join(targetdir, filename))
|
self.image = self.image.convert('L') # convert to grayscale
|
||||||
if notquantize:
|
# Sufix is used to recognise which files need horizontal Panel View.
|
||||||
self.image.save(os.path.join(targetdir, os.path.splitext(filename)[0] + ".jpg"), "JPEG")
|
if sufix == "R":
|
||||||
|
sufix = "_rotated"
|
||||||
else:
|
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:
|
except IOError as e:
|
||||||
raise RuntimeError('Cannot write image in directory %s: %s' % (targetdir, 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))
|
self.image = ImageOps.autocontrast(Image.eval(self.image, lambda a: 255 * (a / 255.) ** gamma))
|
||||||
|
|
||||||
def quantizeImage(self):
|
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
|
colors = len(self.palette) / 3
|
||||||
if colors < 256:
|
if colors < 256:
|
||||||
self.palette += self.palette[:3] * (256 - colors)
|
self.palette += self.palette[:3] * (256 - colors)
|
||||||
@@ -139,46 +155,48 @@ class ComicPage:
|
|||||||
palImg.putpalette(self.palette)
|
palImg.putpalette(self.palette)
|
||||||
self.image = self.image.quantize(palette=palImg)
|
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
|
method = Image.ANTIALIAS
|
||||||
if black_borders:
|
if black_borders:
|
||||||
fill = 'black'
|
fill = 'black'
|
||||||
else:
|
else:
|
||||||
fill = 'white'
|
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 self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
|
||||||
if not upscale:
|
if not upscale:
|
||||||
if isSplit and (self.profile == 'K4' or self.profile == 'KHD'):
|
borderw = (self.size[0] - self.image.size[0]) / 2
|
||||||
borderw = (self.size[0] - self.image.size[0])
|
borderh = (self.size[1] - self.image.size[1]) / 2
|
||||||
borderh = (self.size[1] - self.image.size[1]) / 2
|
self.image = ImageOps.expand(self.image, border=(borderw, borderh), fill=fill)
|
||||||
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)
|
|
||||||
return self.image
|
return self.image
|
||||||
else:
|
else:
|
||||||
method = Image.NEAREST
|
method = Image.BILINEAR
|
||||||
|
|
||||||
if stretch: # if stretching call directly resize() without other considerations.
|
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
|
return self.image
|
||||||
|
|
||||||
ratioDev = float(self.size[0]) / float(self.size[1])
|
ratioDev = float(self.size[0]) / float(self.size[1])
|
||||||
if (float(self.image.size[0]) / float(self.image.size[1])) < ratioDev:
|
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 landscapeMode:
|
||||||
if isSplit and (self.profile == 'K4' or self.profile == 'KHD'):
|
diff = int(self.image.size[1] * ratioDev) - self.image.size[0]
|
||||||
diff = 2
|
self.image = ImageOps.expand(self.image, border=(diff / 2, 0), fill=fill)
|
||||||
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:
|
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)
|
||||||
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
|
return self.image
|
||||||
|
|
||||||
def splitPage(self, targetdir, righttoleft=False, rotate=False):
|
def splitPage(self, targetdir, righttoleft=False, rotate=False):
|
||||||
@@ -189,7 +207,7 @@ class ComicPage:
|
|||||||
if (width > height) != (dstwidth > dstheight):
|
if (width > height) != (dstwidth > dstheight):
|
||||||
if rotate:
|
if rotate:
|
||||||
self.image = self.image.rotate(90)
|
self.image = self.image.rotate(90)
|
||||||
return None
|
return "R"
|
||||||
else:
|
else:
|
||||||
if width > height:
|
if width > height:
|
||||||
# source is landscape, so split by the width
|
# source is landscape, so split by the width
|
||||||
@@ -200,8 +218,8 @@ class ComicPage:
|
|||||||
leftbox = (0, 0, width, height / 2)
|
leftbox = (0, 0, width, height / 2)
|
||||||
rightbox = (0, height / 2, width, height)
|
rightbox = (0, height / 2, width, height)
|
||||||
filename = os.path.splitext(os.path.basename(self.origFileName))
|
filename = os.path.splitext(os.path.basename(self.origFileName))
|
||||||
fileone = targetdir + '/' + filename[0] + '-1' + filename[1]
|
fileone = targetdir + '/' + filename[0] + '-kcca' + filename[1]
|
||||||
filetwo = targetdir + '/' + filename[0] + '-2' + filename[1]
|
filetwo = targetdir + '/' + filename[0] + '-kccb' + filename[1]
|
||||||
try:
|
try:
|
||||||
if righttoleft:
|
if righttoleft:
|
||||||
pageone = self.image.crop(rightbox)
|
pageone = self.image.crop(rightbox)
|
||||||
@@ -218,152 +236,154 @@ class ComicPage:
|
|||||||
else:
|
else:
|
||||||
return None
|
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):
|
def cutPageNumber(self):
|
||||||
widthImg, heightImg = self.image.size
|
if ImageChops.invert(self.image).getbbox() is not None:
|
||||||
delta = 2
|
widthImg, heightImg = self.image.size
|
||||||
diff = delta
|
delta = 2
|
||||||
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
|
diff = delta
|
||||||
oldStat = ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0]
|
fixedThreshold = 5
|
||||||
diff += delta
|
if ImageStat.Stat(self.image).var[0] < 2 * fixedThreshold:
|
||||||
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] - oldStat > 0\
|
return self.image
|
||||||
and diff < heightImg / 4:
|
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]
|
oldStat = ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0]
|
||||||
diff += delta
|
diff += delta
|
||||||
diff -= delta
|
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] - oldStat > 0\
|
||||||
pageNumberCut2 = diff
|
and diff < heightImg / 4:
|
||||||
diff += delta
|
oldStat = ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0]
|
||||||
oldStat = ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg - pageNumberCut2))).var[0]
|
diff += delta
|
||||||
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg - pageNumberCut2))).var[0]\
|
diff -= delta
|
||||||
< fixedThreshold + oldStat and diff < heightImg / 4:
|
pageNumberCut2 = diff
|
||||||
diff += delta
|
diff += delta
|
||||||
diff -= delta
|
oldStat = ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg - pageNumberCut2))).var[0]
|
||||||
pageNumberCut3 = diff
|
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg - pageNumberCut2))).var[0]\
|
||||||
delta = 5
|
< fixedThreshold + oldStat and diff < heightImg / 4:
|
||||||
diff = delta
|
diff += delta
|
||||||
while ImageStat.Stat(self.image.crop((0, heightImg - pageNumberCut2, diff, heightImg))).var[0] < fixedThreshold\
|
diff -= delta
|
||||||
and diff < widthImg:
|
pageNumberCut3 = diff
|
||||||
diff += delta
|
delta = 5
|
||||||
diff -= delta
|
diff = delta
|
||||||
pageNumberX1 = diff
|
while ImageStat.Stat(self.image.crop((0, heightImg - pageNumberCut2, diff, heightImg))).var[0] < fixedThreshold\
|
||||||
diff = delta
|
and diff < widthImg:
|
||||||
while ImageStat.Stat(self.image.crop((widthImg - diff, heightImg - pageNumberCut2,
|
diff += delta
|
||||||
widthImg, heightImg))).var[0] < fixedThreshold and diff < widthImg:
|
diff -= delta
|
||||||
diff += delta
|
pageNumberX1 = diff
|
||||||
diff -= delta
|
diff = delta
|
||||||
pageNumberX2 = widthImg - diff
|
while ImageStat.Stat(self.image.crop((widthImg - diff, heightImg - pageNumberCut2,
|
||||||
if pageNumberCut3 - pageNumberCut1 > 2 * delta\
|
widthImg, heightImg))).var[0] < fixedThreshold and diff < widthImg:
|
||||||
and float(pageNumberX2 - pageNumberX1) / float(pageNumberCut2 - pageNumberCut1) <= 9.0\
|
diff += delta
|
||||||
and ImageStat.Stat(self.image.crop((0, heightImg - pageNumberCut3, widthImg, heightImg))).var[0]\
|
diff -= delta
|
||||||
/ ImageStat.Stat(self.image).var[0] < 0.1\
|
pageNumberX2 = widthImg - diff
|
||||||
and pageNumberCut3 < heightImg / 4 - delta:
|
if pageNumberCut3 - pageNumberCut1 > 2 * delta\
|
||||||
diff = pageNumberCut3
|
and float(pageNumberX2 - pageNumberX1) / float(pageNumberCut2 - pageNumberCut1) <= 9.0\
|
||||||
else:
|
and ImageStat.Stat(self.image.crop((0, heightImg - pageNumberCut3, widthImg, heightImg))).var[0]\
|
||||||
diff = pageNumberCut1
|
/ ImageStat.Stat(self.image).var[0] < 0.1\
|
||||||
self.image = self.image.crop((0, 0, widthImg, heightImg - diff))
|
and pageNumberCut3 < heightImg / 4 - delta:
|
||||||
|
diff = pageNumberCut3
|
||||||
|
else:
|
||||||
|
diff = pageNumberCut1
|
||||||
|
self.image = self.image.crop((0, 0, widthImg, heightImg - diff))
|
||||||
return self.image
|
return self.image
|
||||||
|
|
||||||
def cropWhiteSpace(self, threshold):
|
def cropWhiteSpace(self, threshold):
|
||||||
widthImg, heightImg = self.image.size
|
if ImageChops.invert(self.image).getbbox() is not None:
|
||||||
delta = 10
|
widthImg, heightImg = self.image.size
|
||||||
diff = delta
|
delta = 10
|
||||||
# top
|
diff = delta
|
||||||
while ImageStat.Stat(self.image.crop((0, 0, widthImg, diff))).var[0] < threshold and diff < heightImg:
|
# top
|
||||||
diff += delta
|
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
|
diff -= delta
|
||||||
self.image = self.image.crop((0, diff, widthImg, heightImg))
|
# print "Top crop: %s"%diff
|
||||||
widthImg, heightImg = self.image.size
|
self.image = self.image.crop((0, diff, widthImg, heightImg))
|
||||||
diff = delta
|
widthImg, heightImg = self.image.size
|
||||||
# left
|
diff = delta
|
||||||
while ImageStat.Stat(self.image.crop((0, 0, diff, heightImg))).var[0] < threshold and diff < widthImg:
|
# left
|
||||||
diff += delta
|
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
|
diff -= delta
|
||||||
self.image = self.image.crop((diff, 0, widthImg, heightImg))
|
# print "Left crop: %s"%diff
|
||||||
widthImg, heightImg = self.image.size
|
self.image = self.image.crop((diff, 0, widthImg, heightImg))
|
||||||
diff = delta
|
widthImg, heightImg = self.image.size
|
||||||
# down
|
diff = delta
|
||||||
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] < threshold\
|
# down
|
||||||
and diff < heightImg:
|
while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] < threshold\
|
||||||
diff += delta
|
and diff < heightImg:
|
||||||
diff -= delta
|
diff += delta
|
||||||
# print "Down crop: %s"%diff
|
diff -= delta
|
||||||
self.image = self.image.crop((0, 0, widthImg, heightImg - diff))
|
# print "Down crop: %s"%diff
|
||||||
widthImg, heightImg = self.image.size
|
self.image = self.image.crop((0, 0, widthImg, heightImg - diff))
|
||||||
diff = delta
|
widthImg, heightImg = self.image.size
|
||||||
# right
|
diff = delta
|
||||||
while ImageStat.Stat(self.image.crop((widthImg - diff, 0, widthImg, heightImg))).var[0] < threshold\
|
# right
|
||||||
and diff < widthImg:
|
while ImageStat.Stat(self.image.crop((widthImg - diff, 0, widthImg, heightImg))).var[0] < threshold\
|
||||||
diff += delta
|
and diff < widthImg:
|
||||||
diff -= delta
|
diff += delta
|
||||||
# print "Right crop: %s"%diff
|
diff -= delta
|
||||||
self.image = self.image.crop((0, 0, widthImg - diff, heightImg))
|
# print "Right crop: %s"%diff
|
||||||
# print "New size: %sx%s"%(self.image.size[0],self.image.size[1])
|
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
|
return self.image
|
||||||
|
|
||||||
def addProgressbar(self, file_number, files_totalnumber, size, howoften):
|
# def addProgressbar(self, file_number, files_totalnumber, size, howoften):
|
||||||
if file_number // howoften != float(file_number) / howoften:
|
# if file_number // howoften != float(file_number) / howoften:
|
||||||
return self.image
|
# return self.image
|
||||||
white = (255, 255, 255)
|
# white = (255, 255, 255)
|
||||||
black = (0, 0, 0)
|
# black = (0, 0, 0)
|
||||||
widthDev, heightDev = size
|
# widthDev, heightDev = size
|
||||||
widthImg, heightImg = self.image.size
|
# widthImg, heightImg = self.image.size
|
||||||
pastePt = (
|
# pastePt = (
|
||||||
max(0, (widthDev - widthImg) / 2),
|
# max(0, (widthDev - widthImg) / 2),
|
||||||
max(0, (heightDev - heightImg) / 2)
|
# max(0, (heightDev - heightImg) / 2)
|
||||||
)
|
# )
|
||||||
imageBg = Image.new('RGB', size, white)
|
# imageBg = Image.new('RGB', size, white)
|
||||||
imageBg.paste(self.image, pastePt)
|
# imageBg.paste(self.image, pastePt)
|
||||||
self.image = imageBg
|
# self.image = imageBg
|
||||||
widthImg, heightImg = self.image.size
|
# widthImg, heightImg = self.image.size
|
||||||
draw = ImageDraw.Draw(self.image)
|
# draw = ImageDraw.Draw(self.image)
|
||||||
#Black rectangle
|
# #Black rectangle
|
||||||
draw.rectangle([(0, heightImg - 3), (widthImg, heightImg)], outline=black, fill=black)
|
# draw.rectangle([(0, heightImg - 3), (widthImg, heightImg)], outline=black, fill=black)
|
||||||
#White rectangle
|
# #White rectangle
|
||||||
draw.rectangle([(widthImg * file_number / files_totalnumber, heightImg - 3), (widthImg - 1, heightImg)],
|
# draw.rectangle([(widthImg * file_number / files_totalnumber, heightImg - 3), (widthImg - 1, heightImg)],
|
||||||
outline=black, fill=white)
|
# outline=black, fill=white)
|
||||||
#Making notches
|
# #Making notches
|
||||||
for i in range(1, 10):
|
# for i in range(1, 10):
|
||||||
if i <= (10 * file_number / files_totalnumber):
|
# if i <= (10 * file_number / files_totalnumber):
|
||||||
notch_colour = white # White
|
# notch_colour = white # White
|
||||||
else:
|
# else:
|
||||||
notch_colour = black # Black
|
# notch_colour = black # Black
|
||||||
draw.line([(widthImg * float(i) / 10, heightImg - 3), (widthImg * float(i) / 10, heightImg)],
|
# draw.line([(widthImg * float(i) / 10, heightImg - 3), (widthImg * float(i) / 10, heightImg)],
|
||||||
fill=notch_colour)
|
# fill=notch_colour)
|
||||||
#The 50%
|
# #The 50%
|
||||||
if i == 5:
|
# if i == 5:
|
||||||
draw.rectangle([(widthImg / 2 - 1, heightImg - 5), (widthImg / 2 + 1, heightImg)],
|
# draw.rectangle([(widthImg / 2 - 1, heightImg - 5), (widthImg / 2 + 1, heightImg)],
|
||||||
outline=black, fill=notch_colour)
|
# outline=black, fill=notch_colour)
|
||||||
return self.image
|
# 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
|
||||||
|
|||||||
279
kcc/rarfile.py
279
kcc/rarfile.py
@@ -1,6 +1,6 @@
|
|||||||
# rarfile.py
|
# 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
|
# Permission to use, copy, modify, and/or distribute this software for any
|
||||||
# purpose with or without fee is hereby granted, provided that the above
|
# purpose with or without fee is hereby granted, provided that the above
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
r"""RAR archive reader.
|
r"""RAR archive reader.
|
||||||
|
|
||||||
This is Python module for Rar archive reading. The interface
|
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:
|
Basic logic:
|
||||||
- Parse archive structure with Python.
|
- Parse archive structure with Python.
|
||||||
@@ -34,7 +34,17 @@ Example::
|
|||||||
for f in rf.infolist():
|
for f in rf.infolist():
|
||||||
print f.filename, f.file_size
|
print f.filename, f.file_size
|
||||||
if f.filename == 'README':
|
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,
|
There are few module-level parameters to tune behaviour,
|
||||||
here they are with defaults, and reason to change it::
|
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
|
# export only interesting items
|
||||||
__all__ = ['is_rarfile', 'RarInfo', 'RarFile', 'RarExtFile']
|
__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
|
## 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 struct import pack, unpack
|
||||||
from binascii import crc32
|
from binascii import crc32
|
||||||
from tempfile import mkstemp
|
from tempfile import mkstemp
|
||||||
@@ -148,45 +158,45 @@ except ImportError:
|
|||||||
## Module configuration. Can be tuned after importing.
|
## Module configuration. Can be tuned after importing.
|
||||||
##
|
##
|
||||||
|
|
||||||
# default fallback charset
|
#: default fallback charset
|
||||||
DEFAULT_CHARSET = "windows-1252"
|
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')
|
TRY_ENCODINGS = ('utf8', 'utf-16le')
|
||||||
|
|
||||||
# 'unrar', 'rar' or full path to either one
|
#: 'unrar', 'rar' or full path to either one
|
||||||
UNRAR_TOOL = "unrar"
|
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')
|
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')
|
EXTRACT_ARGS = ('x', '-y', '-idq')
|
||||||
|
|
||||||
# args for testrar()
|
#: args for testrar()
|
||||||
TEST_ARGS = ('t', '-idq')
|
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
|
USE_EXTRACT_HACK = 1
|
||||||
|
|
||||||
# limit the filesize for tmp archive usage
|
#: limit the filesize for tmp archive usage
|
||||||
HACK_SIZE_LIMIT = 20*1024*1024
|
HACK_SIZE_LIMIT = 20*1024*1024
|
||||||
|
|
||||||
# whether to parse file/archive comments.
|
#: whether to parse file/archive comments.
|
||||||
NEED_COMMENTS = 1
|
NEED_COMMENTS = 1
|
||||||
|
|
||||||
# whether to convert comments to unicode strings
|
#: whether to convert comments to unicode strings
|
||||||
UNICODE_COMMENTS = 0
|
UNICODE_COMMENTS = 0
|
||||||
|
|
||||||
# When RAR is corrupt, stopping on bad header is better
|
#: When RAR is corrupt, stopping on bad header is better
|
||||||
# On unknown/misparsed RAR headers reporting is better
|
#: On unknown/misparsed RAR headers reporting is better
|
||||||
REPORT_BAD_HEADER = 0
|
REPORT_BAD_HEADER = 0
|
||||||
|
|
||||||
# Convert RAR time tuple into datetime() object
|
#: Convert RAR time tuple into datetime() object
|
||||||
USE_DATETIME = 0
|
USE_DATETIME = 0
|
||||||
|
|
||||||
# Separator for path name components. RAR internally uses '\\'.
|
#: Separator for path name components. RAR internally uses '\\'.
|
||||||
# Use '/' to be similar with zipfile.
|
#: Use '/' to be similar with zipfile.
|
||||||
PATH_SEP = '\\'
|
PATH_SEP = '\\'
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -320,6 +330,8 @@ class RarMemoryError(RarExecError):
|
|||||||
"""Memory error"""
|
"""Memory error"""
|
||||||
class RarCreateError(RarExecError):
|
class RarCreateError(RarExecError):
|
||||||
"""Create error"""
|
"""Create error"""
|
||||||
|
class RarNoFilesError(RarExecError):
|
||||||
|
"""No files that match pattern were found"""
|
||||||
class RarUserBreak(RarExecError):
|
class RarUserBreak(RarExecError):
|
||||||
"""User stop"""
|
"""User stop"""
|
||||||
class RarUnknownError(RarExecError):
|
class RarUnknownError(RarExecError):
|
||||||
@@ -335,49 +347,57 @@ def is_rarfile(fn):
|
|||||||
|
|
||||||
|
|
||||||
class RarInfo(object):
|
class RarInfo(object):
|
||||||
'''An entry in rar archive.
|
r'''An entry in rar archive.
|
||||||
|
|
||||||
@ivar filename:
|
:mod:`zipfile`-compatible fields:
|
||||||
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).
|
|
||||||
|
|
||||||
@ivar mtime:
|
filename
|
||||||
Optional time field: Modification time, with float seconds.
|
File name with relative path.
|
||||||
Same as .date_time but with more precision.
|
Default path separator is '\\', to change set rarfile.PATH_SEP.
|
||||||
@ivar ctime:
|
Always unicode string.
|
||||||
Optional time field: creation time, with float seconds.
|
date_time
|
||||||
@ivar atime:
|
Modification time, tuple of (year, month, day, hour, minute, second).
|
||||||
Optional time field: last access time, with float seconds.
|
Or datetime() object if USE_DATETIME is set.
|
||||||
@ivar arctime:
|
file_size
|
||||||
Optional time field: archival time, with float seconds.
|
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__ = (
|
__slots__ = (
|
||||||
@@ -417,7 +437,7 @@ class RarInfo(object):
|
|||||||
'header_offset',
|
'header_offset',
|
||||||
'salt',
|
'salt',
|
||||||
'volume_file',
|
'volume_file',
|
||||||
)
|
)
|
||||||
|
|
||||||
def isdir(self):
|
def isdir(self):
|
||||||
'''Returns True if the entry is a directory.'''
|
'''Returns True if the entry is a directory.'''
|
||||||
@@ -431,19 +451,27 @@ class RarInfo(object):
|
|||||||
|
|
||||||
class RarFile(object):
|
class RarFile(object):
|
||||||
'''Parse RAR structure, provide access to files in archive.
|
'''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):
|
def __init__(self, rarfile, mode="r", charset=None, info_callback=None, crc_check = True):
|
||||||
"""Open and parse a RAR archive.
|
"""Open and parse a RAR archive.
|
||||||
|
|
||||||
@param rarfile: archive file name
|
Parameters:
|
||||||
@param mode: only 'r' is supported.
|
|
||||||
@param charset: fallback charset to use, if filenames are not already Unicode-enabled.
|
rarfile
|
||||||
@param info_callback: debug callback, gets to see all archive entries.
|
archive file name
|
||||||
@param crc_check: set to False to disable CRC checks
|
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.rarfile = rarfile
|
||||||
self.comment = None
|
self.comment = None
|
||||||
@@ -455,6 +483,7 @@ class RarFile(object):
|
|||||||
self._needs_password = False
|
self._needs_password = False
|
||||||
self._password = None
|
self._password = None
|
||||||
self._crc_check = crc_check
|
self._crc_check = crc_check
|
||||||
|
self._vol_list = []
|
||||||
|
|
||||||
self._main = None
|
self._main = None
|
||||||
|
|
||||||
@@ -487,6 +516,14 @@ class RarFile(object):
|
|||||||
'''Return RarInfo objects for all files/directories in archive.'''
|
'''Return RarInfo objects for all files/directories in archive.'''
|
||||||
return self._info_list
|
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):
|
def getinfo(self, fname):
|
||||||
'''Return RarInfo for file.'''
|
'''Return RarInfo for file.'''
|
||||||
|
|
||||||
@@ -508,7 +545,8 @@ class RarFile(object):
|
|||||||
raise NoRarEntry("No such file: "+fname)
|
raise NoRarEntry("No such file: "+fname)
|
||||||
|
|
||||||
def open(self, fname, mode = 'r', psw = None):
|
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
|
The object implements io.RawIOBase interface, so it can
|
||||||
be further wrapped with io.BufferedReader and io.TextIOWrapper.
|
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
|
uncompressed files, on compressed files the seeking is implemented
|
||||||
by reading ahead and/or restarting the decompression.
|
by reading ahead and/or restarting the decompression.
|
||||||
|
|
||||||
@param fname: file name or RarInfo instance.
|
Parameters:
|
||||||
@param mode: must be 'r'
|
|
||||||
@param psw: password to use for extracting.
|
fname
|
||||||
|
file name or RarInfo instance.
|
||||||
|
mode
|
||||||
|
must be 'r'
|
||||||
|
psw
|
||||||
|
password to use for extracting.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
if mode != 'r':
|
if mode != 'r':
|
||||||
@@ -569,8 +612,12 @@ class RarFile(object):
|
|||||||
|
|
||||||
For longer files using .open() may be better idea.
|
For longer files using .open() may be better idea.
|
||||||
|
|
||||||
@param fname: filename or RarInfo instance
|
Parameters:
|
||||||
@param psw: password to use for extracting.
|
|
||||||
|
fname
|
||||||
|
filename or RarInfo instance
|
||||||
|
psw
|
||||||
|
password to use for extracting.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
f = self.open(fname, 'r', psw)
|
f = self.open(fname, 'r', psw)
|
||||||
@@ -591,9 +638,14 @@ class RarFile(object):
|
|||||||
def extract(self, member, path=None, pwd=None):
|
def extract(self, member, path=None, pwd=None):
|
||||||
"""Extract single file into current directory.
|
"""Extract single file into current directory.
|
||||||
|
|
||||||
@param member: filename or RarInfo instance
|
Parameters:
|
||||||
@param path: optional destination path
|
|
||||||
@param pwd: optional password to use
|
member
|
||||||
|
filename or RarInfo instance
|
||||||
|
path
|
||||||
|
optional destination path
|
||||||
|
pwd
|
||||||
|
optional password to use
|
||||||
"""
|
"""
|
||||||
if isinstance(member, RarInfo):
|
if isinstance(member, RarInfo):
|
||||||
fname = member.filename
|
fname = member.filename
|
||||||
@@ -604,9 +656,14 @@ class RarFile(object):
|
|||||||
def extractall(self, path=None, members=None, pwd=None):
|
def extractall(self, path=None, members=None, pwd=None):
|
||||||
"""Extract all files into current directory.
|
"""Extract all files into current directory.
|
||||||
|
|
||||||
@param path: optional destination path
|
Parameters:
|
||||||
@param members: optional filename or RarInfo instance list to extract
|
|
||||||
@param pwd: optional password to use
|
path
|
||||||
|
optional destination path
|
||||||
|
members
|
||||||
|
optional filename or RarInfo instance list to extract
|
||||||
|
pwd
|
||||||
|
optional password to use
|
||||||
"""
|
"""
|
||||||
fnlist = []
|
fnlist = []
|
||||||
if members is not None:
|
if members is not None:
|
||||||
@@ -691,6 +748,7 @@ class RarFile(object):
|
|||||||
more_vols = 0
|
more_vols = 0
|
||||||
endarc = 0
|
endarc = 0
|
||||||
volfile = self.rarfile
|
volfile = self.rarfile
|
||||||
|
self._vol_list = [self.rarfile]
|
||||||
while 1:
|
while 1:
|
||||||
if endarc:
|
if endarc:
|
||||||
h = None # don't read past ENDARC
|
h = None # don't read past ENDARC
|
||||||
@@ -705,6 +763,7 @@ class RarFile(object):
|
|||||||
self._fd = fd
|
self._fd = fd
|
||||||
more_vols = 0
|
more_vols = 0
|
||||||
endarc = 0
|
endarc = 0
|
||||||
|
self._vol_list.append(volfile)
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
h.volume = volume
|
h.volume = volume
|
||||||
@@ -729,7 +788,7 @@ class RarFile(object):
|
|||||||
# RAR 2.x does not write RAR_BLOCK_ENDARC
|
# RAR 2.x does not write RAR_BLOCK_ENDARC
|
||||||
if h.flags & RAR_FILE_SPLIT_AFTER:
|
if h.flags & RAR_FILE_SPLIT_AFTER:
|
||||||
more_vols = 1
|
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:
|
if volume == 0 and h.flags & RAR_FILE_SPLIT_BEFORE:
|
||||||
raise NeedFirstVolume("Need to start from first volume")
|
raise NeedFirstVolume("Need to start from first volume")
|
||||||
|
|
||||||
@@ -950,7 +1009,7 @@ class RarFile(object):
|
|||||||
pos += S_COMMENT_HDR.size
|
pos += S_COMMENT_HDR.size
|
||||||
data = hdata[pos : pos_next]
|
data = hdata[pos : pos_next]
|
||||||
cmt = rar_decompress(ver, meth, data, declen, sflags,
|
cmt = rar_decompress(ver, meth, data, declen, sflags,
|
||||||
crc, self._password)
|
crc, self._password)
|
||||||
if not self._crc_check:
|
if not self._crc_check:
|
||||||
h.comment = self._decode_comment(cmt)
|
h.comment = self._decode_comment(cmt)
|
||||||
elif crc32(cmt) & 0xFFFF == crc:
|
elif crc32(cmt) & 0xFFFF == crc:
|
||||||
@@ -1073,7 +1132,7 @@ class RarFile(object):
|
|||||||
|
|
||||||
# decompress
|
# decompress
|
||||||
cmt = rar_decompress(inf.extract_version, inf.compress_type, data,
|
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
|
# check crc
|
||||||
if self._crc_check:
|
if self._crc_check:
|
||||||
@@ -1208,7 +1267,7 @@ class UnicodeFilename:
|
|||||||
|
|
||||||
|
|
||||||
class RarExtFile(RawIOBase):
|
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.
|
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 short reads - .read() and .readinfo() read as much as requested.
|
||||||
- no internal buffer, use io.BufferedReader for that.
|
- no internal buffer, use io.BufferedReader for that.
|
||||||
|
|
||||||
@ivar name:
|
If :mod:`io` module is available (Python 2.6+, 3.x), then this calls
|
||||||
filename of the archive entry.
|
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):
|
#: Filename of the archive entry
|
||||||
"""Fill common fields"""
|
name = None
|
||||||
|
|
||||||
|
def __init__(self, rf, inf):
|
||||||
RawIOBase.__init__(self)
|
RawIOBase.__init__(self)
|
||||||
|
|
||||||
# standard io.* properties
|
# standard io.* properties
|
||||||
@@ -1323,7 +1384,13 @@ class RarExtFile(RawIOBase):
|
|||||||
return self.inf.file_size - self.remain
|
return self.inf.file_size - self.remain
|
||||||
|
|
||||||
def seek(self, ofs, whence = 0):
|
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
|
# disable crc check when seeking
|
||||||
self.crc_check = 0
|
self.crc_check = 0
|
||||||
@@ -1372,8 +1439,17 @@ class RarExtFile(RawIOBase):
|
|||||||
"""Returns True"""
|
"""Returns True"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def writable(self):
|
||||||
|
"""Returns False.
|
||||||
|
|
||||||
|
Writing is not supported."""
|
||||||
|
return False
|
||||||
|
|
||||||
def seekable(self):
|
def seekable(self):
|
||||||
"""Returns True"""
|
"""Returns True.
|
||||||
|
|
||||||
|
Seeking is supported, although it's slow on compressed files.
|
||||||
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def readall(self):
|
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
|
date = 0
|
||||||
mode = 0x20
|
mode = 0x20
|
||||||
fhdr = S_FILE_HDR.pack(len(data), declen, RAR_OS_MSDOS, crc,
|
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
|
fhdr += fname
|
||||||
if flags & RAR_FILE_SALT:
|
if flags & RAR_FILE_SALT:
|
||||||
if not salt:
|
if not salt:
|
||||||
@@ -1757,8 +1833,15 @@ def custom_popen(cmd):
|
|||||||
creationflags = 0x08000000 # CREATE_NO_WINDOW
|
creationflags = 0x08000000 # CREATE_NO_WINDOW
|
||||||
|
|
||||||
# run command
|
# run command
|
||||||
p = Popen(cmd, bufsize = 0, stdout = PIPE, stdin = PIPE, stderr = STDOUT,
|
try:
|
||||||
creationflags = creationflags)
|
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
|
return p
|
||||||
|
|
||||||
def check_returncode(p, out):
|
def check_returncode(p, out):
|
||||||
@@ -1770,9 +1853,9 @@ def check_returncode(p, out):
|
|||||||
|
|
||||||
# map return code to exception class
|
# map return code to exception class
|
||||||
errmap = [None,
|
errmap = [None,
|
||||||
RarWarning, RarFatalError, RarCRCError, RarLockedArchiveError,
|
RarWarning, RarFatalError, RarCRCError, RarLockedArchiveError,
|
||||||
RarWriteError, RarOpenError, RarUserError, RarMemoryError,
|
RarWriteError, RarOpenError, RarUserError, RarMemoryError,
|
||||||
RarCreateError] # codes from rar.txt
|
RarCreateError, RarNoFilesError] # codes from rar.txt
|
||||||
if code > 0 and code < len(errmap):
|
if code > 0 and code < len(errmap):
|
||||||
exc = errmap[code]
|
exc = errmap[code]
|
||||||
elif code == 255:
|
elif code == 255:
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -15,7 +15,7 @@ use_setuptools()
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
NAME = "KindleComicConverter"
|
NAME = "KindleComicConverter"
|
||||||
VERSION = "2.7"
|
VERSION = "2.10"
|
||||||
MAIN = "kcc.py"
|
MAIN = "kcc.py"
|
||||||
|
|
||||||
includefiles = ['README.md', 'MANIFEST.in', 'LICENSE.txt', 'comic2ebook.ico', 'comic2ebook.icns']
|
includefiles = ['README.md', 'MANIFEST.in', 'LICENSE.txt', 'comic2ebook.ico', 'comic2ebook.icns']
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ sys.path.insert(0, 'kcc')
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name = "KindleComicConverter",
|
name = "KindleComicConverter",
|
||||||
version = "2.7",
|
version = "2.10",
|
||||||
author = "Ciro Mattia Gonano",
|
author = "Ciro Mattia Gonano",
|
||||||
author_email = "ciromattia@gmail.com",
|
author_email = "ciromattia@gmail.com",
|
||||||
description = "A tool to convert comics (CBR/CBZ/PDFs/image folders) to MOBI.",
|
description = "A tool to convert comics (CBR/CBZ/PDFs/image folders) to MOBI.",
|
||||||
|
|||||||
Reference in New Issue
Block a user