MetaData writing with Python

Moderator: Kathy_9

Post Reply
wpslo
Posts: 12
Joined: Sat Jun 02, 2018 9:48 pm
operating_system: Windows 7 Professional
System_Drive: C
32bit or 64bit: 32 Bit
motherboard: asus 1025ce
processor: intel Atom
ram: 4gb
Hard_Drive_Capacity: 2 TB

MetaData writing with Python

Post by wpslo »

Hi,
Has anyone figured out how to write metadata to images using Python?
Specifically I want to target the IPTC Description field.
I seem to be able to do this with hard coding using ImageInfo and FileSaveAs but when I try to use
a Variable it just gets overwritten with the original Metadata .

I cannot find a Method within the PSP Method Index which addresses this point.

Can some kind person suggest a workaround / solution ?
Last edited by Kathy_9 on Fri Aug 17, 2018 3:34 pm, edited 1 time in total.
Reason: Moved to Scripting Forum
LeviFiction
Advisor
Posts: 6831
Joined: Thu Oct 02, 2008 1:07 pm
operating_system: Windows 10
System_Drive: C
32bit or 64bit: 64 Bit
motherboard: Alienware M17xR4
processor: Intel Core i7-3630QM CPU - 2_40GH
ram: 6 GB
Video Card: NVIDIA GeForce GTX 660M
sound_card: Sound Blaster Recon3Di
Hard_Drive_Capacity: 500GB
Corel programs: PSP: 8-2023
Location: USA

Re: MetaData writing with Python

Post by LeviFiction »

This should be in the scripting sub-forum.

Can you show us the code where you use a variable? Which information do you change when you do this? I did a break-down of the settings once before, though I can't remember where I posted them.

There are some things that PSP can't edit via built in PSP commands. When that is the case the only thing we can do is manually edit the images directly. So long as you have a good understanding of the formats that accept IPTC metadata you can add it to any image you want.
https://levifiction.wordpress.com/
wpslo
Posts: 12
Joined: Sat Jun 02, 2018 9:48 pm
operating_system: Windows 7 Professional
System_Drive: C
32bit or 64bit: 32 Bit
motherboard: asus 1025ce
processor: intel Atom
ram: 4gb
Hard_Drive_Capacity: 2 TB

Re: MetaData writing with Python

Post by wpslo »

Hi LeviFiction,

Thank you for your prompt response; it would appear that my post has kindly moved my post to the "PSP Scripting" sub-forum.

I am trying to use a Tkinter GUI to load any existing IPTC Description, which will then accept edits and write them back to the image
which is then saved as a TIFF .

This is all working as a "proof of concept" except that I cannot seem to update the MetaData from Python [ as previously stated ] .
Here is the part of the code where I try to achieve the write [ ImageInfo is where I have stored the MetaData I want to write ] .

# ImageInfo & FileSaveAs routines start >>>>>>>>>>>>>>>>>>>>>>>>>

def File_Save_As():
print "093_ImageInfo & FileSaveAs routines start, ImageInfo =" + str(ImageInfo)

App.Do( Environment, 'ImageInfo', {
# 'Description': u'2018081820170416_16:10Metadata_added_: _Italy_Rome_Vatican_Castel_Sa'\
# u'nt_Angelo_\u000D\u000ALoweLeoElaine_\u000AEoF\u000Ahhhhhhhxxxxxx', """
'Description': ImageInfo,
# 'GeneralSettings': {
# 'ExecutionMode': App.Constants.ExecutionMode.Default,
# 'AutoActionMode': App.Constants.AutoActionMode.Match,
# 'Version': ((20,0,0),1)
# }
})



App.Do( Environment, 'FileSaveAs', {
'Encoding': {
'TIF': {
'Compression': App.Constants.TiffCompression.LZW,
'Channels': App.Constants.ColorChannels.RGB,
'EmbedTIFICC': True
}
},
# 'ReturnImageInfo': "wwwwwwwwwww",
# 'ReturnImageInfo': ImageInfo,
'FileName': name,
# 'FileName': u'E:\\zSoftware\\PythonCoral_a\\20170410xxxxx.tif',
'FileFormat': App.Constants.FileFormat.TIF,
'FormatDesc': u'TIF Tagged Image File Format',
'WorkingMode': 0,
'GeneralSettings': {
'ExecutionMode': App.Constants.ExecutionMode.Default,
'AutoActionMode': App.Constants.AutoActionMode.AllAlways,
'Version': ((20,0,0),1)
},
'DefaultProperties': []
})

Any further advice would be welcome.
wpslo
Posts: 12
Joined: Sat Jun 02, 2018 9:48 pm
operating_system: Windows 7 Professional
System_Drive: C
32bit or 64bit: 32 Bit
motherboard: asus 1025ce
processor: intel Atom
ram: 4gb
Hard_Drive_Capacity: 2 TB

Re: MetaData writing with Python

Post by wpslo »

I have solved the problem at least partially.
It was a matter of Scope as, I think, Local variables were being used to write the TIFF metdata not a higher level such as Global.
I have now solved this problem by creating a " class-name.attribute.name ".
Interestingly PSP does not report the TIFF metadata correctly but IrfanView does.

I now encountered another problem which seems to be PSP2018 related and concerns window focus and keyboard shortcuts. .

When I open PSP I have to use the mouse to click on a menu item before keyboard shortcuts will work.
Conversely the keyboard shortcut I have written into my script only works the first time it is run .
Last edited by wpslo on Sun Aug 26, 2018 5:49 pm, edited 1 time in total.
LeviFiction
Advisor
Posts: 6831
Joined: Thu Oct 02, 2008 1:07 pm
operating_system: Windows 10
System_Drive: C
32bit or 64bit: 64 Bit
motherboard: Alienware M17xR4
processor: Intel Core i7-3630QM CPU - 2_40GH
ram: 6 GB
Video Card: NVIDIA GeForce GTX 660M
sound_card: Sound Blaster Recon3Di
Hard_Drive_Capacity: 500GB
Corel programs: PSP: 8-2023
Location: USA

