├── .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.png)
12 | [](img/kindle2.png)
13 | [](img/kindle3.png)
14 | [](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 |
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 |
--------------------------------------------------------------------------------