Kangle - Manga auf dem Kindle

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
Benutzeravatar
darktrym
User
Beiträge: 784
Registriert: Freitag 24. April 2009, 09:26

Ich hab' ein kleines Skript geschrieben, welches Manga-Scans optimiert für den Kindle auf's Gerät zaubert.
Tester und Feedback erwünscht.

Code: Alles auswählen

#! /usr/bin/env python

#Copyright 2011 Daniel Oelschlegel. All rights reserved.
#
#Redistribution and use in source and binary forms, with or without modification, are
#permitted provided that the following conditions are met:
#
#   1. Redistributions of source code must retain the above copyright notice, this list of
#      conditions and the following disclaimer.
#
#   2. Redistributions in binary form must reproduce the above copyright notice, this list
#      of conditions and the following disclaimer in the documentation and/or other materials
#      provided with the distribution.
#
#THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND ANY EXPRESS OR IMPLIED
#WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
#FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR
#CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
#CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
#SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
#ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
#NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
#ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#The views and conclusions contained in the software and documentation are those of the
#authors and should not be interpreted as representing official policies, either expressed
#or implied, of Daniel Oelschlegel.


# this program requires a plugged kindle, at least python 2.* and PIL
# start this program in a subdirectory that contains picture files or directories
# press [ALT] + [z] for rereading the entries in pictures on a kindle

import sys
import os
import Image

# version tracker
version = 0.1
author = "Daniel Oelschlegel"
# global variable counter needed for recursive search
counter = 0
# resolution of your kindle, required for stretching and splitting
width = 600
height = 800
screen = (width, height)
# global variable target_dir is the kindle root directory, dummy value
target_dir = 'D:\\'

# kindle reads the files in order of timestamp
def adjustImage((filename, counter)):
	st = Image.open(filename)
	# prevents IOError with this type
	if st.mode == 'P':
		special = True
	else:
		special = False	
	# resolution of the image, width and height
	(w, h) = st.size
	filename = str(counter).rjust(6, '0') + 'a.jpg'
	# too wide, better splitting in middle and stretching
	if w > h:
		# take the second half and resize
		nd = st.crop((w / 2, 0, w, h)).resize(screen)
		st = st.crop((0, 0, w / 2, h))
		second = os.path.join(target_dir, filename)
		# manga reading style, right to left, save in correct order
		if not special:
			nd.save(second, "JPEG")
		else:
			nd.convert("RGB").save(second, "JPEG")
		filename = filename.replace('a.jpg', 'b.jpg')
	st = st.resize(screen)
	first = os.path.join(target_dir, filename)
	if not special:
		st.save(first, "JPEG")
	else:
		st.convert("RGB").save(first, "JPEG")

# recursive searching
def looking(dir_name):
	dirs = []
	files = []
	for item in os.listdir(dir_name):
		item = os.path.join(dir_name, item)
		if os.path.isfile(item):
			files.append(item)
		else:
			dirs.append(item)
	if files:
		tasks = []
		# some OSes need this, additional sorting
		files.sort()
		for filename in files:
			global counter
			name = filename[-4:].lower()
			# filter for file extensions, 
			# this must be supported by PIL
			if name =='.jpg' or \
				name == '.png' or \
				name == '.gif':
				tasks.append((filename, counter))
				counter += 1
		map(adjustImage, tasks)
	
	# continue with subdirectories
	for dir in dirs:
		looking(dir)

def main():
	global target_dir
	
	if len(sys.argv) < 2:
		print "arguments: <TITLE> <KINDLE_ROOT_DIRECTORY>"
		sys.exit(-1)
	if sys.argv[1] == "--version":
		print "Kangle version " + version + " by " + author
		sys.exit(0)
	
	# sys.argv[2] could look like "D:\"(windows) or "/media/kindle"(unix-like)
	target_dir = sys.argv[2]
	for dir in ["pictures", sys.argv[1]]:
		target_dir = os.path.join(target_dir, dir)
		if not os.path.isdir(target_dir):
			print "creating directory " + target_dir
			os.mkdir(target_dir)
		elif dir == sys.argv[1]:
			print "directory " + dir + " already exists"
	print "converting & transferring ...",
	looking(os.getcwd())
	print "finished"

if __name__ == "__main__":
	main()
„gcc finds bugs in Linux, NetBSD finds bugs in gcc.“[Michael Dexter, Systems 2008]
Bitbucket, Github
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Oha, das ist ja ne feine Idee. Hast du dazu ein Github-Projekt? Dann würde ich wohl da (beizeiten) mal reinschauen und etwas herumhacken. Btw, da wo <COPYRIGHT HOLDER> steht, gehört dein Name rein ;)
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
BlackJack

@darktrym: Ein bisschen Feedback (das kommt davon wenn man danach fragt ;-)):

Es ist mit Tabs eingerückt statt mit vier Leerzeichen pro Ebene. Die Zeilen der Lizenz sehen nach 80 Zeichen umgebrochen bescheiden aus.