Re: MetaData writing with Python

Post by LeviFiction »

If you wrap everything in [code] #Your code between these two tags[/code] then all indentation will be maintained and we won't have to recreate it. xD Not that it was hard in the case of this script but still, always nice to have.

PSP uses the standard Windows newline character. \r\n whereas the TKinter Text widget only uses \n for the newline. That's why PSP doesn't display the description on its own line. If you replace "\n" with "\r\n" then PSP will display it as you expect, though irfanView shows it doublespaced.

So long as I have focus placed on the tkinter dialog box your bound shortcut works perfectly every time for me. Alt+C will cancel the dialog but only if the dialog is active. The best way to handle dialogs like this is to use PSP's StartForeignWindow command.


App.Do( Environment, 'StartForeignWindow', { 'WindowHandle': int(root.winfo_id()) } ) #Give control to the dialog
root.mainloop() #enter the main loop
root.Do( Environment, 'StartForeignWindow', { 'WindowHandle': 0 } ) #Return control to PSP

You can read all about it in PSP's Scripting for Script Author's Guide. Here is a small excerpt.

One of the more powerful features of scripts is that they can contain their own GUI. This makes it possible to write scripts that gather complex data from the user. However, the interaction of a script dialog and PaintShop Pro is rather problematic. Since the dialog is not properly a child of PaintShop Pro, PaintShop Pro is perfectly happy if the dialog ends up behind PaintShop Pro. Also, while the dialog is up the PaintShop Pro UI is not running, so menu items and toolbars don’t update properly.

Finally, if PaintShop Pro attempts to start a new script while a dialog window is running, the Python interpreter will be in an undefined state and the program will crash.

This command exists to deal with these issues. The parameter is the Win32 window handle of the dialog being displayed. For a Tkinter dialog, this is returned by the winfo_id() method. For a wxWindow, use the GetHandle() method.

So long as the window identified in the call to StartForeignWindow exists, PaintShop Pro will ensure that it remains on top of the application, and it will keep the PaintShop Pro UI updating. While a foreign window is running, PaintShop Pro will disable running scripts, loading presets, and loading brush tips.

PaintShop Pro will automatically terminate the foreign window state as soon as the identified window is destroyed. You can also manually terminate the state by calling StartForeignWindow again with a window handle of zero.
If dialogs are not bracketed by calls to StartForeignWindow, PaintShop Pro will allow a second script to be launched, which will usually crash the application
https://levifiction.wordpress.com/
wpslo
Posts: 12
Joined: Sat Jun 02, 2018 9:48 pm
operating_system: Windows 7 Professional
System_Drive: C
32bit or 64bit: 32 Bit
motherboard: asus 1025ce
processor: intel Atom
ram: 4gb
Hard_Drive_Capacity: 2 TB

Re: MetaData writing with Python

Post by wpslo »

Hi LeviFiction,
Thank you for that really helpful reply.
Because of your help I have now been able to reach my goal.
Here is the "production" script in which I have added many comments which
I hope will help other newbies like me. And refresh my own memory as required.
I have taken up your suggestion of the '/code' formatters.


See below for a post of this script with indentation preserved
and a couple of small updates.
Last edited by wpslo on Sun Aug 26, 2018 9:01 pm, edited 1 time in total.
LeviFiction
Advisor
Posts: 6831
Joined: Thu Oct 02, 2008 1:07 pm
operating_system: Windows 10
System_Drive: C
32bit or 64bit: 64 Bit
motherboard: Alienware M17xR4
processor: Intel Core i7-3630QM CPU - 2_40GH
ram: 6 GB
Video Card: NVIDIA GeForce GTX 660M
sound_card: Sound Blaster Recon3Di
Hard_Drive_Capacity: 500GB
Corel programs: PSP: 8-2023
Location: USA

Re: MetaData writing with Python

Post by LeviFiction »

The code format blocks need the [ ] brackets. And the first one does not have a /. It's like HTML except you use [ ] instead of < >

EDIT: I would also like to recommend two things

1) Add this lines of code to your program to handle when people click the close button.

Code: Select all

    root.protocol("WM_DELETE_WINDOW", cancel)
You don't need the lambda here because the WM_DELETE_WINDOW protocol doesn't send an event it just calls the function named here.

The reason for this is right now if you close the widow it just continues running the script and will attempt to save the image as a TIF in spite of the fact that the user closed the window

and

2) Remove the variable names of the buttons from their captions. It is kind of confusing to look at. xD
https://levifiction.wordpress.com/
wpslo
Posts: 12
Joined: Sat Jun 02, 2018 9:48 pm
operating_system: Windows 7 Professional
System_Drive: C
32bit or 64bit: 32 Bit
motherboard: asus 1025ce
processor: intel Atom
ram: 4gb
Hard_Drive_Capacity: 2 TB

Re: MetaData writing with Python

Post by wpslo »

Hello LeviFiction,
I put the "WM_DELETE_WINDOW" in to the "Cancel" function as you suggested; not sure how this differs from
the "sys.exit" command but hey the more the merrier.
Also deleted the variable names from the button widget names.
Tried again to preserve the formatting like this:

Code: Select all

script here
This appears to have worked but then I thought it did last time.


#

Code: Select all

from PSPApp import *
from   Tkinter import *      #ToolKit Interface module
# SEE COMMENTS AT END OF SCRIPT TO LEARN "WOT THIS DUZ!!". USE AT OWN RISK.
# Class.attributes [Class variables] declared here to avoid probelms with scope.
class Description:
    _description = " class._description "
    _name = "class.name"

class Ymd:
    ymdhms = "ymdhms"
    #    def _ymd(ymdhms):
    from datetime import datetime
    now = datetime.now()
    yyyy = str(now.year) ; mm = str(now.month) ; dd = str(now.day)
    hh=str(now.hour) ; mi=str(now.minute) ; ss=str(now.second)
    mm=mm.zfill(2);dd=dd.zfill(2);hh=hh.zfill(2);mi=mi.zfill(2);ss=ss.zfill(2)
    ymdhms = "NM_"+yyyy+mm+dd+"@"+hh+":"+mi+":"+ss  #"\r\n" newline characters
