Browse Source

Initial commit into source control.

Signed-off-by: Billy Barrow <billyb@pcthingz.com>
Billy Barrow 8 years ago
commit
fab57d1d4c

+ 31 - 0
Activity/__init__.py

@@ -0,0 +1,31 @@
+
+class Activity:
+
+    def __init__(self, stack, header_stack, builder, root, show_message, hide_message, update_message_progress, start_work, stop_work, switch_activity):
+        self.widget = None
+        self.stack = stack
+        self.header_widget = None
+        self.header_stack = header_stack
+        self.menu_popover = None
+        self.id = ""
+        self.name = ""
+        self.subtitle = ""
+        self.builder = builder
+        self.root = root
+        self.show_message = show_message
+        self.hide_message = hide_message
+        self.update_message_progress = update_message_progress
+        self.start_work = start_work
+        self.stop_work = stop_work
+        self.switch_activity = switch_activity
+
+        self.on_init()
+
+    def on_init(self):
+        raise NotImplemented()
+
+    def on_exit(self):
+        return True
+
+    def on_open(self, path):
+        raise NotImplemented()

+ 170 - 0
Export/__init__.py

@@ -0,0 +1,170 @@
+import os
+import random
+import threading
+from gi.repository import GLib
+import cv2
+import subprocess
+
+
+class ExportDialog:
+
+    def __init__(self, root, builder, w, h, get_image_call, done_call, path):
+        self.path = path
+        self.builder = builder
+        self.root = root
+        # What gets called to get the image object at process time
+        self.get_image_call = get_image_call
+        self.done_call = done_call
+        self.width = w
+        self.height = h
+        self.image = None
+
+        UI_FILE = "ui/Export.glade"
+        self.builder.add_from_file(UI_FILE)
+
+        self.ui = {}
+
+        components = [
+            "window",
+            "headerbar",
+            "file",
+            "save_button",
+            "cancel_button",
+            "preset",
+            "format",
+            "pngcrush",
+            "width",
+            "height",
+            "quality",
+            "width_spin",
+            "height_spin",
+            "quality_spin"
+        ]
+
+        for component in components:
+            self.ui[component] = self.builder.get_object("Export_%s" % (component))
+
+        # Setup Export Dialog
+        self.ui["window"].set_transient_for(self.root)
+        self.ui["window"].set_titlebar(self.ui["headerbar"])
+
+        self.ui["width"].set_upper(w)
+        self.ui["width"].set_value(w)
+        self.ui["height"].set_upper(h)
+        self.ui["height"].set_value(h)
+
+        self.ui["file"].set_filename(".".join(path.split(".")[:-1]) + "_PF")
+        self.ui["file"].set_current_name(".".join(path.split("/")[-1:][0].split(".")[:-1]) + "_PF")
+
+        # Connect siginals
+        self.ui["preset"].connect("changed", self.on_preset_changed)
+        self.ui["format"].connect("changed", self.on_format_changed)
+        self.ui["width_spin"].connect("changed", self.on_width_changed)
+        self.ui["height_spin"].connect("changed", self.on_height_changed)
+        self.ui["save_button"].connect("clicked", self.on_save_clicked)
+        self.ui["cancel_button"].connect("clicked", self.on_cancel_clicked)
+
+
+        self.ui["window"].show_all()
+
+
+    def on_preset_changed(self, sender):
+        if(sender.get_active() == 0):
+            self.ui["width_spin"].set_sensitive(True)
+            self.ui["height_spin"].set_sensitive(True)
+            self.ui["format"].set_sensitive(True)
+            self.ui["pngcrush"].set_sensitive(self.ui["format"].get_active() == 0)
+            self.ui["quality_spin"].set_sensitive(self.ui["format"].get_active() == 1)
+
+        else:
+            if(sender.get_active() == 1):
+                self.ui["width"].set_value(2048)
+                self.ui["format"].set_active(1)
+                self.ui["quality"].set_value(85)
+
+            elif(sender.get_active() == 2):
+                self.ui["width"].set_value(self.width)
+                self.ui["format"].set_active(0)
+                self.ui["pngcrush"].set_active(True)
+
+            elif(sender.get_active() == 3):
+                self.ui["width"].set_value(1920)
+                self.ui["format"].set_active(1)
+                self.ui["quality"].set_value(67)
+
+            elif(sender.get_active() == 4):
+                self.ui["width"].set_value(self.width)
+                self.ui["format"].set_active(2)
+
+            self.ui["width_spin"].set_sensitive(False)
+            self.ui["height_spin"].set_sensitive(False)
+            self.ui["format"].set_sensitive(False)
+            self.ui["pngcrush"].set_sensitive(False)
+            self.ui["quality_spin"].set_sensitive(False)
+
+    def on_format_changed(self, sender):
+        self.ui["pngcrush"].set_sensitive(sender.get_active() == 0)
+        self.ui["quality_spin"].set_sensitive(sender.get_active() == 1)
+        self.ui["pngcrush"].set_active(False)
+
+    def on_width_changed(self, sender):
+        w = sender.get_value()
+        r = w/self.width
+        self.ui["height_spin"].set_value(r*self.height)
+
+    def on_height_changed(self, sender):
+        h = sender.get_value()
+        r = h/self.height
+        self.ui["width_spin"].set_value(r*self.width)
+
+    def on_cancel_clicked(self, sender):
+        self.ui["window"].close()
+
+    def on_save_clicked(self, sender):
+        self.ui["window"].close()
+        format = self.ui["format"].get_active()
+        quality = self.ui["quality"].get_value()
+        width = self.ui["width"].get_value()
+        height = self.ui["height"].get_value()
+        path = self.ui["file"].get_filename()
+        pngcrush = self.ui["pngcrush"].get_active()
+        threading.Thread(target=self.do_export, args=(format, quality, width,
+                                                      height, path, pngcrush)).start()
+
+    def do_export(self, format, quality, width, height, path, pngcrush):
+        self.image = self.get_image_call(width, height)
+        newPath = path
+        if(format == 0) and (not path.endswith(".png")):
+            newPath += ".png"
+        if(format == 1) and (not path.endswith(".jpg")) and (not path.endswith(".jpeg")):
+            newPath += ".jpg"
+        if(format == 2) and (not path.endswith(".tif")) and (not path.endswith(".tiff")):
+            newPath += ".tiff"
+
+        if(format == 1):
+            # Convert to 8 Bit
+            bpp = int(str(self.image.dtype).replace("uint", ""))
+            im = (self.image/float(2**bpp))*255
+            cv2.imwrite(newPath, im, [int(cv2.IMWRITE_JPEG_QUALITY), int(quality)])
+
+        elif(format == 0) and pngcrush:
+            tempPath = "/tmp/pngcrush-%i.png" % random.randrange(1000000,9999999)
+            cv2.imwrite(tempPath, self.image)
+            subprocess.call(["pngcrush", "-rem gAMA", "-rem cHRM", "-rem iCCP", "-rem sRGB" , "-m 0", "-l 9", "-fix", "-v", "-v", tempPath, newPath])
+
+        else:
+            cv2.imwrite(newPath, self.image)
+
+        GLib.idle_add(self.done_call, newPath)
+
+
+
+
+
+
+
+
+
+
+
+

+ 277 - 0
FocusStack/__init__.py

@@ -0,0 +1,277 @@
+import getpass
+import glob
+import os
+
+import subprocess
+
+import shutil
+import threading
+
+import cv2
+
+import Activity
+from gi.repository import GLib, Gtk, GdkPixbuf, Pango
+
+import Export
+
+
+class FocusStack(Activity.Activity):
+
+    def on_init(self):
+        self.id = "FocusStack"
+        self.name = "Focus Stack"
+        self.subtitle = "Stack Many Images at Different Focus Points"
+
+        UI_FILE = "ui/FocusStack_Activity.glade"
+        self.builder.add_from_file(UI_FILE)
+
+        self.widget = self.builder.get_object("FocusStack_main")
+        self.stack.add_titled(self.widget, self.id, self.name)
+
+        self.header_widget = self.builder.get_object("FocusStack_header")
+        self.header_stack.add_titled(self.header_widget, self.id, self.name)
+
+        # Get all UI Components
+        self.ui = {}
+        components = [
+            "open_window",
+            "open_chooser",
+            "preview_button",
+            "open_header",
+            "open_open_button",
+            "open_cancel_button",
+            "images",
+            "preview",
+            "export_image",
+            "open_pf2",
+            "add_button",
+            "remove_button",
+            "scroll_window",
+            "popovermenu"
+        ]
+
+        self.paths = []
+        self.exporting = False
+
+        for component in components:
+            self.ui[component] = self.builder.get_object("%s_%s" % (self.id, component))
+
+        self.menu_popover = self.ui["popovermenu"]
+
+        self.ui["open_window"].set_transient_for(self.root)
+        self.ui["open_window"].set_titlebar(self.ui["open_header"])
+
+        # Connect Siginals
+        self.ui["add_button"].connect("clicked", self.on_add_clicked)
+        self.ui["remove_button"].connect("clicked", self.on_remove_clicked)
+        self.ui["open_open_button"].connect("clicked", self.on_file_opened)
+        self.ui["open_cancel_button"].connect("clicked", self.on_file_canceled)
+        self.ui["preview_button"].connect("clicked", self.on_preview_clicked)
+        self.ui["export_image"].connect("clicked", self.export_clicked)
+        self.ui["open_pf2"].connect("clicked", self.export_pf2_clicked)
+
+        self.update_enabled()
+
+    def on_add_clicked(self, sender):
+        self.ui["open_window"].show_all()
+
+    def on_file_canceled(self, sender):
+        self.ui["open_window"].hide()
+
+    def on_file_opened(self, sender):
+        self.ui["open_window"].hide()
+        paths = self.ui["open_chooser"].get_filenames()
+        self.show_message("Loading Images", "Please wait while PhotoFiddle loads your images…", True, True)
+        threading.Thread(target=self.add_images, args=(paths,)).start()
+
+    def update_enabled(self):
+        enabled = len(self.get_paths()) > 0
+        self.ui["export_image"].set_sensitive(enabled)
+        self.ui["open_pf2"].set_sensitive(enabled)
+        self.ui["preview_button"].set_sensitive(enabled)
+
+    def add_images(self, paths):
+        c = 0
+        for path in paths:
+            # Create box
+            box = Gtk.HBox()
+            box.set_spacing(6)
+            im = Gtk.Image()
+
+            pb = GdkPixbuf.Pixbuf.new_from_file_at_scale(path, 128, 128, True)
+            im.set_from_pixbuf(pb)
+
+            box.add(im)
+
+            label = Gtk.Label()
+            label.set_ellipsize(Pango.EllipsizeMode.START)
+            label.set_text(path)
+
+            box.add(label)
+            box.show_all()
+            c += 1
+            GLib.idle_add(self.add_image_row, box, c, len(paths))
+
+        GLib.idle_add(self.hide_message)
+
+    def add_image_row(self, row, count, length):
+        self.ui["images"].add(row)
+        self.update_enabled()
+        self.update_message_progress(count, length)
+
+
+    def on_remove_clicked(self, sender):
+        items = self.ui["images"].get_selected_rows()
+        for item in items:
+            item.destroy()
+        self.update_enabled()
+
+
+    def on_open(self, path):
+        self.root.get_titlebar().set_subtitle("Focus Stack")
+        pass
+
+    def on_exit(self):
+        if(not self.exporting):
+            self.root.get_titlebar().set_subtitle("")
+            self.on_init()
+            return True
+        return False
+
+
+    def on_preview_clicked(self, sender):
+        self.show_message("Generating Preview…", "PhotoFiddle is preparing to generate a preview", True, True)
+        paths = self.get_paths()
+        w = self.ui["scroll_window"].get_allocated_width() - 12
+        h = self.ui["scroll_window"].get_allocated_height() - 12
+        threading.Thread(target=self.do_preview, args=(paths, w, h)).start()
+
+
+    def do_stack(self, images, output):
+        GLib.idle_add(self.show_message, "Focus Stacking…", "PhotoFiddle is aligning images…", True, False)
+        if(not os.path.exists("/tmp/stack-%s" % getpass.getuser())):
+            os.mkdir("/tmp/stack-%s" % getpass.getuser())
+
+        command = ["align_image_stack", "-m", "-a", "/tmp/stack-%s/OUT_" % getpass.getuser()] + images
+        subprocess.call(command)
+
+        aligned_list = glob.glob("/tmp/stack-%s/OUT*.tif" % getpass.getuser())
+        aligned_list.reverse()
+
+        GLib.idle_add(self.show_message, "Focus Stacking…", "PhotoFiddle is performing a focus stack…", True, False)
+
+        command = ["enfuse", "--output=/tmp/stacked-%s.tiff" % getpass.getuser(), "--exposure-weight=0",
+                   "--saturation-weight=0", "--contrast-weight=1", "--hard-mask",
+                   "-v"] + aligned_list
+
+        subprocess.call(command)
+
+        shutil.copyfile("/tmp/stacked-%s.tiff" % getpass.getuser(), output)
+        shutil.rmtree("/tmp/stack-%s" % getpass.getuser())
+        os.unlink("/tmp/stacked-%s.tiff" % getpass.getuser())
+
+        GLib.idle_add(self.hide_message)
+
+
+    def get_paths(self):
+        paths = []
+        for item in self.ui["images"].get_children():
+            paths += [item.get_children()[0].get_children()[1].get_text(),]
+
+        return paths
+
+    def do_preview(self, paths, w, h):
+        if (not os.path.exists("/tmp/stack-%s" % getpass.getuser())):
+            os.mkdir("/tmp/stack-%s" % getpass.getuser())
+
+        length = len(paths)*3
+        count = 0
+        images = []
+        for path in paths:
+            GLib.idle_add(self.update_message_progress, count, length)
+            im = cv2.imread(path, 2 | 1)
+            height, width = im.shape[:2]
+
+            # Get fitting size
+            ratio = float(w) / width
+            if (height * ratio > h):
+                ratio = float(h) / height
+
+            nw = width * ratio
+            nh = height * ratio
+
+            count += 1
+            GLib.idle_add(self.update_message_progress, count, length)
+
+            im = cv2.resize(im, (int(nw), int(nh)), interpolation=cv2.INTER_AREA)
+
+            count += 1
+            GLib.idle_add(self.update_message_progress, count, length)
+
+            cv2.imwrite("/tmp/stack-%s/IN%i.tiff" % (getpass.getuser(),count), im)
+            images += ["/tmp/stack-%s/IN%i.tiff" % (getpass.getuser(),count),]
+            count += 1
+
+        self.do_stack(images, "/tmp/stack-preview-%s.tiff" % getpass.getuser())
+        pb = GdkPixbuf.Pixbuf.new_from_file("/tmp/stack-preview-%s.tiff" % getpass.getuser())
+        GLib.idle_add(self.update_preview, pb)
+
+    def update_preview(self, pb):
+        self.ui["preview"].set_from_pixbuf(pb)
+
+
+
+    def get_export_image(self, w, h):
+        GLib.idle_add(self.export_started)
+        self.do_stack(self.paths, "/tmp/export-stack-%s.tiff" % getpass.getuser())
+        GLib.idle_add(self.show_message, "Exporting…", "PhotoFiddle is exporting the focus stack…", True, False)
+
+        im = cv2.imread("/tmp/export-stack-%s.tiff" % getpass.getuser(), 2 | 1)
+        im = cv2.resize(im, (int(w), int(h)), interpolation=cv2.INTER_AREA)
+        return im
+
+    def export_started(self):
+        self.exporting = True
+        self.export_state_changed()
+
+
+    def export_complete(self, path):
+        self.exporting = False
+        self.export_state_changed()
+        self.show_message("Export Complete!", "Your photo has been exported to '%s'" % path)
+
+    def export_state_changed(self):
+        self.ui["export_image"].set_sensitive(not self.exporting)
+        self.ui["open_pf2"].set_sensitive(not self.exporting)
+        self.ui["preview_button"].set_sensitive(not self.exporting)
+
+
+    def export_clicked(self, sender):
+        self.paths = self.get_paths()
+        height, width = cv2.imread(self.paths[0]).shape[:2]
+
+        Export.ExportDialog(self.root, self.builder, width, height, self.get_export_image,
+                            self.export_complete, self.paths[0])
+
+
+    def export_pf2_clicked(self, sender):
+        self.paths = self.get_paths()
+        threading.Thread(target=self.do_export_pf2).start()
+        self.export_started()
+
+    def do_export_pf2(self):
+        export_path = "".join(self.paths[0].split(".")[:-1])
+        export_path += "_FocusStack.tiff"
+        self.do_stack(self.paths, export_path)
+        GLib.idle_add(self.export_pf2_complete, export_path)
+
+    def export_pf2_complete(self, path):
+        self.exporting = False
+        self.export_state_changed()
+        self.switch_activity("PF2", path)
+
+
+
+
+
+

+ 67 - 0
HDR/Methods/Enfuse.py

@@ -0,0 +1,67 @@
+import getpass
+import glob
+import os
+import subprocess
+
+import shutil
+
+from HDR.Methods import Method
+from Tool import Property
+
+
+class Enfuse(Method):
+    def stack(self, files):
+        if(not os.path.exists("/tmp/hdr-%s" % getpass.getuser())):
+            os.mkdir("/tmp/hdr-%s" % getpass.getuser())
+
+        command = ["align_image_stack", "-m", "--gpu", "-a", "/tmp/hdr-%s/OUT_" % getpass.getuser()] + files
+        subprocess.call(command)
+
+        aligned_list = glob.glob("/tmp/hdr-%s/OUT*.tif" % getpass.getuser())
+        aligned_list.reverse()
+        return aligned_list
+
+
+    def run(self, files, output, full_width, full_height):
+        mask = self.props["mask"].options[self.props["mask"].get_value()].lower().replace(" ", "-")
+        weight = self.props["weight"].options[self.props["weight"].get_value()].lower()
+        colourspace = self.props["colourspace"].options[self.props["colourspace"].get_value()]
+
+        command = ["enfuse", "--output=/tmp/hdr-stacked-%s.tiff" % getpass.getuser(),
+                   "--%s" % mask,
+                   "--exposure-weight-function=%s" % weight,
+                   "--blend-colorspace=%s" % colourspace,
+                   "--exposure-width=%.3f" % self.props["width"].get_value(),
+                   "-v"] + files
+
+        print(command)
+
+        subprocess.call(command)
+
+        shutil.copyfile("/tmp/hdr-stacked-%s.tiff" % getpass.getuser(), output)
+        shutil.rmtree("/tmp/hdr-%s" % getpass.getuser())
+        os.unlink("/tmp/hdr-stacked-%s.tiff" % getpass.getuser())
+
+    def on_init(self):
+        self.id = "Enfuse"
+        self.name = "Enfuse"
+        self.properties = [
+            Property("weight", "Weight Function", "Combo", 0, options=[
+                "Gaussian",
+                "Lorentzian",
+                "Half-Sine",
+                "Full-Sine",
+                "Bi-Square"
+            ]),
+            Property("mask", "Mask Type", "Combo", 0, options=[
+                "Soft Mask",
+                "Hard Mask"
+            ]),
+            Property("colourspace", "Blend Colourspace", "Combo", 0, options=[
+                "CIECAM",
+                "CIELAB",
+                "CIELUV",
+                "IDENTITY"
+            ]),
+            Property("width", "Exposure Width", "Spin", 0.2, min=0, max=1)
+        ]

+ 64 - 0
HDR/Methods/Fattal.py