``global`` sollte man vermeiden. Namen auf Modulebene sollten "Konstanten" sein. Und die werden per Konvention komplett in Grossbuchstaben benannt. Dann kann man zum Beispiel auch `width` und `height` in Funktionen ausschreiben ohne Gefahr sie mit den Konstanten zu verwechseln.

Version und Autor könnte man auf Modulebene mit den Namen `__version__` und `__author__` versehen. Die werden von einigen Dokumentationswerkzeugen berücksichtigt, wie zum Beispiel dem `pydoc` aus der Standardbibliothek oder `epydoc`.

Abkürzungen sollte man vermeiden wenn sie nicht wirklich gebräuchlich und damit für den Leser verständlich sind. Was `st` und `nd` in `adjustImage()` bedeuten erschliesst sich mir jedenfalls nicht.

Wenn man abhängig von einer Bedingung einen Namen an `True` oder `False` binden will, benötigt man kein ``if``. Die Bedingung wird ja schon zu einem Wahrheitswert ausgewertet. Also hätte ein ``special = st.mode == 'P'`` ausgereicht. Der Name `special` ist IMHO zu generisch. `is_pallete_mode` wäre zum Beispiel aussagekräftiger. Den Test hätte ich mir aber grundsätzlich gespart. Bei der Umwandlung eines Bildes welches schon im RGB-Modus vorliegt, schadet ein ``.convert('RGB')`` nicht weiter, also kann man das einfach *immer* aufrufen.

`str.rjust()` mit '0' als zweitem Argument kann man durch `str.zfill()` ersetzen. Oder man verwendet Zeichenkettenformatierung mit dem ``%``-Operator oder der etwas neueren `format()`-Methode:

Code: Alles auswählen

In [235]: counter = 42

In [236]: '%06da.jpg' % counter
Out[236]: '000042a.jpg'

In [237]: '{0:06d}a.jpg'.format(counter)
Out[237]: '000042a.jpg'
`looking()` sagt nicht genug über die Funktion aus. Die tut mehr als nur zu schauen. Ausserdem baust Du dort `os.walk()` nach. Wenn Du diese Funktion benutzt, könntest Du den `counter` auch in der Funktion definieren und müsstest ihn nicht als modulglobale Variable führen.

Die ``if``-Abfrage ob `files` nicht leer ist, braucht man nicht.

`map()` ist eine Funktion um eine Liste zu erstellen. Wenn man diese Liste gar nicht haben möchte, sollte man sie nicht missbrauchen um eine explizite Schleife nicht schreiben zu müssen. Damit könnte man den `adjustImage()`-Aufruf in die sowieso schon existierende Schleife davor schreiben und sich die `tasks`-Liste einsparen.

Bei der Entscheidung Datei oder Verzeichnis würde ich auf `isdir()` prüfen. So wie es jetzt ist, können in `dirs` Sachen landen mit denen `os.listdir()` auf die Nase fällt, die aber im umgekehrten Fall sehr wahrscheinlich von dem Test auf die Dateiendungen ausgefiltert werden.

Den Test auf die Dateiendungen könnte man auch einfacher ausdrücken:

Code: Alles auswählen

if filename.lower().endswith(('.jpg', '.png', '.gif')):
    # ...
Bei den ``print``-Ausgaben machst Du für meinen Geschmack zu häufig Gebrauch von ``+`` mit Zeichenketten. Grundsätzlich reicht in vielen Fällen ein Komma, ansonsten setzt man an diesen Stellen in Python eher die Formatierung per ``%``-Operator oder `format()`-Methode ein.
derdon
User
Beiträge: 1316
Registriert: Freitag 24. Oktober 2008, 14:32

Um auf Dateiendungen zu testen, bevorzuge ich os.path:

Code: Alles auswählen

>>> from os import path
>>> root, ext = path.splitext('file.jpg')
>>> root
'file'
>>> ext
'.jpg'
>>> ext[1:] in ('jpg', 'png', 'gif')
True
PS: es sind zwar keine Tabs, sondern 8 Leerzeichen; das ist aber genauso schlimm ;)
BlackJack

@derdon: Es sind Tabs! Das Forum macht für die HTML-Anzeige daraus Leerzeichen, aber wenn Du beim ersten Beitrag auf "zitieren" klickst und mal in Texteingabefeld nachschaust sind da Tabs.
Benutzeravatar
darktrym
User
Beiträge: 784
Registriert: Freitag 24. April 2009, 09:26

Vielen Dank an alle, ein paar Verbesserungen wurden bereits umgesetzt. Das Repository ist erreichbar unter [1].

[1] http://code.google.com/p/kangle/

Code: Alles auswählen

#! /usr/bin/env python