# Class instance [copy] of Ymd.ymdhms created here:
ymdhms = Ymd.ymdhms

def ScriptProperties():
    return {
        'Author': u'',
        'Copyright': u'',
        'Description': u'',
        'Host': u'PaintShop Pro',
        'Host Version': u'20.00'
        }

def Do(Environment):
# EnableOptimizedScriptUndo
    App.Do( Environment, 'EnableOptimizedScriptUndo', {
            'GeneralSettings': {
                'ExecutionMode': App.Constants.ExecutionMode.Default,
                'AutoActionMode': App.Constants.AutoActionMode.Match,
                'Version': ((20,0,0),1)
                }
            })

# ImageInfo: A PSP method. Returns metadata as a Dictionary .
    ImageInfo = App.Do( Environment, 'ReturnImageInfo',)
# Create variable; assign it to the 'Description' value in the ImageInfo object.
    NM_Description = str(ImageInfo['Description'])
    print "044====20180814 2102_NM_TkinterGrid NM_Description = " \
           + NM_Description
#Tk() is the window object constructor of a top level widget
    print "047====20180814__NM_TkinterGrid importing the Tkinter module "
#root created as a window object, window object title method can take a string.
    root = Tk() ; root.title("NM_20180814_NM_TkinterGrid")

# Print "051===Call Function '_ymd' ymdhms = " + ymdhms

# Create the Tkinter GUI on the Top Level root frame.
    frame1 = Frame(root, border=4, relief=SUNKEN, padx=1, pady=1, \
                   width = 250,height = 22 )
    frame1.grid()
    frame2 = Frame(root, border=12, relief=RIDGE, padx=1, pady=1, width = 244 )
    frame2.grid()
    frame3 = Frame(root, border=2, relief=RAISED )
    frame3.grid()

    wf1r1c1 = Label(frame1,text="EXISTING IMAGE METADATA: ", \
                    fg="RoyalBlue4", bg="cyan")
    wf1r1c1.grid( row = 1, column = 1, padx=3, pady=3 )
    wf1r1c1.grid( sticky=(N,S,E,W))

    wf1r1c2 = Button(frame1, text="wf1r1c2 \n" + NM_Description , \
                     fg="maroon",  bg="Light Pink", width=67,
                     anchor=W, justify=LEFT, wraplength=399)
    wf1r1c2.grid( row=1, column=2, padx=3, pady=3)

    wf2r4c1 = Label(frame2, \
                    text="MetaData IPTC_Description Field will be "+\
                    "prepended by: '"+ymdhms+
                    "' Date Stamp                        ",
                    font=('Verdana', 7, 'roman'), justify=CENTER, \
                    bg="LightSkyBlue1", fg="Purple", width=93, height=1 )
    wf2r4c1.grid( row = 1, column = 1, padx=3, pady=3 )

# User can insert their own Description Metadata here:
    wf2r5c1 = Text(frame2, background="Linen", foreground="Purple"   )
    wf2r5c1.focus()
    wf2r5c1.focus_force()
    wf2r5c1.focus_set()
    wf2r5c1.grid(row=5, column=1 )
    wf2r5c1.insert(INSERT, ymdhms+"\r\n")    #\r Windows & \n Tk NewLine chars.

# Captures users input from the Text widget when <Alt-s> pressed.
    def get_description():
        Description._description=wf2r5c1.get("1.0","end-1c")
        print "091>>>>> _description = " + Description._description
        root.destroy()
# Quits GUI <Alt-c> pressed.
    def cancel(event=None):
        print"095\\\\  Cancel"
        wf2r5c1.destroy()            #Renders Button inactive_???
        root.protocol("WM_DELETE_WINDOW", cancel)
        root.destroy()                             #Stops mainloop
        sys.exit("def cancel - exit")              #Exits Python script

    _buttonSave = Button(frame3, height=1, width=15, text="Save", \
                     underline=0, command=get_description)
    _buttonSave.grid(row=1, column=1 )
    _labelSpacer = Label(frame3, text="- - -", bg="Light Salmon" )
    _labelSpacer.grid(row=1, column=2 )
    _buttonCancel = Button(frame3, height=1, width=15, text="Cancel", \
                     underline=0, default="active", command=cancel)
    _buttonCancel.grid(row=1, column=3 )
    root.bind('<Alt-s>', lambda event: get_description())
    root.bind('<Alt-S>', lambda event: get_description())
    root.bind('<Alt-c>', lambda event: cancel())
    root.bind('<Alt-C>', lambda event: cancel())   #'Alt-x' is case sensitive
    _buttonCancel.bind('<Return>', lambda event: cancel())       #Press 'Enter'

# See "Scripting for Script Author's Guide for SFW explanation.
# And a big thank you to LeviFiction for "telling me where to go!"
# Basically this in effect presents the GUI with the focus on theText widget.
    App.Do( Environment, 'StartForeignWindow', {
            'WindowHandle': int(root.winfo_id())   })

    root.mainloop()

# Check this out in the PSP Scripting API Documentation.
    App.Do( Environment, 'StartForeignWindow', {'WindowHandle':0})

    print "124>>>>> Dropped out of Tkinter, _description = " + \
                    Description._description

# ImageInfo, update the Description Dictionary field with any original text
# and any added text.
    App.Do( Environment, 'ImageInfo', {
            'Description': Description._description + NM_Description,
            'GeneralSettings': {
#                'ExecutionMode': App.Constants.ExecutionMode.Default,
                'ExecutionMode': App.Constants.ExecutionMode.Silent,
                'AutoActionMode': App.Constants.AutoActionMode.Match,
                'Version': ((20,0,0),1)
                }
            })
    print "139>>>>>  ymdhms                     = " + ymdhms
    print "140>>>>>  Description._description   = " + Description._description
    print "141>>>>>  NM_Description             = " + NM_Description

