├── .gitignore ├── INSTALL.md ├── LICENSE ├── README.md ├── img ├── kindle1-thumb.png ├── kindle1.png ├── kindle2-thumb.png ├── kindle2.png ├── kindle3-thumb.png ├── kindle3.png ├── kindle4-thumb.png └── kindle4.png ├── mangle-cli.py ├── mangle.pyw ├── mangle ├── __init__.py ├── about.py ├── book.py ├── cbz.py ├── convert.py ├── image.py ├── img │ ├── add_directory.png │ ├── add_file.png │ ├── banner_about.png │ ├── book.png │ ├── export_book.png │ ├── file_new.png │ ├── file_open.png │ ├── remove_files.png │ ├── save_file.png │ ├── shift_down.png │ └── shift_up.png ├── options.py ├── pdfimage.py ├── ui │ ├── about.ui │ ├── book.ui │ └── options.ui └── util.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # generated manga 141 | *.mngl 142 | [0-9][0-9][0-9][0-9][0-9].png 143 | *.manga 144 | *.manga_save 145 | *.cbz 146 | *.pdf 147 | 148 | # vscode 149 | .vscode/ 150 | 151 | *.bat 152 | .idea/ 153 | 154 | *.DS_Store 155 | .AppleDouble 156 | .LSOverride 157 | 158 | # Icon must end with two \r 159 | Icon 160 | 161 | 162 | # Thumbnails 163 | ._* 164 | 165 | # Files that might appear in the root of a volume 166 | .DocumentRevisions-V100 167 | .fseventsd 168 | .Spotlight-V100 169 | .TemporaryItems 170 | .Trashes 171 | .VolumeIcon.icns 172 | .com.apple.timemachine.donotpresent 173 | 174 | # Directories potentially created on remote AFP share 175 | .AppleDB 176 | .AppleDesktop 177 | Network Trash Folder 178 | Temporary Items 179 | .apdisk 180 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Install Instructions 2 | 3 | For local development / building stand-alone binaries. 4 | 5 | You need [Python 2.7](https://www.python.org/downloads/release/python-2718/). 6 | 7 | If you are using Windows 8 | and want to build a stand-alone binary 9 | download the `Windows x86 MSI installer` 10 | because `py2exe` for Python 2 doesn't work with 64 bit. 11 | Any performance increase from 64 bit is negligible. 12 | 13 | Be sure to add `python` to `PATH` during installation. 14 | 15 | Install [virtualenv](https://virtualenv.pypa.io/en/stable/) 16 | globally, if you don't already have it. 17 | 18 | ``` 19 | > pip install virtualenv 20 | ``` 21 | 22 | and install all dependencies in a venv in the mangle directory, e.g. 23 | 24 | ``` 25 | ...\mangle> virtualenv venv 26 | > venv\Scripts\activate 27 | (venv) > pip install -r requirements.txt 28 | ``` 29 | 30 | You can run the app via 31 | 32 | ``` 33 | (venv) > python mangle.pyw 34 | ``` 35 | 36 | Optionally, you can install all the dependencies globally 37 | so you can simply click on the `mangle.pyw` file to run it. 38 | 39 | # Building an Executable (Windows) 40 | 41 | To actually build a stand-alone `.exe`, install 42 | 43 | [Microsoft Visual C++ Compiler for Python 2.7](https://www.microsoft.com/en-us/download/details.aspx?id=44266) 44 | 45 | A standalone binary can be created in the `dist` folder via 46 | 47 | ``` 48 | (venv) > python setup.py 49 | ``` 50 | 51 | You may get an error which can be solved by looking at 52 | https://stackoverflow.com/questions/38444230/error-converting-gui-to-standalone-executable-using-py2exe 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011-2019 Alex Yatskov 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License 14 | along with this program. If not, see . 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mangle 2 | 3 | Mangle is a cross-platform image converter and optimizer built for reading Manga on the Amazon Kindle and other E-ink 4 | devices written in Python. With this application you can easily: 5 | 6 | * Sort and organize images from different directories; bulk rename feature exists for output to the Kindle. 7 | * Optionally re-save images in a format Kindle will be sure to understand with no visible quality loss. 8 | * Downsample and rotate images for optimal viewing on Kindle, convert to grayscale to save space and improve contrast. 9 | * Automatically generate book meta-data so that your Manga is always properly detected and viewable in-order. 10 | 11 | [![](img/kindle1-thumb.png)](img/kindle1.png) 12 | [![](img/kindle2-thumb.png)](img/kindle2.png) 13 | [![](img/kindle3-thumb.png)](img/kindle3.png) 14 | [![](img/kindle4-thumb.png)](img/kindle4.png) 15 | 16 | ## Motivation 17 | 18 | Many years ago I received an Amazon Kindle as a gift. I immediately began playing around with it and reading about 19 | certain undocumented features that the Kindle has to offer. After a couple of hours I discovered it to be the perfect 20 | device for reading Manga is almost always grayscale, and the aspect ratio fits the Kindle's 600x800 pixel screen almost 21 | perfectly. Better yet, the Kindle's undocumented image viewer actually keeps track of the last image you viewed and thus 22 | you are always able to return to the page you left off on when you power on your Kindle. The device supports several 23 | popular image formats (jpeg, png, gif, etc), and is able to dither and downscale images to fit the screen. 24 | 25 | However... The Kindle's image viewer does have certain shortcomings: 26 | 27 | * The Kindle is very picky about file format; any additional embedded data (thumbnails, comments, possibly even EXIF 28 | data) can confuse it. As a result, images may not display properly or even not at all (which actually prevents you 29 | from reading the given book, as one bad panel will prevent you from viewing subsequent images). 30 | * The first image that you view in a Manga (until the Kindle first writes the "bookmark" file) seems to be arbitrary 31 | even when files are named sequentially. About half the time it will correctly pick the first file in the batch, at 32 | other times it will pick out some other image seemingly at random. 33 | * Normally for Kindle to find your Manga scans you have to press Alt + Z on the home screen. I 34 | haven't always had luck with it correctly identifying image directories. At other times, after finding an image 35 | directory the Kindle will appear to hang while trying to access it (forcing you to return to the home screen). 36 | * The Kindle image viewer has no functionality to rotate images. So if there is a horizontally large image (such as 37 | what often happens with dual-page scans), it can be difficult to make out the text because the image is simply 38 | scaled to fit (consequently leaving a lot of wasted space at the bottom of the screen). 39 | * Scanlation images are oftentimes much larger than the 600x800 screen; not only does this make them take more space 40 | on your memory card but it also slows down image loading (the Kindle has to read more data off of the slow SD card 41 | and scale the image). Scanlations often also include color scans of covers and inserts which take up more space than 42 | a grayscale equivalent (which is would be fine for the Kindle's limited display). 43 | * Kindle's image viewer provides no way to sort images (to determine in which order they are shown). This can be very 44 | problematic especially considering that scanlation groups have differing naming conventions, and as a result files 45 | from later chapters may appear before earlier ones when you are reading your Manga (spoilers ftl). 46 | 47 | Mangle was born out of my annoyance with these issues. The program name is a portmanteau of "Manga" and "Kindle"; I 48 | thought it was pretty clever at the time. 49 | 50 | ## Usage 51 | 52 | 1. Add the desired images and image directories to the current book. 53 | 2. Re-order the images as needed (files pre-sorted alphabetically). 54 | 3. Configure the book title and image processing options. 55 | 4. Create a root-level directory on your Kindle called `pictures` (case sensitive). 56 | 5. Export your images, selecting the `pictures` directory you just created. 57 | 6. Enjoy your Manga (if it doesn't show up, press Alt + Z while on the home menu). 58 | 59 | ## Dependencies 60 | 61 | * [PyQt4](https://riverbankcomputing.com/software/pyqt/download) 62 | * [Python 2.7](http://www.python.org/download/releases/2.7/) 63 | * [Pillow (PIL)](https://pypi.org/project/Pillow/) 64 | * [ReportLab](https://pypi.org/project/reportlab/) 65 | 66 | ## Installation 67 | 68 | Pre-built binaries are available for download from the project's [releases 69 | page](https://github.com/FooSoft/mangle/releases). 70 | -------------------------------------------------------------------------------- /img/kindle1-thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/img/kindle1-thumb.png -------------------------------------------------------------------------------- /img/kindle1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/img/kindle1.png -------------------------------------------------------------------------------- /img/kindle2-thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/img/kindle2-thumb.png -------------------------------------------------------------------------------- /img/kindle2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/img/kindle2.png -------------------------------------------------------------------------------- /img/kindle3-thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/img/kindle3-thumb.png -------------------------------------------------------------------------------- /img/kindle3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/img/kindle3.png -------------------------------------------------------------------------------- /img/kindle4-thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/img/kindle4-thumb.png -------------------------------------------------------------------------------- /img/kindle4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/img/kindle4.png -------------------------------------------------------------------------------- /mangle-cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | """python mangle-cli.py extracted_cbz_folder/* 3 | -d directory_name defaults to ./ 4 | -t title defaults 'Unknown' 5 | -o book.outputFormat defaults to 'CBZ only' 6 | 7 | NOTE order of arguments for image files is significant. 8 | 9 | Example: 10 | 11 | mkdir my_comic 12 | cd my_comic 13 | 7z x /full/path/my/comic.cbz 14 | cd .. 15 | python mangle-cli.py -tmy_comic my_comic/* 16 | 17 | """ 18 | 19 | import shutil 20 | import sys 21 | import os 22 | import getopt 23 | 24 | import mangle.cbz 25 | import mangle.image 26 | from mangle.image import ImageFlags 27 | 28 | 29 | class FakeBook: 30 | device = 'Kindle 2/3/Touch' # See mangle/image.py KindleData.Profiles 31 | outputFormat = 'CBZ only' 32 | title = 'Unknown' 33 | imageFlags = ImageFlags.Orient | ImageFlags.Resize | ImageFlags.Quantize 34 | 35 | 36 | try: 37 | opts, args = getopt.getopt(sys.argv[1:], 'd:t:o:') 38 | except getopt.GetoptError, err: 39 | print(str(err)) 40 | sys.exit(2) 41 | 42 | directory = '.' 43 | 44 | book = FakeBook() 45 | book.device = 'Kindle 2/3/Touch' 46 | #book.device = 'Kindle Paperwhite 1 & 2' 47 | book.outputFormat = 'CBZ only' 48 | book.title = 'Unknown' 49 | 50 | 51 | for o,a in opts: 52 | if o == '-d': 53 | directory = a 54 | elif o == '-t': 55 | book.title = a 56 | elif o == '-o': 57 | book.outputFormat = a 58 | 59 | 60 | bookPath = os.path.join(directory, book.title) 61 | 62 | archive = mangle.cbz.Archive(bookPath) 63 | 64 | if not os.path.isdir(bookPath): 65 | os.makedirs(bookPath) 66 | 67 | 68 | for index in range(0, len(args)): 69 | target = os.path.join(bookPath, '%05d.png' % index) # FIXME preserve original; format and name? 70 | 71 | print(index, args[index], target) # cheap display progress 72 | mangle.image.convertImage(args[index], target, str(book.device), book.imageFlags) 73 | archive.addFile(target); 74 | 75 | 76 | if 'Image' not in book.outputFormat: 77 | shutil.rmtree(bookPath) 78 | 79 | archive.close() 80 | 81 | -------------------------------------------------------------------------------- /mangle.pyw: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (C) 2010 Alex Yatskov 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | import sys 20 | 21 | from PyQt4 import QtGui 22 | 23 | from mangle.book import MainWindowBook 24 | 25 | 26 | application = QtGui.QApplication(sys.argv) 27 | filename = sys.argv[1] if len(sys.argv) > 1 else None 28 | window = MainWindowBook(filename) 29 | window.show() 30 | application.exec_() 31 | -------------------------------------------------------------------------------- /mangle/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/mangle/__init__.py -------------------------------------------------------------------------------- /mangle/about.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010 Alex Yatskov 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | from PyQt4 import QtGui, uic 18 | 19 | import util 20 | 21 | 22 | class DialogAbout(QtGui.QDialog): 23 | def __init__(self, parent): 24 | QtGui.QDialog.__init__(self, parent) 25 | uic.loadUi(util.buildResPath('mangle/ui/about.ui'), self) 26 | -------------------------------------------------------------------------------- /mangle/book.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010 Alex Yatskov 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import re 17 | from os.path import basename 18 | import os.path 19 | import tempfile 20 | from zipfile import ZipFile 21 | 22 | from PyQt4 import QtGui, QtCore, QtXml, uic 23 | 24 | from about import DialogAbout 25 | from convert import DialogConvert 26 | from image import ImageFlags 27 | from options import DialogOptions 28 | import util 29 | 30 | 31 | # Sort function use to sort files in a natural order, by lowering 32 | # characters, and manage multi levels of integers (tome 1/ page 1.jpg, etc etc) 33 | # cf: See http://www.codinghorror.com/blog/archives/001018.html 34 | def natural_key(string_): 35 | l = [] 36 | for s in re.split(r'(\d+)', string_): 37 | # QString do not have isdigit, so convert it if need 38 | if isinstance(s, QtCore.QString): 39 | s = unicode(s) 40 | if s.isdigit(): 41 | l.append(int(s)) 42 | else: 43 | l.append(s.lower()) 44 | return l 45 | 46 | 47 | class Book(object): 48 | DefaultDevice = 'Kindle Paperwhite 3/Voyage/Oasis' 49 | DefaultOutputFormat = 'CBZ only' 50 | DefaultOverwrite = True 51 | DefaultImageFlags = ImageFlags.Orient | ImageFlags.Resize | ImageFlags.Quantize 52 | 53 | 54 | def __init__(self): 55 | self.images = [] 56 | self.filename = None 57 | self.modified = False 58 | self.title = None 59 | self.titleSet = False 60 | self.device = Book.DefaultDevice 61 | self.overwrite = Book.DefaultOverwrite 62 | self.imageFlags = Book.DefaultImageFlags 63 | self.outputFormat = Book.DefaultOutputFormat 64 | 65 | 66 | def save(self, filename): 67 | document = QtXml.QDomDocument() 68 | 69 | root = document.createElement('book') 70 | document.appendChild(root) 71 | 72 | root.setAttribute('title', self.title) 73 | root.setAttribute('overwrite', 'true' if self.overwrite else 'false') 74 | root.setAttribute('device', self.device) 75 | root.setAttribute('imageFlags', self.imageFlags) 76 | root.setAttribute('outputFormat', self.outputFormat) 77 | 78 | for filenameImg in self.images: 79 | itemImg = document.createElement('image') 80 | root.appendChild(itemImg) 81 | itemImg.setAttribute('filename', filenameImg) 82 | 83 | textXml = document.toString(4).toUtf8() 84 | 85 | try: 86 | fileXml = open(unicode(filename), 'w') 87 | fileXml.write(textXml) 88 | fileXml.close() 89 | except IOError: 90 | raise RuntimeError('Cannot create book file %s' % filename) 91 | 92 | self.filename = filename 93 | self.modified = False 94 | 95 | 96 | def load(self, filename): 97 | try: 98 | fileXml = open(unicode(filename), 'r') 99 | textXml = fileXml.read() 100 | fileXml.close() 101 | except IOError: 102 | raise RuntimeError('Cannot open book file %s' % filename) 103 | 104 | document = QtXml.QDomDocument() 105 | 106 | if not document.setContent(QtCore.QString.fromUtf8(textXml)): 107 | raise RuntimeError('Error parsing book file %s' % filename) 108 | 109 | root = document.documentElement() 110 | if root.tagName() != 'book': 111 | raise RuntimeError('Unexpected book format in file %s' % filename) 112 | 113 | self.title = root.attribute('title', 'Untitled') 114 | self.overwrite = root.attribute('overwrite', 'true' if Book.DefaultOverwrite else 'false') == 'true' 115 | self.device = root.attribute('device', Book.DefaultDevice) 116 | self.outputFormat = root.attribute('outputFormat', Book.DefaultOutputFormat) 117 | self.imageFlags = int(root.attribute('imageFlags', str(Book.DefaultImageFlags))) 118 | self.filename = filename 119 | self.modified = False 120 | self.images = [] 121 | 122 | items = root.elementsByTagName('image') 123 | if items is None: 124 | return 125 | 126 | for i in xrange(0, len(items)): 127 | item = items.at(i).toElement() 128 | if item.hasAttribute('filename'): 129 | self.images.append(item.attribute('filename')) 130 | 131 | 132 | class MainWindowBook(QtGui.QMainWindow): 133 | def __init__(self, filename=None): 134 | QtGui.QMainWindow.__init__(self) 135 | 136 | uic.loadUi(util.buildResPath('mangle/ui/book.ui'), self) 137 | self.listWidgetFiles.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) 138 | self.actionFileNew.triggered.connect(self.onFileNew) 139 | self.actionFileOpen.triggered.connect(self.onFileOpen) 140 | self.actionFileSave.triggered.connect(self.onFileSave) 141 | self.actionFileSaveAs.triggered.connect(self.onFileSaveAs) 142 | self.actionBookOptions.triggered.connect(self.onBookOptions) 143 | self.actionBookAddFiles.triggered.connect(self.onBookAddFiles) 144 | self.actionBookAddDirectory.triggered.connect(self.onBookAddDirectory) 145 | self.actionBookShiftUp.triggered.connect(self.onBookShiftUp) 146 | self.actionBookShiftDown.triggered.connect(self.onBookShiftDown) 147 | self.actionBookRemove.triggered.connect(self.onBookRemove) 148 | self.actionBookExport.triggered.connect(self.onBookExport) 149 | self.actionHelpAbout.triggered.connect(self.onHelpAbout) 150 | self.actionHelpHomepage.triggered.connect(self.onHelpHomepage) 151 | self.listWidgetFiles.customContextMenuRequested.connect(self.onFilesContextMenu) 152 | self.listWidgetFiles.itemDoubleClicked.connect(self.onFilesDoubleClick) 153 | 154 | self.book = Book() 155 | if filename is not None: 156 | self.loadBook(filename) 157 | 158 | 159 | def closeEvent(self, event): 160 | if not self.saveIfNeeded(): 161 | event.ignore() 162 | 163 | 164 | def dragEnterEvent(self, event): 165 | if event.mimeData().hasUrls(): 166 | event.acceptProposedAction() 167 | 168 | 169 | def dropEvent(self, event): 170 | directories = [] 171 | filenames = [] 172 | 173 | for url in event.mimeData().urls(): 174 | filename = url.toLocalFile() 175 | if self.isImageFile(filename): 176 | filenames.append(filename) 177 | elif os.path.isdir(unicode(filename)): 178 | directories.append(filename) 179 | 180 | self.addImageDirs(directories) 181 | self.addImageFiles(filenames) 182 | 183 | 184 | def onFileNew(self): 185 | if self.saveIfNeeded(): 186 | self.book = Book() 187 | self.listWidgetFiles.clear() 188 | 189 | 190 | def onFileOpen(self): 191 | if not self.saveIfNeeded(): 192 | return 193 | 194 | filename = QtGui.QFileDialog.getOpenFileName( 195 | parent=self, 196 | caption='Select a book file to open', 197 | filter='Mangle files (*.mngl);;All files (*.*)' 198 | ) 199 | if not filename.isNull(): 200 | self.loadBook(self.cleanupBookFile(filename)) 201 | 202 | 203 | def onFileSave(self): 204 | self.saveBook(False) 205 | 206 | 207 | def onFileSaveAs(self): 208 | self.saveBook(True) 209 | 210 | 211 | def onFilesContextMenu(self, point): 212 | menu = QtGui.QMenu(self) 213 | menu.addAction(self.menu_Add.menuAction()) 214 | 215 | if len(self.listWidgetFiles.selectedItems()) > 0: 216 | menu.addAction(self.menu_Shift.menuAction()) 217 | menu.addAction(self.actionBookRemove) 218 | 219 | menu.exec_(self.listWidgetFiles.mapToGlobal(point)) 220 | 221 | 222 | def onFilesDoubleClick(self, item): 223 | services = QtGui.QDesktopServices() 224 | services.openUrl(QtCore.QUrl.fromLocalFile(item.text())) 225 | 226 | 227 | def onBookAddFiles(self): 228 | filenames = QtGui.QFileDialog.getOpenFileNames( 229 | parent=self, 230 | caption='Select image file(s) to add', 231 | filter='Image files (*.jpeg *.jpg *.gif *.png);;Comic files (*.cbz)' 232 | ) 233 | if(self.containsCbzFile(filenames)): 234 | self.addCBZFiles(filenames) 235 | else: 236 | self.addImageFiles(filenames) 237 | 238 | 239 | def onBookAddDirectory(self): 240 | directory = QtGui.QFileDialog.getExistingDirectory(self, 'Select an image directory to add') 241 | if not directory.isNull(): 242 | self.book.title = os.path.basename(os.path.normpath(unicode(directory))) 243 | self.addImageDirs([directory]) 244 | 245 | 246 | def onBookShiftUp(self): 247 | self.shiftImageFiles(-1) 248 | 249 | 250 | def onBookShiftDown(self): 251 | self.shiftImageFiles(1) 252 | 253 | 254 | def onBookRemove(self): 255 | self.removeImageFiles() 256 | 257 | 258 | def onBookOptions(self): 259 | dialog = DialogOptions(self, self.book) 260 | if dialog.exec_() == QtGui.QDialog.Accepted: 261 | self.book.titleSet = True 262 | 263 | 264 | def onBookExport(self): 265 | if len(self.book.images) == 0: 266 | QtGui.QMessageBox.warning(self, 'Mangle', 'This book has no images to export') 267 | return 268 | 269 | if not self.book.titleSet: # if self.book.title is None: 270 | dialog = DialogOptions(self, self.book) 271 | if dialog.exec_() == QtGui.QDialog.Rejected: 272 | return 273 | else: 274 | self.book.titleSet = True 275 | 276 | directory = QtGui.QFileDialog.getExistingDirectory(self, 'Select a directory to export book to') 277 | if not directory.isNull(): 278 | dialog = DialogConvert(self, self.book, directory) 279 | dialog.exec_() 280 | 281 | 282 | def onHelpHomepage(self): 283 | services = QtGui.QDesktopServices() 284 | services.openUrl(QtCore.QUrl('http://foosoft.net/mangle')) 285 | 286 | 287 | def onHelpAbout(self): 288 | dialog = DialogAbout(self) 289 | dialog.exec_() 290 | 291 | 292 | def saveIfNeeded(self): 293 | if not self.book.modified: 294 | return True 295 | 296 | result = QtGui.QMessageBox.question( 297 | self, 298 | 'Mangle', 299 | 'Save changes to the current book?', 300 | QtGui.QMessageBox.Yes | QtGui.QMessageBox.No | QtGui.QMessageBox.Cancel, 301 | QtGui.QMessageBox.Yes 302 | ) 303 | 304 | return ( 305 | result == QtGui.QMessageBox.No or 306 | result == QtGui.QMessageBox.Yes and self.saveBook() 307 | ) 308 | 309 | 310 | def saveBook(self, browse=False): 311 | if self.book.title is None: 312 | QtGui.QMessageBox.warning(self, 'Mangle', 'You must specify a title for this book before saving') 313 | return False 314 | 315 | filename = self.book.filename 316 | if filename is None or browse: 317 | filename = QtGui.QFileDialog.getSaveFileName( 318 | parent=self, 319 | caption='Select a book file to save as', 320 | filter='Mangle files (*.mngl);;All files (*.*)' 321 | ) 322 | if filename.isNull(): 323 | return False 324 | filename = self.cleanupBookFile(filename) 325 | 326 | try: 327 | self.book.save(filename) 328 | except RuntimeError, error: 329 | QtGui.QMessageBox.critical(self, 'Mangle', str(error)) 330 | return False 331 | 332 | return True 333 | 334 | 335 | def loadBook(self, filename): 336 | try: 337 | self.book.load(filename) 338 | except RuntimeError, error: 339 | QtGui.QMessageBox.critical(self, 'Mangle', str(error)) 340 | else: 341 | self.listWidgetFiles.clear() 342 | for image in self.book.images: 343 | self.listWidgetFiles.addItem(image) 344 | 345 | 346 | def shiftImageFile(self, row, delta): 347 | validShift = ( 348 | (delta > 0 and row < self.listWidgetFiles.count() - delta) or 349 | (delta < 0 and row >= abs(delta)) 350 | ) 351 | if not validShift: 352 | return 353 | 354 | item = self.listWidgetFiles.takeItem(row) 355 | 356 | self.listWidgetFiles.insertItem(row + delta, item) 357 | self.listWidgetFiles.setItemSelected(item, True) 358 | 359 | self.book.modified = True 360 | self.book.images[row], self.book.images[row + delta] = ( 361 | self.book.images[row + delta], self.book.images[row] 362 | ) 363 | 364 | 365 | def shiftImageFiles(self, delta): 366 | items = self.listWidgetFiles.selectedItems() 367 | rows = sorted([self.listWidgetFiles.row(item) for item in items]) 368 | 369 | for row in rows if delta < 0 else reversed(rows): 370 | self.shiftImageFile(row, delta) 371 | 372 | 373 | def removeImageFiles(self): 374 | for item in self.listWidgetFiles.selectedItems(): 375 | row = self.listWidgetFiles.row(item) 376 | self.listWidgetFiles.takeItem(row) 377 | self.book.images.remove(item.text()) 378 | self.book.modified = True 379 | 380 | 381 | def addImageFiles(self, filenames): 382 | filenamesListed = [] 383 | for i in xrange(0, self.listWidgetFiles.count()): 384 | filenamesListed.append(self.listWidgetFiles.item(i).text()) 385 | 386 | # Get files but in a natural sorted order 387 | for filename in sorted(filenames, key=natural_key): 388 | if filename not in filenamesListed: 389 | filename = QtCore.QString(filename) 390 | self.listWidgetFiles.addItem(filename) 391 | self.book.images.append(filename) 392 | self.book.modified = True 393 | 394 | 395 | def addImageDirs(self, directories): 396 | filenames = [] 397 | 398 | for directory in directories: 399 | for root, _, subfiles in os.walk(unicode(directory)): 400 | for filename in subfiles: 401 | path = os.path.join(root, filename) 402 | if self.isImageFile(path): 403 | filenames.append(path) 404 | 405 | self.addImageFiles(filenames) 406 | 407 | 408 | def addCBZFiles(self, filenames): 409 | directories = [] 410 | tempDir = tempfile.gettempdir() 411 | filenames.sort() 412 | 413 | filenamesListed = [] 414 | for i in xrange(0, self.listWidgetFiles.count()): 415 | filenamesListed.append(self.listWidgetFiles.item(i).text()) 416 | 417 | for filename in filenames: 418 | folderName = os.path.splitext(basename(str(filename)))[0] 419 | path = tempDir + "/" + folderName + "/" 420 | cbzFile = ZipFile(str(filename)) 421 | for f in cbzFile.namelist(): 422 | if f.endswith('/'): 423 | try: 424 | os.makedirs(path + f) 425 | except: 426 | pass # the dir exists so we are going to extract the images only. 427 | else: 428 | cbzFile.extract(f, path) 429 | if os.path.isdir(unicode(path)): # Add the directories 430 | directories.append(path) 431 | 432 | self.addImageDirs(directories) # Add the files 433 | 434 | 435 | def isImageFile(self, filename): 436 | imageExts = ['.jpeg', '.jpg', '.gif', '.png'] 437 | filename = unicode(filename) 438 | return ( 439 | os.path.isfile(filename) and 440 | os.path.splitext(filename)[1].lower() in imageExts 441 | ) 442 | 443 | def containsCbzFile(self, filenames): 444 | cbzExts = ['.cbz'] 445 | for filename in filenames: 446 | filename = unicode(filename) 447 | result = ( 448 | os.path.isfile(filename) and 449 | os.path.splitext(filename)[1].lower() in cbzExts 450 | ) 451 | if result == True: 452 | return result 453 | return False 454 | 455 | def cleanupBookFile(self, filename): 456 | if len(os.path.splitext(unicode(filename))[1]) == 0: 457 | filename += '.mngl' 458 | return filename 459 | -------------------------------------------------------------------------------- /mangle/cbz.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2011 Marek Kubica 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | import os.path 18 | from zipfile import ZipFile, ZIP_STORED 19 | 20 | 21 | class Archive(object): 22 | def __init__(self, path): 23 | outputDirectory = os.path.dirname(path) 24 | outputFileName = '%s.cbz' % os.path.basename(path) 25 | outputPath = os.path.join(outputDirectory, outputFileName) 26 | self.zipfile = ZipFile(outputPath, 'w', ZIP_STORED) 27 | 28 | 29 | def addFile(self, filename): 30 | arcname = os.path.basename(filename) 31 | self.zipfile.write(filename, arcname) 32 | 33 | 34 | def __enter__(self): 35 | return self 36 | 37 | 38 | def __exit__(self, exc_type, exc_val, exc_tb): 39 | self.close() 40 | 41 | 42 | def close(self): 43 | self.zipfile.close() 44 | -------------------------------------------------------------------------------- /mangle/convert.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010 Alex Yatskov 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | import os 18 | import shutil 19 | from PyQt4 import QtGui, QtCore 20 | 21 | 22 | from image import ImageFlags 23 | import cbz 24 | import image 25 | import pdfimage 26 | 27 | 28 | class DialogConvert(QtGui.QProgressDialog): 29 | def __init__(self, parent, book, directory): 30 | QtGui.QProgressDialog.__init__(self) 31 | 32 | self.book = book 33 | self.bookPath = os.path.join(unicode(directory), unicode(self.book.title)) 34 | 35 | self.timer = None 36 | self.setWindowTitle('Exporting book...') 37 | self.setMaximum(len(self.book.images)) 38 | self.setValue(0) 39 | self.increment = 0 40 | 41 | self.archive = None 42 | if 'CBZ' in self.book.outputFormat: 43 | self.archive = cbz.Archive(self.bookPath) 44 | 45 | self.pdf = None 46 | if "PDF" in self.book.outputFormat: 47 | self.pdf = pdfimage.PDFImage(self.bookPath, str(self.book.title), str(self.book.device)) 48 | 49 | 50 | 51 | def showEvent(self, event): 52 | if self.timer is None: 53 | self.timer = QtCore.QTimer() 54 | self.timer.timeout.connect(self.onTimer) 55 | self.timer.start(0) 56 | 57 | 58 | def hideEvent(self, event): 59 | """Called when the dialog finishes processing.""" 60 | 61 | # Close the archive if we created a CBZ file 62 | if self.archive is not None: 63 | self.archive.close() 64 | # Close and generate the PDF File 65 | if self.pdf is not None: 66 | self.pdf.close() 67 | 68 | # Remove image directory if the user didn't wish for images 69 | if 'Image' not in self.book.outputFormat: 70 | shutil.rmtree(self.bookPath) 71 | 72 | 73 | def convertAndSave(self, source, target, device, flags, archive, pdf): 74 | image.convertImage(source, target, device, flags) 75 | if archive is not None: 76 | archive.addFile(target) 77 | if pdf is not None: 78 | pdf.addImage(target) 79 | 80 | 81 | def onTimer(self): 82 | index = self.value() 83 | pages_split = self.increment 84 | target = os.path.join(self.bookPath, '%05d.png' % (index + pages_split)) 85 | source = unicode(self.book.images[index]) 86 | 87 | if index == 0: 88 | try: 89 | if not os.path.isdir(self.bookPath): 90 | os.makedirs(self.bookPath) 91 | except OSError: 92 | QtGui.QMessageBox.critical(self, 'Mangle', 'Cannot create directory %s' % self.bookPath) 93 | self.close() 94 | return 95 | 96 | try: 97 | base = os.path.join(self.bookPath, unicode(self.book.title)) 98 | 99 | mangaName = base + '.manga' 100 | if self.book.overwrite or not os.path.isfile(mangaName): 101 | manga = open(mangaName, 'w') 102 | manga.write('\x00') 103 | manga.close() 104 | 105 | mangaSaveName = base + '.manga_save' 106 | if self.book.overwrite or not os.path.isfile(mangaSaveName): 107 | mangaSave = open(base + '.manga_save', 'w') 108 | saveData = u'LAST=/mnt/us/pictures/%s/%s' % (self.book.title, os.path.split(target)[1]) 109 | mangaSave.write(saveData.encode('utf-8')) 110 | mangaSave.close() 111 | 112 | except IOError: 113 | QtGui.QMessageBox.critical(self, 'Mangle', 'Cannot write manga file(s) to directory %s' % self.bookPath) 114 | self.close() 115 | return False 116 | 117 | self.setLabelText('Processing %s...' % os.path.split(source)[1]) 118 | 119 | try: 120 | if self.book.overwrite or not os.path.isfile(target): 121 | device = str(self.book.device) 122 | flags = self.book.imageFlags 123 | archive = self.archive 124 | pdf = self.pdf 125 | 126 | # Check if page wide enough to split 127 | if (flags & ImageFlags.SplitRightLeft) or (flags & ImageFlags.SplitLeftRight): 128 | if not image.isSplitable(source): 129 | # remove split flags 130 | splitFlags = [ImageFlags.SplitRightLeft, ImageFlags.SplitLeftRight, ImageFlags.SplitRight, 131 | ImageFlags.SplitLeft] 132 | for f in splitFlags: 133 | flags &= ~f 134 | 135 | # For right page (if requested in options and need for this image) 136 | if flags & ImageFlags.SplitRightLeft: 137 | self.convertAndSave(source, target, device, 138 | flags ^ ImageFlags.SplitRightLeft | ImageFlags.SplitRight, 139 | archive, pdf) 140 | 141 | # Change target for left page 142 | target = os.path.join(self.bookPath, '%05d.png' % (index + pages_split + 1)) 143 | self.increment += 1 144 | 145 | # For right page (if requested), but in inverted mode 146 | if flags & ImageFlags.SplitLeftRight: 147 | self.convertAndSave(source, target, device, 148 | flags ^ ImageFlags.SplitLeftRight | ImageFlags.SplitLeft, 149 | archive, pdf) 150 | 151 | # Change target for left page 152 | target = os.path.join(self.bookPath, '%05d.png' % (index + pages_split + 1)) 153 | self.increment += 1 154 | 155 | # Convert page 156 | self.convertAndSave(source, target, device, flags, archive, pdf) 157 | 158 | except RuntimeError, error: 159 | result = QtGui.QMessageBox.critical( 160 | self, 161 | 'Mangle', 162 | str(error), 163 | QtGui.QMessageBox.Abort | QtGui.QMessageBox.Ignore, 164 | QtGui.QMessageBox.Ignore 165 | ) 166 | if result == QtGui.QMessageBox.Abort: 167 | self.close() 168 | return 169 | 170 | self.setValue(index + 1) 171 | -------------------------------------------------------------------------------- /mangle/image.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010 Alex Yatskov 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | import os 18 | 19 | from PIL import Image, ImageDraw, ImageStat, ImageChops, ImageOps 20 | 21 | 22 | class ImageFlags: 23 | Orient = 1 << 0 24 | Resize = 1 << 1 25 | Frame = 1 << 2 26 | Quantize = 1 << 3 27 | Fit = 1 << 4 28 | SplitRightLeft = 1 << 5 # split right then left 29 | SplitRight = 1 << 6 # split only the right page 30 | SplitLeft = 1 << 7 # split only the left page 31 | SplitLeftRight = 1 << 8 # split left then right page 32 | AutoCrop = 1 << 9 33 | Fill = 1 << 10 34 | Stretch = 1 << 11 35 | 36 | 37 | class KindleData: 38 | Palette4 = [ 39 | 0x00, 0x00, 0x00, 40 | 0x55, 0x55, 0x55, 41 | 0xaa, 0xaa, 0xaa, 42 | 0xff, 0xff, 0xff 43 | ] 44 | 45 | Palette15a = [ 46 | 0x00, 0x00, 0x00, 47 | 0x11, 0x11, 0x11, 48 | 0x22, 0x22, 0x22, 49 | 0x33, 0x33, 0x33, 50 | 0x44, 0x44, 0x44, 51 | 0x55, 0x55, 0x55, 52 | 0x66, 0x66, 0x66, 53 | 0x77, 0x77, 0x77, 54 | 0x88, 0x88, 0x88, 55 | 0x99, 0x99, 0x99, 56 | 0xaa, 0xaa, 0xaa, 57 | 0xbb, 0xbb, 0xbb, 58 | 0xcc, 0xcc, 0xcc, 59 | 0xdd, 0xdd, 0xdd, 60 | 0xff, 0xff, 0xff, 61 | ] 62 | 63 | Palette15b = [ 64 | 0x00, 0x00, 0x00, 65 | 0x11, 0x11, 0x11, 66 | 0x22, 0x22, 0x22, 67 | 0x33, 0x33, 0x33, 68 | 0x44, 0x44, 0x44, 69 | 0x55, 0x55, 0x55, 70 | 0x77, 0x77, 0x77, 71 | 0x88, 0x88, 0x88, 72 | 0x99, 0x99, 0x99, 73 | 0xaa, 0xaa, 0xaa, 74 | 0xbb, 0xbb, 0xbb, 75 | 0xcc, 0xcc, 0xcc, 76 | 0xdd, 0xdd, 0xdd, 77 | 0xee, 0xee, 0xee, 78 | 0xff, 0xff, 0xff, 79 | ] 80 | 81 | Profiles = { 82 | 'Kindle 1': ((600, 800), Palette4), 83 | 'Kindle 2/3/Touch': ((600, 800), Palette15a), 84 | 'Kindle 4 & 5': ((600, 800), Palette15b), 85 | 'Kindle DX/DXG': ((824, 1200), Palette15a), 86 | 'Kindle Paperwhite 1 & 2': ((758, 1024), Palette15b), 87 | 'Kindle Paperwhite 3/Voyage/Oasis': ((1072, 1448), Palette15b), 88 | 'Kobo Mini/Touch': ((600, 800), Palette15b), 89 | 'Kobo Glo': ((768, 1024), Palette15b), 90 | 'Kobo Glo HD': ((1072, 1448), Palette15b), 91 | 'Kobo Aura': ((758, 1024), Palette15b), 92 | 'Kobo Aura HD': ((1080, 1440), Palette15b), 93 | 'Kobo Aura H2O': ((1080, 1430), Palette15a), 94 | } 95 | 96 | 97 | # decorate a function that use image, *** and if there 98 | # is an exception raise by PIL (IOError) then return 99 | # the original image because PIL cannot manage it 100 | def protect_bad_image(func): 101 | def func_wrapper(*args, **kwargs): 102 | # If cannot convert (like a bogus image) return the original one 103 | # args will be "image" and other params are after 104 | try: 105 | return func(*args, **kwargs) 106 | except IOError: # Exception from PIL about bad image 107 | return args[0] 108 | 109 | return func_wrapper 110 | 111 | 112 | @protect_bad_image 113 | def splitLeft(image): 114 | widthImg, heightImg = image.size 115 | 116 | return image.crop((0, 0, widthImg / 2, heightImg)) 117 | 118 | 119 | @protect_bad_image 120 | def splitRight(image): 121 | widthImg, heightImg = image.size 122 | 123 | return image.crop((widthImg / 2, 0, widthImg, heightImg)) 124 | 125 | 126 | @protect_bad_image 127 | def quantizeImage(image, palette): 128 | colors = len(palette) / 3 129 | if colors < 256: 130 | palette = palette + palette[:3] * (256 - colors) 131 | 132 | palImg = Image.new('P', (1, 1)) 133 | palImg.putpalette(palette) 134 | 135 | return image.quantize(palette=palImg) 136 | 137 | 138 | @protect_bad_image 139 | def fitImage(image, size, method=Image.ANTIALIAS): 140 | # copied from ImageOps.contain() from the Python3 version of Pillow 141 | # with division related modifications for Python2 142 | 143 | im_ratio = 1.0 * image.width / image.height 144 | dest_ratio = 1.0 * size[0] / size[1] 145 | 146 | if im_ratio != dest_ratio: 147 | if im_ratio > dest_ratio: 148 | new_height = int(1.0 * image.height / image.width * size[0]) 149 | if new_height != size[1]: 150 | size = (size[0], new_height) 151 | else: 152 | new_width = int(1.0 * image.width / image.height * size[1]) 153 | if new_width != size[0]: 154 | size = (new_width, size[1]) 155 | return image.resize(size, resample=method) 156 | 157 | 158 | @protect_bad_image 159 | def fillImage(image, size): 160 | widthDev, heightDev = size 161 | widthImg, heightImg = image.size 162 | 163 | imgRatio = float(widthImg) / float(heightImg) 164 | devRatio = float(widthDev) / float(heightDev) 165 | 166 | # don't crop 2 page spreads. 167 | if imgRatio > devRatio: 168 | return resizeImage(image, size) 169 | 170 | return ImageOps.fit(image, size, Image.ANTIALIAS) 171 | 172 | 173 | @protect_bad_image 174 | def stretchImage(image, size): 175 | return image.resize(size, Image.ANTIALIAS) 176 | 177 | 178 | @protect_bad_image 179 | def resizeImage(image, size): 180 | widthDev, heightDev = size 181 | widthImg, heightImg = image.size 182 | 183 | if widthImg <= widthDev and heightImg <= heightDev: 184 | return image 185 | 186 | ratioImg = float(widthImg) / float(heightImg) 187 | ratioWidth = float(widthImg) / float(widthDev) 188 | ratioHeight = float(heightImg) / float(heightDev) 189 | 190 | if ratioWidth > ratioHeight: 191 | widthImg = widthDev 192 | heightImg = int(widthDev / ratioImg) 193 | elif ratioWidth < ratioHeight: 194 | heightImg = heightDev 195 | widthImg = int(heightDev * ratioImg) 196 | else: 197 | widthImg, heightImg = size 198 | 199 | return image.resize((widthImg, heightImg), Image.ANTIALIAS) 200 | 201 | 202 | @protect_bad_image 203 | def formatImage(image): 204 | if image.mode == 'RGB': 205 | return image 206 | 207 | return image.convert('RGB') 208 | 209 | 210 | @protect_bad_image 211 | def orientImage(image, size): 212 | widthDev, heightDev = size 213 | widthImg, heightImg = image.size 214 | 215 | if widthImg <= widthDev and heightImg <= heightDev: 216 | return image 217 | 218 | if (widthImg > heightImg) != (widthDev > heightDev): 219 | return image.transpose(Image.ROTATE_90) 220 | return image 221 | 222 | 223 | # We will auto crop the image, by removing just white part around the image 224 | # by inverting colors, and asking a bounder box ^^ 225 | @protect_bad_image 226 | def autoCropImage(image): 227 | try: 228 | x0, y0, xend, yend = ImageChops.invert(image).getbbox() 229 | except TypeError: # bad image, specific to chops 230 | return image 231 | image = image.crop((x0, y0, xend, yend)) 232 | 233 | return image 234 | 235 | 236 | def frameImage(image, foreground, background, size): 237 | widthDev, heightDev = size 238 | widthImg, heightImg = image.size 239 | 240 | pastePt = ( 241 | max(0, (widthDev - widthImg) / 2), 242 | max(0, (heightDev - heightImg) / 2) 243 | ) 244 | 245 | corner1 = ( 246 | pastePt[0] - 1, 247 | pastePt[1] - 1 248 | ) 249 | 250 | corner2 = ( 251 | pastePt[0] + widthImg + 1, 252 | pastePt[1] + heightImg + 1 253 | ) 254 | 255 | imageBg = Image.new(image.mode, size, background) 256 | imageBg.paste(image, pastePt) 257 | 258 | draw = ImageDraw.Draw(imageBg) 259 | draw.rectangle([corner1, corner2], outline=foreground) 260 | 261 | return imageBg 262 | 263 | 264 | def loadImage(source): 265 | try: 266 | return Image.open(source) 267 | except IOError: 268 | raise RuntimeError('Cannot read image file %s' % source) 269 | 270 | 271 | def saveImage(image, target): 272 | try: 273 | image.save(target) 274 | except IOError: 275 | raise RuntimeError('Cannot write image file %s' % target) 276 | 277 | 278 | # Look if the image is more width than hight, if not, means 279 | # it's should not be split (like the front page of a manga, 280 | # when all the inner pages are double) 281 | def isSplitable(source): 282 | image = loadImage(source) 283 | try: 284 | widthImg, heightImg = image.size 285 | return widthImg > heightImg 286 | except IOError: 287 | raise RuntimeError('Cannot read image file %s' % source) 288 | 289 | 290 | def convertImage(source, target, device, flags): 291 | try: 292 | size, palette = KindleData.Profiles[device] 293 | except KeyError: 294 | raise RuntimeError('Unexpected output device %s' % device) 295 | # Load image from source path 296 | image = loadImage(source) 297 | 298 | # Format according to palette 299 | image = formatImage(image) 300 | 301 | # Apply flag transforms 302 | if flags & ImageFlags.SplitRight: 303 | image = splitRight(image) 304 | if flags & ImageFlags.SplitRightLeft: 305 | image = splitLeft(image) 306 | if flags & ImageFlags.SplitLeft: 307 | image = splitLeft(image) 308 | if flags & ImageFlags.SplitLeftRight: 309 | image = splitRight(image) 310 | 311 | # Auto crop the image, but before manage size and co, clean the source so 312 | if flags & ImageFlags.AutoCrop: 313 | image = autoCropImage(image) 314 | if flags & ImageFlags.Orient: 315 | image = orientImage(image, size) 316 | if flags & ImageFlags.Resize: 317 | image = resizeImage(image, size) 318 | if flags & ImageFlags.Fit: 319 | image = fitImage(image, size) 320 | if flags & ImageFlags.Fill: 321 | image = fillImage(image, size) 322 | if flags & ImageFlags.Stretch: 323 | image = stretchImage(image, size) 324 | if flags & ImageFlags.Frame: 325 | image = frameImage(image, tuple(palette[:3]), tuple(palette[-3:]), size) 326 | if flags & ImageFlags.Quantize: 327 | image = quantizeImage(image, palette) 328 | 329 | saveImage(image, target) 330 | -------------------------------------------------------------------------------- /mangle/img/add_directory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/mangle/img/add_directory.png -------------------------------------------------------------------------------- /mangle/img/add_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/mangle/img/add_file.png -------------------------------------------------------------------------------- /mangle/img/banner_about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/mangle/img/banner_about.png -------------------------------------------------------------------------------- /mangle/img/book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/mangle/img/book.png -------------------------------------------------------------------------------- /mangle/img/export_book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/mangle/img/export_book.png -------------------------------------------------------------------------------- /mangle/img/file_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/mangle/img/file_new.png -------------------------------------------------------------------------------- /mangle/img/file_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/mangle/img/file_open.png -------------------------------------------------------------------------------- /mangle/img/remove_files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/mangle/img/remove_files.png -------------------------------------------------------------------------------- /mangle/img/save_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/mangle/img/save_file.png -------------------------------------------------------------------------------- /mangle/img/shift_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/mangle/img/shift_down.png -------------------------------------------------------------------------------- /mangle/img/shift_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FooSoft/mangle/2d710ee74e1fe670f1e0a01857864079a72326fb/mangle/img/shift_up.png -------------------------------------------------------------------------------- /mangle/options.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010 Alex Yatskov 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | from PyQt4 import QtGui, uic 18 | 19 | from image import ImageFlags 20 | import util 21 | 22 | 23 | class DialogOptions(QtGui.QDialog): 24 | def __init__(self, parent, book): 25 | QtGui.QDialog.__init__(self, parent) 26 | 27 | uic.loadUi(util.buildResPath('mangle/ui/options.ui'), self) 28 | self.accepted.connect(self.onAccept) 29 | 30 | self.book = book 31 | self.moveOptionsToDialog() 32 | 33 | 34 | def onAccept(self): 35 | self.moveDialogToOptions() 36 | 37 | 38 | # Get options from current book (like a loaded one) and set the dialog values 39 | def moveOptionsToDialog(self): 40 | self.lineEditTitle.setText(self.book.title or 'Untitled') 41 | self.comboBoxDevice.setCurrentIndex(max(self.comboBoxDevice.findText(self.book.device), 0)) 42 | self.comboBoxFormat.setCurrentIndex(max(self.comboBoxFormat.findText(self.book.outputFormat), 0)) 43 | self.checkboxOverwrite.setChecked(self.book.overwrite) 44 | self.checkboxOrient.setChecked(self.book.imageFlags & ImageFlags.Orient) 45 | self.checkboxResize.setChecked(self.book.imageFlags & ImageFlags.Resize) 46 | self.checkboxFit.setChecked(self.book.imageFlags & ImageFlags.Fit) 47 | self.checkboxFill.setChecked(self.book.imageFlags & ImageFlags.Fill) 48 | self.checkboxStretch.setChecked(self.book.imageFlags & ImageFlags.Stretch) 49 | self.checkboxQuantize.setChecked(self.book.imageFlags & ImageFlags.Quantize) 50 | self.checkboxFrame.setChecked(self.book.imageFlags & ImageFlags.Frame) 51 | 52 | 53 | # Save parameters set on the dialogs to the book object if need 54 | def moveDialogToOptions(self): 55 | # First get dialog values 56 | title = self.lineEditTitle.text() 57 | device = self.comboBoxDevice.currentText() 58 | outputFormat = self.comboBoxFormat.currentText() 59 | overwrite = self.checkboxOverwrite.isChecked() 60 | 61 | # Now compute flags 62 | imageFlags = 0 63 | if self.checkboxOrient.isChecked(): 64 | imageFlags |= ImageFlags.Orient 65 | if self.checkboxResize.isChecked(): 66 | imageFlags |= ImageFlags.Resize 67 | if self.checkboxFit.isChecked(): 68 | imageFlags |= ImageFlags.Fit 69 | if self.checkboxFill.isChecked(): 70 | imageFlags |= ImageFlags.Fill 71 | if self.checkboxStretch.isChecked(): 72 | imageFlags |= ImageFlags.Stretch 73 | if self.checkboxQuantize.isChecked(): 74 | imageFlags |= ImageFlags.Quantize 75 | if self.checkboxFrame.isChecked(): 76 | imageFlags |= ImageFlags.Frame 77 | if self.checkboxSplit.isChecked(): 78 | imageFlags |= ImageFlags.SplitRightLeft 79 | if self.checkboxSplitInverse.isChecked(): 80 | imageFlags |= ImageFlags.SplitLeftRight 81 | if self.checkboxAutoCrop.isChecked(): 82 | imageFlags |= ImageFlags.AutoCrop 83 | 84 | # If we did modified a value, update the book 85 | # and only if we did change something to not 86 | # warn for nothing the user 87 | modified = ( 88 | self.book.title != title or 89 | self.book.device != device or 90 | self.book.overwrite != overwrite or 91 | self.book.imageFlags != imageFlags or 92 | self.book.outputFormat != outputFormat 93 | ) 94 | 95 | if modified: 96 | self.book.modified = True 97 | self.book.title = title 98 | self.book.device = device 99 | self.book.overwrite = overwrite 100 | self.book.imageFlags = imageFlags 101 | self.book.outputFormat = outputFormat 102 | -------------------------------------------------------------------------------- /mangle/pdfimage.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Cristian Lizana 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | import os.path 18 | 19 | from reportlab.pdfgen import canvas 20 | 21 | from image import KindleData 22 | 23 | 24 | class PDFImage(object): 25 | def __init__(self, path, title, device): 26 | outputDirectory = os.path.dirname(path) 27 | outputFileName = '%s.pdf' % os.path.basename(path) 28 | outputPath = os.path.join(outputDirectory, outputFileName) 29 | self.currentDevice = device 30 | self.bookTitle = title 31 | self.pageSize = KindleData.Profiles[self.currentDevice][0] 32 | # pagesize could be letter or A4 for standarization but we need to control some image sizes 33 | self.canvas = canvas.Canvas(outputPath, pagesize=self.pageSize) 34 | self.canvas.setAuthor("Mangle") 35 | self.canvas.setTitle(self.bookTitle) 36 | self.canvas.setSubject("Created for " + self.currentDevice) 37 | 38 | 39 | def addImage(self, filename): 40 | self.canvas.drawImage(filename, 0, 0, width=self.pageSize[0], height=self.pageSize[1], preserveAspectRatio=True, anchor='c') 41 | self.canvas.showPage() 42 | 43 | def __enter__(self): 44 | return self 45 | 46 | def __exit__(self, exc_type, exc_val, exc_tb): 47 | self.close() 48 | 49 | def close(self): 50 | self.canvas.save() 51 | -------------------------------------------------------------------------------- /mangle/ui/about.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | DialogAbout 4 | 5 | 6 | 7 | 0 8 | 0 9 | 470 10 | 200 11 | 12 | 13 | 14 | About 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | QLayout::SetFixedSize 22 | 23 | 24 | 0 25 | 26 | 27 | 28 | 29 | ../img/banner_about.png 30 | 31 | 32 | 33 | 34 | 35 | 36 | 9 37 | 38 | 39 | 40 | 41 | 42 | 350 43 | 0 44 | 45 | 46 | 47 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> 48 | <html><head><meta name="qrichtext" content="1" /><style type="text/css"> 49 | p, li { white-space: pre-wrap; } 50 | </style></head><body style=" font-family:'Sans Serif'; font-size:10pt; font-weight:400; font-style:normal;"> 51 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Sans'; font-size:14pt;">Mangle</span></p> 52 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Sans';">Version 3</span></p> 53 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans';"></p> 54 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Sans';">Manga processor for the Kindle e-book reader. Please see </span><span style=" font-family:'Sans'; font-style:italic;">license.txt</span><span style=" font-family:'Sans';"> for licensing information. Visit the homepage at </span><a href="http://foosoft.net/mangle"><span style=" font-family:'Sans'; text-decoration: underline; color:#0000ff;">http://foosoft.net/mangle/</span></a><span style=" font-family:'Sans';">.</span></p></body></html> 55 | 56 | 57 | true 58 | 59 | 60 | true 61 | 62 | 63 | 64 | 65 | 66 | 67 | Qt::Horizontal 68 | 69 | 70 | 71 | 40 72 | 20 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | Qt::Vertical 81 | 82 | 83 | 84 | 0 85 | 0 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 0 95 | 0 96 | 97 | 98 | 99 | QDialogButtonBox::Ok 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | buttonBox 111 | accepted() 112 | DialogAbout 113 | accept() 114 | 115 | 116 | 294 117 | 177 118 | 119 | 120 | 244 121 | 99 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /mangle/ui/book.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindowBook 4 | 5 | 6 | 7 | 0 8 | 0 9 | 800 10 | 600 11 | 12 | 13 | 14 | true 15 | 16 | 17 | Mangle 18 | 19 | 20 | 21 | 22 | 23 | 24 | QAbstractItemView::ExtendedSelection 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 0 34 | 0 35 | 800 36 | 23 37 | 38 | 39 | 40 | 41 | &File 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | &Book 53 | 54 | 55 | 56 | &Add 57 | 58 | 59 | 60 | 61 | 62 | 63 | &Shift 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | &Help 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | toolBar 90 | 91 | 92 | TopToolBarArea 93 | 94 | 95 | false 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | ../img/file_new.png../img/file_new.png 113 | 114 | 115 | &New 116 | 117 | 118 | New book 119 | 120 | 121 | Ctrl+N 122 | 123 | 124 | true 125 | 126 | 127 | 128 | 129 | 130 | ../img/file_open.png../img/file_open.png 131 | 132 | 133 | &Open... 134 | 135 | 136 | Open book 137 | 138 | 139 | Ctrl+O 140 | 141 | 142 | true 143 | 144 | 145 | 146 | 147 | 148 | ../img/save_file.png../img/save_file.png 149 | 150 | 151 | &Save 152 | 153 | 154 | Save book 155 | 156 | 157 | Ctrl+S 158 | 159 | 160 | true 161 | 162 | 163 | 164 | 165 | Save &as... 166 | 167 | 168 | Save book as 169 | 170 | 171 | Ctrl+Shift+S 172 | 173 | 174 | 175 | 176 | &Exit 177 | 178 | 179 | Ctrl+Q 180 | 181 | 182 | 183 | 184 | 185 | ../img/book.png../img/book.png 186 | 187 | 188 | &Options... 189 | 190 | 191 | 192 | 193 | 194 | ../img/remove_files.png../img/remove_files.png 195 | 196 | 197 | &Remove 198 | 199 | 200 | Remove files 201 | 202 | 203 | Del 204 | 205 | 206 | true 207 | 208 | 209 | 210 | 211 | 212 | ../img/export_book.png../img/export_book.png 213 | 214 | 215 | &Export... 216 | 217 | 218 | Export book 219 | 220 | 221 | Ctrl+E 222 | 223 | 224 | true 225 | 226 | 227 | 228 | 229 | &Homepage... 230 | 231 | 232 | 233 | 234 | &About... 235 | 236 | 237 | About 238 | 239 | 240 | F1 241 | 242 | 243 | 244 | 245 | 246 | ../img/add_file.png../img/add_file.png 247 | 248 | 249 | &Files... 250 | 251 | 252 | Add files 253 | 254 | 255 | Ctrl+F 256 | 257 | 258 | true 259 | 260 | 261 | 262 | 263 | 264 | ../img/add_directory.png../img/add_directory.png 265 | 266 | 267 | &Directory... 268 | 269 | 270 | Add directory 271 | 272 | 273 | Ctrl+D 274 | 275 | 276 | true 277 | 278 | 279 | 280 | 281 | 282 | ../img/shift_up.png../img/shift_up.png 283 | 284 | 285 | &Up 286 | 287 | 288 | Shift files up 289 | 290 | 291 | Ctrl+PgUp 292 | 293 | 294 | true 295 | 296 | 297 | 298 | 299 | 300 | ../img/shift_down.png../img/shift_down.png 301 | 302 | 303 | &Down 304 | 305 | 306 | Shift files down 307 | 308 | 309 | Ctrl+PgDown 310 | 311 | 312 | true 313 | 314 | 315 | 316 | 317 | 318 | 319 | actionFileExit 320 | triggered() 321 | MainWindowBook 322 | close() 323 | 324 | 325 | -1 326 | -1 327 | 328 | 329 | 399 330 | 299 331 | 332 | 333 | 334 | 335 | 336 | -------------------------------------------------------------------------------- /mangle/ui/options.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | DialogOptions 4 | 5 | 6 | 7 | 0 8 | 0 9 | 350 10 | 363 11 | 12 | 13 | 14 | Options 15 | 16 | 17 | 18 | 19 | 20 | Book 21 | 22 | 23 | false 24 | 25 | 26 | 27 | 28 | 29 | Title 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Export 43 | 44 | 45 | 46 | 47 | 48 | QFormLayout::AllNonFixedFieldsGrow 49 | 50 | 51 | 52 | 53 | Device 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | Kindle 1 62 | 63 | 64 | 65 | 66 | Kindle 2/3/Touch 67 | 68 | 69 | 70 | 71 | Kindle 4 & 5 72 | 73 | 74 | 75 | 76 | Kindle DX/DXG 77 | 78 | 79 | 80 | 81 | Kindle Paperwhite 1 & 2 82 | 83 | 84 | 85 | 86 | Kindle Paperwhite 3/Voyage/Oasis 87 | 88 | 89 | 90 | 91 | Kobo Mini/Touch 92 | 93 | 94 | 95 | 96 | Kobo Glo 97 | 98 | 99 | 100 | 101 | Kobo Glo HD 102 | 103 | 104 | 105 | 106 | Kobo Aura 107 | 108 | 109 | 110 | 111 | Kobo Aura HD 112 | 113 | 114 | 115 | 116 | Kobo Aura H2O 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | Format 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | Images & CBZ & PDF 133 | 134 | 135 | 136 | 137 | Images only 138 | 139 | 140 | 141 | 142 | PDF only 143 | 144 | 145 | 146 | 147 | CBZ only 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | Overwrite existing files 158 | 159 | 160 | 161 | 162 | 163 | 164 | Orient images to match aspect ratio 165 | 166 | 167 | 168 | 169 | 170 | 171 | Dither images to match device palette 172 | 173 | 174 | 175 | 176 | 177 | 178 | Draw frame around images 179 | 180 | 181 | 182 | 183 | 184 | 185 | Split images into two pages (right, left) 186 | 187 | 188 | 189 | 190 | 191 | 192 | Split images into two pages (left, right) 193 | 194 | 195 | 196 | 197 | 198 | 199 | Auto crop image (remove white around the image) 200 | 201 | 202 | 203 | 204 | 205 | 206 | Size 207 | 208 | 209 | 210 | 211 | 212 | 213 | Resize images to center on screen 214 | 215 | 216 | 217 | 218 | 219 | 220 | Fit to screen with borders 221 | 222 | 223 | 224 | 225 | 226 | 227 | Fill screen by cropping 228 | 229 | 230 | 231 | 232 | 233 | 234 | Stretch images to fill screen 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | Qt::Vertical 245 | 246 | 247 | 248 | 20 249 | 40 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | buttonBox 267 | accepted() 268 | DialogOptions 269 | accept() 270 | 271 | 272 | 248 273 | 254 274 | 275 | 276 | 157 277 | 274 278 | 279 | 280 | 281 | 282 | buttonBox 283 | rejected() 284 | DialogOptions 285 | reject() 286 | 287 | 288 | 316 289 | 260 290 | 291 | 292 | 286 293 | 274 294 | 295 | 296 | 297 | 298 | 299 | -------------------------------------------------------------------------------- /mangle/util.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010 Alex Yatskov 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | import os.path 18 | import sys 19 | 20 | 21 | def buildResPath(relative): 22 | directory = os.path.dirname(os.path.realpath(sys.argv[0])) 23 | return os.path.join(directory, relative) 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | py2exe-py2==0.6.9; platform_system=='Windows' 2 | PyQt4 @ https://download.lfd.uci.edu/pythonlibs/w4tscw6k/cp27/PyQt4-4.11.4-cp27-cp27m-win32.whl; platform_system=='Windows' 3 | reportlab==3.5.59 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (C) 2013 Jan Martin 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | from distutils.core import setup 20 | import py2exe 21 | import sys 22 | 23 | 24 | sys.argv.append('py2exe') 25 | setup( 26 | name='Mangle', 27 | windows=[{'script': 'mangle.pyw'}], 28 | data_files=[('', ['LICENSE']), 29 | ('mangle/ui', ['mangle/ui/book.ui', 30 | 'mangle/ui/about.ui', 31 | 'mangle/ui/options.ui']), 32 | ('mangle/img', ['mangle/img/add_directory.png', 33 | 'mangle/img/add_file.png', 34 | 'mangle/img/banner_about.png', 35 | 'mangle/img/book.png', 36 | 'mangle/img/export_book.png', 37 | 'mangle/img/file_new.png', 38 | 'mangle/img/file_open.png', 39 | 'mangle/img/remove_files.png', 40 | 'mangle/img/save_file.png', 41 | 'mangle/img/shift_down.png', 42 | 'mangle/img/shift_up.png'])], 43 | options={'py2exe': { 44 | 'bundle_files': 1, 45 | 'includes': ['sip'], 46 | 'packages': ['reportlab.pdfbase', 'reportlab.rl_settings'], 47 | 'dll_excludes': ['MSVCP90.dll', 'w9xpopen.exe'] 48 | }}, 49 | zipfile=None 50 | ) 51 | --------------------------------------------------------------------------------