Phantom - Bildbetrachter in Python mit Tkinter und PIL

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
Benutzeravatar
Michael Schneider
User
Beiträge: 569
Registriert: Samstag 8. April 2006, 12:31
Wohnort: Brandenburg

Hallo allerseits,

nach einem Monat Zwangspause wegen Umzug und resultierender Internetauszeit möchte ich mal einen einfachen Bildbetrachter vorstellen. Er ist noch nicht ganz fertig und wird in folgenden Versionen auch Effektfilter wie Schärfen, Weichzeichnen, Emboss usw. regulär enthalten. Die Funktionen habe ich aber mangels Tests aus dieser Version entfernt. Eure Meinungen und Anregungen sind generell gewünscht und begehrt. Ich hoffe, der Code ist nicht zu lang...

EDIT: Übrigens, die Einzelbildframes kann man mit Rechtsklick jederzeit schließen

Code: Alles auswählen

 #   -*- encoding: iso-8859-1 -*-
from Tkinter import *
from glob import glob
import os
import sys
import Image
import ImageTk
import ImageFilter
import tkFileDialog

class CONST:
    """Verfügbare Konstanten

    PROGPATH    -   Pfad, in dem sich das Startprogramm befindet
    """
    PROGPATH = os.path.split(sys.argv[0])[0]    ##  Pfad des Programms

class Thumbnail(Button):
    def __init__(self, wMaster, Bild):
        self.Bild = Bild
        self.wMainWindow = None

        ##  show image in a new window
        def open_picture(self=self):
            Canvas_Window(self, Bild=self.Bild)
            
        Button.__init__(self, wMaster, image=Bild.get_thumb(), command=open_picture)
        self.iID = self.master.wCanvas.create_window((0, 0), window=self, anchor=NW)
        self.iX, self.iY = 0, 0

    def update_position(self, x, y):
        self.move_to(x=20+x*180, y=20+y*180)

    def move_to(self, x=0, y=0):
        "Verschiebt das Thumbnail an die Stelle x, y"
        iDX = x - self.iX
        iDY = y - self.iY
        self.master.wCanvas.move(self.iID, iDX, iDY)
        self.iX, self.iY = x, y
        

class Thumbnail_Frame(Frame):
    def __init__(self, wMaster):
        Frame.__init__(self, wMaster)
        self.lThumbnails = []       ##  Liste mit Thumbnail Instanzen
        self._create_widgets()

    def _create_widgets(self):
        self.wCanvas = Canvas(self, width=800, height=600)
        self.wCanvas.grid(row=0, column=0)
        self.wScrollV = Scrollbar(self, orient=VERTICAL, command=self.wCanvas.yview)
        self.wScrollV.grid(row=0, column=1, sticky=NSEW)
        self.wCanvas.config(yscrollcommand=self.wScrollV.set)

    def __add__(self, Bild):
        "Fügt das Bild im Overview-Canvas ein"
        self.lThumbnails.append(Thumbnail(self, Bild))

    def update_thumbnails(self):
        iColumnsPerLine = 4
        for iThumb in xrange(len(self.lThumbnails)):
            Thumb = self.lThumbnails[iThumb]
            iY, iX = divmod(iThumb, iColumnsPerLine)
            Thumb.update_position(iX, iY)

        self.update_scrollregion()

    def remove_all_thumbnails(self):
        while self.lThumbnails:
            self.lThumbnails.pop().destroy()

    def update_scrollregion(self):
        self.wCanvas.configure(scrollregion=self.wCanvas.bbox("all"))