#Write the TIF image .
    name = App.TargetDocument.Name
    print "145<<<<<< _name = " + name
    # FileSaveAs
    App.Do( Environment, 'FileSaveAs', {
            'Encoding': {
                'TIF': {
                    'Compression': App.Constants.TiffCompression.LZW,
                    'Channels': App.Constants.ColorChannels.RGB,
                    'EmbedTIFICC': True
                    }
                },
            'FileName': name,
            'FileFormat': App.Constants.FileFormat.TIF,
            'FormatDesc': u'TIF Tagged Image File Format',
            'WorkingMode': 0,
            'GeneralSettings': {
                'ExecutionMode': App.Constants.ExecutionMode.Default,
                'AutoActionMode': App.Constants.AutoActionMode.AllAlways,
                'Version': ((20,0,0),1)
                },
            'DefaultProperties': []
            })

"""
20180826
This is intended to work on an open .JPG image givng the user the facility to
edit any existing data in the IPTC_Description field [ aka IPTC_Caption ] and
save the resultant old and new text to a .TIF iamge file .

New text will be automatically prefaced with a date stamp in ISO8601 format,
i.e yyyymmdd hh:mm:ss, plus a couple of bits of my own.
In these days of digital I despair at the use of anything else [ not
logical, Captain ]..

A GUI is presented with any existing MetaData displayed and with a widget to
allow more MetaData to be added. You will not be able to edit existing MetaData.

I started this in a desultory fashion in 2018 June and really got stuck in
in the last week or so. I had no knowledge of Python when I started in June.

I found "Python In Easy Steps" by Mike McGrath to be an invaluable primer.
After reading this I expect Mike to call round with a nicely stuffed proverbial
brown envelope!

Finally I must give wholehearted thanks to LeviFiction for his patience in
reading my poorly indented script and the vital pointer to the
StartForeignWindow command [ function ?] to control the focus.

NM
"""
#
LeviFiction
Advisor
Posts: 6831
Joined: Thu Oct 02, 2008 1:07 pm
operating_system: Windows 10
System_Drive: C
32bit or 64bit: 64 Bit
motherboard: Alienware M17xR4
processor: Intel Core i7-3630QM CPU - 2_40GH
ram: 6 GB
Video Card: NVIDIA GeForce GTX 660M
sound_card: Sound Blaster Recon3Di
Hard_Drive_Capacity: 500GB
Corel programs: PSP: 8-2023
Location: USA

Re: MetaData writing with Python

Post by LeviFiction »

Sorry for the poor explanation.

WM_DELETE_WINDOW is an event like hitting the cancel button. So you don't want to put it int he cancel function, but next to the bind commands. It catches the event of closing the dialog with the "X" button in the upper-right corner.

So you would put it here

Code: Select all

    root.protocol("WM_DELETE_WINDOW", cancel)
    root.bind('<Alt-s>', lambda event: get_description())
    root.bind('<Alt-S>', lambda event: get_description())
    root.bind('<Alt-c>', lambda event: cancel())
    root.bind('<Alt-C>', lambda event: cancel())   #'Alt-x' is case sensitive
    _buttonCancel.bind('<Return>', lambda event: cancel())       #Press 'Enter'
https://levifiction.wordpress.com/
wpslo
Posts: 12
Joined: Sat Jun 02, 2018 9:48 pm
operating_system: Windows 7 Professional
System_Drive: C
32bit or 64bit: 32 Bit
motherboard: asus 1025ce
processor: intel Atom
ram: 4gb
Hard_Drive_Capacity: 2 TB

Re: MetaData writing with Python

Post by wpslo »

Hi LeviFiction,

That did the job.
I had noticed that behaviour { clicking "X" presented the FileSaveAs dialog] so thanks for the fix.

Have posted the revised script below.

Many Thanks,
wpslo

#

Code: Select all

# SEE COMMENTS AT END OF SCRIPT TO LEARN "WOT THIS DUZ!!". USE AT OWN RISK.

from PSPApp import *
from   Tkinter import *      #ToolKit Interface module
# Class.attributes [Class variables] declared here to avoid problems with scope.
class Description:
    _description = " class._description "
    _name = "class.name"

class Ymd:
    ymdhms = "ymdhms"
    #    def _ymd(ymdhms):
    from datetime import datetime
    now = datetime.now()
    yyyy = str(now.year) ; mm = str(now.month) ; dd = str(now.day)
    hh=str(now.hour) ; mi=str(now.minute) ; ss=str(now.second)
    mm=mm.zfill(2);dd=dd.zfill(2);hh=hh.zfill(2);mi=mi.zfill(2);ss=ss.zfill(2)
    ymdhms = "NM_"+yyyy+mm+dd+"@"+hh+":"+mi+":"+ss  #"\r\n" newline characters
# Class instance [copy] of Ymd.ymdhms created here:
ymdhms = Ymd.ymdhms

def ScriptProperties():
    return {
        'Author': u'',
        'Copyright': u'',
        'Description': u'',
        'Host': u'PaintShop Pro',
        'Host Version': u'20.00'
        }

def Do(Environment):
# EnableOptimizedScriptUndo
    App.Do( Environment, 'EnableOptimizedScriptUndo', {
            'GeneralSettings': {
                'ExecutionMode': App.Constants.ExecutionMode.Default,
                'AutoActionMode': App.Constants.AutoActionMode.Match,
                'Version': ((20,0,0),1)
                }
            })

# ImageInfo: A PSP method. Returns metadata as a Dictionary .
    ImageInfo = App.Do( Environment, 'ReturnImageInfo',)
# Create variable; assign it to the 'Description' value in the ImageInfo object.
    NM_Description = str(ImageInfo['Description'])
    print "046====20180814 2102_NM_TkinterGrid NM_Description = " \
           + NM_Description
