1536 lines
62 KiB
Python
Executable File
1536 lines
62 KiB
Python
Executable File
"""
|
|
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('<Button-3>', '+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()
|