@@ -0,0 +1,64 @@
+import getpass
+import os
+import subprocess
+
+import shutil
+
+import cv2
+
+from HDR.Methods import Method
+from Tool import Property
+
+
+class Fattal(Method):
+    def stack(self, files):
+        if(not os.path.exists("/tmp/hdr-%s" % getpass.getuser())):
+            os.mkdir("/tmp/hdr-%s" % getpass.getuser())
+
+        command = ["align_image_stack", "--gpu", "-a", "/tmp/hdr-%s/OUT_" % getpass.getuser(), "-o",
+                       "/tmp/hdr-%s/OUT" % getpass.getuser()] + files
+
+        print(command)
+        subprocess.call(command)
+
+        return ["/tmp/hdr-%s/OUT.hdr" % getpass.getuser(),]
+
+    def run(self, files, output, full_width, full_height):
+        im = cv2.imread(files[0], 2 | 1)
+        ph, pw = im.shape[:2]
+        cv2.imwrite(files[0], cv2.resize(im, (int(full_width), int(full_height)), interpolation = cv2.INTER_AREA))
+
+
+        command = "pfsin --radiance '%s' | pfstmo_fattal02 -b %.2f -g %.2f -s %.2f -n %.2f -d %.2f -w %.2f -k %.2f -- | pfsout '%s'" % \
+                  (files[0],
+                   self.props["beta"].get_value(),
+                   self.props["gamma"].get_value(),
+                   self.props["saturation"].get_value(),
+                   self.props["noise"].get_value(),
+                   self.props["detail"].get_value(),
+                   self.props["white"].get_value(),
+                   self.props["black"].get_value(),
+                   "/tmp/hdr-out-%s.tiff" % getpass.getuser())
+
+        print(command)
+        os.system(command)
+
+        im = cv2.imread("/tmp/hdr-out-%s.tiff" % getpass.getuser(), 2 | 1)
+        cv2.imwrite(output, cv2.resize(im, (int(pw), int(ph)), interpolation=cv2.INTER_AREA))
+
+        os.unlink("/tmp/hdr-out-%s.tiff" % getpass.getuser())
+        shutil.rmtree("/tmp/hdr-%s" % getpass.getuser())
+
+    def on_init(self):
+        self.id = "Fattal"
+        self.name = "Fattal"
+        self.properties = [
+            Property("beta", "Beta", "Slider", 0.9, min=0.6, max=1),
+            Property("gamma", "Gamma", "Slider", 0.8, min=0.1, max=1),
+            Property("saturation", "Saturation", "Slider", 0.8, min=0.1, max=1),
+            Property("detail", "Lightness", "Slider", 3, min=0, max=9),
+            Property("noise", "Noise Reduction", "Slider", 0.0, min=0, max=1),
+            Property("white", "Overexposure", "Slider", 0.5, min=0, max=1),
+            Property("black", "Underexposure", "Slider", 0.5, min=0, max=1),
+
+        ]

+ 85 - 0
HDR/Methods/Mantuik.py

@@ -0,0 +1,85 @@
+import getpass
+import os
+import subprocess
+
+import shutil
+
+from HDR.Methods import Method
+from Tool import Property
+
+
+class Mantuik06(Method):
+    def stack(self, files):
+        if(not os.path.exists("/tmp/hdr-%s" % getpass.getuser())):
+            os.mkdir("/tmp/hdr-%s" % getpass.getuser())
+
+        command = ["align_image_stack", "--gpu", "-a", "/tmp/hdr-%s/OUT_" % getpass.getuser(), "-o",
+                   "/tmp/hdr-%s/OUT" % getpass.getuser()] + files
+
+        print(command)
+        subprocess.call(command)
+
+        return ["/tmp/hdr-%s/OUT.hdr" % getpass.getuser(),]
+
+    def run(self, files, output, full_width, full_height):
+
+        command = "pfsin --radiance '%s' | pfstmo_mantiuk06 -f %.2f -s %.2f | pfsgamma --gamma %.2f | pfsout '%s'" % \
+                  (files[0], self.props["contrast"].get_value(), self.props["saturation"].get_value(),
+                   self.props["gamma"].get_value(), output)
+
+        print(command)
+
+        os.system(command)
+
+        shutil.rmtree("/tmp/hdr-%s" % getpass.getuser())
+
+    def on_init(self):
+        self.id = "Mantuik06"
+        self.name = "Mantuik '06"
+        self.properties = [
+            Property("contrast", "Contrast", "Slider", 0.1, min=0, max=1),
+            Property("saturation", "Saturation", "Slider", 0.8, min=0, max=2),
+            Property("gamma", "Gamma", "Slider", 2.0, min=0, max=3)
+        ]
+
+
+class Mantuik08(Method):
+    def stack(self, files):
+        if(not os.path.exists("/tmp/hdr-%s" % getpass.getuser())):
+            os.mkdir("/tmp/hdr-%s" % getpass.getuser())
+
+        command = ["align_image_stack", "--gpu", "-a", "/tmp/hdr-%s/OUT_" % getpass.getuser(), "-o",
+                   "/tmp/hdr-%s/OUT" % getpass.getuser()] + files
+
+        print(command)
+        subprocess.call(command)
+
+        return ["/tmp/hdr-%s/OUT.hdr" % getpass.getuser(),]
+
+    def run(self, files, output, full_width, full_height):
+        display = self.props["display"].options[self.props["display"].get_value()].lower().replace(" ", "_")
+        saturation = self.props["saturation"].get_value()
+        contrast = self.props["contrast"].get_value()
+
+        command = "pfsin --radiance '%s' | pfstmo_mantiuk08 --display-function pd=%s -c%.2f -e%.2f | pfsout '%s'" % \
+                  (files[0], display, saturation, contrast, output)
+
+        print(command)
+
+        os.system(command)
+
+        shutil.rmtree("/tmp/hdr-%s" % getpass.getuser())
+
+    def on_init(self):
+        self.id = "Mantuik08"
+        self.name = "Mantuik '08"
+        self.properties = [
+            Property("display", "Display", "Combo", 0, options=[
+                "LCD",
+                "LCD Office",
+                "LCD Bright",
+                "CRT"
+            ]),
+            Property("saturation", "Saturation", "Slider", 1, min=0, max=2),
+            Property("contrast", "Contrast", "Slider", 1, min=0, max=10)
+        ]

+ 154 - 0
HDR/Methods/__init__.py

@@ -0,0 +1,154 @@
+from gi.repository import Gtk
+
+class Method:
+    def run(self, files, output, full_width, full_height):
+        raise NotImplementedError()
+
+    def stack(self, files):
+        raise NotImplementedError()
+
+    def __init__(self):
+        self.id = ""
+        self.name = ""
+        self.properties = []
+
+        # Let the tool set values
+        self.on_init()
+
+        self.props = {}
+        for property in self.properties:
+            self.props[property.id] = property
+
+        # Create widget for tool
+
+        self.widget = Gtk.Grid()
+        self.widget.set_column_spacing(8)
+        self.widget.set_row_spacing(6)
+        self.widget.set_halign(Gtk.Align.FILL)
+        self.widget.set_hexpand(True)
+
+        vpos = 0
+        for property in self.properties:
+            # Create Header
+            if(property.type == "Header"):
+                header = Gtk.HBox()
+                header.set_margin_top(6)
+
+                if(vpos != 0):
+                    # Add a Separator
+                    separator = Gtk.Separator()
+                    separator.set_margin_top(6)
+                    separator.show()
+                    self.widget.attach(separator, 0, vpos, 3, 1)
+                    vpos += 1
+
+                title = Gtk.Label()
+                title.set_halign(Gtk.Align.START)
+                if(property.is_subheading):
+                    title.set_markup("<i>%s</i>" % property.name)
+                else:
+                    title.set_markup("<b>%s</b>" % property.name)
+
+                header.add(title)
+
+                self.widget.attach(header, 0, vpos, 3, 1)
+
+            # Create Combo
+            elif(property.type == "Combo"):
+                label = Gtk.Label()
+                label.set_halign(Gtk.Align.END)
+                label.set_justify(Gtk.Justification.RIGHT)
+                label.set_text(property.name)
+                self.widget.attach(label, 0, vpos, 1, 1)
+
+                combo = Gtk.ComboBoxText()
+                combo.set_hexpand(True)
+                self.widget.attach(combo, 1, vpos, 1, 1)
+                for option in property.options:
+                    combo.append_text(option)
+
+                property.set_widget(combo)
+
+
+            # Create Spin
+            elif (property.type == "Spin"):
+                label = Gtk.Label()
+                label.set_halign(Gtk.Align.END)
+                label.set_justify(Gtk.Justification.RIGHT)
+                label.set_text(property.name)
+                self.widget.attach(label, 0, vpos, 1, 1)
+
+                adjustment = Gtk.Adjustment()
+                adjustment.set_lower(property.min)
+                adjustment.set_upper(property.max)
+                adjustment.set_step_increment((property.max - property.min) / 100)
+                property.set_widget(adjustment)
+
+                spin = Gtk.SpinButton()
+                spin.set_adjustment(adjustment)
+                spin.set_hexpand(True)
+                spin.set_digits(3)
+                property.ui_widget = spin
+
+                self.widget.attach(spin, 1, vpos, 1, 1)
+
+            # Create Toggle
+            elif(property.type == "Toggle"):
+                label = Gtk.Label()
+                label.set_halign(Gtk.Align.END)
+                label.set_justify(Gtk.Justification.RIGHT)
+                label.set_text(property.name)
+                self.widget.attach(label, 0, vpos, 1, 1)
+
+                toggle = Gtk.ToggleButton()
+                toggle.set_label("Enable")
+                toggle.set_hexpand(True)
+                self.widget.attach(toggle, 1, vpos, 1, 1)
+
+                property.set_widget(toggle)
+
+            # Create Slider
+            elif (property.type == "Slider"):
+                label = Gtk.Label()
+                label.set_halign(Gtk.Align.END)
+                label.set_justify(Gtk.Justification.RIGHT)
+                label.set_text(property.name)
+                self.widget.attach(label, 0, vpos, 1, 1)
+
+                adjustment = Gtk.Adjustment()
+                adjustment.set_lower(property.min)
+                adjustment.set_upper(property.max)
+                adjustment.set_step_increment((property.max - property.min)/100)
+                property.set_widget(adjustment)
+
+                slider = Gtk.Scale()
+                slider.set_adjustment(adjustment)
+                slider.set_digits(2)
+                slider.set_hexpand(True)
+                slider.set_value_pos(Gtk.PositionType.RIGHT)
+                property.ui_widget = slider
+
+                self.widget.attach(slider, 1, vpos, 1, 1)
+
+            if(property.type != "Header"):
+                # Create reset button
+                icon = Gtk.Image()
+                icon.set_from_icon_name("edit-clear-symbolic", Gtk.IconSize.BUTTON)
+                reset_button = Gtk.Button()
+                reset_button.set_image(icon)
+                reset_button.connect("clicked", property.reset_value)
+                property.reset_button = reset_button
+                self.widget.attach(reset_button, 2, vpos, 1, 1)
+
+            vpos += 1
+
+
+        separator = Gtk.Separator()
+        separator.set_margin_top(6)
+        separator.show()
+        self.widget.attach(separator, 0, vpos, 3, 1)
+
+        self.widget.show_all()
+
+    def on_init(self):
+        raise NotImplementedError()

+ 301 - 0
HDR/__init__.py

@@ -0,0 +1,301 @@
+import getpass
+import glob
+import os
+
+import subprocess
+
+import shutil
+import threading
+
+import cv2
+
+import Activity
+from gi.repository import GLib, Gtk, GdkPixbuf, Pango
+
+import Export
+from HDR.Methods import Enfuse
+from HDR.Methods import Fattal
+from HDR.Methods import Mantuik
+
+
+class HDR(Activity.Activity):
+
+    def on_init(self):
+        self.id = "HDR"
+        self.name = "High Dynamic Range Stack"
+        self.subtitle = "Stack Many Images at Different Exposures"
+
+        UI_FILE = "ui/HDR_Activity.glade"
+        self.builder.add_from_file(UI_FILE)
+
+        self.widget = self.builder.get_object("HDR_main")
+        self.stack.add_titled(self.widget, self.id, self.name)
+
+        self.header_widget = self.builder.get_object("HDR_header")
+        self.header_stack.add_titled(self.header_widget, self.id, self.name)
+
+        # Get all UI Components
+        self.ui = {}
+        components = [
+            "open_window",
+            "open_chooser",
+            "preview_button",
+            "open_header",
+            "open_open_button",
+            "open_cancel_button",
+            "images",
+            "preview",
+            "export_image",
+            "open_pf2",
+            "add_button",
+            "remove_button",
+            "scroll_window",
+            "popovermenu",
+            "stack",
+            "method"
+        ]
+
+        self.paths = []
+        self.exporting = False
+        self.settings = {}
+
+        for component in components:
+            self.ui[component] = self.builder.get_object("%s_%s" % (self.id, component))
+
+        self.menu_popover = self.ui["popovermenu"]
+
+        self.ui["open_window"].set_transient_for(self.root)
+        self.ui["open_window"].set_titlebar(self.ui["open_header"])
+
+        # Connect Siginals
+        self.ui["add_button"].connect("clicked", self.on_add_clicked)
+        self.ui["remove_button"].connect("clicked", self.on_remove_clicked)
+        self.ui["open_open_button"].connect("clicked", self.on_file_opened)
+        self.ui["open_cancel_button"].connect("clicked", self.on_file_canceled)
+        self.ui["preview_button"].connect("clicked", self.on_preview_clicked)
+        self.ui["export_image"].connect("clicked", self.export_clicked)
+        self.ui["open_pf2"].connect("clicked", self.export_pf2_clicked)
+        self.ui["method"].connect("changed", self.on_method_changed)
+
+        self.method = None
+
+        self.methods = [
+            Enfuse.Enfuse(),
+            Fattal.Fattal(),
+            Mantuik.Mantuik06(),
+            Mantuik.Mantuik08(),
+        ]
+
+        self.methodMap = {}
+        for method in self.methods:
+            self.methodMap[method.name] = method
+            self.ui["method"].append_text(method.name)
+            self.ui["stack"].add(method.widget)
+
+        self.ui["method"].set_active(0)
+
+        self.update_enabled()
+
+    def on_add_clicked(self, sender):
+        self.ui["open_window"].show_all()
+
+    def on_file_canceled(self, sender):
+        self.ui["open_window"].hide()
+
+    def on_file_opened(self, sender):
+        self.ui["open_window"].hide()
+        paths = self.ui["open_chooser"].get_filenames()
+        self.show_message("Loading Images", "Please wait while PhotoFiddle loads your images…", True, True)
+        threading.Thread(target=self.add_images, args=(paths,)).start()
+
+
+    def on_method_changed(self, sender):
+        self.ui["stack"].set_visible_child(self.methodMap[self.ui["method"].get_active_text()].widget)
+
+
+    def update_enabled(self):
+        enabled = len(self.get_paths()) > 0
+        self.ui["export_image"].set_sensitive(enabled)
+        self.ui["open_pf2"].set_sensitive(enabled)
+        self.ui["preview_button"].set_sensitive(enabled)
+
+    def add_images(self, paths):
+        c = 0
+        for path in paths:
+            # Create box
+            box = Gtk.HBox()
+            box.set_spacing(6)
+            im = Gtk.Image()
+
+            pb = GdkPixbuf.Pixbuf.new_from_file_at_scale(path, 128, 128, True)
+            im.set_from_pixbuf(pb)
+
+            box.add(im)
+
+            label = Gtk.Label()
+            label.set_ellipsize(Pango.EllipsizeMode.START)
+            label.set_text(path)
+
+            box.add(label)
+            box.show_all()
+            c += 1
+            GLib.idle_add(self.add_image_row, box, c, len(paths))
+
+        GLib.idle_add(self.hide_message)
+
+    def add_image_row(self, row, count, length):
+        self.ui["images"].add(row)
+        self.update_enabled()
+        self.update_message_progress(count, length)
+
+
+    def on_remove_clicked(self, sender):
+        items = self.ui["images"].get_selected_rows()
+        for item in items:
+            item.destroy()
+        self.update_enabled()
+
+
+    def on_open(self, path):
+        self.root.get_titlebar().set_subtitle("High Dynamic Range")
+        pass
+
+    def on_exit(self):
+        if(not self.exporting):
+            self.root.get_titlebar().set_subtitle("")
+            self.on_init()
+            return True
+        return False
+
+
+    def on_preview_clicked(self, sender):
+        self.show_message("Generating Preview…", "PhotoFiddle is preparing to generate a preview", True, True)
+        paths = self.get_paths()
+        self.method = self.methodMap[self.ui["method"].get_active_text()]
+        w = self.ui["scroll_window"].get_allocated_width() - 12
+        h = self.ui["scroll_window"].get_allocated_height() - 12
+        threading.Thread(target=self.do_preview, args=(paths, w, h)).start()
+
+
+    def do_stack(self, paths, output, w, h):
+        if (not os.path.exists("/tmp/hdr-%s" % getpass.getuser())):
+            os.mkdir("/tmp/hdr-%s" % getpass.getuser())
+
+
+        GLib.idle_add(self.show_message, "HDR Stacking…", "PhotoFiddle is processing images…", True, True)
+
+        length = len(paths)*3
+        count = 0
+        images = []
+        height = 0
+        width = 0
+        for path in paths:
+            GLib.idle_add(self.update_message_progress, count, length)
+            im = cv2.imread(path, 2 | 1)
+            height, width = im.shape[:2]
+
+            # Get fitting size
+            ratio = float(w) / width
+            if (height * ratio > h):
+                ratio = float(h) / height
+
+            nw = width * ratio
+            nh = height * ratio
+
+            count += 1
+            GLib.idle_add(self.update_message_progress, count, length)
+
+            im = cv2.resize(im, (int(nw), int(nh)), interpolation=cv2.INTER_AREA)
+
+            count += 1
+            GLib.idle_add(self.update_message_progress, count, length)
+
+            cv2.imwrite("/tmp/hdr-%s/IN%i.tiff" % (getpass.getuser(), count), im)
+            images += ["/tmp/hdr-%s/IN%i.tiff" % (getpass.getuser(), count),]
+            count += 1
+
+        GLib.idle_add(self.show_message, "HDR Stacking…", "PhotoFiddle is aligning images…", True, False)
+
+        stacked_list = self.method.stack(images)
+
+        GLib.idle_add(self.show_message, "HDR Stacking…", "PhotoFiddle is performing an HDR stack…", True, False)
+
+        self.method.run(stacked_list, output, width, height)
+
+
+        GLib.idle_add(self.hide_message)
+
+
+    def get_paths(self):
+        paths = []
+        for item in self.ui["images"].get_children():
+            paths += [item.get_children()[0].get_children()[1].get_text(),]
+
+        return paths
+
+    def do_preview(self, paths, w, h):
+        self.do_stack(paths, "/tmp/hdr-preview-%s.tiff" % getpass.getuser(), w, h)
+        pb = GdkPixbuf.Pixbuf.new_from_file("/tmp/hdr-preview-%s.tiff" % getpass.getuser())
+        GLib.idle_add(self.update_preview, pb)
+
+    def update_preview(self, pb):
+        self.ui["preview"].set_from_pixbuf(pb)
+
+
+
+    def get_export_image(self, w, h):
+        GLib.idle_add(self.export_started)
+        self.do_stack(self.paths, "/tmp/export-hdr-%s.tiff" % getpass.getuser(), w, h)
+        GLib.idle_add(self.show_message, "Exporting…", "PhotoFiddle is exporting the HDR photo…", True, False)
+
+        im = cv2.imread("/tmp/export-hdr-%s.tiff" % getpass.getuser(), 2 | 1)
+        return im
+
+    def export_started(self):
+        self.exporting = True
+        self.export_state_changed()
+
+
+    def export_complete(self, path):
+        self.exporting = False
+        self.export_state_changed()
+        self.show_message("Export Complete!", "Your photo has been exported to '%s'" % path)
+
+    def export_state_changed(self):
+        self.ui["export_image"].set_sensitive(not self.exporting)
+        self.ui["open_pf2"].set_sensitive(not self.exporting)
+        self.ui["preview_button"].set_sensitive(not self.exporting)
+
+
+    def export_clicked(self, sender):
+        self.method = self.methodMap[self.ui["method"].get_active_text()]
+        self.paths = self.get_paths()
+        height, width = cv2.imread(self.paths[0]).shape[:2]
+
+        Export.ExportDialog(self.root, self.builder, width, height, self.get_export_image,
+                            self.export_complete, self.paths[0])
+
+
+    def export_pf2_clicked(self, sender):
+        self.method = self.methodMap[self.ui["method"].get_active_text()]
+        GLib.idle_add(self.export_started)
+        self.paths = self.get_paths()
+        threading.Thread(target=self.do_export_pf2).start()
+
+    def do_export_pf2(self):
+        export_path = "".join(self.paths[0].split(".")[:-1])
+        export_path += "_HDR.tiff"
+        height, width = cv2.imread(self.paths[0]).shape[:2]
+        self.do_stack(self.paths, export_path, width, height)
+        GLib.idle_add(self.export_pf2_complete, export_path)
+
+    def export_pf2_complete(self, path):
+        self.exporting = False
+        self.export_state_changed()
+        self.switch_activity("PF2", path)
+
+
+
+
+
+

+ 279 - 0
LightStack/__init__.py