class Overview_Window(Toplevel):
    def __init__(self, wMaster=None):
        ##  initialisiert Variablen
        self.sCurrPath = CONST.PROGPATH

        ##  initialisiert Basisklasse
        if not wMaster:
            wMaster = Tk()
            wMaster.iconify()
        Toplevel.__init__(self, wMaster)
        self.title("Phantom View: %s" % self.sCurrPath)

        self._create_widgets()

    def _create_widgets(self):
        """Erzeugt die Elemente des Übersichtfensters"""
        ##  Menuezeile erzeugen und einfuegen
        wMenuLine = Menu(self)
        wMenuLine.add_command(label="Ende", command=self.destroy)
        wMenuLine.add_command(label="Verzeichnis", command=self._choose_directory)
        self.config(menu=wMenuLine)
        
        self.wThumbnailFrame = Thumbnail_Frame(self)
        self.wThumbnailFrame.grid(row=0, column=0)

    def _choose_directory(self):
        import tkFileDialog
        sDir = tkFileDialog.askdirectory(initialdir=self.sCurrPath)
        if sDir:
            self._change_path(sDir)

    def _change_path(self, sDir):
        if os.path.isdir(sDir):
            self.sCurrPath = sDir
            self.title("Phantom View: %s" % self.sCurrPath)
            self.remove_all_thumbnails()
            self.load_dir(sDir)

    def __add__(self, Bild):
        if not isinstance(Bild, BILD):
            print Bild, "ist keine Instanz von BILD"
            return
        
        else:
            self.wThumbnailFrame + Bild     ##  Thumbnail des Bilds hinzufügen

    def load_dir(self, sDir):
        if os.path.isdir(sDir):
            ##  bringt die Canvasposition (0, 0) ins Bild
            wCanvas = self.wThumbnailFrame.wCanvas
            wCanvas.xview("moveto", "0")
            wCanvas.yview("moveto", "0")

            for sExtension in ("jpg", "jpeg", "pgj", "gpj", "gif", "tif", "png"):
                for sBild in glob(os.path.join(sDir,"*."+sExtension)):
                    print "lade Bild", sBild
                    Bild = BILD(sBild)
                    #Bild.add_filter(name="thumbnail", size=(800, 800), antialias=1)

                    self + Bild

                    self.update_thumbnails()
                    self.update_idletasks()


    def update_thumbnails(self):
        self.wThumbnailFrame.update_thumbnails()

    def remove_all_thumbnails(self):
        self.wThumbnailFrame.remove_all_thumbnails()

class Dummy_Callback:
    import tkMessageBox
    
    def __call__(self, *args, **kw):
        self.tkMessageBox.showerror("Achtung!", "Noch keine Funktion verlinkt!")
        
class Canvas_Frame(Frame):
    """Frame, der die den Canvas enthält"""
    def __init__(self, wMaster, Bild=None, width=800, height=600):
        Frame.__init__(self, wMaster)

        ##  Variablen initialisieren
        self.iWidth, self.iHeight = width, height
        self.iImageItem = 0

        ##  Widgets erzeugen
        self._create_widgets()

        ##  wenn ein Bild angegeben ist, zeige es an
        if Bild:
            self.show_bild(Bild)


    def _create_widgets(self):
        ##  widgets erzeugen
        self.wCanvas = Canvas(self, width=self.iWidth, height=self.iHeight)
        self.wScrollX = Scrollbar(self, orient=HORIZONTAL, command=self.wCanvas.xview)
        self.wScrollY = Scrollbar(self, orient=VERTICAL, command=self.wCanvas.yview)

        ##  widgets plazieren
        self.wCanvas.grid(row=0, column=0, sticky=NSEW)
        self.wScrollX.grid(row=1, column=0, sticky=EW)
        self.wScrollY.grid(row=0, column=1, sticky=NS)

        ##  Konfiguration des Canvas-Widgets
        self.wCanvas.config(xscrollcommand=self.wScrollX.set,
                            yscrollcommand=self.wScrollY.set)
        ##  bei Rechtsklick Fenster schliessen
        self.wCanvas.bind("<3>", self._destroy)

    def show_bild(self, Bild=None):
        if Bild:    ##  Bild wenn neues Bild angegeben, sonst gespeichertes Bild aktualisieren
            self.Bild = Bild
        tCenter = self.Bild.get_stats("center")
        if self.iImageItem:
            self.wCanvas.delete(self.iImageItem)
        print self.Bild.fZoom
        self.iImageItem = self.wCanvas.create_image(tCenter[0], tCenter[1], image=self.Bild())
        self._update_scrollregion()

    def update_bild(self, fZoom=0.0):
        """Ändert das Bild, mögliche Argumente: fZoom(0.0):float"""
        if fZoom > 0.0:
            self.Bild.fZoom = fZoom
            self.show_bild()

    def fit_bild(self, show_zoom_callback):
        """Passt das Bild in die Framegröße ein."""
        iTargetW, iTargetH = 800, 600
        dStats = self.Bild.get_stats()
        iSourceW, iSourceH = dStats["width"], dStats["height"]
        fZoom = min(float(iTargetW)/iSourceW,
                    float(iTargetH)/iSourceH)
        self.update_bild(fZoom=fZoom)
        show_zoom_callback(fZoom)   ##  zeige den Zoom im Optionsfeld an
            
    def _update_scrollregion(self):
        self.wCanvas.config(scrollregion=self.wCanvas.bbox("all"))

    def _destroy(self, Event=None):
        if self.Bild:
            self.Bild.del_image()
        self.master.destroy()