#Tk() is the window object constructor of a top level widget
    print "049====20180814__NM_TkinterGrid importing the Tkinter module "
#root created as a window object, window object title method can take a string.
    root = Tk() ; root.title("NM_20180814_NM_TkinterGrid")

# Print "053===Call Function '_ymd' ymdhms = " + ymdhms

# Create the Tkinter GUI on the Top Level root frame.
    frame1 = Frame(root, border=4, relief=SUNKEN, padx=1, pady=1, \
                   width = 250,height = 22 )
    frame1.grid()
    frame2 = Frame(root, border=12, relief=RIDGE, padx=1, pady=1, width = 244 )
    frame2.grid()
    frame3 = Frame(root, border=2, relief=RAISED )
    frame3.grid()

    wf1r1c1 = Label(frame1,text="EXISTING IMAGE METADATA: ", \
                    fg="RoyalBlue4", bg="cyan")
    wf1r1c1.grid( row = 1, column = 1, padx=3, pady=3 )
    wf1r1c1.grid( sticky=(N,S,E,W))

    wf1r1c2 = Button(frame1, text="wf1r1c2 \n" + NM_Description , \
                     fg="maroon",  bg="Light Pink", width=67,
                     anchor=W, justify=LEFT, wraplength=399)
    wf1r1c2.grid( row=1, column=2, padx=3, pady=3)

    wf2r4c1 = Label(frame2, \
                    text="MetaData IPTC_Description Field will be "+\
                    "prepended by: '"+ymdhms+
                    "' Date Stamp                        ",
                    font=('Verdana', 7, 'roman'), justify=CENTER, \
                    bg="LightSkyBlue1", fg="Purple", width=93, height=1 )
    wf2r4c1.grid( row = 1, column = 1, padx=3, pady=3 )

# User can insert their own Description Metadata here:
    wf2r5c1 = Text(frame2, background="Linen", foreground="Purple"   )
    wf2r5c1.focus()
    wf2r5c1.focus_force()
    wf2r5c1.focus_set()
    wf2r5c1.grid(row=5, column=1 )
    wf2r5c1.insert(INSERT, ymdhms+"\r\n")    #\r Windows & \n Tk NewLine chars.

# Captures users input from the Text widget when <Alt-s> pressed.
    def get_description():
        Description._description=wf2r5c1.get("1.0","end-1c")
        print "093>>>>> _description = " + Description._description
        root.destroy()
# Quits GUI <Alt-c> pressed.
    def cancel(event=None):
        print"097\\\\  Cancel"
        wf2r5c1.destroy()            #Renders Button inactive_???
        root.destroy()                             #Stops mainloop
        sys.exit("def cancel - exit")              #Exits Python script

    _buttonSave = Button(frame3, height=1, width=15, text="Save", \
                     underline=0, command=get_description)
    _buttonSave.grid(row=1, column=1 )
    _labelSpacer = Label(frame3, text="- - -", bg="Light Salmon" )
    _labelSpacer.grid(row=1, column=2 )
    _buttonCancel = Button(frame3, height=1, width=15, text="Cancel", \
                     underline=0, default="active", command=cancel)
    _buttonCancel.grid(row=1, column=3 )
# Captures clicking the Close Button [ "X" on the Windows Program Title Bar ]
# event and exits the dialog right here. Again many thanks to LeviFiction .
    root.protocol("WM_DELETE_WINDOW", cancel)
    root.bind('<Alt-s>', lambda event: get_description())
    root.bind('<Alt-S>', lambda event: get_description())
    root.bind('<Alt-c>', lambda event: cancel())
    root.bind('<Alt-C>', lambda event: cancel())   #'Alt-x' is case sensitive
    _buttonCancel.bind('<Return>', lambda event: cancel())       #Press 'Enter'

# See "Scripting for Script Author's Guide for SFW explanation.
# And a big thank you to LeviFiction for "telling me where to go!"
# Basically this in effect presents the GUI with the focus on theText widget.
    App.Do( Environment, 'StartForeignWindow', {
            'WindowHandle': int(root.winfo_id())   })

    root.mainloop()

# Check this out in the PSP Scripting API Documentation.
    App.Do( Environment, 'StartForeignWindow', {'WindowHandle':0})

    print "130>>>>> Dropped out of Tkinter, _description = " + \
                    Description._description

# ImageInfo, update the Description Dictionary field with any original text
# and any added text.
    App.Do( Environment, 'ImageInfo', {
            'Description': Description._description + NM_Description,
            'GeneralSettings': {
#                'ExecutionMode': App.Constants.ExecutionMode.Default,
                'ExecutionMode': App.Constants.ExecutionMode.Silent,
                'AutoActionMode': App.Constants.AutoActionMode.Match,
                'Version': ((20,0,0),1)
                }
            })
    print "144>>>>>  ymdhms                     = " + ymdhms
    print "145>>>>>  Description._description   = " + Description._description
    print "146>>>>>  NM_Description             = " + NM_Description

#Write the TIF image .
    name = App.TargetDocument.Name
    print "145<<<<<< _name = " + name
    # FileSaveAs
    App.Do( Environment, 'FileSaveAs', {
            'Encoding': {
                'TIF': {
                    'Compression': App.Constants.TiffCompression.LZW,
                    'Channels': App.Constants.ColorChannels.RGB,
                    'EmbedTIFICC': True
                    }
                },
            'FileName': name,
            'FileFormat': App.Constants.FileFormat.TIF,
            'FormatDesc': u'TIF Tagged Image File Format',
            'WorkingMode': 0,
            'GeneralSettings': {
                'ExecutionMode': App.Constants.ExecutionMode.Default,
                'AutoActionMode': App.Constants.AutoActionMode.AllAlways,
                'Version': ((20,0,0),1)
                },
            'DefaultProperties': []
            })