@@ -0,0 +1,279 @@
+import getpass
+import glob
+import os
+
+import subprocess
+
+import shutil
+import threading
+
+import cv2
+
+import Activity
+from gi.repository import GLib, Gtk, GdkPixbuf, Pango
+
+import Export
+
+
+class LightStack(Activity.Activity):
+
+    def on_init(self):
+        self.id = "LightStack"
+        self.name = "Light Stack"
+        self.subtitle = "Stack Many Images to Make Light Trails"
+
+        UI_FILE = "ui/LightStack_Activity.glade"
+        self.builder.add_from_file(UI_FILE)
+
+        self.widget = self.builder.get_object("LightStack_main")
+        self.stack.add_titled(self.widget, self.id, self.name)
+
+        self.header_widget = self.builder.get_object("LightStack_header")
+        self.header_stack.add_titled(self.header_widget, self.id, self.name)
+
+        # Get all UI Components
+        self.ui = {}
+        components = [
+            "open_window",
+            "open_chooser",
+            "preview_button",
+            "open_header",
+            "open_open_button",
+            "open_cancel_button",
+            "images",
+            "preview",
+            "export_image",
+            "open_pf2",
+            "add_button",
+            "remove_button",
+            "scroll_window",
+            "popovermenu"
+        ]
+
+        self.paths = []
+        self.exporting = False
+
+        for component in components:
+            self.ui[component] = self.builder.get_object("%s_%s" % (self.id, component))
+
+        self.menu_popover = self.ui["popovermenu"]
+
+        self.ui["open_window"].set_transient_for(self.root)
+        self.ui["open_window"].set_titlebar(self.ui["open_header"])
+
+        # Connect Siginals
+        self.ui["add_button"].connect("clicked", self.on_add_clicked)
+        self.ui["remove_button"].connect("clicked", self.on_remove_clicked)
+        self.ui["open_open_button"].connect("clicked", self.on_file_opened)
+        self.ui["open_cancel_button"].connect("clicked", self.on_file_canceled)
+        self.ui["preview_button"].connect("clicked", self.on_preview_clicked)
+        self.ui["export_image"].connect("clicked", self.export_clicked)
+        self.ui["open_pf2"].connect("clicked", self.export_pf2_clicked)
+
+        self.update_enabled()
+
+    def on_add_clicked(self, sender):
+        self.ui["open_window"].show_all()
+
+    def on_file_canceled(self, sender):
+        self.ui["open_window"].hide()
+
+    def on_file_opened(self, sender):
+        self.ui["open_window"].hide()
+        paths = self.ui["open_chooser"].get_filenames()
+        self.show_message("Loading Images", "Please wait while PhotoFiddle loads your images…", True, True)
+        threading.Thread(target=self.add_images, args=(paths,)).start()
+
+    def update_enabled(self):
+        enabled = len(self.get_paths()) > 0
+        self.ui["export_image"].set_sensitive(enabled)
+        self.ui["open_pf2"].set_sensitive(enabled)
+        self.ui["preview_button"].set_sensitive(enabled)
+
+    def add_images(self, paths):
+        c = 0
+        for path in paths:
+            # Create box
+            box = Gtk.HBox()
+            box.set_spacing(6)
+            im = Gtk.Image()
+
+            pb = GdkPixbuf.Pixbuf.new_from_file_at_scale(path, 128, 128, True)
+            im.set_from_pixbuf(pb)
+
+            box.add(im)
+
+            label = Gtk.Label()
+            label.set_ellipsize(Pango.EllipsizeMode.START)
+            label.set_text(path)
+
+            box.add(label)
+            box.show_all()
+            c += 1
+            GLib.idle_add(self.add_image_row, box, c, len(paths))
+
+        GLib.idle_add(self.hide_message)
+
+    def add_image_row(self, row, count, length):
+        self.ui["images"].add(row)
+        self.update_enabled()
+        self.update_message_progress(count, length)
+
+
+    def on_remove_clicked(self, sender):
+        items = self.ui["images"].get_selected_rows()
+        for item in items:
+            item.destroy()
+        self.update_enabled()
+
+
+    def on_open(self, path):
+        self.root.get_titlebar().set_subtitle("Light Stack")
+        pass
+
+    def on_exit(self):
+        if(not self.exporting):
+            self.root.get_titlebar().set_subtitle("")
+            self.on_init()
+            return True
+        return False
+
+
+    def on_preview_clicked(self, sender):
+        self.show_message("Generating Preview…", "PhotoFiddle is preparing to generate a preview", True, True)
+        paths = self.get_paths()
+        w = self.ui["scroll_window"].get_allocated_width() - 12
+        h = self.ui["scroll_window"].get_allocated_height() - 12
+        threading.Thread(target=self.do_preview, args=(paths, w, h)).start()
+
+
+    def do_stack(self, images):
+
+        GLib.idle_add(self.show_message, "Light Stacking…", "PhotoFiddle is performing a light stack…", True, True)
+        GLib.idle_add(self.update_message_progress, 0,1)
+
+        im = cv2.imread(images[0], 2 | 1)
+        bpp = int(str(im.dtype).replace("uint", "").replace("float", ""))
+        np = float(2 ** bpp - 1)
+        c = 1
+        GLib.idle_add(self.update_message_progress, c, len(images))
+        try:
+            for f in images[1:]:
+                layer= cv2.imread(f, 2 | 1)
+                im[layer > im] = layer[layer > im]
+                im[im < 0.0] = 0.0
+                im[im > np] = np
+                c += 1
+                GLib.idle_add(self.update_message_progress, c, len(images))
+
+            GLib.idle_add(self.hide_message)
+            return im
+        except:
+            GLib.idle_add(self.show_message, "Stack Failed.", "All images in the stack must be the same size.", False, False)
+            raise Exception()
+
+
+    def get_paths(self):
+        paths = []
+        for item in self.ui["images"].get_children():
+            paths += [item.get_children()[0].get_children()[1].get_text(),]
+
+        return paths
+
+    def do_preview(self, paths, w, h):
+        if (not os.path.exists("/tmp/lightstack-%s" % getpass.getuser())):
+            os.mkdir("/tmp/lightstack-%s" % getpass.getuser())
+
+        length = len(paths)*3
+        count = 0
+        images = []
+        for path in paths:
+            GLib.idle_add(self.update_message_progress, count, length)
+            im = cv2.imread(path, 2 | 1)
+            height, width = im.shape[:2]
+
+            # Get fitting size
+            ratio = float(w) / width
+            if (height * ratio > h):
+                ratio = float(h) / height
+
+            nw = width * ratio
+            nh = height * ratio
+
+            count += 1
+            GLib.idle_add(self.update_message_progress, count, length)
+
+            im = cv2.resize(im, (int(nw), int(nh)), interpolation=cv2.INTER_AREA)
+
+            count += 1
+            GLib.idle_add(self.update_message_progress, count, length)
+
+            cv2.imwrite("/tmp/lightstack-%s/IN%i.tiff" % (getpass.getuser(),count), im)
+            images += ["/tmp/lightstack-%s/IN%i.tiff" % (getpass.getuser(),count),]
+            count += 1
+
+        im = self.do_stack(images)
+        cv2.imwrite("/tmp/lightstack-preview-%s.tiff" % getpass.getuser(), im)
+        pb = GdkPixbuf.Pixbuf.new_from_file("/tmp/lightstack-preview-%s.tiff" % getpass.getuser())
+        shutil.rmtree("/tmp/lightstack-%s" % getpass.getuser())
+        GLib.idle_add(self.update_preview, pb)
+
+    def update_preview(self, pb):
+        self.ui["preview"].set_from_pixbuf(pb)
+
+
+
+    def get_export_image(self, w, h):
+        GLib.idle_add(self.export_started)
+        im = self.do_stack(self.paths)
+        GLib.idle_add(self.show_message, "Exporting…", "PhotoFiddle is exporting the light stack…", True, False)
+
+        return im
+
+    def export_started(self):
+        self.exporting = True
+        self.export_state_changed()
+
+
+    def export_complete(self, path):
+        self.exporting = False
+        self.export_state_changed()
+        self.show_message("Export Complete!", "Your photo has been exported to '%s'" % path)
+
+    def export_state_changed(self):
+        self.ui["export_image"].set_sensitive(not self.exporting)
+        self.ui["open_pf2"].set_sensitive(not self.exporting)
+        self.ui["preview_button"].set_sensitive(not self.exporting)
+
+
+    def export_clicked(self, sender):
+        self.paths = self.get_paths()
+        height, width = cv2.imread(self.paths[0]).shape[:2]
+
+        Export.ExportDialog(self.root, self.builder, width, height, self.get_export_image,
+                            self.export_complete, self.paths[0])
+
+
+    def export_pf2_clicked(self, sender):
+        GLib.idle_add(self.export_started)
+        self.paths = self.get_paths()
+        threading.Thread(target=self.do_export_pf2).start()
+
+    def do_export_pf2(self):
+        export_path = "".join(self.paths[0].split(".")[:-1])
+        export_path += "_LightStack.tiff"
+        im = self.do_stack(self.paths)
+        GLib.idle_add(self.show_message, "Sending to Editor", "Exporting photo to the editor…", True)
+        cv2.imwrite(export_path, im)
+        GLib.idle_add(self.export_pf2_complete, export_path)
+
+    def export_pf2_complete(self, path):
+        self.exporting = False
+        self.export_state_changed()
+        self.switch_activity("PF2", path)
+
+
+
+
+
+

+ 21 - 0
PF2/Histogram.py

@@ -0,0 +1,21 @@
+import cv2
+import numpy
+
+
+class Histogram:
+    @staticmethod
+    def draw_hist(image, path):
+        bpp = float(str(image.dtype).replace("uint", "").replace("float", ""))
+        img = ((image/2**bpp)*255).astype('uint8')
+        bpp = float(str(img.dtype).replace("uint", "").replace("float", ""))
+        hv = 2 ** bpp
+        hist = numpy.zeros(shape=(128, 330, 3))
+
+        color = ('b', 'g', 'r')
+        for i, col in enumerate(color):
+            histr = cv2.calcHist([img], [i], None, [hv], [0, hv])
+            for i2, hval in enumerate(histr):
+                hi = max(histr)
+                hist[int(-(hval / hi) * 127) + 127][int((i2 / hv) * 330)][i] = 255;
+
+        cv2.imwrite(path, hist)

+ 70 - 0
PF2/Tools/BlackWhite.py

@@ -0,0 +1,70 @@
+import cv2
+import numpy
+
+import Tool
+
+class BlackWhite(Tool.Tool):
+    def on_init(self):
+        self.id = "black_white"
+        self.name = "Black and White"
+        self.icon_path = "ui/PF2_Icons/BlackWhite.png"
+        self.properties = [
+            Tool.Property("enabled", "Black and White", "Header", False, has_toggle=True, has_button=False),
+            Tool.Property("method", "Method", "Combo", 0, options=[
+                "Average",
+                "Weighted Average",
+                "Luma",
+                "Custom Weight"
+            ]),
+            Tool.Property("customHeader", "Custom Weight", "Header", None, has_toggle=False, has_button=False, is_subheading=True),
+            Tool.Property("red", "Red Value", "Spin", 0.333, max=1, min=0),
+            Tool.Property("green", "Green Value", "Spin", 0.333, max=1, min=0),
+            Tool.Property("blue", "Blue Value", "Spin", 0.333, max=1, min=0),
+        ]
+
+    def on_update(self, image):
+        if(self.props["enabled"].get_value()):
+            mode = self.props["method"].get_value()
+
+            bpp = float(str(image.dtype).replace("uint", "").replace("float", ""))
+            np = float(2 ** bpp - 1)
+            out = image.astype(numpy.float32)
+
+            if(mode == 0):
+                bc = out[0:, 0:, 0]
+                gc = out[0:, 0:, 1]
+                rc = out[0:, 0:, 2]
+
+                out = (bc + gc + rc) / 3
+
+            elif(mode == 1):
+                bc = out[0:, 0:, 0]
+                gc = out[0:, 0:, 1]
+                rc = out[0:, 0:, 2]
+
+                out = 0.114 * bc + 0.587 * gc + 0.299 * rc
+
+            elif(mode == 2):
+                hsl = cv2.cvtColor(out, cv2.COLOR_BGR2HSV)
+                out = hsl[0:, 0:, 2]
+
+            elif(mode == 3):
+                r = self.props["red"].get_value()
+                g = self.props["green"].get_value()
+                b = self.props["blue"].get_value()
+
+                bc = out[0:, 0:, 0]
+                gc = out[0:, 0:, 1]
+                rc = out[0:, 0:, 2]
+
+                out = b * bc + g * gc + r * rc
+
+            out[out < 0.0] = 0.0
+            out[out > np] = np
+
+            out = cv2.cvtColor(out, cv2.COLOR_GRAY2BGR)
+
+            return out.astype(image.dtype)
+        else:
+            return image
+

+ 251 - 0
PF2/Tools/Colours.py

@@ -0,0 +1,251 @@
+import cv2
+import numpy
+
+import Tool
+
+class Colours(Tool.Tool):
+    def on_init(self):
+        self.id = "colours"
+        self.name = "Colours"
+        self.icon_path = "ui/PF2_Icons/Colours.png"
+        self.properties = [
+            Tool.Property("header", "Colours", "Header", None, has_toggle=False, has_button=False),
+            Tool.Property("overall_saturation", "Saturation", "Slider", 0, max=50, min=-50),
+            Tool.Property("hue", "Hue", "Slider", 0, max=50, min=-50),
+            Tool.Property("kelvin", "Colour Temperature", "Slider", 6500, max=15000, min=1000),
+            Tool.Property("header_ts", "Tonal Saturation", "Header", None, has_toggle=False, has_button=False),
+            Tool.Property("highlight_saturation", "Highlight Saturation", "Slider", 0, max=50, min=-50),
+            Tool.Property("midtone_saturation", "Midtone Saturation", "Slider", 0, max=50, min=-50),
+            Tool.Property("shadow_saturation", "Shadow Saturation", "Slider", 0, max=50, min=-50),
+            # Red
+            Tool.Property("header_red", "Red", "Header", None, has_toggle=False, has_button=False),
+            Tool.Property("red_overall_brightness", "Overall Brightness", "Slider", 0, max=50, min=-50),
+            Tool.Property("red_highlight_brightness", "Highlight Brightness", "Slider", 0, max=50, min=-50),
+            Tool.Property("red_midtone_brightness", "Midtone Brightness", "Slider", 0, max=50, min=-50),
+            Tool.Property("red_shadow_brightness", "Shadow Brightness", "Slider", 0, max=50, min=-50),
+            # Green
+            Tool.Property("header_green", "Green", "Header", None, has_toggle=False, has_button=False),
+            Tool.Property("green_overall_brightness", "Overall Brightness", "Slider", 0, max=50, min=-50),
+            Tool.Property("green_highlight_brightness", "Highlight Brightness", "Slider", 0, max=50, min=-50),
+            Tool.Property("green_midtone_brightness", "Midtone Brightness", "Slider", 0, max=50, min=-50),
+            Tool.Property("green_shadow_brightness", "Shadow Brightness", "Slider", 0, max=50, min=-50),
+            # Blue
+            Tool.Property("header_blue", "Blue", "Header", None, has_toggle=False, has_button=False),
+            Tool.Property("blue_overall_brightness", "Overall Brightness", "Slider", 0, max=50, min=-50),
+            Tool.Property("blue_highlight_brightness", "Highlight Brightness", "Slider", 0, max=50, min=-50),
+            Tool.Property("blue_midtone_brightness", "Midtone Brightness", "Slider", 0, max=50, min=-50),
+            Tool.Property("blue_shadow_brightness", "Shadow Brightness", "Slider", 0, max=50, min=-50),
+            # Bleed
+            Tool.Property("header_bleed", "Tonal Bleed", "Header", None, has_toggle=False, has_button=False),
+            Tool.Property("highlight_bleed", "Highlight Bleed", "Slider", 0.5, max=1, min=0.001),
+            Tool.Property("midtone_bleed", "Midtone Bleed", "Slider", 0.5, max=1, min=0.001),
+            Tool.Property("shadow_bleed", "Shadow Bleed", "Slider", 0.5, max=1, min=0.001),
+        ]
+
+    def on_update(self, image):
+        if(not self.is_default()):
+            im = image
+            hue = self.props["hue"].get_value()
+            saturation = self.props["overall_saturation"].get_value()
+            ct = self.props["kelvin"].get_value()/100.0
+            hs = self.props["highlight_saturation"].get_value()
+            ms = self.props["midtone_saturation"].get_value()
+            ss = self.props["shadow_saturation"].get_value()
+            rob = self.props["red_overall_brightness"].get_value()
+            rhb = self.props["red_highlight_brightness"].get_value()
+            rmb = self.props["red_midtone_brightness"].get_value()
+            rsb = self.props["red_shadow_brightness"].get_value()
+            gob = self.props["green_overall_brightness"].get_value()
+            ghb = self.props["green_highlight_brightness"].get_value()
+            gmb = self.props["green_midtone_brightness"].get_value()
+            gsb = self.props["green_shadow_brightness"].get_value()
+            bob = self.props["blue_overall_brightness"].get_value()
+            bhb = self.props["blue_highlight_brightness"].get_value()
+            bmb = self.props["blue_midtone_brightness"].get_value()
+            bsb = self.props["blue_shadow_brightness"].get_value()
+            chbl = self.props["highlight_bleed"].get_value()
+            cmbl = self.props["midtone_bleed"].get_value()
+            csbl = self.props["shadow_bleed"].get_value()
+
+            bpp = float(str(im.dtype).replace("uint", "").replace("float", ""))
+            np = float(2 ** bpp - 1)
+
+            out = im.astype(numpy.float32)
+            isHr = self._is_highlight(out, (3.00 / chbl))
+            isMr = self._is_midtone(out, (3.00 / cmbl))
+            isSr = self._is_shadow(out, (3.00 / csbl))
+
+            # Colour Temperature
+            if(int(ct) != 65):
+                r = 0
+                if(ct <= 66):
+                    print(ct <= 66)
+                    r = 255
+                else:
+                    r = ct - 60
+                    r = 329.698727446 * numpy.math.pow(r, -0.1332047592)
+                    if(r < 0):
+                        r = 0
+                    if(r > 255):
+                        r = 255
+
+                g = 0
+                if(ct <= 66):
+                    g = ct
+                    g = 99.4708025861 * numpy.math.log(g) - 161.1195681661
+                    if (g < 0):
+                        g = 0
+                    if (g > 255):
+                        g = 255
+                else:
+                    g = ct - 60
+                    g = 288.1221695283 * numpy.math.pow(g, -0.0755148492)
+                    if (g < 0):
+                        g = 0
+                    if (g > 255):
+                        g = 255
+
+
+                b = 0
+                if(ct >= 66):
+                    b = 255
+                elif(ct <= 19):
+                    b = 0
+                else:
+                    b = ct - 10
+                    b = 138.5177312231 * numpy.math.log(b) - 305.0447927307
+                    if (b < 0):
+                        b = 0
+                    if (b > 255):
+                        b = 255
+
+                r = (r/255.0)
+                g = (g / 255.0)
+                b = (b / 255.0)
+
+                # Red
+                out[0:, 0:, 2] = out[0:, 0:, 2] * r
+                # Green
+                out[0:, 0:, 1] = out[0:, 0:, 1] * g
+                # Blue
+                out[0:, 0:, 0] = out[0:, 0:, 0] * b
+
+
+
+            #Converting to HSV
+
+            out = cv2.cvtColor(out, cv2.COLOR_BGR2HSV)
+
+            #Hue...
+            if (hue != 0.0):
+                out[0:, 0:, 0] = out[0:, 0:, 0] + (hue / 100.0) * 255
+
+            #Saturation...
+            if (saturation != 0.0):
+                out[0:, 0:, 1] = out[0:, 0:, 1] + (saturation / 10000.0) * 255
+
+            #Saturation Highlights...
+            if (hs != 0.0):
+                out[0:, 0:, 1] = (out[0:, 0:, 1] + ((hs * isHr[0:, 0:, 1]) / 10000.0) * 255)
+
+            #Saturation Midtones...
+            if (ms != 0.0):
+                out[0:, 0:, 1] = (out[0:, 0:, 1] + ((ms * isMr[0:, 0:, 1]) / 10000.0) * 255)
+
+            #Saturation Shadows...
+            if (ss != 0.0):
+                out[0:, 0:, 1] = (out[0:, 0:, 1] + ((ss * isSr[0:, 0:, 1]) / 10000.0) * 255)
+
+            out[out < 0.0] = 0.0
+            out[out > 4294967296.0] = 4294967296.0
+
+            out = cv2.cvtColor(out, cv2.COLOR_HSV2BGR)
+
+            #Red...
+            if (rob != 0.0):
+                out[0:, 0:, 2] = out[0:, 0:, 2] + (rob / 100.0) * np
+
+            # Highlights
+            if (rhb != 0.0):
+                out[0:, 0:, 2] = (out[0:, 0:, 2] + ((rhb * isHr[0:, 0:, 1]) / 100.0) * np)
+
+            # Midtones
+            if (rmb != 0.0):
+                out[0:, 0:, 2] = (out[0:, 0:, 2] + ((rmb * isMr[0:, 0:, 1]) / 100.0) * np)
+
+            # Shadows
+            if (rsb != 0.0):
+                out[0:, 0:, 2] = (out[0:, 0:, 2] + ((rsb * isSr[0:, 0:, 1]) / 100.0) * np)
+
+            #Green...
+            if (gob != 0.0):
+                out[0:, 0:, 1] = out[0:, 0:, 1] + (gob / 100.0) * np
+
+            # Highlights
+            if (ghb != 0.0):
+                out[0:, 0:, 1] = (out[0:, 0:, 1] + ((ghb * isHr[0:, 0:, 1]) / 100.0) * np)
+
+            # Midtones
+            if (gmb != 0.0):
+                out[0:, 0:, 1] = (out[0:, 0:, 1] + ((gmb * isMr[0:, 0:, 1]) / 100.0) * np)
+
+            # Shadows
+            if (gsb != 0.0):
+                out[0:, 0:, 1] = (out[0:, 0:, 1] + ((gsb * isSr[0:, 0:, 1]) / 100.0) * np)
+
+            #Blue...
+            if (bob != 0.0):
+                out[0:, 0:, 0] = out[0:, 0:, 0] + (bob / 100.0) * np
+
+            # Highlights
+            if (bhb != 0.0):
+                out[0:, 0:, 0] = (out[0:, 0:, 0] + ((bhb * isHr[0:, 0:, 1]) / 100.0) * np)
+
+            # Midtones
+            if (bmb != 0.0):
+                out[0:, 0:, 0] = (out[0:, 0:, 0] + ((bmb * isMr[0:, 0:, 1]) / 100.0) * np)
+
+            # Shadows
+            if (bsb != 0.0):
+                out[0:, 0:, 0] = (out[0:, 0:, 0] + ((bsb * isSr[0:, 0:, 1]) / 100.0) * np)
+
+
+            out[out < 0.0] = 0.0
+            out[out > np] = np
+            return out.astype(im.dtype)
+        else:
+            return image
+
+    def _is_highlight(self, image, bleed_value = 6.0):
+        bleed = float(image.max() / bleed_value)
+        mif = image.max() / 3.0 * 2.0
+        icopy = image.copy()
+
+        icopy[icopy < mif - bleed] = 0.0
+        icopy[(icopy < mif) * (icopy != 0.0)] = ((mif - (icopy[(icopy < mif) * (icopy != 0.0)])) / bleed) * -1 + 1
+        icopy[icopy >= mif] = 1.0
+        return icopy
+
+    def _is_midtone(self, image, bleed_value = 6.0):
+        bleed = float(image.max() / bleed_value)
+        mif = image.max() / 3.0
+        mir = image.max() / 3.0 * 2.0
+        icopy = image.copy()
+
+        icopy[icopy < mif - bleed] = 0.0
+        icopy[icopy > mir + bleed] = 0.0
+
+        icopy[(icopy < mif) * (icopy != 0.0)] = ((mif - (icopy[(icopy < mif) * (icopy != 0.0)])) / bleed) * -1 + 1
+        icopy[(icopy > mir) * (icopy != 0.0)] = (((icopy[(icopy > mir) * (icopy != 0.0)]) - mir) / bleed) * -1 + 1
+        icopy[(icopy >= mif) * (icopy <= mir)] = 1.0
+        return icopy
+
+    def _is_shadow(self, image, bleed_value=6.0):
+        bleed = float(image.max() / bleed_value)
+        mir = image.max() / 3.0
+        icopy = image.copy()
+
+        icopy[icopy <= mir] = 1.0
+        icopy[icopy > mir + bleed] = 0.0
+        icopy[icopy > mir] = (((icopy[(icopy > mir) * (icopy != 0.0)]) - mir) / bleed) * -1 + 1
+        return icopy

