Merge branch 'FooSoft-master'

This commit is contained in:
Jan Martin 2015-09-29 22:19:00 +10:00
commit e72891e9c3
11 changed files with 184 additions and 13 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ dist
*.pyo *.pyo
*.mngl *.mngl
*.cbz *.cbz
*.bat

View File

@ -1,3 +1,4 @@
<<<<<<< HEAD
## Downloads ## Downloads
- [Mangle for Windows](ftp://foosoft.net/releases/mangle/mangle_win.zip) - [Mangle for Windows](ftp://foosoft.net/releases/mangle/mangle_win.zip)
- [Mangle for MacOS (Old)](ftp://foosoft.net/releases/mangle/mangle_osx.zip) - [Mangle for MacOS (Old)](ftp://foosoft.net/releases/mangle/mangle_osx.zip)
@ -9,3 +10,100 @@
- [ReportLab](https://pypi.python.org/pypi/reportlab) - [ReportLab](https://pypi.python.org/pypi/reportlab)
- [natsort](https://pypi.python.org/pypi/natsort/3.0.1) - [natsort](https://pypi.python.org/pypi/natsort/3.0.1)
- [py2exe](http://www.py2exe.org/) (optional, for Windows distribution only) - [py2exe](http://www.py2exe.org/) (optional, for Windows distribution only)
=======
# Mangle #
Many years ago I received an [Amazon Kindle](http://en.wikipedia.org/wiki/Kindle) gift. I immediately began playing
around with it and reading about certain undocumented features that the Kindle has to offer. After a couple of hours I
discovered it to be the perfect device for reading [Manga](http://en.wikipedia.org/wiki/Manga) is almost always
grayscale, and the aspect ratio fits the Kindle's 600x800 pixel screen almost perfectly. Better yet, the Kindle's
undocumented image viewer actually keeps track of the last image you viewed and thus you are always able to return to
the page you left off on when you power on your Kindle. The device supports several popular image formats (jpeg, png,
gif, etc), and is able to dither and downscale images to fit the screen.
However... The Kindle's image viewer does have certain shortcomings:
* The Kindle is very picky about file format; any additional embedded data (thumbnails, comments, possibly even exif
data) can confuse it. As a result, images may not display properly or even not at all (which actually prevents you
from reading the given book, as one bad panel will prevent you from viewing subsequent images).
* The first image that you view in a Manga (until the Kindle first writes the "bookmark" file) seems to be arbitrary
even when files are named sequentially. About half the time it will correctly pick the first file in the batch, at
other times it will pick out some other image seemingly at random.
* Normally for Kindle to find your Manga scans you have to press Alt+Z on the home screen. I haven't always had luck
with it correctly identifying image directories. At other times, after finding an image directory the Kindle will
appear to hang while trying to access it (forcing you to return to the home screen).
* The Kindle image viewer has no functionality to rotate images. So if there is a horizontally large image (such as
what often happens with dual-page scans), it can be difficult to make out the text because the image is simply
scaled to fit (consequently leaving a lot of wasted space at the bottom of the screen).
* Scanlation images are oftentimes much larger than the 600x800 screen; not only does this make them take more space
on your memory card but it also slows down image loading (the Kindle has to read more data off of the slow SD card
and scale the image). Scanlations often also include color scans of covers and inserts which take up more space than
a grayscale equivalent (which is would be fine for the Kindle's limited display).
* Kindle's image viewer provides no way to sort images (to determine in which order they are shown). This can be very
problematic especially considering that scanlation groups have differing naming conventions, and as a result files
from later chapters may appear before earlier ones when you are reading your Manga (spoilers ftl).
I was annoyed with these issues and thus Mangle was born (the program name is a mix of "Manga" and "Kindle" in case you
haven't figured it out yet; I thought it was pretty clever at the time). Fortunately you can get all the benefits of my
work without really doing anything (and it won't even cost you anything since Mangle is free,
[GPL](http://www.gnu.org/licenses/gpl-3.0.txt) software. With Mangle you can easily:
* Sort and organize images from different directories; bulk rename feature for output to the Kindle.
* Optionally re-save images in a format Kindle will be sure to understand with no visible quality loss.
* Downsample and rotate images for optimal viewing on Kindle, convert to grayscale to save space and improve contrast.
* Automatically generate book meta-data so that your Manga is always properly detected and viewable in-order.
Here is a recent screenshot showing off some of the export options that you can configure on a per-book basis in Mangle:
![Mangle options dialog](http://foosoft.net/projects/mangle/img/options.png)
You can also check out what Mangle output looks like on the Kindle on the [action
shots](http://foosoft.net/projects/mangle/action/) page.
Mangle is cross platform, and doesn't require an install (it's a standalone executable that you can run from anywhere).
It is also "environmentally friendly" by not messing with your registry or modifying your system in any way. If you
ever want to uninstall it, just delete the executable and you're done.
## Usage Instructions ##
Mangle is pretty easy to use, so this won't be really in-depth. If you have any questions drop me a line though.
1. Add images to the current book by selecting the `Book | Add | Files` or `Book | Add | Directory` menu items.
2. If certain images are not in the order you want, select them in the window, and select the `Book | Shift | Up` or
`Book | Shift | Down` menu items.
3. Configure the book title and image processing options by selecting `Book | Options`; this will be the title you see
in the Kindle home menu.
4. Create a root-level directory on your SD memory card/Kindle called `pictures` (case might matter).
5. Once you are satisfied with the your images and options select `Book | Export` and select the `pictures` directory
you just created.
6. After the export is complete your new Manga books will show up along with all your other books (if they don't for
some reason, press `Alt+Z` while on the home menu).
## The Usual Disclaimer ##
You probably know how this goes by now... Mess around with your Kindle at your own risk. Honestly, nothing bad is going
to happen; however if something *does* then it's your problem.
## Running From Source ##
Because Mangle is written in Python, a scripting language, it's trivial to get it up and running on the operating system
of your choice. First you should make sure that you have the required dependencies installed:
* [PyQT4](http://www.riverbankcomputing.com/software/pyqt/download)
* [Python 2.7](http://www.python.org/download/releases/2.7/)
* [Python Imaging Library (PIL)](http://www.pythonware.com/products/pil/)
* [ReportLab](https://pypi.python.org/pypi/reportlab)
* [py2exe](http://www.py2exe.org/) (optional, for Windows distribution only)
Now you can fetch the [latest version of the code](https://github.com/FooSoft/mangle/) and run the `mangle.pyw` script
to execute Mangle.
## Downloads ##
If you don't want to run Mangle from source, you can use the following pre-built binaries. As I don't have the means to
make MacOS X releases myself, I am providing the slightly out of date (and unsupported) package built by Rob White in
its place. Linux users should execute the Python scripts with the interpreter and libraries installed on their system.
* [magnle_win.zip](http://dl.foosoft.net/mangle/mangle_win.zip)
* [mangle_osx.zip](http://dl.foosoft.net/mangle/mangle_osx.zip)
>>>>>>> cedfe3fc512a8f58680d8fa0618143d423c3112d

View File

@ -13,14 +13,13 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
from os.path import basename from os.path import basename
import os.path import os.path
import tempfile import tempfile
from zipfile import ZipFile from zipfile import ZipFile
from PyQt4 import QtGui, QtCore, QtXml, uic from PyQt4 import QtGui, QtCore, QtXml, uic
from natsort import natsorted
from about import DialogAbout from about import DialogAbout
from convert import DialogConvert from convert import DialogConvert
@ -29,6 +28,13 @@ from options import DialogOptions
import util import util
# Sort function use to sort files in a natural order, by lowering
# characters, and manage multi levels of integers (tome 1/ page 1.jpg, etc etc)
def natural_key(string_):
"""See http://www.codinghorror.com/blog/archives/001018.html"""
return [int(s) if s.isdigit() else s.lower() for s in re.split(r'(\d+)', string_)]
class Book(object): class Book(object):
DefaultDevice = 'Kindle Paperwhite' DefaultDevice = 'Kindle Paperwhite'
DefaultOutputFormat = 'CBZ only' DefaultOutputFormat = 'CBZ only'
@ -368,13 +374,15 @@ class MainWindowBook(QtGui.QMainWindow):
for i in xrange(0, self.listWidgetFiles.count()): for i in xrange(0, self.listWidgetFiles.count()):
filenamesListed.append(self.listWidgetFiles.item(i).text()) filenamesListed.append(self.listWidgetFiles.item(i).text())
for filename in natsorted(filenames): # Get files but in a natural sorted order
for filename in sorted(filenames, key=natural_key):
if filename not in filenamesListed: if filename not in filenamesListed:
filename = QtCore.QString(filename) filename = QtCore.QString(filename)
self.listWidgetFiles.addItem(filename) self.listWidgetFiles.addItem(filename)
self.book.images.append(filename) self.book.images.append(filename)
self.book.modified = True self.book.modified = True
def addImageDirs(self, directories): def addImageDirs(self, directories):
filenames = [] filenames = []
@ -387,6 +395,7 @@ class MainWindowBook(QtGui.QMainWindow):
self.addImageFiles(filenames) self.addImageFiles(filenames)
def addCBZFiles(self, filenames): def addCBZFiles(self, filenames):
directories = [] directories = []
tempDir = tempfile.gettempdir() tempDir = tempfile.gettempdir()

View File

@ -129,6 +129,14 @@ class DialogConvert(QtGui.QProgressDialog):
# Change target once again for left page # Change target once again for left page
target = os.path.join(self.bookPath, '%05d.png' % (index * 2 + 1)) target = os.path.join(self.bookPath, '%05d.png' % (index * 2 + 1))
# For right page (if requested), but in inverted mode
if(self.book.imageFlags & ImageFlags.SplitInverse):
# New path based on modified index
target = os.path.join(self.bookPath, '%05d.png' % (index * 2 + 0))
self.convertAndSave(source, target, device, flags ^ ImageFlags.SplitInverse | ImageFlags.SplitLeft, archive, pdf)
# Change target once again for left page
target = os.path.join(self.bookPath, '%05d.png' % (index * 2 + 1))
# Convert page # Convert page
self.convertAndSave(source, target, device, flags, archive, pdf) self.convertAndSave(source, target, device, flags, archive, pdf)

View File

@ -19,14 +19,17 @@ import os
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
class ImageFlags: class ImageFlags:
Orient = 1 << 0 Orient = 1 << 0
Resize = 1 << 1 Resize = 1 << 1
Frame = 1 << 2 Frame = 1 << 2
Quantize = 1 << 3 Quantize = 1 << 3
Stretch = 1 << 4 Stretch = 1 << 4
Split = 1 << 5 Split = 1 << 5 # split right then left
SplitRight = 1 << 6 SplitRight = 1 << 6 # split only the right page
SplitLeft = 1 << 7 # split only the left page
SplitInverse = 1 << 8 # split left then right page
class KindleData: class KindleData:
@ -82,22 +85,39 @@ class KindleData:
'Kindle DX': ((824, 1200), Palette15a), 'Kindle DX': ((824, 1200), Palette15a),
'Kindle DXG': ((824, 1200), Palette15a), 'Kindle DXG': ((824, 1200), Palette15a),
'Kindle Touch': ((600, 800), Palette15a), 'Kindle Touch': ((600, 800), Palette15a),
'Kindle Paperwhite': ((758, 1024), Palette15b) # resolution given in manual, see http://kindle.s3.amazonaws.com/Kindle_Paperwhite_Users_Guide.pdf 'Kindle Paperwhite': ((758, 1024), Palette15b), # resolution given in manual, see http://kindle.s3.amazonaws.com/Kindle_Paperwhite_Users_Guide.pdf
'KoBo Aura H2o': ((1080, 1430), Palette15a), # resolution from http://www.fnac.com/Liseuse-Numerique-Kobo-by-Fnac-Kobo-Aura-H2O-Noir/a7745120/w-4
} }
# decorate a function that use image, *** and if there
# is an exception raise by PIL (IOError) then return
# the original image because PIL cannot manage it
def protect_bad_image(func):
def func_wrapper(*args, **kwargs):
# If cannot convert (like a bogus image) return the original one
# args will be "image" and other params are after
try:
return func(*args, **kwargs)
except IOError: # Exception from PIL about bad image
return args[0]
return func_wrapper
@protect_bad_image
def splitLeft(image): def splitLeft(image):
widthImg, heightImg = image.size widthImg, heightImg = image.size
return image.crop((0, 0, widthImg / 2, heightImg)) return image.crop((0, 0, widthImg / 2, heightImg))
@protect_bad_image
def splitRight(image): def splitRight(image):
widthImg, heightImg = image.size widthImg, heightImg = image.size
return image.crop((widthImg / 2, 0, widthImg, heightImg)) return image.crop((widthImg / 2, 0, widthImg, heightImg))
@protect_bad_image
def quantizeImage(image, palette): def quantizeImage(image, palette):
colors = len(palette) / 3 colors = len(palette) / 3
if colors < 256: if colors < 256:
@ -109,10 +129,14 @@ def quantizeImage(image, palette):
return image.quantize(palette=palImg) return image.quantize(palette=palImg)
@protect_bad_image
def stretchImage(image, size): def stretchImage(image, size):
widthDev, heightDev = size widthDev, heightDev = size
return image.resize((widthDev, heightDev), Image.ANTIALIAS) return image.resize((widthDev, heightDev), Image.ANTIALIAS)
@protect_bad_image
def resizeImage(image, size): def resizeImage(image, size):
widthDev, heightDev = size widthDev, heightDev = size
widthImg, heightImg = image.size widthImg, heightImg = image.size
@ -136,19 +160,21 @@ def resizeImage(image, size):
return image.resize((widthImg, heightImg), Image.ANTIALIAS) return image.resize((widthImg, heightImg), Image.ANTIALIAS)
@protect_bad_image
def formatImage(image): def formatImage(image):
if image.mode == 'RGB': if image.mode == 'RGB':
return image return image
return image.convert('RGB') return image.convert('RGB')
@protect_bad_image
def orientImage(image, size): def orientImage(image, size):
widthDev, heightDev = size widthDev, heightDev = size
widthImg, heightImg = image.size widthImg, heightImg = image.size
if (widthImg > heightImg) != (widthDev > heightDev): if (widthImg > heightImg) != (widthDev > heightDev):
return image.rotate(90, Image.BICUBIC, True) return image.rotate(90, Image.BICUBIC, True)
return image return image
@ -204,10 +230,20 @@ def convertImage(source, target, device, flags):
# Format according to palette # Format according to palette
image = formatImage(image) image = formatImage(image)
# Apply flag transforms # Apply flag transforms
# Second pass of first split
if flags & ImageFlags.SplitRight: if flags & ImageFlags.SplitRight:
image = splitRight(image) image = splitRight(image)
if flags & ImageFlags.Split: # First pass of first split option
if (flags & ImageFlags.Split):
image = splitLeft(image) image = splitLeft(image)
# First pass of second splitting option
if flags & ImageFlags.SplitLeft:
image = splitLeft(image)
# second pass of second splitting option
if (flags & ImageFlags.SplitInverse):
image = splitRight(image)
if flags & ImageFlags.Orient: if flags & ImageFlags.Orient:
image = orientImage(image, size) image = orientImage(image, size)
if flags & ImageFlags.Resize: if flags & ImageFlags.Resize:

BIN
mangle/img/book.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 827 B

After

Width:  |  Height:  |  Size: 545 B

View File

@ -66,6 +66,8 @@ class DialogOptions(QtGui.QDialog):
imageFlags |= ImageFlags.Frame imageFlags |= ImageFlags.Frame
if self.checkboxSplit.isChecked(): if self.checkboxSplit.isChecked():
imageFlags |= ImageFlags.Split imageFlags |= ImageFlags.Split
if self.checkboxSplitInverse.isChecked():
imageFlags |= ImageFlags.SplitInverse
modified = ( modified = (
self.book.title != title or self.book.title != title or

View File

@ -180,6 +180,10 @@
</property> </property>
</action> </action>
<action name="actionBookOptions"> <action name="actionBookOptions">
<property name="icon">
<iconset>
<normaloff>../img/book.png</normaloff>../img/book.png</iconset>
</property>
<property name="text"> <property name="text">
<string>&amp;Options...</string> <string>&amp;Options...</string>
</property> </property>

View File

@ -101,6 +101,11 @@
<string>Kindle Paperwhite</string> <string>Kindle Paperwhite</string>
</property> </property>
</item> </item>
<item>
<property name="text">
<string>KoBo Aura H2o</string>
</property>
</item>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
@ -170,6 +175,13 @@
<string>Split images into two pages (right, left)</string> <string>Split images into two pages (right, left)</string>
</property> </property>
</widget> </widget>
</item>
<item>
<widget class="QCheckBox" name="checkboxSplitInverse">
<property name="text">
<string>Split images into two pages (left, right)</string>
</property>
</widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">

View File

@ -32,6 +32,7 @@ setup(
('mangle/img', ['mangle/img/add_directory.png', ('mangle/img', ['mangle/img/add_directory.png',
'mangle/img/add_file.png', 'mangle/img/add_file.png',
'mangle/img/banner_about.png', 'mangle/img/banner_about.png',
'mangle/img/book.png',
'mangle/img/export_book.png', 'mangle/img/export_book.png',
'mangle/img/file_new.png', 'mangle/img/file_new.png',
'mangle/img/file_open.png', 'mangle/img/file_open.png',