class Zoom_Frame(Frame):
    """Frame mit Knöpfen zur Größenkontrolle des Bildes"""
    def __init__(self, wMaster):
        Frame.__init__(self, wMaster)
        self._init_vars()
        self._init_widgets()

    def _init_vars(self):
        self.vZoom = StringVar()
        self.vZoom.set("100%")

    def show_zoom(self, fZoom):
        print "aendere Zoom in %i%%" % round(100*fZoom)
        self.vZoom.set("%i%%" % round(100*fZoom))
        
    def _init_widgets(self):
        self.vZoom.set(self.master.Bild.get_zoom())
        self.wOptionMenu = wOM = OptionMenu(self, self.vZoom, self.master.Bild.get_zoom())
        wMenu = Menu(wOM, tearoff=False)
        for sEntry in ("25%", "50%", "75%", "100%", "150%", "200%", "400%"):
            def set_zoom(sValue=sEntry):
                wOM["text"] = sValue
                self.vZoom.set(sValue)
                print "neuer Zoomlevel: %s" % self.vZoom.get()

                ##  Ändere Zoomlevel und zeige das neue Bild an
                self.master.wCanvasFrame.update_bild(fZoom=float(sValue[:-1])/100)

            if sEntry == "100%":
                set_zoom_100 = set_zoom

            def create_entry(sEntry=sEntry, Callback=None):
                wMenu.add_command(label=sEntry, command=Callback)

            create_entry(Callback=set_zoom)
                
            
        wOM.config(menu=wMenu)
        wOM.grid(row=0, column=1, padx=10)

        self.wButton100 = wB100 = Button(self, text=r"100%", command=set_zoom_100)
        wB100.grid(row=0, column=2, padx=10)

        self.wButtonFit = wBF = Button(self, text="Einpassen", command=self.fit_bild)
        wBF.grid(row=0, column=3, padx=10)

    def fit_bild(self):
        self.master.wCanvasFrame.fit_bild(self.show_zoom)

        
class Canvas_Window(Toplevel):
    """Fenster für das anzuzeigende Bild"""
    def __init__(self, wMaster, Bild=None):
        Toplevel.__init__(self, wMaster)
        self.Bild = Bild
        self._init_widgets(Bild)
        self.set_title(Bild)

    def _init_widgets(self, Bild):
        self.wCanvasFrame = Canvas_Frame(self, Bild=Bild)
        self.wCanvasFrame.grid(row=1, column=1)

        self.wZoomFrame = Zoom_Frame(self)
        self.wZoomFrame.grid(row=2, column=1)
        self.wZoomFrame.fit_bild()  ##  Bild einpassen
        
    def set_title(self, Bild):
        if Bild:
            FILENAME = 1
            self.title(os.path.split(Bild.sName)[FILENAME])
        