+ 147 - 0
PF2/Tools/Contrast.py

@@ -0,0 +1,147 @@
+import numpy
+
+import Tool
+
+
+class Contrast(Tool.Tool):
+    def on_init(self):
+        self.id = "contrast"
+        self.name = "Brightness and Contrast"
+        self.icon_path = "ui/PF2_Icons/Contrast.png"
+        self.properties = [
+            Tool.Property("header", "Brightness and Contrast", "Header", None, has_toggle=False, has_button=False),
+            Tool.Property("overall_brightness", "Brightness", "Slider", 0, max=50, min=-50),
+            Tool.Property("overall_contrast", "Contrast", "Slider", 0, max=50, min=-50),
+            Tool.Property("tonal_header", "Tonal Brightness and Contrast", "Header", None, has_toggle=False, has_button=False),
+            Tool.Property("highlight_brightness", "Highlight Brightness", "Slider", 0, max=50, min=-50),
+            Tool.Property("highlight_contrast", "Highlight Contrast", "Slider", 0, max=50, min=-50),
+            Tool.Property("midtone_brightness", "Midtone Brightness", "Slider", 0, max=50, min=-50),
+            Tool.Property("midtone_contrast", "Midtone Contrast", "Slider", 0, max=50, min=-50),
+            Tool.Property("shadow_brightness", "Shadow Brightness", "Slider", 0, max=50, min=-50),
+            Tool.Property("shadow_contrast", "Shadow Contrast", "Slider", 0, max=50, min=-50),
+            Tool.Property("tonal_header", "Tonal Bleed Fraction", "Header", None, has_toggle=False,
+                          has_button=False),
+            Tool.Property("highlight_bleed", "Highlight Bleed", "Slider", 0.5, max=1, min=0.001),
+            Tool.Property("midtone_bleed", "Midtone Bleed", "Slider", 0.5, max=1, min=0.001),
+            Tool.Property("shadow_bleed", "Shadow Bleed", "Slider", 0.5, max=1, min=0.001),
+        ]
+
+    def on_update(self, im):
+        # Apply Contrast Stuff
+        if(not self.is_default()):
+            ob = self.props["overall_brightness"].get_value()
+            oc = -self.props["overall_contrast"].get_value()
+
+            hb = self.props["highlight_brightness"].get_value()
+            hc = self.props["highlight_contrast"].get_value()
+            mb = self.props["midtone_brightness"].get_value()
+            mc = self.props["midtone_contrast"].get_value()
+            sb = self.props["shadow_brightness"].get_value()
+            sc = self.props["shadow_contrast"].get_value()
+
+            hbl = self.props["highlight_bleed"].get_value()
+            mbl = self.props["midtone_bleed"].get_value()
+            sbl = self.props["shadow_bleed"].get_value()
+
+            # Add overall brightness
+            hb += ob
+            mb += ob
+            sb += ob
+
+            # Add overall contrast
+
+            hc -= oc
+            mc -= oc
+            sc -= oc
+
+
+            # Bits per pixel
+            bpp = float(str(im.dtype).replace("uint", "").replace("float", ""))
+            # Pixel value range
+            np = float(2 ** bpp - 1)
+
+            out = im.astype(numpy.float32)
+
+            # Highlights
+
+            isHr = self._is_highlight(out, (3.0 / hbl))
+            if (hc != 0.0):
+                # Highlight Contrast
+                hn = np + 4
+                hc = (hc / 100.0) * np + 0.8
+                out = (((hn * ((hc * isHr) + np)) / (np * (hn - (hc * isHr)))) * (out - np / 2.0) + np / 2.0)
+
+            if (hb != 0.0):
+                # Highlight Brightness
+                out = (out + ((hb * isHr) / 100.0) * np)
+
+            # Midtones
+
+            isMr = self._is_midtone(out, (3.0 / mbl))
+            if (mc != 0.0):
+                # Midtone Contrast
+                hn = np + 4
+                mc = (mc / 100.0) * np + 0.8
+                out = (((hn * ((mc * isMr) + np)) / (np * (hn - (mc * isMr)))) * (out - np / 2.0) + np / 2.0)
+
+            if (mb != 0.0):
+                # Midtone Brightness
+                out = (out + ((mb * isMr) / 100.0) * np)
+
+            # Shadows
+
+            isSr = self._is_shadow(out, (3.0 / sbl))
+            if (sc != 0.0):
+                # Shadow Contrast
+                hn = np + 4
+                sc = (sc / 100.0) * np + 0.8
+                out = (((hn * ((sc * isSr) + np)) / (np * (hn - (sc * isSr)))) * (out - np / 2.0) + np / 2.0)
+
+            if (sb != 0.0):
+                # Shadow Brightness
+                out = (out + ((sb * isSr) / 100.0) * np)
+
+            # Clip any values out of bounds
+            out[out < 0.0] = 0.0
+            out[out > np] = np
+
+            return out.astype(im.dtype)
+        else:
+            return im
+
+
+
+    def _is_highlight(self, image, bleed_value = 6.0):
+        bleed = float(image.max() / bleed_value)
+        mif = image.max() / 3.0 * 2.0
+        icopy = image.copy()
+
+        icopy[icopy < mif - bleed] = 0.0
+        icopy[(icopy < mif) * (icopy != 0.0)] = ((mif - (icopy[(icopy < mif) * (icopy != 0.0)])) / bleed) * -1 + 1
+        icopy[icopy >= mif] = 1.0
+        return icopy
+
+    def _is_midtone(self, image, bleed_value = 6.0):
+        bleed = float(image.max() / bleed_value)
+        mif = image.max() / 3.0
+        mir = image.max() / 3.0 * 2.0
+        icopy = image.copy()
+
+        icopy[icopy < mif - bleed] = 0.0
+        icopy[icopy > mir + bleed] = 0.0
+
+        icopy[(icopy < mif) * (icopy != 0.0)] = ((mif - (icopy[(icopy < mif) * (icopy != 0.0)])) / bleed) * -1 + 1
+        icopy[(icopy > mir) * (icopy != 0.0)] = (((icopy[(icopy > mir) * (icopy != 0.0)]) - mir) / bleed) * -1 + 1
+        icopy[(icopy >= mif) * (icopy <= mir)] = 1.0
+        return icopy
+
+    def _is_shadow(self, image, bleed_value=6.0):
+        bleed = float(image.max() / bleed_value)
+        mir = image.max() / 3.0
+        icopy = image.copy()
+
+        icopy[icopy <= mir] = 1.0
+        icopy[icopy > mir + bleed] = 0.0
+        icopy[icopy > mir] = (((icopy[(icopy > mir) * (icopy != 0.0)]) - mir) / bleed) * -1 + 1
+        return icopy
+

+ 51 - 0
PF2/Tools/Denoise.py

@@ -0,0 +1,51 @@
+import cv2
+
+import Tool
+from scipy import ndimage
+
+class Denoise(Tool.Tool):
+    def on_init(self):
+        self.id = "denoise"
+        self.name = "Denoise"
+        self.icon_path = "ui/PF2_Icons/Denoise.png"
+        self.properties = [
+            # Detailer
+            Tool.Property("enabled", "Denoise", "Header", False, has_toggle=True, has_button=False),
+            Tool.Property("strength", "Strength", "Slider", 0, max=100, min=0),
+            Tool.Property("w_strength", "White Strength", "Slider", 20, max=100, min=0),
+            Tool.Property("b_strength", "Black Strength", "Slider", 70, max=100, min=0),
+            Tool.Property("method", "Method", "Combo", 0, options=[
+                "Mean",
+                "Gaussian",
+            ]),
+        ]
+
+    def on_update(self, image):
+        im = image
+        if(self.props["enabled"].get_value()):
+            bpp = int(str(im.dtype).replace("uint", "").replace("float", ""))
+            np = float(2 ** bpp - 1)
+
+            method = self.props["method"].get_value()
+            strength = self.props["strength"].get_value()
+            b_strength = self.props["b_strength"].get_value()
+            w_strength = self.props["w_strength"].get_value()
+
+            filtered = None
+            if(method == 0):
+                filtered = ndimage.median_filter(im, 3)
+            elif(method == 1):
+                filtered = ndimage.gaussian_filter(im, 2)
+
+            w_filter = im
+            w_filter[im > (float(np) / 10) * 9] = filtered[im > (float(np) / 10) * 9]
+
+            b_filter = im
+            b_filter[im < (float(np) / 10)] = filtered[im < (float(np) / 10)]
+
+            # Blend
+            im = cv2.addWeighted(filtered, (strength / 100), im, 1 - (strength / 100), 0)
+            im = cv2.addWeighted(w_filter, (w_strength / 100), im, 1 - (w_strength / 100), 0)
+            im = cv2.addWeighted(b_filter, (b_strength / 100), im, 1 - (b_strength / 100), 0)
+
+        return im

+ 131 - 0
PF2/Tools/Details.py

@@ -0,0 +1,131 @@
+import cv2
+import numpy
+
+import Tool
+from PF2.Tools import Contrast
+
+
+class Details(Tool.Tool):
+    def on_init(self):
+        self.id = "details"
+        self.name = "Details and Edges"
+        self.icon_path = "ui/PF2_Icons/Details.png"
+        self.properties = [
+            # Detailer
+            Tool.Property("d_enabled", "Detailer", "Header", False, has_toggle=True, has_button=False),
+            Tool.Property("d_strength", "Strength", "Slider", 30, max=100, min=0),
+            Tool.Property("d_detail", "Detail", "Slider", 15, max=100, min=0),
+            Tool.Property("d_sint", "Highlight Intensity", "Slider", 0, max=100, min=-100),
+            Tool.Property("d_mint", "Midtone Intensity", "Slider", 0, max=100, min=-100),
+            Tool.Property("d_hint", "Shadow Intensity", "Slider", 0, max=100, min=-100),
+            Tool.Property("d_slum", "Highlight Luminosity", "Slider", 0, max=100, min=-100),
+            Tool.Property("d_mlum", "Midtone Luminosity", "Slider", 0, max=100, min=-100),
+            Tool.Property("d_hlum", "Shadow Luminosity", "Slider", 0, max=100, min=-100),
+            Tool.Property("d_pcont", "Restorative Contrast", "Slider", 0, max=100, min=0),
+            # Edges
+            Tool.Property("e_enabled", "Edges", "Header", False, has_toggle=True, has_button=False),
+            Tool.Property("e_strength", "Strength", "Slider", 30, max=100, min=0),
+            Tool.Property("e_fthresh", "First Threshold", "Slider", 100, max=1000, min=0),
+            Tool.Property("e_sthresh", "Second Threshold", "Slider", 200, max=1000, min=0),
+        ]
+
+        self.contrast_tool = Contrast.Contrast()
+        self.contrast_tool_restore = Contrast.Contrast()
+
+    def on_update(self, image):
+        im = image
+        if(self.props["d_enabled"].get_value()):
+            strength = self.props["d_strength"].get_value()
+            detail = self.props["d_detail"].get_value()
+            self.contrast_tool.props["highlight_contrast"].set_value(self.props["d_hint"].get_value())
+            self.contrast_tool.props["midtone_contrast"].set_value(self.props["d_mint"].get_value())
+            self.contrast_tool.props["shadow_contrast"].set_value(self.props["d_sint"].get_value())
+            self.contrast_tool.props["highlight_brightness"].set_value(self.props["d_hlum"].get_value())
+            self.contrast_tool.props["midtone_brightness"].set_value(self.props["d_mlum"].get_value())
+            self.contrast_tool.props["shadow_brightness"].set_value(self.props["d_slum"].get_value())
+            pcont = self.props["d_pcont"].get_value()
+
+            # Convert to Grayscale
+            gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
+
+            # Invert
+            edged = gray.max() - gray
+
+            # Apply Brightness and Contrast
+            edged = self.contrast_tool.on_update(edged)
+
+            # Blur
+            if(detail > 0):
+                blur_size = 2 * round((round(detail) + 1) / 2) - 1
+                blurred = cv2.GaussianBlur(edged, (int(blur_size), int(blur_size)), 0)
+            else:
+                blurred = edged
+
+
+            # Overlay
+            colour = cv2.cvtColor(blurred, cv2.COLOR_GRAY2BGR)
+            bpp = int(str(im.dtype).replace("uint", "").replace("float", ""))
+            blended = self._overlay(colour, im, float((2 ** bpp) - 1), im.dtype)
+
+            # Restore Contrast
+            self.contrast_tool_restore.props["highlight_contrast"].set_value(pcont * 0.25)
+            self.contrast_tool_restore.props["midtone_contrast"].set_value(pcont * 0.5)
+            self.contrast_tool_restore.props["shadow_contrast"].set_value(pcont * 0.25)
+            cfixed = self.contrast_tool_restore.on_update(blended)
+
+            # Blend
+            if(strength != 100):
+                im = cv2.addWeighted(cfixed, (strength/100), image, 1 - (strength/100), 0).astype(image.dtype)
+            else:
+                im = cfixed.astype(image.dtype)
+
+
+        if(self.props["e_enabled"].get_value()):
+            strength = self.props["e_strength"].get_value()
+            t1 = self.props["e_fthresh"].get_value()
+            t2 = self.props["e_sthresh"].get_value()
+
+            # Get bits per pixel
+            bpp = int(str(im.dtype).replace("uint", "").replace("float", ""))
+
+            # Convert to 8 Bit
+            eight = ((im / float(2 ** bpp)) * 255).astype(numpy.uint8)
+
+            # Make Grayscale
+            grey = cv2.cvtColor(eight, cv2.COLOR_BGR2GRAY)
+
+            # Find edges
+            edged = cv2.Canny(grey, t1, t2)
+
+            # Convert edges to colour
+            colour = cv2.cvtColor(edged, cv2.COLOR_GRAY2BGR)
+
+            colour[colour != 0] = 255
+            colour[colour == 0] = 128
+
+            # Convert edges to current bpp
+            nbpp = (colour / 255.0) * (2 ** bpp)
+
+            # Blur?
+            blurred = cv2.GaussianBlur(nbpp, (7, 7), 0)
+
+            # Uhh
+            blurred[blurred == (2 ** bpp) - 1] = ((2 ** bpp) - 1) / 2.0
+
+            # I'm going to be honest, I just copied this one from
+            # PhotoFiddle 1, I don't know what I was doing
+            overlayed = self._overlay(blurred, im, float((2 ** bpp) - 1), im.dtype)
+
+            out = cv2.addWeighted(overlayed, (strength / 100), im, 1 - (strength / 100), 0)
+
+            im = out
+
+        return im
+
+
+    def _overlay(self, B, A, bpp, utype):
+        a = A / bpp
+        b = B / bpp
+        merged = (1 - 2 * b) * a ** 2 + 2 * a * b
+        return (merged * bpp).astype(utype)
+

+ 84 - 0
PF2/Tools/Exposure.py

@@ -0,0 +1,84 @@
+import cv2
+
+import Tool
+from scipy import ndimage
+
+class Exposure(Tool.Tool):
+    def on_init(self):
+        self.id = "exposure"
+        self.name = "Exposure"
+        self.icon_path = "ui/PF2_Icons/Exposure.png"
+        self.properties = [
+            # Detailer
+            Tool.Property("enabled", "Exposure", "Header", False, has_toggle=False, has_button=False),
+            Tool.Property("exposure", "Overall Exposure", "Slider", 0, max=50, min=-50),
+            Tool.Property("h_exposure", "Highlight Exposure", "Slider", 0, max=50, min=-50),
+            Tool.Property("m_exposure", "Midtone Exposure", "Slider", 0, max=50, min=-50),
+            Tool.Property("s_exposure", "Shadow Exposure", "Slider", 0, max=50, min=-50),
+        ]
+
+    def on_update(self, image):
+        im = image
+        if(not self.is_default()):
+            ex = self.props["exposure"].get_value() * 0.0001
+            hex = self.props["h_exposure"].get_value() * 0.0001
+            mex = self.props["m_exposure"].get_value() * 0.0001
+            sex = self.props["s_exposure"].get_value() * 0.0001
+
+
+            if(hex != 0):
+                ish = self._is_highlight(im)
+                mul = 2**hex
+                im[ish > 0] = im[ish > 0]*mul
+
+            if(mex != 0):
+                ish = self._is_midtone(im)
+                mul = 2**mex
+                im[ish > 0] = im[ish > 0] * mul
+
+            if(sex != 0):
+                ish = self._is_shadow(im)
+                mul = 2**sex
+                im[ish > 0] = im[ish > 0] * mul
+
+            if(ex != 0):
+                mul = 2 ** ex
+                im = im * mul
+
+
+        return im
+
+
+    def _is_highlight(self, image, bleed_value = 6.0):
+        bleed = float(image.max() / bleed_value)
+        mif = image.max() / 3.0 * 2.0
+        icopy = image.copy()
+
+        icopy[icopy < mif - bleed] = 0.0
+        icopy[(icopy < mif) * (icopy != 0.0)] = ((mif - (icopy[(icopy < mif) * (icopy != 0.0)])) / bleed) * -1 + 1
+        icopy[icopy >= mif] = 1.0
+        return icopy
+
+    def _is_midtone(self, image, bleed_value = 6.0):
+        bleed = float(image.max() / bleed_value)
+        mif = image.max() / 3.0
+        mir = image.max() / 3.0 * 2.0
+        icopy = image.copy()
+
+        icopy[icopy < mif - bleed] = 0.0
+        icopy[icopy > mir + bleed] = 0.0
+
+        icopy[(icopy < mif) * (icopy != 0.0)] = ((mif - (icopy[(icopy < mif) * (icopy != 0.0)])) / bleed) * -1 + 1
+        icopy[(icopy > mir) * (icopy != 0.0)] = (((icopy[(icopy > mir) * (icopy != 0.0)]) - mir) / bleed) * -1 + 1
+        icopy[(icopy >= mif) * (icopy <= mir)] = 1.0
+        return icopy
+
+    def _is_shadow(self, image, bleed_value=6.0):
+        bleed = float(image.max() / bleed_value)
+        mir = image.max() / 3.0
+        icopy = image.copy()
+
+        icopy[icopy <= mir] = 1.0
+        icopy[icopy > mir + bleed] = 0.0
+        icopy[icopy > mir] = (((icopy[(icopy > mir) * (icopy != 0.0)]) - mir) / bleed) * -1 + 1
+        return icopy