# Copyright 2011 Daniel Oelschlegel. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without 
# modification, are permitted provided that the following conditions are met:
#
#   1. Redistributions of source code must retain the above copyright notice,
#   this list of conditions and the following disclaimer.
#
#   2. Redistributions in binary form must reproduce the above copyright 
#   notice, this list of conditions and the following disclaimer in the 
#   documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
# NO EVENT SHALL Daniel Oelschlegel OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and documentation are
# those of the authors and should not be interpreted as representing official
# policies, either expressed or implied, of Daniel Oelschlegel.


# this program requires a plugged kindle, at least python 2.* and PIL
# start this program in a subdirectory that contains picture files or 
# directories
# press [ALT] + [z] for rereading the entries in pictures on a kindle

import sys
import os
try:
    import Image
except ImportError:
    print "please install PIL"
    sys.exit(-2)

__author__ = "Daniel Oelschlegel"
__copyright__ = "Copyright 2011, " + __author__
#__credits__ = ["", ""]
__license__ = "BSD"
__version__ = "0.2"

class Kangle(object):
    """Kangle makes manga scans readable on a kindle device."""
    # needed for recursive search
    __counter = 0
    # resolution of your kindle, required for stretching
    resolution = (600, 800)
    # target_dir is the kindle root directory, dummy value
    __target_dir = 'D:\\'

    def resetCounter(self):
        self.__counter = 0

    def run(self, dir):
        self.resetCounter()
        print "converting & transferring ...",
        self.looking(dir)
        print "finished"

    # kindle reads the files in order of timestamp
    def adjustImage(self, filename, counter):
        """Adjust the image file filename to kindle screen and use counter for naming."""
        first = Image.open(filename)
        # prevents IOError with this type, convert is often unneeded and slow
        palleteMode = first.mode == 'P'
        # resolution of the image
        (width, height) = first.size
        filename = '%06da.jpg' % self.__counter
        # too wide, better splitting in middle and stretching
        if width > height:
            # take the second half and resize
            second = first.crop((width / 2, 0, width, height))
            second = second.resize(self.resolution)
            first = first.crop((0, 0, width / 2, height))
            fullName = os.path.join(self.__target_dir, filename)
            # manga reading style, right to left, save in correct order
            if not palleteMode:
                second.save(fullName, "JPEG")
            else:
                second.convert("RGB").save(fullName, "JPEG")
            filename = filename.replace('a.jpg', 'b.jpg')
        first = first.resize(self.resolution)
        fullName = os.path.join(self.__target_dir, filename)
        if not palleteMode:
            first.save(fullName, "JPEG")
        else:
            first.convert("RGB").save(fullName, "JPEG")

    # optimized recursive search
    def looking(self, dir_name):
        """Find supported pictures in dir_name."""
        dirs = []
        files = []
        for item in os.listdir(dir_name):
            item = os.path.join(dir_name, item)
            if os.path.isdir(item):
                dirs.append(item)
            else:
                files.append(item)
        # some OSes need this, additional sorting
        files.sort()
        for filename in files:
            # filter for file extensions, 
            # this must be supported by PIL
            if filename[-4:].lower() in ['.jpg', '.png', '.gif']:
                self.adjustImage(filename, self.__counter)
                self.__counter += 1
        
        # continue with subdirectories
        for dir in dirs:
            self.looking(dir)

    def __init__(self, title, target_dir):
        self.__target_dir = target_dir
        for dir in ["pictures", title]:
            self.__target_dir = os.path.join(self.__target_dir, dir)
            if not os.path.isdir(self.__target_dir):
                print "creating directory " , self.__target_dir
                os.mkdir(self.__target_dir)
            elif dir == sys.argv[1]:
                print "directory ", dir, " already exists"

if __name__ == "__main__":
    try:
        # sys.argv[2] could look like "D:\"(windows) or "/media/kindle"(unix)
        title, target_dir = sys.argv[1], sys.argv[2]
    except IndexError:
        if len(sys.argv) < 3 and sys.argv[1] == "--version":
            print "Kangle version ", __version__," by ", __author__
            #TODO: credit line
            sys.exit(0) 
        else:
            print "arguments: <TITLE> <KINDLE_ROOT_DIRECTORY>"
            sys.exit(-1)
   
    kangle = Kangle(title, target_dir)    
    kangle.run(os.getcwd())
    
Zuletzt geändert von darktrym am Montag 9. Mai 2011, 13:28, insgesamt 1-mal geändert.
„gcc finds bugs in Linux, NetBSD finds bugs in gcc.“[Michael Dexter, Systems 2008]
Bitbucket, Github
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Und wozu ist nun die Klasse? Was ist denn *semantisch* ein Kangle-Exemplar?
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
derdon
User
Beiträge: 1316
Registriert: Freitag 24. Oktober 2008, 14:32

BlackJack hat geschrieben:@derdon: Es sind Tabs! Das Forum macht für die HTML-Anzeige daraus Leerzeichen, aber wenn Du beim ersten Beitrag auf "zitieren" klickst und mal in Texteingabefeld nachschaust sind da Tabs.
Entschuldigung, das wusste ich nicht.
Antworten