class BILD:
    """Verwaltet Bilddaten und -funktionen

    Der Bool'sche Wert einer BILD-Instanz ist True, wenn ein Bild geladen ist, sonst False"""
    def __init__(self, Source, preload=False):
        self.imgBild = None     ##  None als Bild initialisieren
        self.phThumb = None     ##  Thumbnail initialisieren
        self.phBild = None      ##  Bild als Photoimage
        self.sName = ""         ##  Name des Bildes
        self.tThumbSize = (0, 0)    ##  letzte Größe des Thumbnails
        self.lFilters = []      ##  Liste der Bearbeitungsfilter
        self.iItemNum = 0       ##  Itemnummer des Bildes im OverviewCanvas
        self.fZoom  = 0.5       ##  Zoomfaktor
        self.Source = Source

        if preload:
            self._load_image()

    def _load_image(self):
        """Lade image in Bild-Instanz"""
        ########################################################################################
        ##  initialisiere das Objekt je nach Typ der Quelle                                     
        if type(self.Source) == type(""):

            ###############################################
            ##  Bild aus Datei gelesen
            if os.path.isfile(self.Source):
                self.imgBild = Image.open(self.Source)
                self.sName = self.Source

        if self.imgBild:
            self.tSize = self.iWidth, self.iHeight = self.imgBild.size
            self.tCenter = self.iCenterX, self.iCenterY = self.iWidth / 2, self.iHeight / 2

    def get_zoom(self):
        """Liefert die Zoomstufe als String in Prozent, z.B. 87%"""
        if (self.imgBild) and (self.phBild):
            return "%i%%" % (100*self.phBild.width()/self.imgBild.size[0])
        else:
            return "0%"

    def get_image(self):
        """Liefert das Originalimage, lädt es wenn notwendig."""
        if not self.imgBild:
            self._load_image()
        return self.imgBild

    def __nonzero__(self):
        if self.Source:
            return True
        else:
            return False

    def del_image(self):
        "Löscht das intern gespeicherte Image, um Ressourcen freizugeben."""
        del self.imgBild
        self.imgBild = None
        
    def __call__(self, size=(), keepratio=True, match=True):
        "Create PhotoImage and return it"""
        if not self:    ##  Gib None zurück, wenn kein Bild geladen ist
            return None
        
        ##  wenn keine Filter angegeben, liefere nur das PhotoImage vom Originalbild
        if not self.lFilters:
            imgBild = self.get_image().copy()

        ##  sonst wende die Filter auf das Originalbild an
        else:
            imgBild = self._create_filtered_image()

        #self.phBild = ImageTk.PhotoImage(self.resize_image(imgBild, size, keepratio, match))
        if not self.fZoom == 1.0:
            imgBild = self._apply_zoom(imgBild)
        
        self.phBild = ImageTk.PhotoImage(imgBild)

        return self.phBild
    
    def get_thumb(self, size=(160, 160), reset=False):
        """Erzeugt und liefert ein Thumbnail des Bildes.

        Parameter:
            size((160, 160))    - Größe des Thumbnails
            reset(False)        - wenn True wird die Erzeugung eines neuen Thumbs erzwungen"""
        
        if (not self.phThumb) or reset or (not size==self._tThumbSize):
            #img = self._create_filtered_image()    ## gefiltertes Bild als Vorlage
            if self.imgBild:
                img = self.imgBild.copy()
            else:
                img = Image.open(self.Source)
            img.thumbnail(size, 1)
            self.phThumb = ImageTk.PhotoImage(img)
            self.tThumbSize = size
        return self.phThumb
    
    def get_stats(self, sStat=""):
        imgTemp = self._create_filtered_image()
        dStats = {}
        dStats["size"] = dStats["width"], dStats["height"] = imgTemp.size
        dStats["center"] = dStats["center x"], dStats["center y"] = dStats["width"] / 2, dStats["height"] / 2
        if sStat:
            return dStats.get(sStat, "")
        return dStats

    def set_item(self, iItemNum):
        "Speichert ID (Itemnummer) des Bildes im Overview Canvas."
        self.iItemNum = iItemNum

    def get_item(self):
        "Liefert die ID des Bildes im Overview Canvas."
        return self.iItemNum
        
    def _create_filtered_image(self):
        "Rechnet die Filter in das Image ein."""
        imgTemp = self.get_image().copy()   ##  Kopie des Originals anlegen
        for dFilter in self.lFilters:
            imgTemp = self._apply_filter(dFilter, imgTemp)
        return imgTemp

    def _apply_zoom(self, imgSource, sFilter=Image.LINEAR):
        iW, iH = imgSource.size
        return imgSource.resize((int(iW*self.fZoom), int(iH*self.fZoom)), sFilter)
    
    def _apply_filter(self, dFilter, imgTemp):
        """Wendet einen von mehreren Filtern an:

        Thumbnail erzeugen:
            name = thumbnail,
            size = Tuple der Zielgröße,
            antialias = Ausbesserung der Ränder

        Effektfilter:
            name = blur         - Verschwimmen
            name = contour      - Konturen verstärken
            name = edge_enhance - Kanten verstärken
            name = edge_enhance_more - Kanten mehr verstärken
            name = emboss       - Reliefbild
            name = sharpen      - Schärfen
            name = smooth       - Weichzeichnen
            name = smooth_more  - stark Weichzeichnen"""
        
        ##  erzeuge Thumbnail
        if dFilter.get("name", "") == "thumbnail":
            tTargetSize = dFilter.get("size", (100,100))
            iAntialias = dFilter.get("antialias", 1)
            imgTemp.thumbnail(tTargetSize, iAntialias)

        ##  Effektfilter
        if dFilter.get("name", "") in ("blur", "contour", "edge_enhance", "edge_enhance_more",
                                       "emboss", "sharpen", "smooth", "smooth_more"):
            imgTemp = imgTemp.filter(getattr(ImageFilter, dFilter.get("name").upper()))
            
        return imgTemp

    def add_filter(self, **dFilter):
        """Fuege das Filterdictionary dFilter der Bildfilterliste hinzu"""
        if not dFilter == {}:
            self.lFilters.append(dFilter)

def main():
    OW = Overview_Window()
    OW.load_dir("./")
    OW.update_thumbnails()
    OW.mainloop()
    
if __name__ == "__main__":
    main()
Grüße,
Michael
Diese Nachricht zersört sich in 5 Sekunden selbst ...
Antworten