+ 132 - 0
PF2/Tools/HueEqualiser.py

@@ -0,0 +1,132 @@
+import cv2
+import numpy
+
+import Tool
+
+class HueEqualiser(Tool.Tool):
+    def on_init(self):
+        self.id = "hueequaliser"
+        self.name = "Hue Equaliser"
+        self.icon_path = "ui/PF2_Icons/HueEqualiser.png"
+        self.properties = [
+            Tool.Property("header", "Hue Equaliser", "Header", None, has_toggle=False, has_button=False),
+            Tool.Property("bleed", "Hue Bleed", "Slider", 0.5, max=2.0, min=0.01),
+            # Red
+            Tool.Property("header_red", "Red", "Header", None, has_toggle=False, has_button=False),
+            Tool.Property("red_value", "Value", "Slider", 0, max=50, min=-50),
+            Tool.Property("red_saturation", "Saturation", "Slider", 0, max=50, min=-50),
+            # Yellow
+            Tool.Property("header_yellow", "Yellow", "Header", None, has_toggle=False, has_button=False),
+            Tool.Property("yellow_value", "Value", "Slider", 0, max=50, min=-50),
+            Tool.Property("yellow_saturation", "Saturation", "Slider", 0, max=50, min=-50),
+            # Green
+            Tool.Property("header_green", "Green", "Header", None, has_toggle=False, has_button=False),
+            Tool.Property("green_value", "Value", "Slider", 0, max=50, min=-50),
+            Tool.Property("green_saturation", "Saturation", "Slider", 0, max=50, min=-50),
+            # Cyan
+            Tool.Property("header_cyan", "Cyan", "Header", None, has_toggle=False, has_button=False),
+            Tool.Property("cyan_value", "Value", "Slider", 0, max=50, min=-50),
+            Tool.Property("cyan_saturation", "Saturation", "Slider", 0, max=50, min=-50),
+            # Blue
+            Tool.Property("header_blue", "Blue", "Header", None, has_toggle=False, has_button=False),
+            Tool.Property("blue_value", "Value", "Slider", 0, max=50, min=-50),
+            Tool.Property("blue_saturation", "Saturation", "Slider", 0, max=50, min=-50),
+            # Violet
+            Tool.Property("header_violet", "Violet", "Header", None, has_toggle=False, has_button=False),
+            Tool.Property("violet_value", "Value", "Slider", 0, max=50, min=-50),
+            Tool.Property("violet_saturation", "Saturation", "Slider", 0, max=50, min=-50),
+        ]
+
+    def on_update(self, image):
+        hues = {
+            "red": 0,
+            "yellow": 60,
+            "green": 120,
+            "cyan": 180,
+            "blue": 240,
+            "violet": 300,
+            "_red": 360,
+        }
+
+        out = image
+
+        if(not self.is_default()):
+            bleed = self.props["bleed"].get_value()
+
+            out = out.astype(numpy.float32)
+
+            # Convert to HSV colorspace
+            out = cv2.cvtColor(out, cv2.COLOR_BGR2HSV)
+
+            # Bits per pixel
+            bpp = float(str(image.dtype).replace("uint", "").replace("float", ""))
+            # Pixel value range
+            np = float(2 ** bpp - 1)
+
+            imhue = out[0:, 0:, 0]
+            imsat = out[0:, 0:, 1]
+            imval = out[0:, 0:, 2]
+
+            for hue in hues:
+                hsat = self.props["%s_saturation" % hue.replace('_', '')].get_value()
+                hval = self.props["%s_value" % hue.replace('_', '')].get_value()
+
+                isHue = self._is_hue(imhue, hues[hue], (3.5/bleed))
+
+                imsat = imsat + ((hsat / 10000) * 255) * isHue
+                imval = imval + ((hval / 1000) * np) * isHue
+
+                # Clip any values out of bounds
+                imval[imval < 0.0] = 0.0
+                imval[imval > np] = np
+
+                imsat[imsat < 0.0] = 0.0
+                imsat[imsat > 1.0] = 1.0
+
+
+
+
+            out[0:, 0:, 1] = imsat
+            out[0:, 0:, 2] = imval
+
+
+            # Convert back to BGR colorspace
+            out = cv2.cvtColor(out, cv2.COLOR_HSV2BGR)
+
+            out = out.astype(image.dtype)
+
+
+
+
+        return out
+
+
+    def _is_hue(self, image, hue_value, bleed_value = 3.5):
+        mif = hue_value - 30
+        mir = hue_value + 30
+        if (mir > 360):
+            mir = 360
+        if (mif < 0):
+            mif = 0
+
+        bleed = float(360 / bleed_value)
+        icopy = image.copy()
+
+        print(bleed, mif, mir)
+
+        if(mif != 0):
+            icopy[icopy < mif - bleed] = 0.0
+            icopy[icopy > mir + bleed] = 0.0
+
+            icopy[(icopy < mif) * (icopy != 0.0)] = (((mif - (icopy[(icopy < mif) * (icopy != 0.0)]))/360.0) / (bleed/360.0)) * -1 + 1
+
+            icopy[(icopy > mir) * (icopy != 0.0)] = ((((icopy[(icopy > mir) * (icopy != 0.0)]) - mir)/360.0) / (bleed/360.0)) * -1 + 1
+
+        icopy[(icopy >= mif) * (icopy <= mir)] = 1.0
+
+        if(mif == 0):
+            icopy[icopy > mir + bleed] = 0.0
+
+            icopy[(icopy > mir) * (icopy != 0.0)] = ((((icopy[(icopy > mir) * (icopy != 0.0)]) - mir) / 360.0) / (bleed/360.0)) * -1 + 1
+
+        return icopy

+ 76 - 0
PF2/Tools/Tonemap.py

@@ -0,0 +1,76 @@
+import cv2
+
+import Tool
+
+
+class Tonemap(Tool.Tool):
+    def on_init(self):
+        self.id = "tonemap"
+        self.name = "Tone Mapping"
+        self.icon_path = "ui/PF2_Icons/Tonemap.png"
+        self.properties = [
+            Tool.Property("enabled", "Tone Mapping", "Header", False, has_toggle=True, has_button=False),
+            Tool.Property("strength", "Strength", "Slider", 90, max=100, min=0),
+            Tool.Property("bleed", "Bleed", "Slider", 10, max=100, min=0),
+            Tool.Property("contrast", "Contrast", "Slider", 25, max=100, min=0),
+            Tool.Property("two_pass", "Two Pass", "Toggle", False)
+        ]
+
+    def on_update(self, image):
+        if(self.props["enabled"].get_value()):
+            blur = self.props["bleed"].get_value()
+            first_opacity = (self.props["contrast"].get_value() * -1) + 100
+            second_opacity = self.props["strength"].get_value()
+            two_pass = self.props["two_pass"].get_value()
+
+            im = image
+
+            iterations = 1
+            if(two_pass):
+                iterations = 2
+
+            for i in range(iterations):
+                # Convert to Grayscale
+                gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
+
+                # Invert Grayscale Image
+                inverted = self._invert(gray)
+
+                # Blur
+                if(blur > 0):
+                    height, width = inverted.shape[:2]
+                    imsize = (height + width) / float(2)
+                    blur_size = 2 * round(round((imsize * (blur / 100.0)) / 2)) - 1
+                    blurred = cv2.GaussianBlur(inverted, (int(blur_size), int(blur_size)), 0)
+                else:
+                    # Or, don't blur
+                    blurred = inverted
+
+                # First round of Blending
+                colour = cv2.cvtColor(blurred, cv2.COLOR_GRAY2BGR)
+                colouredMap = cv2.addWeighted(colour, (first_opacity / 100), im, 1 - (first_opacity / 100), 0)
+
+                # Overlay
+                bpp = int(str(im.dtype).replace("uint", "").replace("float", ""))
+                blended = self._overlay(colouredMap, im, float((2 ** bpp) - 1), im.dtype)
+
+                # Second round of Blending
+                im = cv2.addWeighted(blended, (second_opacity / 100), im, 1 - (second_opacity / 100), 0)
+
+            return im
+
+        else:
+            return image
+
+
+
+
+
+    def _invert(self, image):
+        return image.max() - image
+
+    def _overlay(self, B, A, bpp, utype):
+        a = A / bpp
+        b = B / bpp
+        merged = (1 - 2 * b) * a ** 2 + 2 * a * b
+        return (merged * bpp).astype(utype)

+ 88 - 0
PF2/Tools/WhiteBalance.py

@@ -0,0 +1,88 @@
+import cv2
+import numpy
+
+import Tool
+
+class WhiteBalance(Tool.Tool):
+    def on_init(self):
+        self.id = "whitebalance"
+        self.name = "Colour Temperature"
+        self.icon_path = "ui/PF2_Icons/WhiteBalance.png"
+        self.properties = [
+            Tool.Property("enabled", "Colour Temperature", "Header", False, has_toggle=True, has_button=False),
+            Tool.Property("kelvin", "Kelvins", "Slider", 6500, max=15000, min=1000),
+            Tool.Property("strength", "Strength", "Slider", 25, max=100, min=0),
+        ]
+
+    def on_update(self, image):
+        if(self.props["enabled"].get_value()):
+            ct = self.props["kelvin"].get_value()/100.0
+            st = self.props["strength"].get_value()
+
+            bpp = float(str(image.dtype).replace("uint", "").replace("float", ""))
+            np = float(2 ** bpp - 1)
+
+            r = 0
+            print(ct)
+            if(ct <= 66):
+                print(ct <= 66)
+                r = 255
+            else:
+                r = ct - 60
+                r = 329.698727446 * numpy.math.pow(r, -0.1332047592)
+                if(r < 0):
+                    r = 0
+                if(r > 255):
+                    r = 255
+
+            g = 0
+            if(ct <= 66):
+                g = ct
+                g = 99.4708025861 * numpy.math.log(g) - 161.1195681661
+                if (g < 0):
+                    g = 0
+                if (g > 255):
+                    g = 255
+            else:
+                g = ct - 60
+                g = 288.1221695283 * numpy.math.pow(g, -0.0755148492)
+                if (g < 0):
+                    g = 0
+                if (g > 255):
+                    g = 255
+
+
+            b = 0
+            if(ct >= 66):
+                b = 255
+            elif(ct <= 19):
+                b = 0
+            else:
+                b = ct - 10
+                b = 138.5177312231 * numpy.math.log(b) - 305.0447927307
+                if (b < 0):
+                    b = 0
+                if (b > 255):
+                    b = 255
+
+            r = (r/255.0)
+            g = (g / 255.0)
+            b = (b / 255.0)
+
+            print(r, g, b)
+
+            # Red
+            # image[0:, 0:, 2] = (image[0:, 0:, 2] * (((st / 100.0)*-1)+1)) + ((st / 100.0) * (r - np/2.0))
+            image[0:, 0:, 2] = image[0:, 0:, 2] * r
+            # # Green
+            # image[0:, 0:, 1] = (image[0:, 0:, 1] * (((st / 100.0)*-1)+1)) + ((st / 100.0) * (g - np/2.0))
+            image[0:, 0:, 1] = image[0:, 0:, 1] * g
+            # # Blue
+            # image[0:, 0:, 0] = (image[0:, 0:, 0] * (((st / 100.0)*-1)+1)) + ((st / 100.0) * (b - np/2.0))
+            image[0:, 0:, 0] = image[0:, 0:, 0] * b
+
+            image[image < 0.0] = 0.0
+            image[image > np] = np
+
+        return image
+

+ 0 - 0
PF2/Tools/__init__.py


+ 567 - 0
PF2/__init__.py