"""
20180826
This is intended to work on an open .JPG image givng the user the facility to
edit any existing data in the IPTC_Description field [ aka IPTC_Caption ] and
save the resultant old and new text to a .TIF iamge file .

New text will be automatically prefaced with a date stamp in ISO8601 format,
i.e yyyymmdd hh:mm:ss, plus a couple of bits of my own.
In these days of digital I despair at the use of anything else [ not
logical, Captain ]..

A GUI is presented with any existing MetaData displayed and with a widget to
allow more MetaData to be added. You will not be able to edit existing MetaData.

I started this in a desultory fashion in 2018 June and really got stuck in
in the last week or so. I had no knowledge of Python when I started in June.

I found "Python In Easy Steps" by Mike McGrath to be an invaluable primer.
After reading this I expect Mike to call round with a nicely stuffed proverbial
brown envelope!

Finally I must give wholehearted thanks to LeviFiction for his patience in
reading my poorly indented script and the vital pointer to the
StartForeignWindow command [ function ?] to control the focus.

NM
"""
#
wpslo
Posts: 12
Joined: Sat Jun 02, 2018 9:48 pm
operating_system: Windows 7 Professional
System_Drive: C
32bit or 64bit: 32 Bit
motherboard: asus 1025ce
processor: intel Atom
ram: 4gb
Hard_Drive_Capacity: 2 TB

Re: MetaData writing with Python

Post by wpslo »

Here is an updated version. See footnotes at end of script for the changes.

#

Code: Select all

#"[code] insert your script here 
" will preserve formatting when
# posting a script on the forum . No quotes.
# SEE COMMENTS AT END OF SCRIPT TO LEARN "WOT THIS DUZ!!" AND
# FOR A LOG OF UPDATES.
# USE AT OWN RISK AND FEEL FREE TO HACK A COPY ABOUT.

from PSPApp import *
from Tkinter import * #ToolKit Interface module
from re import *
import os
# Here we get the full name of the image and strip off the extension .

imageFilename = os.path.realpath(App.TargetDocument.Title)
print "015 os.path.realpath = " + imageFilename

#*** Target Image Filename Formatting Routines Commence:

patternObj= compile('\s-[0-99]+') #Find " -n" suffix added by PSP, note space!
is_valid = patternObj.findall(imageFilename) #FINDALL method
print "021 patternObj.findall = " + str(patternObj.findall)

if is_valid:
startMeth = is_valid[0]
print "025 is_valid " + str(is_valid[0])
imageFilenameRed = imageFilename.replace(is_valid[0],"")
print "027 imageFilenameRed = " + imageFilenameRed + " is_valid = " \
+ str(len(is_valid))
else:
print "not is_valid"
print "not valid " + str(is_valid)
window = Tk()
window.title("Dodgy_File_Encountered")
frame=Frame(window)
dodgyfile=Label(frame, text="dodgy"+imageFilename)
def dialog(event=NONE):
window.quit()
window.destroy()
# sys.exit("Abort - Dodgy Filename") # NO Crashes PSP
print "Dodgy File Name EXIT "
btnAbort=Button(frame,text="'Click' to Acknowledge & Abort",command=dialog)
btnAbort.pack(side = RIGHT, padx=15)
dodgyfile.pack(side=LEFT, padx=11)
frame.pack(padx = 20, pady=20)
print "window mainloop follows"
window.mainloop()

#*** Target Image Filename Formatting Routines End ***


# Class.attributes [Class variables] declared here to avoid problems with scope.

class Workstore:
# _description = " class._description "
_name = "class._name"
_JPG_switch = _TIF_switch = False #REQUIRED
# _descCharCount = 0
# _wf2r5c1 = "_wf2r5c1"
# _label = "_label"
# _CharCount = 0

_name = imageFilenameRed #Includes full path & edited filename


7
class Ymd:
ymdhms = "ymdhms"
from datetime import datetime
now = datetime.now()
yyyy = str(now.year) ; mm = str(now.month) ; dd = str(now.day)
hh=str(now.hour) ; mi=str(now.minute) ; ss=str(now.second)
mm=mm.zfill(2);dd=dd.zfill(2);hh=hh.zfill(2);mi=mi.zfill(2);ss=ss.zfill(2)
ymdhms = "NM_"+yyyy+mm+dd+"@"+hh+":"+mi+":"+ss #"\r\n" newline characters
# Class instance [copy] of Ymd.ymdhms created here:
ymdhms = Ymd.ymdhms

def ScriptProperties():
return {
'Author': u'',
'Copyright': u'',
'Description': u'',
'Host': u'PaintShop Pro',
'Host Version': u'20.00'
}

def Do(Environment):
# EnableOptimizedScriptUndo
App.Do( Environment, 'EnableOptimizedScriptUndo', {
'GeneralSettings': {
'ExecutionMode': App.Constants.ExecutionMode.Default,
'AutoActionMode': App.Constants.AutoActionMode.Match,
'Version': ((20,0,0),1)
}
})

# ImageInfo: A PSP method. Returns metadata as a Dictionary .
ImageInfo = App.Do( Environment, 'ReturnImageInfo',)
# Create variable; assign it to the 'Description' value in the ImageInfo object.
IPTC_Description = str(ImageInfo['Description'])
_descCharCount = len(IPTC_Description)

# _name,ext = os.path.split(App.TargetDocument.Title)
# print "077 _name = " + _name


# print "073 IPTC_Description = " \
# + IPTC_Description \
# + "_descCharCount = " + str(_descCharCount)

#Tk() is the window object constructor of a top level widget
print "085 Importing the Tkinter module "
#root created as a window object, window object title method can take a string.
root = Tk() ; root.title("20180911_1755_FileFormatWordCount")

# _sv = StringVar()
_iv = _ivTotal = IntVar()

# Character count functions in the Text widget
def on_every_keyboard_input(_event):
string_in_text = Workstore._wf2c1r5.get('1.0', 'end-1c')
_iv = len(string_in_text)
wf1c2r3['text'] = len(string_in_text) #_iv
_ivTotal = _iv + _descCharCount
wf1c2r4['text'] = _ivTotal
# print "099 _iv = " + str(_iv) + "_ivTotal = " + str(_ivTotal)

