""" WImageEdit.py Wesley R. Elsberry 7419: magick {srcfn} -gravity east -chop 2x0% +repage -channel RGB -contrast-stretch 1x1% -colorspace Gray -negate -rotate 90 {destfn} 1940s_bwneg_willard_one_woman_one_car_one_tree_7419 7420 : magick {srcfn} -channel RGB -contrast-stretch 1x1% -colorspace Gray -negate -gravity south -chop 0x10% +repage {destfn} 940s_bwneg_willard_river_landscape_7420 magick {srcfn} -gravity east -chop 2x0% -gravity west -chop 5x0% -gravity north -chop 0x7% -gravity south -chop 0x7% +repage -channel RGB -contrast-stretch 0.1x0.1% -colorspace Gray -negate -rotate 0 {destfn} Notes: Bug: Next Image doesn't actually manage to do it in one click, but does work with two clicks. Resize upward: Probably should detect upsizing and use the process from the ImageMagick docs: magick input.png -colorspace RGB +sigmoidal-contrast 11.6933 \ -define filter:filter=Sinc -define filter:window=Jinc -define filter:lobes=3 \ -resize 400% -sigmoidal-contrast 11.6933 -colorspace sRGB output.png'); 2023-01-03 - Refactored event loop so that if there are more than 10 lines of code to handle an event, that gets an event handler function and call. - Except for the swapping of layouts; that apparently needs to actually happen in the event loop - Added 'actions' to set common controls based on the type of image we are handling - Color positive, color nagetive, B&W positive, B&W negative - Crop always enabled - Added 'Previous Image' button to allow backing up. 2023-01-10 - Found out how to add a background via ImageMagick - Example: magick /Volumes/mediaphotos/photos/photos/pixstar-frame-for-margaret/staging/1960s/2023_01_04_23_02_16.jpg -crop +667+199 -crop -620-199 +repage -channel RGB -contrast-stretch 0.02x0.02% -sigmoidal-contrast -4,40% -colorspace Gray -sharpen 0x1.6 -resize 1024x720 -repage +300+20 '(' +clone -background black -shadow 70x10+5+% ')' '(' granite: -sigmoidal-contrast 4,99% -crop 128x96+0+0 -resize 1024x768 +repage ')' -reverse -layers merge -colorspace Gray tempimg.jpg 2023-01-12 - Have Background (with fixed ImageMagick 'granite' image) working - Selected image is as wide or as tall as the background - Background is cropped and resized to fit the full output area - Selected image is overlaid on the backgorund - Future - Select an image to use for the background - Add drop shadow to highlight the selected image - Sharpen is now an option 2023-01-28 - Need to add Sharpen and Background to actions """ CHATGPT = """ Write a Python function that detects the screen size for use at initialization of a PySimpleGui program Here's a Python function that detects the screen size using the tkinter library, which can be used to initialize a PySimpleGUI program: python import tkinter as tk def detect_screen_size(): root = tk.Tk() screen_width = root.winfo_screenwidth() screen_height = root.winfo_screenheight() root.destroy() return screen_width, screen_height This function creates a temporary Tk object from the tkinter library, which provides access to the screen size using the winfo_screenwidth and winfo_screenheight methods. These methods return the width and height of the screen in pixels, respectively. After obtaining the screen size, the function destroys the temporary Tk object to avoid any unwanted windows from appearing. To use this function in a PySimpleGUI program, simply call it at the beginning of your script and pass the returned values to the set_global_icon() method (if you're using an icon), or use them to set the size parameter in the Window() constructor: python import PySimpleGUI as sg # Detect screen size screen_width, screen_height = detect_screen_size() # Initialize PySimpleGUI window sg.Window("My Window", size=(screen_width, screen_height)).read() Model: GPT-4 Wesley Elsberry Write a pair of functions in Python to extract EXIF information from a given JPEG filename, and write common modified EXIF fields (comment, copywrite, etc.) to a given JPEG filename. To extract and modify EXIF information from a JPEG file, you can use the Pillow library (a fork of the Python Imaging Library - PIL). First, you need to install the library if you haven't already: Copy code pip install Pillow from PIL import Image from PIL.ExifTags import TAGS from typing import Dict def extract_exif(jpeg_filename: str) -> Dict[str, str]: exif_data = {} image = Image.open(jpeg_filename) info = image._getexif() if info: for tag, value in info.items(): decoded = TAGS.get(tag, tag) exif_data[decoded] = value return exif_data def modify_exif(jpeg_filename: str, updated_exif: Dict[str, str]): image = Image.open(jpeg_filename) info = image._getexif() exif = {TAGS[key]: value for key, value in info.items() if key in TAGS} for key, value in updated_exif.items(): if key in exif: exif[key] = value exif_bytes = image.info['exif'] # Convert the updated exif dict back to bytes from PIL.ExifTags import GPSTAGS from io import BytesIO new_exif_bytes = BytesIO() new_exif = Image.Exif() for key, value in exif.items(): new_exif[key] = value new_exif_bytes = new_exif.tobytes() # Save the image with the updated EXIF data output_filename = 'modified_' + jpeg_filename image.save(output_filename, 'jpeg', exif Write a Python PySimpleGUI program that displays thumbnails of JPEG files in a selected directory, retrieves and displays EXIF data from a selected thumbnail, permits editing of EXIF information, and will write back the edited EXIF information to the selected thumbnail's JPEG file. ! First, you need to install the PySimpleGUI and Pillow libraries if you haven't already: bash Copy code pip install PySimpleGUI Pillow Here's a simple PySimpleGUI program that meets your requirements: python Copy code import os import glob import PySimpleGUI as sg from PIL import Image, ImageTk from typing import Dict # Import the previously defined functions from exif_utils import extract_exif, modify_exif def create_thumbnail(image_path: str, thumbnail_size=(100, 100)) -> ImageTk.PhotoImage: image = Image.open(image_path) image.thumbnail(thumbnail_size) return ImageTk.PhotoImage(image) def display_thumbnails(folder: str) -> Dict[str, str]: file_types = ('*.jpg', '*.jpeg') files = [] for file_type in file_types: files.extend(glob.glob(os.path.join(folder, file_type))) thumbnails = {} for file in files: try: thumbnail = create_thumbnail(file) thumbnails[file] = thumbnail except OSError: pass return thumbnails # Main program sg.theme('DarkBlue') layout = [ [sg.Text('Select a directory:'), sg.Input(), sg.FolderBrowse()], [sg.Text('Click on a thumbnail to view and edit EXIF data')], [sg.Image(key='selected_image'), sg.Multiline(key='exif_data', size=(40, 20), disabled=True)], [sg.Button('Edit EXIF'), sg.Button('Save EXIF'), sg.Button('Exit')] ] window = sg.Window('Image EXIF Viewer', layout) selected_image_path = None thumbnails = {} while True: event, values = window.read() if event == sg.WIN_CLOSED or event == 'Exit': break elif event == 'Browse': folder = values[0] if folder: thumbnails = display_thumbnails(folder) if thumbnails: for file, thumbnail in thumbnails.items(): layout.append([sg.Button('', image_data=thumbnail, key=file, border_width=0)]) window.layout(layout) else: sg.popup('No JPEG files found in the selected folder.') elif event in thumbnails: selected_image_path = event window['selected_image'].update(data=thumbnails[selected_image_path]) exif_data = extract_exif(selected_image_path) window['exif_data'].update('\n'.join(f'{key}: {value}' for key, value in exif_data.items())) elif event == 'Edit EXIF' and selected_image_path: exif_data = extract_exif(selected_image_path) edit_exif_layout = [[sg.Text(f'{key}:'), sg.Input(value, key=key)] for key, value in exif_data.items()] edit_exif_layout.append([sg.Button('Update'), sg.Button('Cancel')]) edit_exif_window = sg.Window('Edit EXIF Data', edit_exif_layout) while True: edit_event, edit_values = edit_exif_window.read() if edit_event == 'Update': updated_exif = {key: value for key, value in edit_values.items()} modify_exif(selected_image_path, updated_exif) exif_data = extract_exif(selected_image_path) window['exif_data'].update('\n'.join(f'{key}: {value}' for key, value in exif_data.items())) edit_exif_window.close() break elif edit_event == sg.WIN_CLOSED or edit_event == 'Cancel': edit_exif_window network error """ import io import os import glob from pathlib import Path import tkinter as tk import PySimpleGUI as sg from PIL import Image import PIL from PIL import ImageGrab import sqlite3 # Not yet doing anything import traceback import simplejson as json import base64 import wand # Not yet doing anything from wand.image import Image from wand.display import display import itertools as it # print(help(Image)) # Globals... how could I let this happen? I was in a hurry. PROGNAME = 'WImageEdit' WINDOWLOCATION = (0, 20) DISPW = 400 DISPH = 400 def detect_screen_size(): root = tk.Tk() screen_width = root.winfo_screenwidth() screen_height = root.winfo_screenheight() root.destroy() return screen_width, screen_height def get_files_of_types_path(mypath, myfiletypes={".jpg", ".JPG", ".png", ".PNG", ".gif", ".GIF", ".jpeg", ".JPEG", ".jfif", ".JFIF"}): """ I liked the idea of using Pathlib instead of glob, but it didn't work right, so now I have this code fossil. """ files = [str(x) for x in (p.resolve() for p in Path(mypath).glob("**/*") if p.suffix in myfiletypes)] print(mypath) print("gfot", files) return files def get_files_of_types(mypath, patterns=["*.jpg", "*.JPG", "*.png", "*.PNG", "*.gif", "*.GIF", "*.jpeg", "*.JPEG", "*.jfif", "*.JFIF"]): """ Get all files of the types in the list of patterns. This runs glob as many times as there are patterns. FML """ try: os.chdir(mypath) except: estr = f"Error: {traceback.format_exc()}" print(estr) files1 = [os.path.abspath(x).replace(r"\\", r'/') for x in sorted(it.chain.from_iterable(glob.iglob(pattern) for pattern in patterns))] return files1 def save_element_as_file(element, filename): """ I saw this somewhere and thought it looked handy. I haven't used it yet. Saves any element as an image file. Element needs to have an underlyiong Widget available (almost if not all of them do) :param element: The element to save :param filename: The filename to save to. The extension of the filename determines the format (jpg, png, gif, ?) """ widget = element.Widget box = (widget.winfo_rootx(), widget.winfo_rooty(), widget.winfo_rootx() + widget.winfo_width(), widget.winfo_rooty() + widget.winfo_height()) grab = ImageGrab.grab(bbox=box) grab.save(filename) def resize_image(image_path, resize=None): #image_path: "C:User/Image/img.jpg" """ To display an image, one needs a byte representation. This function produces the display-ready byte array, plus image sizes for the original and scaled images. """ try: if isinstance(image_path, str): img = PIL.Image.open(image_path) else: try: img = PIL.Image.open(io.BytesIO(base64.b64decode(image_path))) except Exception as e: data_bytes_io = io.BytesIO(image_path) img = PIL.Image.open(data_bytes_io) cur_width, cur_height = img.size if resize: new_width, new_height = resize scale = min(new_height/cur_height, new_width/cur_width) # img = img.resize((int(cur_width*scale), int(cur_height*scale)), PIL.Image.ANTIALIAS) img = img.resize((int(cur_width*scale), int(cur_height*scale)), PIL.Image.LANCZOS) print(cur_width, cur_height, img.size[0], img.size[1]) mywidth, myheight = img.size bio = io.BytesIO() img.save(bio, format="PNG") del img except: estr = f"Error: {traceback.format_exc()}" print(estr) return bio.getvalue(), mywidth, myheight, cur_width, cur_height # Oh, heck. More globals. Will the cruft never end? PROCJSON = 'imgedit_proc.json' if os.path.exists(PROCJSON): dproc = json.load(open(PROCJSON,"r")) else: dproc = {'images': {}} def update_proc(values, filendx=0): """ Update the process metadata. Preserves known-good settings to apply to particular source files and saves them in the current directory. """ values2 = values.copy() print([k for k in values.keys()]) print([k for k in values2.keys()]) ignore = ['-IMAGE-', '-IMAGE2-', '-GRAPH-', '-NEWPREFIX-', '-NEWSUFFIX-'] for igi in ignore: if igi in values2: values2.pop(igi) if (0): if '-IMAGE-' in values2: values2.pop('-IMAGE-') if '-IMAGE2-' in values2: values2.pop("-IMAGE2-") if '-GRAPH-' in values2: values2.pop('-GRAPH-') srcpath, srcfn = os.path.split(values2['-FILE-']) dproc[srcfn] = values2 json.dump(dproc, open(PROCJSON,"w")) def restore_from_proc(srcfn, window, persist={}): """ Transfers process metadata from known-good settings for a file and sets UI elements accordingly. """ ignore = ['-IMAGE-', '-IMAGE2-', '-GRAPH-', '-NEWPREFIX-', '-NEWSUFFIX-'] values = dproc.get(srcfn, None) if values in [None, {}]: return False for kk, vv in values.items(): if not kk in ignore: print(f"restore {kk} as {vv}") window[kk].update(vv) for kk, vv in persist.items(): print(f"persist {kk} as {vv}") window[kk].update(vv) filendx = dproc.get("filendx", 0) return filendx def create_tables(): """ I am thinking of using SQLite to store image metadata, including processing settings. But I haven't gotten much furhter than thinking that might be a good thing. """ try: sqlstr = """ CREATE TABLE fileprocess ( id INTEGER PRIMARY KEY, sourcefilename TEXT, sourcefilesize INTEGER, sourcefilehash INTEGER, hashtype TEXT, stepnumber INTEGER, processtype TEXT, destfilename TEXT ) ; """ pass except: #estr = f'Error: {traceback.format_exc()}' #print(estr) pass class Holder(object): """ Simple minimal class as a convenient place to stuff things. """ def __init__(self): pass def set_process_state(ctx, event, window, values): """ Routine to make sense of the UI elements so far as making an ImageMagick command is concerned. ctx.imgdata = {'data': imgdata, 'width': imgwidth, 'height': imgheight, 'origwidth': origwidth, 'origheight': origheight} """ print("set_process_state") # Read the UI elements and create the cmd proc_fxn = "magick" proc_src = "{srcfn}" proc_dest = "{destfn}" srcfn = values["-FILE-"] srcbase, srcext = os.path.splitext(srcfn) if event in ["Process to File"]: destfn = values["-NEWNAME-"] + srcext.lower() else: destfn = 'tempimg' + srcext.lower() crop_part = "" chop_left = "" chop_right = "" chop_top = "" chop_bottom = "" repage = "" color_correction = "" negate = "" grayscale = "" contrast_stretch = "" resize = "" brightcontrast = "" sigmoidalcontrast = "" rotate = "" flip = "" flop = "" sharpen = "" bgshift = "" background = "" if not values["-CROP-"] in [None, '']: crop_part = values["-CROP-"] if values["-CHOPLEFT_CB-"] in [True]: chop_left = "-gravity west -chop " + values["-CHOPLEFT_GEO-"] if values["-CHOPRIGHT_CB-"] in [True]: chop_right = "-gravity east -chop " + values["-CHOPRIGHT_GEO-"] if values["-CHOPTOP_CB-"] in [True]: chop_top = "-gravity north -chop " + values["-CHOPTOP_GEO-"] if values["-CHOPBOTTOM_CB-"] in [True]: chop_bottom = "-gravity south -chop " + values["-CHOPBOTTOM_GEO-"] if values["-INVERT_CB-"] in [True]: negate = "-negate" if values["-COLORCORRECTION_CB-"] in [True]: color_correction = "-channel RGB" if values["-GRAYSCALE_CB-"] in [True]: grayscale = "-colorspace Gray" if values["-CONTRASTSTRETCH_CB-"] in [True]: contrast_stretch = "-contrast-stretch " + values["-CONTRASTSTRETCH_GEO-"] if values["-RESIZE_CB-"] in [True]: resize = "-resize " + values["-RESIZE_GEO-"] # Rotation? rotation_radio = "0" if values['-ROTATE-90-'] in [True]: rotate = "-rotate 90" elif values['-ROTATE-180-'] in [True]: rotate = "-rotate 180" elif values['-ROTATE-270-'] in [True]: rotate = "-rotate 270" elif values['-ROTATE-C-'] in [True]: rotate = "-rotate %s" % values['-ROTATE-CUSTOM-'] # Flip? if values['-FLIP-'] in [True]: flip = '-flip' # Flop? if values['-FLOP-'] in [True]: flop = '-flop' if values['-SHARPEN-'] in [True]: sharpen = '-sharpen %s' % values['-SHARPEN_GEO-'] # Brightness-Contrast? if values['-BRIGHTNESS-CONTRAST-'] in [True]: brightcontrast = "-brightness-contrast %s" % values['-BRIGHTNESS-CONTRAST_GEO-'] if values['-SIGMOIDAL-CONTRAST-'] in [True]: if values["-SIGCON_UP-"] in [True]: sigconsign = "+" else: sigconsign = "-" sigmoidalcontrast = "-sigmoidal-contrast %s%s" % (sigconsign, values['-SIGMOIDAL-CONTRAST_GEO-']) if 0 < len(chop_top+chop_bottom+chop_right+chop_left): repage = "+repage" if values['-BACKGROUND_CB-'] in [True]: # Figure out background stuff """ Example: magick /Volumes/mediaphotos/photos/photos/pixstar-frame-for-margaret/staging/1960s/2023_01_04_23_02_16.jpg -crop +667+199 -crop -620-199 +repage -channel RGB -contrast-stretch 0.02x0.02% -sigmoidal-contrast -4,40% -colorspace Gray -sharpen 0x1.6 -resize 1024x720 -repage +300+20 '(' +clone -background black -shadow 70x10+5+% ')' '(' granite: -sigmoidal-contrast 4,99% -crop 128x96+0+0 -resize 1024x768 +repage ')' -reverse -layers merge -colorspace Gray tempimg.jpg """ print("calculations for background") shadow = True origwidth = ctx.imgdata['origwidth'] origheight = ctx.imgdata['origheight'] if values['-CROP-'] in [None, '']: cdx = origwidth cdy = origheight print("interactive crop", ctx.crop_params) else: # Parse the crop string print("parsing the crop") print("-CROP-", values['-CROP-']) cparts = [x.strip() for x in values['-CROP-'].split(" ")] print("cparts", cparts) cparts0 = cparts[1] print("cparts0", cparts0) cparts1 = cparts[3] print("cparts1", cparts1) print(cparts0.split("+")) t0, tl, tt = cparts0.split("+") tl = int(tl) tt = int(tt) print(cparts1.split("-")) b0, br, bb = cparts1.split("-") br = int(br) bb = int(bb) ctx.crop_params = [tl, tt, br, bb] print("parsed crop", ctx.crop_params) cdx = origwidth - (ctx.crop_params[0]+ctx.crop_params[2]) cdy = origheight - (ctx.crop_params[1]+ctx.crop_params[3]) if 0 < len(rotate) and (values['-ROTATE-90-'] or values['-ROTATE-270-']): rcdx = cdy rcdy = cdx else: rcdx = cdx rcdy = cdy # What's our desired aspect ratio? asprat = 4.0/3.0 # Based on default aspect ratio if rcdy > rcdx: targetwidth = int(round(asprat * rcdy)) targetheight = rcdy rasprat = rcdx / rcdy tasprat = targetwidth / targetheight print(f"default aspect ratio {'%4.2f' % asprat}, portrait case: crop-rotate aspect ratio {'%4.2f' % rasprat} {rcdx}x{rcdy}, target {'%4.2f' % tasprat} {targetwidth}x{targetheight}") else: rasprat = rcdx / rcdy targetwidth = max(rcdx, int(round(asprat*rcdy))) targetheight = int(round(targetwidth / asprat)) tasprat = targetwidth / targetheight print(f"default aspect ratio {'%4.2f' % asprat}, landscape case: crop-rotate aspect ratio {'%4.2f' % rasprat} {rcdx}x{rcdy}, target {'%4.2f' % tasprat} {targetwidth}x{targetheight}") # Override if there is a resize operation if values["-RESIZE_CB-"] in [True]: # Interpret geo rw, rh = values["-RESIZE_GEO-"].split('x') asprat = float(rw) / float(rh) targetwidth = float(rw) targetheight = float(rh) rasprat = rcdx / rcdy tasprat = targetwidth / targetheight print(f"resize aspect ratio {'%4.2f' % asprat}, sole case: crop-rotate aspect ratio {'%4.2f' % rasprat} {rcdx}x{rcdy}, target {'%4.2f' % tasprat} {targetwidth}x{targetheight}") imgasprat = (rcdx +0.0) / (rcdy + 0.0) if imgasprat == asprat: background = "" # No room for background elif imgasprat > asprat: # Pano print('Landscape background') virtheight = rcdx / asprat shiftt = int(round(0.5*(targetheight-linmap(rcdy, 0, virtheight, 0, targetheight)))) bgshift = "-repage +%s+%s" % (0,shiftt) dropshadow = " " # " '(' +clone -background black -shadow 70x10+5+% ')' " gy = int(round(linmap(1.0/asprat, 0, 1, 0, 128))) graniteback = f" '(' granite: -sigmoidal-contrast 4,99% -crop 128x{gy}+0+0 -resize {targetwidth}x{targetheight} {grayscale} +repage ')' " bgpost = "-reverse -layers merge" background = " ".join([bgshift, dropshadow, graniteback, bgpost]) print(origwidth, origheight, rcdx, rcdy, targetwidth, targetheight, shiftt) pass else: """ Problem: background is the wrong size Portrait background 2088 2093 1780 1758 1780 1335 214 -PROCESSIMAGE_B- magick /Users/wesley.elsberry/personal/_projects/pixstar-baywing/staging/sort/1960--1961maybe-elsberry-square-_2022_04_18_23_16_05.jpg -crop +141+147 -crop -167-188 +repage -channel RGB -contrast-stretch 0.02x0.02% -sigmoidal-contrast +1,50% -colorspace Gray -sharpen 0x1.6 -repage +214+0 '(' granite: -sigmoidal-contrast 4,99% -crop 128x96+0+0 -resize 1780x1335 -colorspace Gray +repage ')' -reverse -layers merge tempimg.jpg 1994 1758 400 352 default aspect ratio 1.33, landscape case: crop-rotate aspect ratio 1.00 1764x1763, target 1.33 1764x1323 Portrait background 2088 2093 1764 1763 1764 1323 220 -PROCESSIMAGE_B- magick /Users/wesley.elsberry/personal/_projects/pixstar-baywing/staging/sort/1960--1961maybe-elsberry-square-_2022_04_18_23_16_05.jpg -crop +141+147 -crop -183-183 +repage -channel RGB -contrast-stretch 0.02x0.02% -sigmoidal-contrast +1,50% -colorspace Gray -sharpen 0x1.6 -repage +220+0 '(' granite: -sigmoidal-contrast 4,99% -crop 128x96+0+0 -resize 1764x1323 -colorspace Gray +repage ')' -reverse -layers merge tempimg.jpg """ # Vertical print("Portrait background") virtwidth = rcdy * asprat shiftr = int(round(0.5*(targetwidth-linmap(rcdx, 0, virtwidth, 0, targetwidth)))) bgshift = "-repage +%s+%s" % (shiftr,0) dropshadow = " " # " '(' +clone -background black -shadow 70x10+5+% ')' " gy = int(round(linmap(1.0/asprat, 0, 1, 0, 128))) graniteback = f" '(' granite: -sigmoidal-contrast 4,99% -crop 128x{gy}+0+0 -resize {targetwidth}x{targetheight} {grayscale} +repage ')' " bgpost = "-reverse -layers merge" background = " ".join([bgshift, dropshadow, graniteback, bgpost]) print(origwidth, origheight, rcdx, rcdy, targetwidth, targetheight, shiftr) pass # I want to make a command out of a bunch of things that may # or may not be there. So I'm creating a list of strings # and only passing non-zero length strings to be joined # into the final form of the command. # The order of processing here is fiddly. Be careful if # you muck around with it. # The 'repage' element is particularly important. cmd_elements = [y for y in [str(x) for x in [proc_fxn, srcfn, crop_part, chop_left, chop_right, chop_top, chop_bottom, flip, flop, repage, rotate, color_correction, contrast_stretch, brightcontrast, sigmoidalcontrast, grayscale, negate, resize, sharpen, background, destfn] ] if 0 < len(y) ] cmd = " ".join(cmd_elements) window["-PROCESS-"].update(cmd) # Yet another global. Great. file_types = [("JPEG (*.jpg)", "*.jpg"), ("All files (*.*)", "*.*")] def new_graph(imgwidth, imgheight, key='-GRAPH-'): """ PSG graph element as a function. """ mygraph = sg.Graph( canvas_size=(imgwidth, imgheight), graph_bottom_left=(0, 0), graph_top_right=(imgwidth, imgheight), key=key, enable_events=True, background_color='lightblue', drag_submits=True, right_click_menu=[[],['Erase item',]] ) return mygraph def make_layout(ctx, imgwidth=None, imgheight=None, reservew=0.3, reserveh=0.66667): """ Changing some of the UI elements at runtime was going to be hard. The advice from the PSG author? Replace the whole layout. So this function sets thing up for the occasinal sawp-out of layouts. The tricky part with this is that PSG does not allow a new layout item with an existing key to be created. 2023-03-11: Notion: Graph sizes could be larger on larger screens. Need screen size to inform the layout. """ global DISPW, DISPH if not 'screeninfo' in dir(ctx): # Call for screen size ctx.screeninfo = detect_screen_size() ctx.screen_width = ctx.screeninfo[0] ctx.screen_height = ctx.screeninfo[1] if imgwidth in [None]: # Adjust imgwidth # Reserve 9% of screen width, and there's two of them imgwidth = int(ctx.screen_width * (1.0 - reservew) * 0.5) ctx.imgdata['width'] = imgwidth DISPW = imgwidth else: print("imgwidth as passed:", imgwidth) if imgheight in [None]: # Adjust imgheight # Reserve 1/3rd sh imgheight = int(ctx.screen_height * reserveh) ctx.imgdata['height'] = imgheight DISPH = imgheight else: print("imgheight as passsd:", imgheight) print(f"Screen {ctx.screen_width} x {ctx.screen_height}, Image {ctx.imgdata['width']} x {ctx.imgdata['height']} ") # T : Text, R : RadioButton # I grabbed part of my layout from a demo of graphing over an image. # Most of those functions I didn't need. graphcol = [[sg.T('Figure Ops', enable_events=True)], [sg.R('Crop', 1, key='-RECT-', enable_events=True)], #[sg.R('Draw Circle', 1, key='-CIRCLE-', enable_events=True)], #[sg.R('Draw Line', 1, key='-LINE-', enable_events=True)], #[sg.R('Draw points', 1, key='-POINT-', enable_events=True)], [sg.R('Erase item', 1, key='-ERASE-', enable_events=True)], [sg.R('Erase all', 1, key='-CLEAR-', enable_events=True)], #[sg.R('Send to back', 1, key='-BACK-', enable_events=True)], #[sg.R('Bring to front', 1, key='-FRONT-', enable_events=True)], #[sg.R('Move Everything', 1, key='-MOVEALL-', enable_events=True)], #[sg.R('Move Stuff', 1, key='-MOVE-', enable_events=True)], #[sg.B('Save Image', key='-SAVE-')], [sg.T('Crop Nudge (pixels):'), sg.Input('10', size=(7,1), key="-CROP_NUDGE-", enable_events=True)], [sg.T(' '),sg.T(' '),sg.T(' '),sg.Button('T^', key="-CROP_NUDGE_RAISE_TOP-", enable_events=True)], [sg.T(' '),sg.T(' '),sg.T(' '),sg.Button('T-', key="-CROP_NUDGE_LOWER_TOP-", enable_events=True)], [sg.Button('<-L', key="-CROP_NUDGE_REDUCE_LEFT-", enable_events=True), sg.Button('L->', key="-CROP_NUDGE_INCREASE_LEFT-", enable_events=True), sg.T(' '), sg.Button('<-R', key="-CROP_NUDGE_REDUCE_RIGHT-", enable_events=True), sg.Button('R->', key="-CROP_NUDGE_INCREASE_RIGHT-", enable_events=True), ], [sg.T(' '),sg.T(' '),sg.T(' '), sg.Button('B^', key="-CROP_NUDGE_RAISE_BOTTOM-", enable_events=True)], [sg.T(' '),sg.T(' '),sg.T(' '), sg.Button('B-', key="-CROP_NUDGE_LOWER_BOTTOM-", enable_events=True), ], ] graphlayout = [[new_graph(imgwidth,imgheight), sg.Col(graphcol, key='-COL-') ], ] control_frame = [ [ sg.Text("Image File"), sg.Input(ctx.fields['-FILE-'], size=(64, 1), key="-FILE-", enable_events=True), sg.FileBrowse("Browse", file_types=file_types, key='-BROWSE-'), sg.Button("Previous Image", key="-PREVIOUSIMAGE-", enable_events=True,), sg.Button("Next Image", key="-NEXTIMAGE-", enable_events=True,), sg.Button("Load Image", key="-LOADIMAGE_B-", enable_events=True,), sg.T(" Actions:"), sg.R("None","-ACTIONS-", default=True, key="-ACTION_NONE-"), sg.R("Color Pos","-ACTIONS-", default=False, key="-ACTION_COLORPOSITIVE-", enable_events=True,), sg.R("Color Neg","-ACTIONS-", default=False, key="-ACTION_COLORNEGATIVE-", enable_events=True,), sg.R("B&W Pos","-ACTIONS-", default=False, key="-ACTION_BWPOSITIVE-", enable_events=True,), sg.R("B&W Neg","-ACTIONS-", default=False, key="-ACTION_BWNEGATIVE-", enable_events=True,), ], [ sg.T("New File Prefix, Suffix:"), sg.Input(ctx.fields['-NEWPREFIX-'], size=(24,1), key="-NEWPREFIX-", enable_events=True), sg.Input(ctx.fields['-NEWSUFFIX-'], size=(24,1), key="-NEWSUFFIX-", enable_events=True), sg.Text(key='-INFO-', size=(60, 1))], [ sg.Text("Process:"), [ sg.Checkbox('Crop', default=False, key='-CROP_CB-'), sg.Input('', size=(36,1), key="-CROP-"), sg.Checkbox('Chop Left', default=False, key='-CHOPLEFT_CB-'), sg.Input('1x0%', size=(16,1), key="-CHOPLEFT_GEO-"), sg.Checkbox('Chop Right', default=False, key='-CHOPRIGHT_CB-'), sg.Input('1x0%', size=(16,1), key="-CHOPRIGHT_GEO-"), sg.Checkbox('Chop Top', default=False, key='-CHOPTOP_CB-'), sg.Input('0x1%', size=(16,1), key="-CHOPTOP_GEO-"), sg.Checkbox('Chop Bottom', default=False, key='-CHOPBOTTOM_CB-'), sg.Input('0x1%', size=(16,1), key="-CHOPBOTTOM_GEO-"), ], [ sg.T("Rotation (degrees)", key='-ROTATION-TEXT-'), sg.R('None','-ROTATE-', default=True, key='-ROTATE-0-'), sg.R('90','-ROTATE-', key='-ROTATE-90-'), sg.R('180','-ROTATE-', key='-ROTATE-180-'), sg.R('270','-ROTATE-', key='-ROTATE-270-'), sg.R('Custom', '-ROTATE-', key='-ROTATE-C-'), sg.Input("0", size=(16,1), key='-ROTATE-CUSTOM-'), sg.T(" "), sg.Checkbox('Flop (horizontal)', default=False, key='-FLOP-'), sg.Checkbox('Flip (vertical)', default=False, key='-FLIP-'), ], [ sg.Checkbox('Brightness/Contrast', default=False, key='-BRIGHTNESS-CONTRAST-'), sg.Input("0x0%", size=(16,1), key='-BRIGHTNESS-CONTRAST_GEO-'), sg.T(" "), sg.Checkbox('Sigmoidal Contrast', default=False, key='-SIGMOIDAL-CONTRAST-'), sg.R("Increase",'-SIGCON-R-',default=True, key="-SIGCON_UP-"), sg.R("Decrease",'-SIGCON-R-',default=False, key="-SIGCON_DOWN-"), sg.Input("1,50%", size=(16,1), key='-SIGMOIDAL-CONTRAST_GEO-'), sg.T(" "), sg.Checkbox('Sharpen', default=False, key='-SHARPEN-'), sg.Input("0x2.0", size=(12,1), key='-SHARPEN_GEO-'), ], [ sg.Checkbox('Invert', default=False, key='-INVERT_CB-'), sg.Checkbox('Color Correction', default=False, key='-COLORCORRECTION_CB-'), sg.Checkbox('Contrast Stretch', default=False, key='-CONTRASTSTRETCH_CB-'), sg.Input('0.02x0.02%', size=(16,1), key="-CONTRASTSTRETCH_GEO-"), sg.Checkbox('Resize', default=False, key='-RESIZE_CB-'), sg.Input('1024x756', size=(16,1), key="-RESIZE_GEO-"), sg.Checkbox('Grayscale', default=False, key='-GRAYSCALE_CB-'), sg.Checkbox('Background', default=False, key='-BACKGROUND_CB-'), ], sg.Input("magick {srcfn} -gravity east -chop 0x0% +repage -channel RGB -contrast-stretch 1x1% -colorspace Gray -negate {destfn}", size=(128,1), key="-PROCESS-"), sg.Button("Update Command", key="-UPDATE_B-"), sg.Button("Process Image", key="-PROCESSIMAGE_B-"), ], [ sg.Text("New name:"), sg.Input(ctx.fields['-NEWNAME-'], size=(128,1), key="-NEWNAME-"), sg.Button("Process to File", key="-PROCESS2FILE_B-"), ], ] image_right_frame = [ [sg.Image(key="-IMAGE2-")] ] image_left_frame = [ [sg.Image(key="-IMAGE-", enable_events=True)] # [sg.Graph(key="-IMAGE-", enable_events=True, drag_submits=True)] ] layout = [ control_frame, [sg.Frame("", graphlayout), #sg.Frame("", image_left_frame), sg.Frame("", image_right_frame)], ] return layout # linmap def linmap(dy, dx1, dx2, rx1, rx2): """ Linearly map value from domain into range. So dx1 === rx1, dx2 === rx2, and however dy relates to dx1 and dx2 is how the output relates to rx1 and rx2. Based on 1989 Pascal code by Wesley R. Elsberry. """ assert dx1 != dx2, "Empty domain." assert rx1 != rx2, "Empty range." ry = (dy - dx1 + 0.0) * ((rx2 - rx1)/(dx2-dx1 + 0.0)) + rx1 return ry def make_crop(ctx, myrect, origwidth, origheight, cropwidth, cropheight): """ Cropping has an issue. I'm drawing a rectangle on an image that's almost certainly not the size of the source image I want to crop, so what coordinates do I use on the source image to make the crop. """ mycrop = "" try: print(myrect, origwidth, origheight, cropwidth, cropheight) # ((10, 261), (358, 7)) 6048 4024 400 266 # origin (0,0) at bottom left, (width, height) at upper right # fixing the drag direction problem 2023-01-02 # Using min/max should make the direction of click-and-drag irrelevant tl = min(myrect[0][0], myrect[1][0]) tr = max(myrect[0][0], myrect[1][0]) tt = max(myrect[0][1], myrect[1][1]) tb = min(myrect[0][1], myrect[1][1]) # Estimate the pixel offsets in the source image. # This is made for how ImageMagick specifies cropping. # It will have to change if the image library changes. origl = int(round(linmap(tl, 0, cropwidth, 0, origwidth))) origr = int(round(linmap(tr, cropwidth, 0, 0, origwidth))) origb = int(round(linmap(tb, 0, cropheight, 0, origheight))) origt = int(round(linmap(tt, cropheight, 0, 0, origheight))) ctx.crop_params = [origl, origt, origr,origb] ctx.cropped_params = [cropwidth, cropheight] # Make the crop string mycrop = f"-crop +{origl}+{origt} -crop -{origr}-{origb} +repage" except: estr = f"Error: {traceback.format_exc()}" print(estr) return mycrop # ==== Event handlers ---------------------------------------- def eh_refresh_newname(ctx, event, window, values, params={}): """ Time to refresh the new file name. """ try: srcfn = values["-FILE-"] srcpath, srcfile = os.path.split(os.path.abspath(srcfn)) srcbase, srcext = os.path.splitext(srcfile) ctx.newprefix = values['-NEWPREFIX-'] ctx.newsuffix = values['-NEWSUFFIX-'] newbase = values['-NEWPREFIX-'] + srcbase + values['-NEWSUFFIX-'] ctx.fields['-FILE-'] = srcfn ctx.fields['-NEWNAME-'] = newbase window['-NEWNAME-'].update(newbase) return srcfn, srcpath, srcfile, srcbase, srcext, newbase except: estr = f"Error: {traceback.format_exc()}" print(estr) return False # ==== Actions ----------------------------------- def eh_action_set(window, params): for kk in params.keys(): try: window[kk].update(params[kk]) except: estr = f"Error: {traceback.format_exc()}" print(estr) def eh_action_colorpos(ctx, event, window, values, params= { '-CROP_CB-':True, '-RECT-': True, '-INVERT_CB-': False, '-COLORCORRECTION_CB-': True, '-CONTRASTSTRETCH_CB-': True, '-SIGMOIDAL-CONTRAST-': True, '-GRAYSCALE_CB-': False, '-SHARPEN-': True, '-BACKGROUND_CB-': True, } ): """ Set values for processing a color positive """ eh_action_set(window, params) def eh_action_colorneg(ctx, event, window, values, params= { '-CROP_CB-':True, '-RECT-': True, '-INVERT_CB-': True, '-COLORCORRECTION_CB-': True, '-CONTRASTSTRETCH_CB-': True, '-SIGMOIDAL-CONTRAST-': True, '-GRAYSCALE_CB-': False, '-SHARPEN-': True, '-BACKGROUND_CB-': True, } ): """ Set values for processing a color negative """ eh_action_set(window, params) def eh_action_bwpos(ctx, event, window, values, params= { '-CROP_CB-':True, '-INVERT_CB-': False, '-RECT-': True, '-COLORCORRECTION_CB-': True, '-CONTRASTSTRETCH_CB-': True, '-SIGMOIDAL-CONTRAST-': True, '-GRAYSCALE_CB-': True, '-SHARPEN-': True, '-BACKGROUND_CB-': True, } ): """ Set values for processing a black-and-white positive """ eh_action_set(window, params) def eh_action_bwneg(ctx, event, window, values, params= { '-CROP_CB-':True, '-RECT-': True, '-INVERT_CB-': True, '-COLORCORRECTION_CB-': True, '-CONTRASTSTRETCH_CB-': True, '-SIGMOIDAL-CONTRAST-': True, '-GRAYSCALE_CB-': True, '-SHARPEN-': True, '-BACKGROUND_CB-': True, } ): """ Set values for processing a black-and-white negative """ eh_action_set(window, params) def eh_action_dispatch(ctx, event, window, values): """ Call the appropriate action. """ if event in ['-ACTION_COLORPOSITIVE-']: eh_action_colorpos(ctx, event, window, values) elif event in ['-ACTION_COLORNEGATIVE-']: eh_action_colorneg(ctx, event, window, values) elif event in ['-ACTION_BWPOSITIVE-']: eh_action_bwpos(ctx, event, window, values) elif event in ['-ACTION_BWNEGATIVE-']: eh_action_bwneg(ctx, event, window, values) # ==== Process to file -------------------------------- def eh_process2file(ctx, event, window, values): try: update_proc(values) print(values) if (0): srcfn = values["-FILE-"] srcpath, srcfile = os.path.split(srcfn) srcbase, srcext = os.path.splitext(srcfn) ctx.lastfile = srcfile destfn = values['-NEWNAME-'] + srcext filevals = eh_refresh_newname(ctx, event, window, values) destfn = "_ie_" if filevals: srcfn, filepath, filebase, fileleft, fileext, newbase = filevals destfn = values['-NEWNAME-'] + fileext else: pass if srcfn == destfn: estr = "Error: Source and destination filenames match! Skipping." print(estr) window['-INFO-'].update(estr) return False if os.path.exists(srcfn): # Make the cmd cmd = values['-PROCESS-'] cmd = cmd.replace('{srcfn}', srcfn) cmd = cmd.replace(values["-FILE-"], srcfn) cmd = cmd.replace('{destfn}', destfn) cmd = cmd.replace('tempimg.jpg', destfn) print(cmd) os.system(cmd) except: estr = f"Error: {traceback.format_exc()}" print(estr) return False # ==== Graph --------------------------------------------- def eh_graph(ctx, event, window, values): """ """ def bounded(gwidth, gheight, x, y): x = min(gwidth,max(0,x)) y = min(gheight, max(0,y)) return x, y try: # graph = window["-GRAPH-"] #ctx.graph = Holder() gwidth = ctx.imgdata['width'] gheight = ctx.imgdata['height'] ctx.graph.x, ctx.graph.y = values["-GRAPH-"] ctx.graph.x, ctx.graph.y = bounded(gwidth, gheight, ctx.graph.x, ctx.graph.y) if not ctx.graph.dragging: ctx.graph.start_point = (ctx.graph.x, ctx.graph.y) ctx.graph.dragging = True ctx.graph.drag_figures = ctx.graph.get_figures_at_location((ctx.graph.x,ctx.graph.y)) ctx.graph.lastxy = ctx.graph.x, ctx.graph.y else: ctx.graph.end_point = (ctx.graph.x, ctx.graph.y) if ctx.graph.prior_rect: ctx.graph.graph.delete_figure(ctx.graph.prior_rect) ctx.graph.delta_x, ctx.graph.delta_y = ctx.graph.x - ctx.graph.lastxy[0], ctx.graph.y - ctx.graph.lastxy[1] ctx.graph.lastxy = ctx.graph.x,ctx.graph.y if None not in (ctx.graph.start_point, ctx.graph.end_point): if values['-RECT-']: ctx.graph.prior_rect = ctx.graph.graph.draw_rectangle(ctx.graph.start_point, ctx.graph.end_point, fill_color=None, line_color='red') elif values['-ERASE-']: for figure in ctx.graph.drag_figures: ctx.graph.graph.delete_figure(figure) if not ctx.imgdata in [None, {}]: ctx.graph.graph = window["-GRAPH-"] # type: sg.Graph ctx.graph.graph.draw_image(data=ctx.imgdata['data'], location=(0,ctx.imgdata['height'])) elif values['-CLEAR-']: ctx.graph.graph.erase() if not ctx.imgdata in [None, {}]: ctx.graph.graph = window["-GRAPH-"] # type: sg.Graph ctx.graph.graph.draw_image(data=ctx.imgdata['data'], location=(0,ctx.imgdata['height'])) window["-INFO-"].update(value=f"mouse {values['-GRAPH-']}") except: estr = f"Error: {traceback.format_exc()}" print(estr) return False # ==== Load Image --------------------------------------------- def eh_load_image(ctx, event, window, values): """ Loads the selected image in both source and dest views. """ try: print(event) if event in ('-FILE-'): print('-FILE- event', ctx.fileselected, values['-FILE-']) if os.path.abspath(ctx.fileselected) != os.path.abspath(values['-FILE-']): print('Selected file change detected.') # Something changed, follow through foldername = os.path.split(os.path.abspath(values['-FILE-']))[0] or values['-BROWSE-'] or '.' print(f"new {foldername=}") ctx.fileselected = os.path.abspath(values['-FILE-']) print(f"get_files_of_types(foldername) {get_files_of_types(foldername)}") ctx.files = [os.path.abspath(x) for x in sorted(get_files_of_types(foldername))] print(f"ctx.files of {foldername=} {ctx.files}") print("different file", foldername, ctx.files, ctx.fileselected) ctx.filendx = 0 try: ctx.filendx = ctx.files.index(ctx.fileselected) print("New file index", ctx.filendx) except: estr = f"Error: {traceback.format_exc()}" print(estr) else: # Already processed return False # set_process_state(ctx, event, window, values) if (0): filename = values["-FILE-"] ctx.fields['-FILE-'] = filename filepath, filebase = os.path.split(filename) print("calling restore_from_proc", filebase) fileleft, fileext = os.path.splitext(filebase) newname = fileleft + "_ie_" ctx.fields['-NEWNAME-'] = newname window['-NEWNAME-'].update(newname) else: ctx.filevals = eh_refresh_newname(ctx, event, window, values) if ctx.filevals: filename, filepath, filebase, fileleft, fileext, newbase = ctx.filevals else: pass persist = {'-NEWPREFIX-': values['-NEWPREFIX-'], '-NEWSUFFIX-': values['-NEWSUFFIX-']} # Use last good settings as basis, if available if not ctx.lastfile in [None, '']: restore_from_proc(ctx.lastfile, window, persist=persist) # If this file has its own settings, set those restore_from_proc(filebase, window, persist=persist) if os.path.exists(filename): print("resizing for image canvas, both images", DISPW, DISPH) imgdata, imgwidth, imgheight, origwidth, origheight = resize_image(values["-FILE-"],resize=(DISPW,DISPH)) ctx.imgdata = {'data': imgdata, 'width': imgwidth, 'height': imgheight, 'origwidth': origwidth, 'origheight': origheight} # Need to replace the window layout, restore again layout = None newlayout = make_layout(ctx, imgwidth=ctx.imgdata['width'], imgheight=ctx.imgdata['height']) ctx.window1 = sg.Window(PROGNAME, newlayout, return_keyboard_events=True, finalize=True, location=WINDOWLOCATION) return True except: estr = f"Error: {traceback.format_exc()}" print(estr) return False # ==== process_image --------------------------------------------- def eh_process_image(ctx, event, window, values): """ """ try: print(event) # set_process_state(ctx, event, window, values) srcfn = values["-FILE-"] srcbase, srcext = os.path.splitext(srcfn) destfn = 'tempimg' + srcext if os.path.exists(srcfn): # Make the cmd cmd = values['-PROCESS-'] cmd = cmd.replace('{srcfn}', srcfn) cmd = cmd.replace(values["-FILE-"], srcfn) cmd = cmd.replace('{destfn}', destfn) cmd = cmd.replace('tempimg.jpg', destfn) print(cmd) os.system(cmd) if os.path.exists(destfn): imgdata, imgwidth, imgheight, origwidth, origheight = resize_image(destfn,resize=(DISPW,DISPH)) ctx.imgdata2 = {'data': imgdata, 'width': imgwidth, 'height': imgheight, 'origwidth': origwidth, 'origheight': origheight} window["-IMAGE2-"].update(data=imgdata) if (0): image = Image.open(destfn) image.thumbnail((DISPW, DISPH)) bio = io.BytesIO() image.save(bio, format="PNG") window["-IMAGE2-"].update(data=bio.getvalue()) except: estr = f"Error: {traceback.format_exc()}" print(estr) return False # ==== Prefix Suffix --------------------------------------------- def eh_prefix_suffix(ctx, event, window, values): """ """ try: ctx.fields['-NEWPREFIX-'] = values['-NEWPREFIX-'] ctx.fields['-NEWSUFFIX-'] = values['-NEWSUFFIX-'] pass except: estr = f"Error: {traceback.format_exc()}" print(estr) return False # ==== Template --------------------------------------------- def eh_process_crop_nudge(ctx, event, window, values): """ """ def update_crop_text(ctx, window, nl=0, nt=0, nr=0, nb=0): origl, origt, origr,origb = ctx.crop_params origl += nl origt += nt origr += nr origb += nb ctx.crop_params = [origl, origt, origr,origb] # Make the crop string mycrop = f"-crop +{origl}+{origt} -crop -{origr}-{origb} +repage" print("New crop:", mycrop) window['-CROP-'].update(mycrop) try: np = int(values['-CROP_NUDGE-']) nl = 0 nt = 0 nr = 0 nb = 0 if event in ['-CROP_NUDGE_RAISE_TOP-']: nt = -np elif event in ['-CROP_NUDGE_LOWER_TOP-']: nt = np elif event in ['-CROP_NUDGE_RAISE_BOTTOM-']: nb = np elif event in ['-CROP_NUDGE_LOWER_BOTTOM-']: nb = -np elif event in ['-CROP_NUDGE_REDUCE_LEFT-']: nl = -np elif event in ['-CROP_NUDGE_INCREASE_LEFT-']: nl = np elif event in ['-CROP_NUDGE_REDUCE_RIGHT-']: nr = np elif event in ['-CROP_NUDGE_INCREASE_RIGHT-']: nr = -np update_crop_text(ctx, window, nl, nt, nr, nb) except: estr = f"Error: {traceback.format_exc()}" print(estr) return False # ==== Template --------------------------------------------- def eh_template(ctx, event, window, values): """ """ try: pass except: estr = f"Error: {traceback.format_exc()}" print(estr) return False # ==== Event Loop ------------------------------------------------------- def sg_event_loop_window_1(): """ So far, there is just the one window. This is cobbled together out of two demo programs, and then heavily modified. Sue me. I'd like to do more encapsulation of logic, offload things here in the loop to function calls. That's off in the maybe-if-only future. For the moment, it mostly works. """ try: ctx = Holder() ctx.fields = { '-FILE-': "", '-NEWNAME-': "new_name", '-NEWPREFIX-': "proc_", '-NEWSUFFIX-': "_ie_", } ctx.filendx = 0 ctx.files = sorted(get_files_of_types(".")) ctx.fileselected = "" ctx.lastfile = None # Default image width and height needs to be in ctx.imgdata = {'width':400, 'height': 400} ctx.imgdata2 = {'width':400, 'height': 400} layout = make_layout(ctx) proc_events = ["-PROCESS-", "-CHOPLEFT_CB-", "-CHOPLEFT_GEO-", "-CHOPRIGHT_CB-", "-CHOPRIGHT_GEO-", "-CHOPTOP_CB-", "-CHOPTOP_GEO-", "-CHOPBOTTOM_CB-", "-CHOPBOTTOM_GEO-", "-INVERT_CB-", "-COLORCORRECTION_CB-", "-GRAYSCALE_CB-", "-CONTRASTSTRETCH_CB-", "-CONTRASTSTRETCH_GEO-", "-NEWNAME-" ] # print(help(sg.Window)) #print(help(sg.Image)) sg.theme('Dark Blue 3') # print(layout) window = sg.Window("WImageEdit", layout, return_keyboard_events=True, location=WINDOWLOCATION) except: estr = f"Error: {traceback.format_exc()}" print(estr) print("Failed to create GUI window, quitting.") return False # Pre-loop setup # get the graph element for ease of use later ctx.graph = Holder() ctx.graph.dragging = False ctx.graph.lastxy = 0, 0 ctx.graph.graph = window["-GRAPH-"] # type: sg.Graph # graph.draw_image(data=logo200, location=(0,400)) ctx.graph.dragging = False ctx.graph.start_point = ctx.graph.end_point = ctx.graph.prior_rect = None # graph.bind('', '+RIGHT+') ctx.crop_params = [0,0,0,0] ctx.cropped_params = [0,0] print("entering event loop") running = True while running: event, values = window.read() # print(event) if event == "Exit" or event == sg.WIN_CLOSED: break # ==== Actions ---------------- if event in ["-ACTION-", "-ACTION_COLORPOSITIVE-","-ACTION_COLORNEGATIVE-","-ACTION_BWPOSITIVE-","-ACTION_BWNEGATIVE-"]: eh_action_dispatch(ctx, event, window, values) # ---- Graph element ----------------------- if event in ('-MOVE-', '-MOVEALL-'): ctx.graph.graph.set_cursor(cursor='fleur') # not yet released method... coming soon! elif not event.startswith('-GRAPH-'): ctx.graph.graph.set_cursor(cursor='left_ptr') # not yet released method... coming soon! if event in ["-GRAPH-"]: # if there's a "Graph" event, then it's a mouse eh_graph(ctx, event, window, values) elif event.endswith('+UP'): # The drawing has ended because mouse up window["-INFO-"].update(value=f"grabbed rectangle from {ctx.graph.start_point} to {ctx.graph.end_point}") ctx.rect = (ctx.graph.start_point, ctx.graph.end_point) ctx.graph.rect = (ctx.graph.start_point, ctx.graph.end_point) # Set chop points mycrop = make_crop(ctx, ctx.rect, ctx.imgdata['origwidth'], ctx.imgdata['origheight'], ctx.imgdata['width'], ctx.imgdata['height']) ctx.graph.start_point, ctx.graph.end_point = None, None # enable grabbing a new rect ctx.graph.dragging = False ctx.graph.prior_rect = None window["-CROP-"].update(value=mycrop) elif event.endswith('+RIGHT+'): # Right click window["-INFO-"].update(value=f"Right clicked location {values['-GRAPH-']}") elif event.endswith('+MOTION+'): # window["-INFO-"].update(value=f"mouse freely moving {values['-GRAPH-']}") elif event == '-SAVE-': # filename = sg.popup_get_file('Choose file (PNG, JPG, GIF) to save to', save_as=True) filename=r'test.jpg' save_element_as_file(window['-GRAPH-'], filename) elif event == 'Erase item': window["-INFO-"].update(value=f"Right click erase at {values['-GRAPH-']}") if values['-GRAPH-'] != (None, None): ctx.graph.drag_figures = ctx.graph.graph.get_figures_at_location(values['-GRAPH-']) for figure in ctx.graph.drag_figures: ctx.graph.graph.delete_figure(figure) if window.find_element_with_focus().Key == '-IMAGE-': print('IMAGE event ', event, values) if event in ["-IMAGE-+UP", "-IMAGE-+DOWN"]: print('IMAGE mouse event ', event, values) if event in ["-PREVIOUSIMAGE-"]: print('-PREVIOUSIMAGE- event') try: nextindex = (ctx.filendx - 1) % len(ctx.files) # Wrap via modulo nextfile = ctx.files[nextindex] if nextfile == values['-FILE-']: nextindex = (ctx.filendx + 1) % len(ctx.files) # Wrap via modulo print(f"Current index {ctx.filendx}, next index {nextindex}") ctx.filendx = nextindex window["-FILE-"].update(ctx.files[nextindex]) window.write_event_value('-LOADIMAGE_B-', True) except: estr = f"Error: {traceback.format_exc()}" print(estr) print(f"ctx.filendx={ctx.filendx}, len(ctx.files)={len(ctx.files)} {ctx.files}") if event in ["-NEXTIMAGE-"]: print('-NEXTIMAGE- event') try: nextindex = (ctx.filendx + 1) % len(ctx.files) # Wrap via modulo nextfile = ctx.files[nextindex] if nextfile == values['-FILE-']: nextindex = (ctx.filendx + 1) % len(ctx.files) # Wrap via modulo print(f"Current index {ctx.filendx}, next index {nextindex}") ctx.filendx = nextindex window["-FILE-"].update(ctx.files[nextindex]) window.write_event_value('-LOADIMAGE_B-', True) except: estr = f"Error: {traceback.format_exc()}" print(estr) print(f"ctx.filendx={ctx.filendx}, len(ctx.files)={len(ctx.files)} {ctx.files}") if event in ["Load Image", "-LOADIMAGE_B-", "-FILE-"]: # ---- Load Image ---------------- if eh_load_image(ctx, event, window, values): persist = {'-NEWPREFIX-': values['-NEWPREFIX-'], '-NEWSUFFIX-': values['-NEWSUFFIX-']} window.Close() window = ctx.window1 filename, filepath, filebase, fileleft, fileext, newbase = ctx.filevals ctx.files = sorted(get_files_of_types(filepath)) restore_from_proc(filebase, window, persist=persist) window['-FILE-'].update(ctx.fields['-FILE-']) filevals = eh_refresh_newname(ctx, event, window, values) # window['-NEWNAME-'].update(ctx.fields['-NEWNAME-']) if filevals: filename, filepath, filebase, fileleft, fileext, newbase = filevals else: pass ctx.graph.graph = window["-GRAPH-"] ctx.graph.graph.draw_image(data=ctx.imgdata['data'], location=(0,ctx.imgdata['height'])) window["-IMAGE2-"].update(data=ctx.imgdata['data']) print("image canvas loaded, both images") running = True if event in ["Update Command", "-UPDATE_B-"]: print(event) set_process_state(ctx, event, window, values) if event in ["Process Image", "-PROCESSIMAGE_B-"]: eh_process_image(ctx, event, window, values) if event in ["Process to File", "-PROCESS2FILE_B-"]: # set_process_state(ctx, event, window, values) print(event) eh_process2file(ctx, event, window, values) if event in ['-NEWPREFIX-', '-NEWSUFFIX-']: eh_prefix_suffix(ctx, event, window, values) # Crop Nudge handling if event in [ '-CROP_NUDGE_RAISE_TOP-','-CROP_NUDGE_LOWER_TOP-','-CROP_NUDGE_RAISE_BOTTOM-','-CROP_NUDGE_LOWER_BOTTOM-', '-CROP_NUDGE_REDUCE_LEFT-','-CROP_NUDGE_INCREASE_LEFT-','-CROP_NUDGE_REDUCE_RIGHT-','-CROP_NUDGE_INCREASE_RIGHT-']: eh_process_crop_nudge(ctx, event, window, values) running = True window.close() if __name__ == "__main__": # files = get_files_of_types(".") # print(os.getcwd(), files) sg_event_loop_window_1()