@@ -0,0 +1,567 @@
+import ast
+import time
+from gi.repository import GLib, Gtk, Gdk, GdkPixbuf
+import Activity
+import Export
+import threading
+import cv2
+import numpy
+import getpass
+import os
+
+from PF2 import Histogram
+from PF2.Tools import BlackWhite
+from PF2.Tools import Colours
+from PF2.Tools import Contrast
+from PF2.Tools import Details
+from PF2.Tools import Tonemap
+from PF2.Tools import HueEqualiser
+
+
+class PF2(Activity.Activity):
+
+    def on_init(self):
+        self.id = "PF2"
+        self.name = "Edit a Photo"
+        self.subtitle = "Edit a Raster Image with PhotoFiddle"
+
+        UI_FILE = "ui/PF2_Activity.glade"
+        self.builder.add_from_file(UI_FILE)
+
+        self.widget = self.builder.get_object("PF2_main")
+        self.stack.add_titled(self.widget, self.id, self.name)
+
+        self.header_widget = self.builder.get_object("PF2_header")
+        self.header_stack.add_titled(self.header_widget, self.id, self.name)
+
+        # Get all UI Components
+        self.ui = {}
+        components = [
+            "control_reveal",
+            "histogram_reveal",
+            "layers_reveal",
+            "upper_peak_toggle",
+            "lower_peak_toggle",
+            "histogram",
+            "layer_stack",
+            "preview",
+            "scroll_window",
+            "open_button",
+            "original_toggle",
+            "tools",
+            "tool_stack",
+            "open_window",
+            "open_header",
+            "open_cancel_button",
+            "open_open_button",
+            "open_chooser",
+            "popovermenu",
+            "show_hist",
+            "export_image",
+            "undo",
+            "redo",
+            "zoom_toggle",
+            "zoom_reveal",
+            "zoom",
+            "reset"
+        ]
+
+        for component in components:
+            self.ui[component] = self.builder.get_object("%s_%s" % (self.id, component))
+
+        self.menu_popover = self.ui["popovermenu"]
+
+        # Set up tools
+        self.tools = [
+            Contrast.Contrast(),
+            Tonemap.Tonemap(),
+            Details.Details(),
+            Colours.Colours(),
+            HueEqualiser.HueEqualiser(),
+            BlackWhite.BlackWhite(),
+        ]
+
+        self.tool_map = {}
+
+        for tool in self.tools:
+            self.ui["tools"].add(tool.tool_button)
+            self.ui["tool_stack"].add(tool.widget)
+            self.tool_map[tool.tool_button] = tool
+            tool.tool_button.connect("clicked", self.on_tool_button_clicked)
+            tool.connect_on_change(self.on_tool_change)
+
+        # Set the first tool to active
+        self.tools[0].tool_button.set_active(True)
+        self.ui["tools"].show_all()
+
+        # Disable ui components by default
+        self.ui["original_toggle"].set_sensitive(False)
+        self.ui["export_image"].set_sensitive(False)
+
+        # Setup editor variables
+        self.is_editing = False
+        self.has_loaded = False
+        self.image_path = ""
+        self.bit_depth = 8
+        self.image = None
+        self.original_image = None
+        self.pwidth = 0
+        self.pheight = 0
+        self.awidth = 0
+        self.aheight = 0
+        self.pimage = None
+        self.poriginal = None
+        self.change_occurred = False
+        self.additional_change_occurred = False
+        self.running_stack = False
+        self.upper_peak_on = False
+        self.lower_peak_on = False
+        self.is_exporting = False
+        self.is_zooming = False
+        self.undo_stack = []
+        self.undo_position = 0
+        self.undoing = False
+
+        # Setup Open Dialog
+        self.ui["open_window"].set_transient_for(self.root)
+        self.ui["open_window"].set_titlebar(self.ui["open_header"])
+
+        # Connect UI signals
+        self.ui["open_button"].connect("clicked", self.on_open_clicked)
+        self.ui["preview"].connect("draw", self.on_preview_draw)
+        self.ui["original_toggle"].connect("toggled", self.on_preview_toggled)
+        self.ui["upper_peak_toggle"].connect("toggled", self.on_upper_peak_toggled)
+        self.ui["lower_peak_toggle"].connect("toggled", self.on_lower_peak_toggled)
+        self.ui["show_hist"].connect("toggled", self.toggle_hist)
+        self.ui["export_image"].connect("clicked", self.on_export_clicked)
+        self.ui["open_open_button"].connect("clicked", self.on_file_opened)
+        self.ui["open_cancel_button"].connect("clicked", self.on_file_canceled)
+        self.ui["zoom_toggle"].connect("toggled", self.on_zoom_toggled)
+        self.ui["zoom"].connect("value-changed", self.on_zoom_changed)
+        self.ui["undo"].connect("clicked", self.on_undo)
+        self.ui["redo"].connect("clicked", self.on_redo)
+        self.ui["reset"].connect("clicked", self.on_reset)
+
+
+    def on_tool_button_clicked(self, sender):
+        if(sender.get_active()):
+            self.ui["tool_stack"].set_visible_child(self.tool_map[sender].widget)
+            for key in self.tool_map:
+                if(key != sender):
+                    key.set_active(False)
+
+        elif(self.ui["tool_stack"].get_visible_child() == self.tool_map[sender].widget):
+            sender.set_active(True)
+
+
+    def on_open_clicked(self, sender):
+        self.ui["open_window"].show_all()
+
+    def on_file_opened(self, sender, path=None):
+        self.has_loaded = False
+        self.image = None
+        self.undo_position = 0
+        self.undo_stack = []
+        self.update_undo_state()
+        self.ui["open_window"].hide()
+        self.ui["control_reveal"].set_reveal_child(True)
+        self.ui["control_reveal"].set_sensitive(True)
+        self.ui["original_toggle"].set_sensitive(True)
+        self.ui["export_image"].set_sensitive(True)
+        self.ui["reset"].set_sensitive(True)
+        self.is_editing = True
+        if(path == None):
+            self.image_path = self.ui["open_chooser"].get_filename()
+        else:
+            self.image_path = path
+
+        w = (self.ui["scroll_window"].get_allocated_width() - 12) * self.ui["zoom"].get_value()
+        h = (self.ui["scroll_window"].get_allocated_height() - 12) * self.ui["zoom"].get_value()
+
+        self.show_message("Loading Photo…", "Please wait while PhotoFiddle loads your photo", True)
+        self.root.get_titlebar().set_subtitle("%s…" % (self.image_path))
+
+        thread = threading.Thread(target=self.open_image,
+                                  args=(w, h))
+        thread.start()
+
+    def on_zoom_toggled(self, sender):
+        state = sender.get_active()
+        self.ui["zoom_reveal"].set_reveal_child(state)
+        if(not state):
+            self.ui["zoom"].set_value(1)
+
+    def on_zoom_changed(self, sender):
+        threading.Thread(target=self.zoom_delay).start()
+
+
+    def zoom_delay(self):
+        if(not self.is_zooming):
+            self.is_zooming = True
+            time.sleep(0.5)
+            GLib.idle_add(self.on_preview_draw, None, None)
+            self.is_zooming = False
+
+
+
+    def on_file_canceled(self, sender):
+        self.ui["open_window"].hide()
+
+    def on_preview_draw(self, sender, arg):
+        if(self.is_editing):
+            w = (self.ui["scroll_window"].get_allocated_width() - 12) * self.ui["zoom"].get_value()
+            h = (self.ui["scroll_window"].get_allocated_height() - 12) * self.ui["zoom"].get_value()
+            if(self.pheight != h) or (self.pwidth != w):
+                thread = threading.Thread(target=self.resize_preview,
+                                          args=(w, h))
+                thread.start()
+
+    def on_preview_toggled(self, sender):
+        if(sender.get_active()):
+            threading.Thread(target=self.draw_hist, args=(self.original_image,)).start()
+            self.show_original()
+        else:
+            threading.Thread(target=self.draw_hist, args=(self.image,)).start()
+            self.show_current()
+
+    def toggle_hist(self, sender):
+        show = sender.get_active()
+        self.ui["histogram_reveal"].set_reveal_child(show)
+
+
+    def on_open(self, path):
+        self.root.get_titlebar().set_subtitle("Raster Editor")
+        if(path != None):
+            # We have been sent here from another
+            # activity, clear any existing profiles
+            self.image_path = path
+            dataPath = self.get_data_path()
+            if(os.path.exists(dataPath)):
+                os.unlink(dataPath)
+
+            self.on_file_opened(None, path)
+
+    def on_exit(self):
+        if(self.is_exporting):
+            return False
+        else:
+            fname = "/tmp/phf2-preview-%s.png" % getpass.getuser()
+            if (os.path.exists(fname)):
+                os.unlink(fname)
+
+            self.root.get_titlebar().set_subtitle("")
+            self.on_init()
+            return True
+
+
+    def show_original(self):
+        self.ui["preview"].set_from_pixbuf(self.poriginal)
+        if (not self.ui["original_toggle"].get_active()):
+            self.ui["original_toggle"].set_active(True)
+
+    def show_current(self):
+        self.ui["preview"].set_from_pixbuf(self.pimage)
+        if (self.ui["original_toggle"].get_active()):
+            self.ui["original_toggle"].set_active(False)
+
+
+    def on_tool_change(self, tool, property):
+        if(self.has_loaded):
+            if(not self.change_occurred):
+                self.change_occurred = True
+                thread = threading.Thread(target=self.update_image)
+                thread.start()
+            else:
+                self.additional_change_occurred = True
+
+    def on_lower_peak_toggled(self, sender):
+        self.lower_peak_on = sender.get_active()
+        if (self.lower_peak_on):
+            thread = threading.Thread(target=self.process_peaks, args=(True,))
+            thread.start()
+        else:
+            thread = threading.Thread(target=self.update_image, args=(True,))
+            thread.start()
+
+    def on_upper_peak_toggled(self, sender):
+        self.upper_peak_on = sender.get_active()
+        if (self.lower_peak_on):
+            thread = threading.Thread(target=self.process_peaks, args=(True,))
+            thread.start()
+        else:
+            thread = threading.Thread(target=self.update_image, args=(True,))
+            thread.start()
+
+    def image_opened(self, depth):
+        self.root.get_titlebar().set_subtitle("%s (%s Bit)" % (self.image_path, depth))
+        self.hide_message()
+
+
+    def on_export_clicked(self, sender):
+        Export.ExportDialog(self.root, self.builder, self.awidth, self.aheight, self.get_export_image, self.on_export_complete, self.image_path)
+
+    def on_export_complete(self, filename):
+        self.is_exporting = False
+        self.on_export_state_change()
+        self.show_message("Export Complete!", "Your photo has been exported to '%s'" % filename)
+
+    def on_export_state_change(self):
+        self.ui["control_reveal"].set_sensitive(not self.is_exporting)
+        self.ui["export_image"].set_sensitive(not self.is_exporting)
+        self.ui["open_button"].set_sensitive(not self.is_exporting)
+
+    def on_export_started(self):
+        self.is_exporting = True
+        self.on_export_state_change()
+
+    def update_undo_state(self):
+        self.ui["undo"].set_sensitive(self.undo_position > 0)
+        self.ui["redo"].set_sensitive(len(self.undo_stack)-1 > self.undo_position)
+
+    def on_undo(self, sender):
+        self.undo_position -= 1
+        self.update_undo_state()
+        self.update_from_undo_stack(self.undo_stack[self.undo_position])
+
+    def on_redo(self, sender):
+        self.undo_position += 1
+        self.update_undo_state()
+        self.update_from_undo_stack(self.undo_stack[self.undo_position])
+
+    def on_reset(self, sender):
+        for tool in self.tools:
+            tool.reset()
+
+
+
+    ## Background Tasks ##
+    def open_image(self, w, h):
+        self.load_image_data()
+        try:
+            self.resize_preview(w, h)
+        except:
+            pass
+        while(self.image == None):
+            time.sleep(1)
+        GLib.idle_add(self.image_opened, str(self.image.dtype).replace("uint", "").replace("float", ""))
+        time.sleep(1)
+        self.has_loaded = True
+
+
+
+    def resize_preview(self, w, h):
+        # Inhibit undo stack to prevent
+        # Adding an action on resize
+        self.undoing = True
+
+        self.original_image = cv2.imread(self.image_path, 2 | 1)
+        height, width = self.original_image.shape[:2]
+
+        self.aheight = height
+        self.awidth = width
+
+        self.pheight = h
+        self.pwidth = w
+
+        # Get fitting size
+        ratio = float(w)/width
+        if(height*ratio > h):
+            ratio = float(h)/height
+
+        nw = width * ratio
+        nh = height * ratio
+
+        # Do quick ui resize
+        if(self.image != None) and (os.path.exists("/tmp/phf2-preview-%s.png" % getpass.getuser())):
+            # If we have an edited version, show that
+            self.pimage = GdkPixbuf.Pixbuf.new_from_file_at_scale("/tmp/phf2-preview-%s.png" % getpass.getuser(),
+                                                                         int(nw), int(nh), True)
+            GLib.idle_add(self.show_current)
+
+        self.poriginal = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.image_path,
+                                                            int(nw), int(nh), True)
+        if(self.image == None):
+            # Otherwise show the original
+            GLib.idle_add(self.show_original)
+
+
+        # Resize OPENCV Copy
+        self.original_image = cv2.resize(self.original_image, (int(nw), int(nh)), interpolation = cv2.INTER_AREA)
+
+        self.image = numpy.copy(self.original_image)
+
+        # Update image
+        if (not self.change_occurred):
+            self.change_occurred = True
+            self.update_image()
+        else:
+            self.additional_change_occurred = True
+
+    def update_image(self, immediate=False):
+        if(not immediate):
+            time.sleep(0.5)
+        self.additional_change_occurred = False
+        GLib.idle_add(self.start_work)
+        self.image = numpy.copy(self.original_image)
+        self.image = self.run_stack(self.image)
+        if(self.additional_change_occurred):
+            self.update_image()
+        else:
+            self.save_image_data()
+            self.draw_hist(self.image)
+            self.process_peaks()
+            self.update_preview()
+            GLib.idle_add(self.stop_work)
+            self.change_occurred = False
+            self.undoing = False
+
+
+
+    def run_stack(self, image, callback=None):
+        if(not self.running_stack):
+            self.running_stack = True
+            ntools = len(self.tools)
+            count = 1
+            for tool in self.tools:
+                if(callback != None):
+                    # For showing progress
+                    callback(tool.name, ntools, count-1)
+                image = tool.on_update(image)
+                count += 1
+            self.running_stack = False
+            return image
+
+        else:
+            while(self.running_stack):
+                time.sleep(1)
+
+            return self.run_stack(image, callback)
+
+    def update_preview(self):
+        fname = "/tmp/phf2-preview-%s.png" % getpass.getuser()
+        if(os.path.exists(fname)):
+            os.unlink(fname)
+        cv2.imwrite(fname, self.image)
+        self.pimage = GdkPixbuf.Pixbuf.new_from_file(fname)
+        GLib.idle_add(self.show_current)
+
+    def draw_hist(self, image):
+        path = "/tmp/phf2-hist-%s.png" % getpass.getuser()
+        Histogram.Histogram.draw_hist(image, path)
+        GLib.idle_add(self.update_hist_ui, path)
+
+    def update_hist_ui(self, path):
+        try:
+            self.ui["histogram"].set_from_pixbuf(GdkPixbuf.Pixbuf.new_from_file(path))
+        except:
+            pass
+
+
+    def process_peaks(self, do_update=False):
+        bpp = float(str(self.image.dtype).replace("uint", "").replace("float", ""))
+        if(self.upper_peak_on):
+            self.image[(self.image == 2 ** bpp - 1).all(axis=2)] = 1
+        if(self.lower_peak_on):
+            self.image[(self.image == 0).all(axis=2)] = 2 ** bpp - 2
+
+        if(do_update):
+            self.update_preview()
+
+
+    ## FILE STUFF ##
+
+    def get_data_path(self):
+        return "%s/.%s.pf2" % ("/".join(self.image_path.split("/")[:-1]), self.image_path.split("/")[-1:][0])
+
+    def save_image_data(self):
+        path = self.get_data_path()
+        print(path)
+        f = open(path, "w")
+
+        toolDict = {}
+        for tool in self.tools:
+            toolDict[tool.id] = tool.get_properties_as_dict()
+
+        if(not self.undoing) and (self.has_loaded):
+            if(len(self.undo_stack)-1 != self.undo_position):
+                self.undo_stack = self.undo_stack[:self.undo_position+1]
+            if(self.undo_stack[self.undo_position] != toolDict):
+                self.undo_stack += [toolDict,]
+                self.undo_position = len(self.undo_stack)-1
+                GLib.idle_add(self.update_undo_state)
+
+        data = {
+            "path":self.image_path,
+            "format-revision":1,
+            "layers": {
+                "base":{
+                    "tools": toolDict
+                }
+            }
+        }
+
+        f.write(str(data))
+        f.close()
+
+    def load_image_data(self):
+        path = self.get_data_path()
+        loadDefaults = True
+        if(os.path.exists(path)):
+            f = open(path, 'r')
+            sdata = f.read()
+            try:
+                data = ast.literal_eval(sdata)
+                if(data["format-revision"] == 1):
+                    for tool in self.tools:
+                        if(tool.id in data["layers"]["base"]["tools"]):
+                            GLib.idle_add(tool.load_properties,data["layers"]["base"]["tools"][tool.id])
+
+                    self.undo_stack = [data["layers"]["base"]["tools"],]
+                    self.undo_position = 0
+                    loadDefaults = False
+            except:
+                GLib.idle_add(self.show_message,"Unable to load previous edits…",
+                                                "The edit file for this photo is corrupted and could not be loaded.")
+
+        if(loadDefaults):
+            for tool in self.tools:
+                GLib.idle_add(tool.reset)
+
+            toolDict = {}
+            for tool in self.tools:
+                toolDict[tool.id] = tool.get_properties_as_dict()
+
+            self.undo_stack = [toolDict, ]
+            self.undo_position = 0
+
+        GLib.idle_add(self.update_undo_state)
+        time.sleep(2)
+        self.undoing = False
+
+
+    def update_from_undo_stack(self, data):
+        self.undoing = True
+        for tool in self.tools:
+            if (tool.id in data):
+                tool.load_properties(data[tool.id])
+
+    def get_export_image(self, w, h):
+        GLib.idle_add(self.on_export_started)
+        GLib.idle_add(self.show_message, "Exporting Photo", "Please wait…", True, True)
+        img = cv2.imread(self.image_path, 2 | 1)
+        img = cv2.resize(img, (int(w), int(h)), interpolation=cv2.INTER_AREA)
+        img = self.run_stack(img, self.export_progress_callback)
+        GLib.idle_add(self.show_message, "Exporting Photo", "Saving to filesystem…", True, True)
+        GLib.idle_add(self.update_message_progress, 1, 1)
+        return img
+
+    def export_progress_callback(self, name, count, current):
+        GLib.idle_add(self.show_message, "Exporting Photo", "Processing: %s" % name, True, True)
+        GLib.idle_add(self.update_message_progress, current, count)
+
+
+
+
+
+
+

+ 185 - 0
PhotoFiddle.py

@@ -0,0 +1,185 @@
+#!/usr/bin/python3
+from gi import require_version
+
+import FocusStack
+import HDR
+import LightStack
+
+require_version('Gtk', '3.0')
+from gi.repository import GLib, Gtk, Gio
+import sys
+import PF2
+
+
+# MENU UI FILE
+MENU_FILE = "ui/menu.ui"
+# UI FILE
+UI_FILE = "ui/PhotoFiddle.glade"
+
+Gtk.Settings.get_default().set_property("gtk_application_prefer_dark_theme", True)
+
+
+class App(Gtk.Application):
+    def __init__(self):
+        Gtk.Application.__init__(self,
+                                 application_id="com.pcthingz.photofiddle2",
+                                 flags=Gio.ApplicationFlags.FLAGS_NONE)
+
+        self.connect("activate", self.activateCb)
+
+    def do_startup(self):
+        Gtk.Application.do_startup(self)
+
+        self.builder = Gtk.Builder()
+        self.builder.add_from_file(UI_FILE)
+
+        # TODO: Menu
+
+        self.builder.add_from_file(MENU_FILE)
+
+        action = Gio.SimpleAction.new("about", None)
+        action.connect("activate", self.on_about)
+        self.add_action(action)
+
+        action = Gio.SimpleAction.new("quit", None)
+        action.connect("activate", self.quit)
+        self.add_action(action)
+
+        self.set_app_menu(self.builder.get_object("app-menu"))
+
+    def activateCb(self, app):
+
+        self.builder = Gtk.Builder()
+        self.builder.add_from_file(UI_FILE)
+        self.builder.connect_signals(self)
+
+        self.window = self.builder.get_object('window')
+        self.window.set_wmclass("PhotoFiddle", "PhotoFiddle")
+        self.window.set_titlebar(self.builder.get_object('header_bar'))
+        app.add_window(self.window)
+
+        self.stack = self.builder.get_object('stack')
+        self.stack.add_titled(self.builder.get_object('initial_box'), "init", "PhotoFiddle")
+
+        self.header_stack = self.builder.get_object('header_stack')
+        self.no_header = self.builder.get_object('initial_header')
+        self.header_stack.add_titled(self.no_header, "noh", "No Header")
+
+        spinner = self.builder.get_object('spinner')
+
+        # Initialse Activities
+        args = (self.stack, self.header_stack, self.builder, self.window, self.show_message, self.hide_message,
+                self.update_message_progress, spinner.start, spinner.stop, self.switch_activity_id)
+
+        self.activities = [PF2.PF2(*args), FocusStack.FocusStack(*args), HDR.HDR(*args), LightStack.LightStack(*args)]
+
+        self.header_stack.set_visible_child_name("noh")
+
+        self.window.show_all()
+        self.window.maximize()
+
+        self.add_activities()
+
+    activity_map = {}
+
+    def add_activities(self):
+        activity_list = self.builder.get_object('activity_list')
+        for activity in self.activities:
+            # Add activities to the man menu
+            item = Gtk.VBox()
+            item.set_margin_left(18)
+            item.set_margin_right(18)
+            item.set_margin_top(6)
+            item.set_margin_bottom(6)
+
+            title = Gtk.Label()
+            title.set_markup("<b>%s</b>" % activity.name)
+            title.set_halign(Gtk.Align.START)
+            item.add(title)
+            subtitle = Gtk.Label()
+            subtitle.set_markup("<i>%s</i>" % activity.subtitle)
+            subtitle.set_halign(Gtk.Align.START)
+            item.add(subtitle)
+
+            item.show()
+            title.show()
+            subtitle.show()
+            activity_list.add(item)
+            self.activity_map[item.get_parent()] = activity
+
+    current_activity = None
+
+    def activity_selected(self, sender, row):
+        activity = self.activity_map[row]
+        self.switch_activity(activity)
+
+    def switch_activity(self, activity, path=None):
+        self.current_activity = activity
+        self.stack.set_visible_child(activity.widget)
+        if(activity.header_widget != None):
+            self.header_stack.set_visible_child(activity.header_widget)
+
+        back = self.builder.get_object("back_button")
+        back.set_sensitive(True)
+
+        menu = self.builder.get_object("menu_button")
+        menu.set_popover(activity.menu_popover)
+
+        activity.on_open(path)
+
+
+    def go_back(self, sender):
+        if(self.current_activity.on_exit()):
+            self.hide_message()
+            back = self.builder.get_object("back_button")
+            back.set_sensitive(False)
+
+            menu = self.builder.get_object("menu_button")
+            menu.set_popover(None)
+
+            self.header_stack.set_visible_child(self.no_header)
+            self.stack.set_visible_child_name("init")
+
+    def quit(self, sender, item):
+        self.window.close()
+
+    def on_about(self, sender, item):
+        self.builder.get_object('about_dialog').run()
+        self.builder.get_object('about_dialog').hide()
+
+    def show_message(self, title, subtitle, ongoing=False, progressive=False):
+        self.builder.get_object('info_title').set_text(title)
+        self.builder.get_object('info_subtitle').set_text(subtitle)
+        if(ongoing):
+            self.builder.get_object('info_spinner').start()
+        else:
+            self.builder.get_object('info_spinner').stop()
+        self.builder.get_object('info_progress').set_visible(progressive)
+        self.builder.get_object('info_reveal').set_reveal_child(True)
+        self.builder.get_object('info_bar').set_show_close_button(not ongoing)
+
+    def hide_message(self):
+        self.builder.get_object('info_reveal').set_reveal_child(False)
+
+    def update_message_progress(self, step, steps):
+        self.builder.get_object('info_progress').set_fraction(float(step)/float(steps))
+
+    def on_message_close(self, sender, arg):
+        self.hide_message()
+
+    def switch_activity_id(self, activity_id, file=None):
+        self.current_activity.on_exit()
+        for activity in self.activities:
+            if(activity.id == activity_id):
+                self.switch_activity(activity, file)
+                return True
+
+        return False
+
+
+
+
+## MAIN ##
+if __name__ == '__main__':
+     app = App()
+     app.run(sys.argv)

+ 336 - 0
Tool/__init__.py