# Create the Tkinter GUI on the Top Level root frame.
frame1 = Frame(root, border=4, relief=SUNKEN, padx=1, pady=1, \
width = 250, height = 11 )
frame1.grid()
frame2 = Frame(root, border=12, relief=RIDGE, padx=1, pady=1, width = 244 )
frame2.grid()
frame3 = Frame(root, border=2, relief=RAISED )
frame3.grid()

wf1c1r1 = Label(frame1,text= "EXISTING IMAGE METADATA:", \
fg="RoyalBlue4", bg="cyan", \
justify=RIGHT )
wf1c1r1.grid( column = 1, row = 1, padx=3, pady=3, sticky=W, columnspan=2)

wf1c1r2 = Label(frame1,text= "Character Count = " , \
fg="RoyalBlue4", bg="cyan", \
justify=LEFT)
wf1c1r2.grid( column = 1, row = 2, padx=3, pady=2, sticky=W)

wf1c2r2 = Label(frame1,text= str(_descCharCount), \
fg="RoyalBlue4", bg="cyan", \
justify=LEFT)
wf1c2r2.grid( column = 2, row = 2, padx=3, pady=2, sticky=E)

wf1c1r3 = Label(frame1,text= "Characters Added = " , \
fg="RoyalBlue4", bg="cyan", \
justify=LEFT)
wf1c1r3.grid( column = 1, row = 3, padx=5, pady=1, sticky=W)

wf1c2r3 = Label(frame1,text= str( _iv.get()), \
fg="RoyalBlue4", bg="cyan", \
justify=LEFT)
wf1c2r3.grid( column = 2, row = 3, padx=3, pady=2, sticky=E)

wf1c1r4 = Label(frame1,text= "Total Characters for Update = " , \
fg="RoyalBlue4", bg="cyan", \
justify=LEFT)
wf1c1r4.grid( column = 1, row = 4, padx=5, pady=1)

wf1c2r4 = Label(frame1,text= str( _iv.get()+_descCharCount), \
fg="RoyalBlue4", bg="cyan", \
justify=LEFT)
wf1c2r4.grid( column = 2, row = 4, padx=3, pady=2, sticky=E)

wf1c3r1 = Text(frame1, \
fg="maroon", bg="Light Pink", width=67, height=7)
wf1c3r1.insert(1.0, IPTC_Description)
wf1c3r1.grid( row=1, column=3, padx=3, pady=3, rowspan=4)

# Place a scrollbar on the right hand side of the text widget
wf1c4r1_SB = Scrollbar(frame1, command=wf1c3r1.yview)
wf1c4r1_SB.grid( column=4, row=1 )

wf2c1r1 = Label(frame2, \
text="MetaData IPTC_Description Field will be "+\
"prepended by: '"+ymdhms+
"' Date Stamp ",
font=('Verdana', 7, 'roman'), justify=CENTER, \
bg="LightSkyBlue1", fg="Purple", width=93, height=1 )
wf2c1r1.grid( column = 1, row = 1, padx=3, pady=3 )


# User can insert their own Description Metadata here:
Workstore._wf2c1r5 = Text(frame2, background="Linen", foreground="Purple", \
height=7 )
Workstore._wf2c1r5.focus()
Workstore._wf2c1r5.focus_force()
Workstore._wf2c1r5.focus_set()
Workstore._wf2c1r5.grid(column=1, row=5)
Workstore._wf2c1r5.insert(INSERT, ymdhms+"\r\n")
print "128 Just before lambda event"
Workstore._wf2c1r5.bind('<KeyPress>' , on_every_keyboard_input)
Workstore._wf2c1r5.bind('<KeyRelease>', on_every_keyboard_input)

# Captures users input from the Text widget when <Alt-'a key'> is pressed.
# Note that the Caption/Description IPTC field has a 2000 character limit .
def get_description_JPG():
Workstore._description=Workstore._wf2c1r5.get("1.0","end-1c")
Workstore._JPG_switch = True # Allows JPG FileSaveAs to run
print "100>>>>> _description JPG = " + Workstore._description
root.destroy()
def get_description_TIF():
Workstore._description=Workstore._wf2c1r5.get("1.0","end-1c")
Workstore._TIF_switch = True # Allows TIF FileSaveAs to run
print "105>>>>> _description TIF = " + Workstore._description
root.destroy()
# Quits GUI <Alt-c> pressed.
def cancel(event=None):
print"215 Cancel"
Workstore._wf2c1r5.destroy() #Renders Button inactive_???
root.destroy() #Stops mainloop
sys.exit("def cancel - exit") #Exits Python script
# Assembles MetaData for JPG save
_buttonSaveJPG = Button(frame3, height=1, width=15, text="Save JPG", \
underline=5, command=get_description_JPG)
_buttonSaveJPG.grid(row=1, column=1 )
# SpacerA betwen the Buttons
_labelSpacer = Label(frame3, text="- - -", bg="Light Salmon" )
_labelSpacer.grid(row=1, column=2 )
# Assembles MetaData for TIF save
_buttonSaveTIF = Button(frame3, height=1, width=15, text="Save TIF", \
underline=5, command=get_description_TIF)
_buttonSaveTIF.grid(row=1, column=3 )
# SpacerB betwen the Buttons
_labelSpacer = Label(frame3, text="- - -", bg="Light Salmon" )
_labelSpacer.grid(row=1, column=4 )
# Cancel Button
_buttonCancel = Button(frame3, height=1, width=15, text="Cancel", \
underline=0, default="active", command=cancel)
_buttonCancel.grid(row=1, column=5 )
# Captures clicking the Close Button [ "X" on the Windows Program Title Bar ]
# event and exits the dialog right here. Again many thanks to LeviFiction .
root.protocol("WM_DELETE_WINDOW", cancel)
root.bind('<Alt-j>', lambda event: get_description_JPG())
root.bind('<Alt-J>', lambda event: get_description_JPG())
_buttonSaveJPG.bind('<Return>',lambda event: get_description_JPG()) #Enter
root.bind('<Alt-t>', lambda event: get_description_TIF())
root.bind('<Alt-T>', lambda event: get_description_TIF())
_buttonSaveTIF.bind('<Return>',lambda event: get_description_TIF()) #Enter
root.bind('<Alt-c>', lambda event: cancel())
root.bind('<Alt-C>', lambda event: cancel())
_buttonCancel.bind('<Return>', lambda event: cancel()) #Press 'Enter'