@@ -0,0 +1,336 @@
+from gi.repository import GLib, Gtk
+
+class Tool:
+    def __init__(self):
+        self.id = ""
+        self.name = ""
+        self.properties = []
+        self.icon_path = ""
+        self.on_change_callback = None
+
+        # Let the tool set values
+        self.on_init()
+
+        self.props = {}
+        for property in self.properties:
+            self.props[property.id] = property
+
+        # Create widget for tool
+
+        self.widget = Gtk.Grid()
+        self.widget.set_column_spacing(8)
+        self.widget.set_row_spacing(6)
+        self.widget.set_halign(Gtk.Align.FILL)
+        self.widget.set_hexpand(True)
+
+        vpos = 0
+        for property in self.properties:
+            # Create Header
+            if(property.type == "Header"):
+                header = Gtk.HBox()
+                header.set_margin_top(6)
+
+                if(vpos != 0):
+                    # Add a Separator
+                    separator = Gtk.Separator()
+                    separator.set_margin_top(6)
+                    separator.show()
+                    self.widget.attach(separator, 0, vpos, 3, 1)
+                    vpos += 1
+
+                title = Gtk.Label()
+                title.set_halign(Gtk.Align.START)
+                if(property.is_subheading):
+                    title.set_markup("<i>%s</i>" % property.name)
+                else:
+                    title.set_markup("<b>%s</b>" % property.name)
+
+                header.add(title)
+
+                if(property.has_toggle):
+                    toggle = Gtk.Switch()
+                    toggle.set_halign(Gtk.Align.END)
+                    header.add(toggle)
+                    property.on_toggle_callback = self.on_toggled
+                    property.set_widget(toggle)
+
+                elif(property.has_button):
+                    button = Gtk.Button()
+                    button.set_halign(Gtk.Align.END)
+                    button.set_label(property.button_label)
+                    header.add(button)
+                    property.set_widget(button)
+
+                self.widget.attach(header, 0, vpos, 3, 1)
+
+            # Create Combo
+            elif(property.type == "Combo"):
+                label = Gtk.Label()
+                label.set_halign(Gtk.Align.END)
+                label.set_justify(Gtk.Justification.RIGHT)
+                label.set_text(property.name)
+                self.widget.attach(label, 0, vpos, 1, 1)
+
+                combo = Gtk.ComboBoxText()
+                combo.set_hexpand(True)
+                self.widget.attach(combo, 1, vpos, 1, 1)
+                for option in property.options:
+                    combo.append_text(option)
+
+                property.set_widget(combo)
+
+
+            # Create Spin
+            elif (property.type == "Spin"):
+                label = Gtk.Label()
+                label.set_halign(Gtk.Align.END)
+                label.set_justify(Gtk.Justification.RIGHT)
+                label.set_text(property.name)
+                self.widget.attach(label, 0, vpos, 1, 1)
+
+                adjustment = Gtk.Adjustment()
+                adjustment.set_lower(property.min)
+                adjustment.set_upper(property.max)
+                adjustment.set_step_increment((property.max - property.min) / 100)
+                property.set_widget(adjustment)
+
+                spin = Gtk.SpinButton()
+                spin.set_adjustment(adjustment)
+                spin.set_hexpand(True)
+                spin.set_digits(3)
+                property.ui_widget = spin
+
+                self.widget.attach(spin, 1, vpos, 1, 1)
+
+            # Create Toggle
+            elif(property.type == "Toggle"):
+                label = Gtk.Label()
+                label.set_halign(Gtk.Align.END)
+                label.set_justify(Gtk.Justification.RIGHT)
+                label.set_text(property.name)
+                self.widget.attach(label, 0, vpos, 1, 1)
+
+                toggle = Gtk.ToggleButton()
+                toggle.set_label("Enable")
+                toggle.set_hexpand(True)
+                self.widget.attach(toggle, 1, vpos, 1, 1)
+
+                property.set_widget(toggle)
+
+            # Create Slider
+            elif (property.type == "Slider"):
+                label = Gtk.Label()
+                label.set_halign(Gtk.Align.END)
+                label.set_justify(Gtk.Justification.RIGHT)
+                label.set_text(property.name)
+                self.widget.attach(label, 0, vpos, 1, 1)
+
+                adjustment = Gtk.Adjustment()
+                adjustment.set_lower(property.min)
+                adjustment.set_upper(property.max)
+                adjustment.set_step_increment((property.max - property.min)/100)
+                property.set_widget(adjustment)
+
+                slider = Gtk.Scale()
+                slider.set_adjustment(adjustment)
+                slider.set_hexpand(True)
+                slider.set_value_pos(Gtk.PositionType.RIGHT)
+                property.ui_widget = slider
+
+                self.widget.attach(slider, 1, vpos, 1, 1)
+
+            if(property.type != "Header"):
+                # Create reset button
+                icon = Gtk.Image()
+                icon.set_from_icon_name("edit-clear-symbolic", Gtk.IconSize.BUTTON)
+                reset_button = Gtk.Button()
+                reset_button.set_image(icon)
+                reset_button.connect("clicked", property.reset_value)
+                property.reset_button = reset_button
+                self.widget.attach(reset_button, 2, vpos, 1, 1)
+
+            # Connect on change
+            property.connect_on_change(self.on_change)
+            vpos += 1
+
+        for property in self.properties:
+            if(property.type == "Header") and (property.has_toggle):
+                self.on_toggled(property, property.widget.get_active())
+
+        separator = Gtk.Separator()
+        separator.set_margin_top(6)
+        separator.show()
+        self.widget.attach(separator, 0, vpos, 3, 1)
+
+        self.widget.show_all()
+
+        self.tool_button = Gtk.ToggleButton()
+        self.tool_button.set_tooltip_text(self.name)
+        icon = Gtk.Image()
+        icon.set_from_file(self.icon_path)
+        self.tool_button.set_image(icon)
+
+
+    def on_toggled(self, sender, value):
+        si = self.properties.index(sender)
+        for property in self.properties[si+1:]:
+            if(property.type == "Header") and (property.has_toggle):
+                break
+            else:
+                property.set_enabled(value)
+
+    def is_default(self):
+        res = True
+        for prop in self.properties:
+            if(prop.get_value() != prop.default):
+                res = False
+                break
+
+        return res
+
+    def connect_on_change(self, callback):
+        self.on_change_callback = callback
+
+    def on_change(self, sender):
+        if(self.on_change_callback != None):
+            self.on_change_callback(self, sender)
+
+
+    def on_init(self):
+        raise NotImplementedError()
+
+    def on_button_pressed(self):
+        raise NotImplementedError()
+
+    def on_update(self, image):
+        raise NotImplementedError()
+
+    def load_properties(self, dict):
+        for key in dict:
+            if(key in self.props):
+                self.props[key].set_value(dict[key])
+
+    def get_properties_as_dict(self):
+        dict = {}
+        for prop in self.properties:
+            dict[prop.id] = prop.get_value()
+        return dict
+
+    def reset(self):
+        for prop in self.properties:
+            prop.reset_value()
+
+
+
+class Property:
+    def __init__(self, id, name, type, default, **kwargs):
+        self.id = id
+        self.name = name
+        self.type = type
+        self.on_change_callback = None
+        # Types Include:
+        #   Slider,
+        #   Toggle,
+        #   Spin,
+        #   Combo,
+        #   Header
+
+        if(self.type == "Slider") or (self.type == "Spin"):
+            # Slider and Spinner
+            self.max = kwargs["max"]
+            self.min = kwargs["min"]
+            self.ui_widget = None
+
+        if(self.type == "Combo"):
+            self.options = kwargs["options"]
+
+        if(self.type == "Header"):
+            self.has_toggle = kwargs["has_toggle"]
+            self.has_button = kwargs["has_button"]
+            if(self.has_button):
+                self.button_callback = kwargs["button_callback"]
+                self.button_label = kwargs["button_label"]
+
+            if("is_subheading" in kwargs):
+                self.is_subheading = kwargs["is_subheading"]
+            else:
+                self.is_subheading = False
+
+        self.value = default
+        self.default = default
+
+        self.widget = None
+        self.reset_button = None
+        self.on_toggle_callback = None
+
+    def get_value(self):
+        return self.value
+
+    def set_value(self, value):
+        self.value = value
+        if(self.type == "Header") and (self.has_toggle):
+            self.widget.set_active(value)
+
+        if(self.type == "Slider") or (self.type == "Spin"):
+            self.widget.set_value(value)
+
+        if(self.type == "Toggle") or (self.type == "Combo"):
+            self.widget.set_active(value)
+
+        self.on_change()
+
+    def reset_value(self, sender=None):
+        self.set_value(self.default)
+        return self.default
+
+    def update_value(self, sender, arg=None):
+        if(self.type == "Header") and (self.has_toggle):
+            self.set_value(arg)
+            if(self.on_toggle_callback != None):
+                self.on_toggle_callback(self, arg)
+
+        if(self.type == "Header") and (self.has_button):
+            self.button_callback()
+
+        if(self.type == "Slider") or (self.type == "Spin"):
+            self.set_value(sender.get_value())
+
+        if(self.type == "Toggle") or (self.type == "Combo"):
+            self.set_value(sender.get_active())
+
+    def set_widget(self, object):
+        self.widget = object
+        self.reset_value()
+
+        if(self.type == "Header") and (self.has_toggle):
+            object.connect("state-set", self.update_value)
+
+        if(self.type == "Header") and (self.has_button):
+            object.connect("clicked", self.update_value)
+
+        if(self.type == "Slider") or (self.type == "Spin"):
+            object.connect("value-changed", self.update_value)
+
+        if(self.type == "Toggle"):
+            object.connect("toggled", self.update_value)
+
+        if(self.type == "Combo"):
+            object.connect("changed", self.update_value)
+
+    def set_enabled(self, enabled):
+        if(self.type == "Slider") or (self.type == "Spin"):
+            self.ui_widget.set_sensitive(enabled)
+        elif(self.widget != None):
+            self.widget.set_sensitive(enabled)
+
+        if(self.reset_button != None):
+            self.reset_button.set_sensitive(enabled)
+
+    def connect_on_change(self, callback):
+        self.on_change_callback = callback
+
+    def on_change(self):
+        if(self.on_change_callback != None):
+            self.on_change_callback(self)
+
+

+ 16 - 0
Tool/test.py

@@ -0,0 +1,16 @@
+import Tool
+
+class Test_Tool(Tool.Tool):
+    def on_init(self):
+        self.id = "TestTool"
+        self.name = "Test Tool"
+        self.icon_path = "ui/testtool.png"
+        self.properties = [
+            Tool.Property("header", "Test Tool", "Header", None, has_toggle=False, has_button=True, button_callback=None, button_label="Auto Contrast"),
+            Tool.Property("value", "Value", "Slider", 50, max=100, min=0),
+            Tool.Property("header", "Test Tool 2", "Header", False, has_toggle=True, has_button=False),
+            Tool.Property("value", "Value2", "Spin", 50, max=100, min=0),
+            Tool.Property("header", "Test Tool 2", "Header", None, has_toggle=False, has_button=False),
+            Tool.Property("value", "Value3", "Toggle", True),
+            Tool.Property("value", "Long Label", "Combo", 0, options=["Option1", "Option2", "Option3"])
+        ]

BIN
icon-texture.png


+ 92 - 0
icon.svg

@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="256px"
+   height="256px"
+   viewBox="0 0 256 256"
+   version="1.1"
+   id="SVGRoot"
+   inkscape:version="0.92.1 r"
+   sodipodi:docname="icon.svg">
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="426.78021"
+     inkscape:cy="165.17339"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1016"
+     inkscape:window-x="0"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:grid-bbox="true"
+     showguides="true"
+     inkscape:guide-bbox="true" />
+  <defs
+     id="defs5036" />
+  <metadata
+     id="metadata5039">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1"
+     inkscape:groupmode="layer"
+     inkscape:label="Layer 1">
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:24.04902077;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
+       d="M 22.764311,50.331802 22.686554,205.6682 H 160.65219 c 0,0 72.66728,-0.53139 72.66728,-72.08441 V 50.331802 Z"
+       id="path5597"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccccc" />
+    <flowRoot
+       xml:space="preserve"
+       id="flowRoot5607"
+       style="font-style:normal;font-weight:normal;font-size:40px;line-height:25px;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill-opacity:0;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;"
+       transform="matrix(1.4250772,0,0,1.4250772,-5.0246945,26.108469)"><flowRegion
+         style="stroke:none;fill:none;"
+         id="flowRegion5609"><rect
+           style="stroke:none;none;"
+           id="rect5611"
+           width="172"
+           height="201.31712"
+           x="36"
+           y="34" /></flowRegion><flowPara
+         id="flowPara5613" /></flowRoot>    <g
+       aria-label="PF"
+       transform="matrix(4.0378903,0,0,4.0378903,-84.791196,-3.600463)"
+       style="font-style:normal;font-weight:normal;font-size:40px;line-height:25px;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       id="flowRoot5615">
+      <path
+         d="m 38.85,39.880625 c 7.4,0 13.44,-1.48 13.44,-8.72 0,-5.48 -3.84,-8.2 -11.48,-8.2 h -9.16 v 27.68 h 4.92 v -10.76 z m 8.4,-8.52 c 0,3.72 -3.88,4 -8.2,4 h -2.48 v -7.88 h 3.72 c 3.68,0 6.96,0.48 6.96,3.88 z"
+         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Cantarell;-inkscape-font-specification:'Cantarell Bold';letter-spacing:-2.18354273px;fill:#000000;fill-opacity:1;stroke:none"
+         id="path5676" />
+      <path
+         d="m 59.458333,27.480625 h 13 v -4.52 h -17.92 v 27.68 h 4.92 v -10.52 h 11.4 v -4.52 h -11.4 z"
+         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Cantarell;-inkscape-font-specification:'Cantarell Bold';letter-spacing:-2.18354273px;fill:#000000;fill-opacity:1;stroke:none"
+         id="path5678" />
+    </g>
+  </g>
+</svg>

+ 288 - 0
ui/Export.glade

@@ -0,0 +1,288 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <object class="GtkHeaderBar" id="Export_headerbar">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="title">Export Photo</property>
+    <child>
+      <object class="GtkButton" id="Export_cancel_button">
+        <property name="label">gtk-cancel</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="use_stock">True</property>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton" id="Export_save_button">
+        <property name="label">gtk-save</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="use_stock">True</property>
+        <style>
+          <class name="suggested-action"/>
+        </style>
+      </object>
+      <packing>
+        <property name="pack_type">end</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkAdjustment" id="Export_height">
+    <property name="upper">100</property>
+    <property name="step_increment">1</property>
+    <property name="page_increment">10</property>
+  </object>
+  <object class="GtkAdjustment" id="Export_quality">
+    <property name="upper">100</property>
+    <property name="step_increment">1</property>
+    <property name="page_increment">10</property>
+  </object>
+  <object class="GtkAdjustment" id="Export_width">
+    <property name="upper">100</property>
+    <property name="step_increment">1</property>
+    <property name="page_increment">10</property>
+  </object>
+  <object class="GtkWindow" id="Export_window">
+    <property name="width_request">750</property>
+    <property name="can_focus">False</property>
+    <property name="modal">True</property>
+    <property name="type_hint">dialog</property>
+    <property name="deletable">False</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkFileChooserWidget" id="Export_file">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="action">save</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkExpander">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="margin_left">18</property>
+            <property name="margin_right">18</property>
+            <property name="margin_top">18</property>
+            <property name="margin_bottom">18</property>
+            <child>
+              <object class="GtkBox">
+                <property name="name">Watermark</property>
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="valign">start</property>
+                <property name="margin_top">6</property>
+                <property name="margin_bottom">18</property>
+                <property name="vexpand">False</property>
+                <property name="orientation">vertical</property>
+                <property name="spacing">6</property>
+                <child>
+                  <object class="GtkGrid">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="halign">center</property>
+                    <property name="hexpand">True</property>
+                    <property name="row_spacing">6</property>
+                    <property name="column_spacing">12</property>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="halign">end</property>
+                        <property name="label" translatable="yes">Preset</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="halign">end</property>
+                        <property name="label" translatable="yes">Format</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="halign">end</property>
+                        <property name="label" translatable="yes">Width</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">2</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="halign">end</property>
+                        <property name="label" translatable="yes">Height</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">3</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkComboBoxText" id="Export_preset">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="active">4</property>
+                        <items>
+                          <item translatable="yes">Custom Settings</item>
+                          <item translatable="yes">Export For Facebook</item>
+                          <item translatable="yes">Export For Flickr</item>
+                          <item translatable="yes">Export For Direct Web Publishing</item>
+                          <item translatable="yes">Export For Printing</item>
+                        </items>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkSpinButton" id="Export_width_spin">
+                        <property name="visible">True</property>
+                        <property name="sensitive">False</property>
+                        <property name="can_focus">True</property>
+                        <property name="text" translatable="yes">0</property>
+                        <property name="input_purpose">digits</property>
+                        <property name="adjustment">Export_width</property>
+                        <property name="climb_rate">1</property>
+                        <property name="snap_to_ticks">True</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">2</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkSpinButton" id="Export_height_spin">
+                        <property name="visible">True</property>
+                        <property name="sensitive">False</property>
+                        <property name="can_focus">True</property>
+                        <property name="text" translatable="yes">0</property>
+                        <property name="adjustment">Export_height</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">3</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkComboBoxText" id="Export_format">
+                        <property name="visible">True</property>
+                        <property name="sensitive">False</property>
+                        <property name="can_focus">False</property>
+                        <property name="active">2</property>
+                        <items>
+                          <item translatable="yes">PNG</item>
+                          <item translatable="yes">JPEG</item>
+                          <item translatable="yes">TIFF</item>
+                        </items>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkSpinButton" id="Export_quality_spin">
+                        <property name="visible">True</property>
+                        <property name="sensitive">False</property>
+                        <property name="can_focus">True</property>
+                        <property name="text" translatable="yes">90</property>
+                        <property name="adjustment">Export_quality</property>
+                        <property name="value">90</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">4</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="halign">end</property>
+                        <property name="label" translatable="yes">JPEG Quality</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">4</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkSwitch" id="Export_pngcrush">
+                        <property name="visible">True</property>
+                        <property name="sensitive">False</property>
+                        <property name="can_focus">True</property>
+                        <property name="halign">start</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">5</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="halign">end</property>
+                        <property name="label" translatable="yes">PNG Crush</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">5</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">False</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+            </child>
+            <child type="label">
+              <object class="GtkLabel">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">Save Options</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>

+ 342 - 0
ui/FocusStack_Activity.glade

@@ -0,0 +1,342 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <object class="GtkBox" id="FocusStack_header">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="spacing">6</property>
+    <child>
+      <object class="GtkButton" id="FocusStack_preview_button">
+        <property name="label" translatable="yes">Generate Preview</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+  </object>
+  <object class="GtkBox" id="FocusStack_main">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="margin_top">1</property>
+    <child>
+      <object class="GtkScrolledWindow" id="FocusStack_scroll_window">
+        <property name="width_request">600</property>
+        <property name="height_request">500</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="margin_left">18</property>
+        <property name="margin_right">18</property>
+        <property name="margin_top">18</property>
+        <property name="margin_bottom">18</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <property name="shadow_type">in</property>
+        <child>
+          <object class="GtkViewport">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkImage" id="FocusStack_preview">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="hexpand">True</property>
+                <property name="pixbuf">logo.png</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkRevealer">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">end</property>
+        <property name="hexpand">False</property>
+        <property name="transition_type">slide-left</property>
+        <property name="reveal_child">True</property>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <object class="GtkSeparator">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_left">6</property>
+                <property name="orientation">vertical</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <child>
+                  <object class="GtkBox">
+                    <property name="width_request">338</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="margin_left">18</property>
+                    <property name="margin_right">18</property>
+                    <property name="margin_top">18</property>
+                    <property name="margin_bottom">18</property>
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">6</property>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">6</property>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="halign">start</property>
+                            <property name="label" translatable="yes">&lt;b&gt;Images&lt;/b&gt;</property>
+                            <property name="use_markup">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkScrolledWindow">
+                            <property name="height_request">150</property>
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="hscrollbar_policy">never</property>
+                            <property name="shadow_type">in</property>
+                            <child>
+                              <object class="GtkViewport">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <child>
+                                  <object class="GtkListBox" id="FocusStack_images">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                  </object>
+                                </child>
+                              </object>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">True</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkButtonBox">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="spacing">6</property>
+                            <property name="layout_style">end</property>
+                            <child>
+                              <object class="GtkButton" id="FocusStack_add_button">
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="receives_default">True</property>
+                                <child>
+                                  <object class="GtkImage">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="icon_name">list-add-symbolic</property>
+                                  </object>
+                                </child>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">False</property>
+                                <property name="position">0</property>
+                                <property name="non_homogeneous">True</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkButton" id="FocusStack_remove_button">
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="receives_default">True</property>
+                                <child>
+                                  <object class="GtkImage">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="icon_name">list-remove-symbolic</property>
+                                  </object>
+                                </child>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">False</property>
+                                <property name="position">1</property>
+                                <property name="non_homogeneous">True</property>
+                              </packing>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">False</property>
+                            <property name="position">2</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <placeholder/>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">True</property>
+                        <property name="fill">True</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">2</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkHeaderBar" id="FocusStack_open_header">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="title">Open Photo</property>
+    <child>
+      <object class="GtkButton" id="FocusStack_open_cancel_button">
+        <property name="label">gtk-cancel</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="use_stock">True</property>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton" id="FocusStack_open_open_button">
+        <property name="label">gtk-add</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="use_stock">True</property>
+        <style>
+          <class name="suggested-action"/>
+        </style>
+      </object>
+      <packing>
+        <property name="pack_type">end</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkPopoverMenu" id="FocusStack_popovermenu">
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_left">6</property>
+        <property name="margin_right">6</property>
+        <property name="margin_top">6</property>
+        <property name="margin_bottom">6</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkModelButton" id="FocusStack_export_image">
+            <property name="visible">True</property>
+            <property name="sensitive">False</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="text" translatable="yes">Export Image</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="FocusStack_open_pf2">
+            <property name="visible">True</property>
+            <property name="sensitive">False</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="text" translatable="yes">Send to PhotoFiddle Editor</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="submenu">main</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkFileFilter" id="PF2_filefilter">
+    <mime-types>
+      <mime-type>image/jpeg</mime-type>
+      <mime-type>image/bmp</mime-type>
+      <mime-type>image/png</mime-type>
+      <mime-type>image/tiff</mime-type>
+      <mime-type>image/x-tiff</mime-type>
+    </mime-types>
+  </object>
+  <object class="GtkWindow" id="FocusStack_open_window">
+    <property name="can_focus">False</property>
+    <property name="modal">True</property>
+    <property name="type_hint">dialog</property>
+    <property name="deletable">False</property>
+    <child>
+      <object class="GtkFileChooserWidget" id="FocusStack_open_chooser">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="filter">PF2_filefilter</property>
+        <property name="select_multiple">True</property>
+      </object>
+    </child>
+  </object>
+</interface>

+ 449 - 0
ui/HDR_Activity.glade

@@ -0,0 +1,449 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <object class="GtkFileFilter" id="HDR_filefilter">
+    <mime-types>
+      <mime-type>image/jpeg</mime-type>
+      <mime-type>image/bmp</mime-type>
+      <mime-type>image/png</mime-type>
+      <mime-type>image/tiff</mime-type>
+      <mime-type>image/x-tiff</mime-type>
+    </mime-types>
+  </object>
+  <object class="GtkWindow" id="HDR_open_window">
+    <property name="can_focus">False</property>
+    <property name="modal">True</property>
+    <property name="type_hint">dialog</property>
+    <property name="deletable">False</property>
+    <child>
+      <object class="GtkFileChooserWidget" id="HDR_open_chooser">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="filter">HDR_filefilter</property>
+        <property name="select_multiple">True</property>
+      </object>
+    </child>
+  </object>
+  <object class="GtkBox" id="HDR_header">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="spacing">6</property>
+    <child>
+      <object class="GtkButton" id="HDR_preview_button">
+        <property name="label" translatable="yes">Generate Preview</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+  </object>
+  <object class="GtkBox" id="HDR_main">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="margin_top">1</property>
+    <child>
+      <object class="GtkScrolledWindow" id="HDR_scroll_window">
+        <property name="width_request">600</property>
+        <property name="height_request">500</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="margin_left">18</property>
+        <property name="margin_right">18</property>
+        <property name="margin_top">18</property>
+        <property name="margin_bottom">18</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <property name="shadow_type">in</property>
+        <child>
+          <object class="GtkViewport">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkImage" id="HDR_preview">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="hexpand">True</property>
+                <property name="pixbuf">logo.png</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkRevealer">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">end</property>
+        <property name="hexpand">False</property>
+        <property name="transition_type">slide-left</property>
+        <property name="reveal_child">True</property>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <object class="GtkSeparator">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_left">6</property>
+                <property name="orientation">vertical</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <child>
+                  <object class="GtkBox">
+                    <property name="width_request">338</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="margin_left">18</property>
+                    <property name="margin_right">18</property>
+                    <property name="margin_top">18</property>
+                    <property name="margin_bottom">18</property>
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">6</property>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="valign">start</property>
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">6</property>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="halign">start</property>
+                            <property name="label" translatable="yes">&lt;b&gt;Settings&lt;/b&gt;</property>
+                            <property name="use_markup">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="spacing">6</property>
+                            <child>
+                              <object class="GtkLabel">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="halign">end</property>
+                                <property name="label" translatable="yes">Method</property>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkComboBoxText" id="HDR_method">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="hexpand">True</property>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">1</property>
+                              </packing>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkSeparator">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">2</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkStack" id="HDR_stack">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="vhomogeneous">False</property>
+                            <property name="transition_type">crossfade</property>
+                            <property name="interpolate_size">True</property>
+                            <child>
+                              <placeholder/>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">3</property>
+                          </packing>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">False</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">6</property>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="halign">start</property>
+                            <property name="label" translatable="yes">&lt;b&gt;Images&lt;/b&gt;</property>
+                            <property name="use_markup">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkScrolledWindow">
+                            <property name="height_request">150</property>
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="hscrollbar_policy">never</property>
+                            <property name="shadow_type">in</property>
+                            <child>
+                              <object class="GtkViewport">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <child>
+                                  <object class="GtkListBox" id="HDR_images">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                  </object>
+                                </child>
+                              </object>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">True</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkButtonBox">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="spacing">6</property>
+                            <property name="layout_style">end</property>
+                            <child>
+                              <object class="GtkButton" id="HDR_add_button">
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="receives_default">True</property>
+                                <child>
+                                  <object class="GtkImage">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="icon_name">list-add-symbolic</property>
+                                  </object>
+                                </child>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">False</property>
+                                <property name="position">0</property>
+                                <property name="non_homogeneous">True</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkButton" id="HDR_remove_button">
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="receives_default">True</property>
+                                <child>
+                                  <object class="GtkImage">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="icon_name">list-remove-symbolic</property>
+                                  </object>
+                                </child>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">False</property>
+                                <property name="position">1</property>
+                                <property name="non_homogeneous">True</property>
+                              </packing>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">False</property>
+                            <property name="position">2</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkSeparator">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">3</property>
+                          </packing>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">True</property>
+                        <property name="fill">True</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">2</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkHeaderBar" id="HDR_open_header">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="title">Open Photo</property>
+    <child>
+      <object class="GtkButton" id="HDR_open_cancel_button">
+        <property name="label">gtk-cancel</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="use_stock">True</property>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton" id="HDR_open_open_button">
+        <property name="label">gtk-add</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="use_stock">True</property>
+        <style>
+          <class name="suggested-action"/>
+        </style>
+      </object>
+      <packing>
+        <property name="pack_type">end</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkPopoverMenu" id="HDR_popovermenu">
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_left">6</property>
+        <property name="margin_right">6</property>
+        <property name="margin_top">6</property>
+        <property name="margin_bottom">6</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkModelButton" id="HDR_export_image">
+            <property name="visible">True</property>
+            <property name="sensitive">False</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="text" translatable="yes">Export Image</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="HDR_open_pf2">
+            <property name="visible">True</property>
+            <property name="sensitive">False</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="text" translatable="yes">Send to PhotoFiddle Editor</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="submenu">main</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkAdjustment" id="HDR_width">
+    <property name="upper">1</property>
+    <property name="value">0.20000000000000001</property>
+    <property name="step_increment">0.01</property>
+    <property name="page_increment">10</property>
+  </object>
+</interface>

+ 342 - 0
ui/LightStack_Activity.glade

@@ -0,0 +1,342 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <object class="GtkFileFilter" id="LightStack_filefilter">
+    <mime-types>
+      <mime-type>image/jpeg</mime-type>
+      <mime-type>image/bmp</mime-type>
+      <mime-type>image/png</mime-type>
+      <mime-type>image/tiff</mime-type>
+      <mime-type>image/x-tiff</mime-type>
+    </mime-types>
+  </object>
+  <object class="GtkWindow" id="LightStack_open_window">
+    <property name="can_focus">False</property>
+    <property name="modal">True</property>
+    <property name="type_hint">dialog</property>
+    <property name="deletable">False</property>
+    <child>
+      <object class="GtkFileChooserWidget" id="LightStack_open_chooser">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="filter">LightStack_filefilter</property>
+        <property name="select_multiple">True</property>
+      </object>
+    </child>
+  </object>
+  <object class="GtkBox" id="LightStack_header">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="spacing">6</property>
+    <child>
+      <object class="GtkButton" id="LightStack_preview_button">
+        <property name="label" translatable="yes">Generate Preview</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+  </object>
+  <object class="GtkBox" id="LightStack_main">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="margin_top">1</property>
+    <child>
+      <object class="GtkScrolledWindow" id="LightStack_scroll_window">
+        <property name="width_request">600</property>
+        <property name="height_request">500</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="margin_left">18</property>
+        <property name="margin_right">18</property>
+        <property name="margin_top">18</property>
+        <property name="margin_bottom">18</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <property name="shadow_type">in</property>
+        <child>
+          <object class="GtkViewport">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkImage" id="LightStack_preview">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="hexpand">True</property>
+                <property name="pixbuf">logo.png</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkRevealer">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">end</property>
+        <property name="hexpand">False</property>
+        <property name="transition_type">slide-left</property>
+        <property name="reveal_child">True</property>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <object class="GtkSeparator">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_left">6</property>
+                <property name="orientation">vertical</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <child>
+                  <object class="GtkBox">
+                    <property name="width_request">338</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="margin_left">18</property>
+                    <property name="margin_right">18</property>
+                    <property name="margin_top">18</property>
+                    <property name="margin_bottom">18</property>
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">6</property>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">6</property>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="halign">start</property>
+                            <property name="label" translatable="yes">&lt;b&gt;Images&lt;/b&gt;</property>
+                            <property name="use_markup">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkScrolledWindow">
+                            <property name="height_request">150</property>
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="hscrollbar_policy">never</property>
+                            <property name="shadow_type">in</property>
+                            <child>
+                              <object class="GtkViewport">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <child>
+                                  <object class="GtkListBox" id="LightStack_images">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                  </object>
+                                </child>
+                              </object>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">True</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkButtonBox">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="spacing">6</property>
+                            <property name="layout_style">end</property>
+                            <child>
+                              <object class="GtkButton" id="LightStack_add_button">
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="receives_default">True</property>
+                                <child>
+                                  <object class="GtkImage">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="icon_name">list-add-symbolic</property>
+                                  </object>
+                                </child>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">False</property>
+                                <property name="position">0</property>
+                                <property name="non_homogeneous">True</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkButton" id="LightStack_remove_button">
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="receives_default">True</property>
+                                <child>
+                                  <object class="GtkImage">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="icon_name">list-remove-symbolic</property>
+                                  </object>
+                                </child>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">False</property>
+                                <property name="position">1</property>
+                                <property name="non_homogeneous">True</property>
+                              </packing>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">False</property>
+                            <property name="position">2</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <placeholder/>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">True</property>
+                        <property name="fill">True</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">2</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkHeaderBar" id="LightStack_open_header">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="title">Open Photo</property>
+    <child>
+      <object class="GtkButton" id="LightStack_open_cancel_button">
+        <property name="label">gtk-cancel</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="use_stock">True</property>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton" id="LightStack_open_open_button">
+        <property name="label">gtk-add</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="use_stock">True</property>
+        <style>
+          <class name="suggested-action"/>
+        </style>
+      </object>
+      <packing>
+        <property name="pack_type">end</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkPopoverMenu" id="LightStack_popovermenu">
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_left">6</property>
+        <property name="margin_right">6</property>
+        <property name="margin_top">6</property>
+        <property name="margin_bottom">6</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkModelButton" id="LightStack_export_image">
+            <property name="visible">True</property>
+            <property name="sensitive">False</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="text" translatable="yes">Export Image</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="LightStack_open_pf2">
+            <property name="visible">True</property>
+            <property name="sensitive">False</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="text" translatable="yes">Send to PhotoFiddle Editor</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="submenu">main</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
+</interface>

+ 743 - 0
ui/PF2_Activity.glade

@@ -0,0 +1,743 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <object class="GtkHeaderBar" id="PF2_bulk_open_header">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="title">Open Photo</property>
+    <child>
+      <object class="GtkButton" id="PF2_bulk_open_close_button">
+        <property name="label">gtk-cancel</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="use_stock">True</property>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton" id="PF2_bulk_open_export_button">
+        <property name="label">Export</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <style>
+          <class name="suggested-action"/>
+        </style>
+      </object>
+      <packing>
+        <property name="pack_type">end</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkFileFilter" id="PF2_filefilter">
+    <mime-types>
+      <mime-type>image/jpeg</mime-type>
+      <mime-type>image/bmp</mime-type>
+      <mime-type>image/png</mime-type>
+      <mime-type>image/tiff</mime-type>
+      <mime-type>image/x-tiff</mime-type>
+    </mime-types>
+  </object>
+  <object class="GtkWindow" id="PF2_bulk_open">
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="halign">start</property>
+            <property name="margin_left">18</property>
+            <property name="margin_right">18</property>
+            <property name="margin_top">8</property>
+            <property name="margin_bottom">8</property>
+            <property name="label" translatable="yes">Select the other images you would like to process with the current settings…</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkFileChooserWidget" id="PF2_bulk_open_chooser">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="filter">PF2_filefilter</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+  <object class="GtkWindow" id="PF2_open_window">
+    <property name="width_request">700</property>
+    <property name="height_request">400</property>
+    <property name="can_focus">False</property>
+    <property name="modal">True</property>
+    <property name="type_hint">dialog</property>
+    <property name="deletable">False</property>
+    <child>
+      <object class="GtkFileChooserWidget" id="PF2_open_chooser">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="filter">PF2_filefilter</property>
+      </object>
+    </child>
+  </object>
+  <object class="GtkBox" id="PF2_main">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="margin_top">1</property>
+    <child>
+      <object class="GtkScrolledWindow" id="PF2_scroll_window">
+        <property name="width_request">500</property>
+        <property name="height_request">500</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="margin_left">18</property>
+        <property name="margin_right">18</property>
+        <property name="margin_top">18</property>
+        <property name="margin_bottom">18</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <property name="shadow_type">in</property>
+        <child>
+          <object class="GtkViewport">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkImage" id="PF2_preview">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="hexpand">True</property>
+                <property name="pixbuf">logo.png</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkRevealer" id="PF2_control_reveal">
+        <property name="visible">True</property>
+        <property name="sensitive">False</property>
+        <property name="can_focus">False</property>
+        <property name="halign">end</property>
+        <property name="hexpand">False</property>
+        <property name="transition_type">slide-left</property>
+        <property name="reveal_child">True</property>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <object class="GtkSeparator">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_left">6</property>
+                <property name="orientation">vertical</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="margin_left">18</property>
+                    <property name="margin_right">18</property>
+                    <property name="margin_top">18</property>
+                    <property name="margin_bottom">18</property>
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">6</property>
+                    <child>
+                      <object class="GtkRevealer" id="PF2_histogram_reveal">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="reveal_child">True</property>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="orientation">vertical</property>
+                            <child>
+                              <object class="GtkLabel">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="halign">start</property>
+                                <property name="label" translatable="yes">&lt;b&gt;Histogram&lt;/b&gt;</property>
+                                <property name="use_markup">True</property>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkBox">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="halign">center</property>
+                                <property name="margin_top">6</property>
+                                <child>
+                                  <object class="GtkToggleButton" id="PF2_lower_peak_toggle">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">True</property>
+                                    <property name="receives_default">True</property>
+                                    <property name="halign">end</property>
+                                    <property name="margin_bottom">12</property>
+                                    <property name="hexpand">True</property>
+                                    <child>
+                                      <object class="GtkImage">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="icon_name">dialog-warning-symbolic</property>
+                                      </object>
+                                    </child>
+                                  </object>
+                                  <packing>
+                                    <property name="expand">False</property>
+                                    <property name="fill">True</property>
+                                    <property name="position">0</property>
+                                  </packing>
+                                </child>
+                                <child>
+                                  <object class="GtkImage" id="PF2_histogram">
+                                    <property name="width_request">330</property>
+                                    <property name="height_request">128</property>
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="halign">center</property>
+                                    <property name="margin_left">6</property>
+                                    <property name="margin_right">6</property>
+                                    <property name="margin_bottom">12</property>
+                                  </object>
+                                  <packing>
+                                    <property name="expand">True</property>
+                                    <property name="fill">True</property>
+                                    <property name="position">1</property>
+                                  </packing>
+                                </child>
+                                <child>
+                                  <object class="GtkToggleButton" id="PF2_upper_peak_toggle">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">True</property>
+                                    <property name="receives_default">True</property>
+                                    <property name="halign">start</property>
+                                    <property name="margin_bottom">12</property>
+                                    <property name="hexpand">False</property>
+                                    <child>
+                                      <object class="GtkImage">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="icon_name">dialog-warning-symbolic</property>
+                                      </object>
+                                    </child>
+                                  </object>
+                                  <packing>
+                                    <property name="expand">False</property>
+                                    <property name="fill">True</property>
+                                    <property name="pack_type">end</property>
+                                    <property name="position">2</property>
+                                  </packing>
+                                </child>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">1</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkSeparator">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">2</property>
+                              </packing>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkRevealer" id="PF2_layers_reveal">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="transition_type">slide-up</property>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="orientation">vertical</property>
+                            <property name="spacing">6</property>
+                            <child>
+                              <object class="GtkLabel">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="halign">start</property>
+                                <property name="label" translatable="yes">&lt;b&gt;Layers&lt;/b&gt;</property>
+                                <property name="use_markup">True</property>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkScrolledWindow">
+                                <property name="height_request">150</property>
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="hscrollbar_policy">never</property>
+                                <property name="shadow_type">in</property>
+                                <child>
+                                  <object class="GtkViewport">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <child>
+                                      <object class="GtkListBox">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                      </object>
+                                    </child>
+                                  </object>
+                                </child>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">1</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkSeparator">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                                <property name="position">2</property>
+                              </packing>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">6</property>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="halign">start</property>
+                            <property name="label" translatable="yes">&lt;b&gt;Toolbox&lt;/b&gt;</property>
+                            <property name="use_markup">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkFlowBox" id="PF2_tools">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="halign">start</property>
+                            <property name="valign">start</property>
+                            <property name="orientation">vertical</property>
+                            <property name="selection_mode">none</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkSeparator">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">2</property>
+                          </packing>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">2</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkScrolledWindow">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="hscrollbar_policy">never</property>
+                        <child>
+                          <object class="GtkViewport">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="hexpand">True</property>
+                            <property name="shadow_type">none</property>
+                            <child>
+                              <object class="GtkStack" id="PF2_tool_stack">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="hhomogeneous">False</property>
+                                <property name="vhomogeneous">False</property>
+                                <property name="transition_type">slide-left-right</property>
+                                <child>
+                                  <placeholder/>
+                                </child>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">True</property>
+                        <property name="fill">True</property>
+                        <property name="position">3</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">2</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkHeaderBar" id="PF2_open_header">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="title">Open Photo</property>
+    <child>
+      <object class="GtkButton" id="PF2_open_cancel_button">
+        <property name="label">gtk-cancel</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="use_stock">True</property>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton" id="PF2_open_open_button">
+        <property name="label">gtk-open</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="use_stock">True</property>
+        <style>
+          <class name="suggested-action"/>
+        </style>
+      </object>
+      <packing>
+        <property name="pack_type">end</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkPopoverMenu" id="PF2_popovermenu">
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_left">6</property>
+        <property name="margin_right">6</property>
+        <property name="margin_top">6</property>
+        <property name="margin_bottom">6</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkModelButton" id="PF2_export_image">
+            <property name="visible">True</property>
+            <property name="sensitive">False</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="tooltip_text" translatable="yes">Export the current image using the current settings</property>
+            <property name="text" translatable="yes">Export Image</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="PF2_export_image2">
+            <property name="sensitive">False</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="tooltip_text" translatable="yes">Exports this image, and other images using the current settings.</property>
+            <property name="text" translatable="yes">Bulk Export</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkSeparator">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkCheckButton" id="PF2_show_hist">
+            <property name="label" translatable="yes">Show Histogram</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">False</property>
+            <property name="tooltip_text" translatable="yes">Toggle the histogram</property>
+            <property name="active">True</property>
+            <property name="draw_indicator">True</property>
+            <style>
+              <class name="right"/>
+            </style>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">3</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="PF2_reset">
+            <property name="visible">True</property>
+            <property name="sensitive">False</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="tooltip_text" translatable="yes">Reset all settings for this image</property>
+            <property name="text" translatable="yes">Reset All</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">4</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="submenu">main</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
+  <object class="GtkAdjustment" id="PF2_zoom">
+    <property name="lower">1</property>
+    <property name="upper">4</property>
+    <property name="value">1</property>
+    <property name="step_increment">0.25</property>
+    <property name="page_increment">10</property>
+  </object>
+  <object class="GtkBox" id="PF2_header">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="spacing">6</property>
+    <child>
+      <object class="GtkButton" id="PF2_open_button">
+        <property name="label">gtk-open</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="tooltip_text" translatable="yes">Open a photo to edit</property>
+        <property name="use_stock">True</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkToggleButton" id="PF2_original_toggle">
+        <property name="visible">True</property>
+        <property name="sensitive">False</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="tooltip_text" translatable="yes">Toggle original image</property>
+        <child>
+          <object class="GtkImage">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="icon_name">user-not-tracked-symbolic</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton" id="PF2_undo">
+        <property name="visible">True</property>
+        <property name="sensitive">False</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="tooltip_text" translatable="yes">Undo</property>
+        <child>
+          <object class="GtkImage">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="icon_name">edit-undo-symbolic</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton" id="PF2_redo">
+        <property name="visible">True</property>
+        <property name="sensitive">False</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="tooltip_text" translatable="yes">Redo</property>
+        <child>
+          <object class="GtkImage">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="icon_name">edit-redo-symbolic</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">3</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkSeparator">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">4</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkToggleButton" id="PF2_zoom_toggle">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <child>
+          <object class="GtkImage">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="icon_name">edit-find-symbolic</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">5</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkRevealer" id="PF2_zoom_reveal">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="transition_type">slide-right</property>
+        <child>
+          <object class="GtkScale">
+            <property name="width_request">250</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="adjustment">PF2_zoom</property>
+            <property name="round_digits">1</property>
+            <property name="digits">3</property>
+            <property name="draw_value">False</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">6</property>
+      </packing>
+    </child>
+  </object>
+</interface>

BIN
ui/PF2_Icons/BlackWhite.png


BIN
ui/PF2_Icons/Colours.png


BIN
ui/PF2_Icons/Contrast.png


BIN
ui/PF2_Icons/Denoise.png


BIN
ui/PF2_Icons/Details.png


BIN
ui/PF2_Icons/HueEqualiser.png


BIN
ui/PF2_Icons/Tonemap.png


BIN
ui/logo-small.png


BIN
ui/logo.png


+ 19 - 0
ui/menu.ui

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <menu id="app-menu">
+
+    <section>
+      <item>
+        <attribute name="action">app.about</attribute>
+        <attribute name="label" translatable="yes">_About</attribute>
+      </item>
+      <item>
+        <attribute name="action">app.quit</attribute>
+        <attribute name="label" translatable="yes">_Quit</attribute>
+        <attribute name="accel">&lt;Primary&gt;q</attribute>
+    </item>
+    </section>
+  </menu>
+</interface>

BIN
ui/testtool.png