# See "Scripting for Script Author's Guide for SFW explanation.
# And a big thank you to LeviFiction for "telling me where to go!"

# This presents the GUI with the focus on theText widget.
App.Do( Environment, 'StartForeignWindow', {
'WindowHandle': int(root.winfo_id()) })

root.mainloop()

# Check this out in the PSP Scripting API Documentation.
App.Do( Environment, 'StartForeignWindow', {'WindowHandle':0})

print "216>>>>> Dropped out of Tkinter, _description = " + \
Workstore._description

# ImageInfo, update the Description Dictionary field with any original text
# and any added text.
App.Do( Environment, 'ImageInfo', {
'Description': Workstore._description + IPTC_Description,
'GeneralSettings': {
# 'ExecutionMode': App.Constants.ExecutionMode.Default,
'ExecutionMode': App.Constants.ExecutionMode.Silent,
'AutoActionMode': App.Constants.AutoActionMode.Match,
'Version': ((20,0,0),1)
}
})
print "250 ymdhms = " + ymdhms
print "251 Workstore._description = " + Workstore._description
print "252 IPTC_Description = " + IPTC_Description

#Write the image name .
# _name = len.App.TargetDocument.Name, -4
print "256 _name = " + _name


# FileSaveAs JPG
print "260 JPG_switch = " + str(Workstore._JPG_switch)
print "261 TIF_switch = " + str(Workstore._TIF_switch)
if Workstore._JPG_switch:
Workstore._JPG_switch = False
App.Do( Environment, 'FileSaveAs', {
'Encoding': {
'JPG': {
'Variant': App.Constants.JpegFormat.Standard,
'CompressionFactor': 1,
'ChromaSubSampling': \
App.Constants.ChromaSubSampling.YCC_1x1_1x1_1x1,
'EXIF': True,
'EmbedJPGICC': True
}
},
'FileName': _name+".jpg", # Add extension
# 'FileName': App.TargetDocument.Name,
'FileFormat': App.Constants.FileFormat.JPG,
'FormatDesc': u'JPG JPEG ',
'WorkingMode': 0,
'GeneralSettings': {
'ExecutionMode': App.Constants.ExecutionMode.Silent,
'AutoActionMode': App.Constants.AutoActionMode.PromoteAsk,
'Version': ((20,0,0),1)
},
'DefaultProperties': []
})
# _name = _name+".jpg"
# _name.close()

# FileSaveAs TIF
print "289 JPG_switch = " + str(Workstore._JPG_switch)
print "290 TIF_switch = " + str(Workstore._TIF_switch)
if Workstore._TIF_switch:
Workstore._TIF_switch = False
App.Do( Environment, 'FileSaveAs', {
'Encoding': {
'TIF': {
'Compression': App.Constants.TiffCompression.LZW,
'Channels': App.Constants.ColorChannels.RGB,
'EmbedTIFICC': True
}
},
'FileName': _name+".tif", # Add extension
'FileFormat': App.Constants.FileFormat.TIF,
'FormatDesc': u'TIF Tagged Image File Format',
'WorkingMode': 0,
'GeneralSettings': {
'ExecutionMode': App.Constants.ExecutionMode.Silent,
'AutoActionMode': App.Constants.AutoActionMode.AllAlways,
'Version': ((20,0,0),1)
},
'DefaultProperties': []
})


"""
20180911
When opening a .ARW [ Sony RAW image file ] I noticed that PSP seems to
stick a " -n" suffix on the filename and will save it with this filename .
" -n": this is "space dash and n" is a number .
Presumably this is to protect the original image.

App.TargetDocument.Name is used to get the image directory path and filename.

I have used the "findall" method to check for this suffix .
If the suffix is found it is stripped off using "replace" .
Other methods are doubtless available.

If the suffix is not found a message is displayed "Dodgy_File_Encountered".
You will crash out from here . Perhaps LeviFiction would kindly supply a
more elegant solution?

Some keyboard shortcuts have been added and the GUI reduced in size.


CURRENTLY THERE IS NO WARNING IF AN EXISTING FILE WILL BE OVERWRITTEN!!


20180830
Options to save to JPG or TIF added.
Scrollbar added to the Text Input widget.
Button widget changed to Text widget to gain the Scrollbar option .
Support for <Enter> added to the three click option buttons.


20180826
This is intended to work on an open .JPG image givng the user the facility to
edit any existing data in the IPTC_Description field [ aka IPTC_Caption ] and
save the resultant old and new text to a .TIF iamge file .

New text will be automatically prefaced with a date stamp in ISO8601 format,
i.e yyyymmdd hh:mm:ss, plus a couple of bits of my own.
In these days of digital I despair at the use of anything else [ not
logical, Captain ]..

A GUI is presented with any existing MetaData displayed and with a widget to
allow more MetaData to be added. You will not be able to edit existing MetaData.

I started this in a desultory fashion in 2018 June and really got stuck in
in the last week or so. I had no knowledge of Python when I started in June.

I found "Python In Easy Steps" by Mike McGrath to be an invaluable primer.
After reading this I expect Mike to call round with a nicely stuffed proverbial
brown envelope!

Finally I must give wholehearted thanks to LeviFiction for his patience in
reading my poorly indented script and the vital pointer to the
StartForeignWindow command [ function ?] to control the focus.

NM
"""
#[/code]
Post Reply