Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
using appex to modify a photo creation date directly
-
hello
from the examples i can read the exif data and date, but how can i modify the photo exif and/or image in place?
appex.get_attachments('public.jpeg') returns strings of adress, but they are not the same as the local id needed to create an asset.. So i dont find the trick to do it. I often have problems with paths...
thanks for your help! -
Hi, I am not quite sure that I do understand your question correctly. To deal with exif (PIL) access in a more sane way you can use piexif [1]. Modifying the exif of the provided PIL Image alone won't change a thing for the images in the hosting app though, you would have to write the data back. I somehow doubt that this is possible, as iOS is usually quite anal when it comes to cross app write access. But I might be wrong.
-
With piexif.py, you can change some Exif of a PIL image and of course you can save a PIL image, thus..
-
You cannot modify a photo in-place that was passed to the app extension.
You can however use the
photos
module for that. Here's a simple example that lets you pick a photo, and set its creation date with a date picker:import photos, dialogs asset = photos.pick_asset() new_date = dialogs.datetime_dialog('Set Creation Date') asset.creation_date = new_date
-
If you really need to modify photos from the app extension, here's a hackish way you could do that. It's somewhat slow, and relies on undocumented (private) API, but it should work:
import appex import photos from objc_util import ObjCInstance def assets_for_attachments(attachments): all_assets = photos.get_assets() matching_assets = [] for a in all_assets: path = str(ObjCInstance(a).pathForOriginalFile()) if path in attachments: matching_assets.append(a) return matching_assets attachments = appex.get_attachments() assets = assets_for_attachments(attachments) # Now you can use the photos module to manipulate the assets... (as in previous example)
-
thanks a lot for the quick answer! I'll try your code asap and feedback. Thanks!
-
just a quick feedback: it works perfectly. I will post my final code when ready (a couple days). The idea is to have a tool to organize my photos from the photo app, in the order i want, not the order Apple wants, by changing the creation date. I'm pretty sure that this feature is one of the most wanted (photo ordering), for many years, and i still dont understand why they wont provide it to us.
-
@omz UPDATE: it works for original files only. I the selected photo is a modified one, then i dont get any asset for it. Looking at the pathname i guess i could hack it, but there must be a better way. Any idea? Thanks!
-
@jmv38 This is the best I could come up with:
import appex import photos from objc_util import ObjCInstance def assets_for_attachments(attachments): all_assets = photos.get_assets() matching_assets = [] for a in all_assets: objc_asset = ObjCInstance(a) path_orig = str(objc_asset.pathForOriginalFile()) path_edit = str(objc_asset.pathForFullsizeRenderImageFile()) if path_orig in attachments or path_edit in attachments: matching_assets.append(a) return matching_assets attachments = appex.get_attachments() assets = assets_for_attachments(attachments) # Now you can use the photos module to manipulate the assets...
-
looks great! I'll try it and feedback. Thanks!
EDIT: seems to work as expected. Thanks! -
my code for anyone interested
''' Program to order photos in ios photo app. author jmv38, may 28th 2017. this program is intended to be called from photo app after selecting some photos it will set all photos to same date+time (smallest or biggest) or it will add/subtract some seconds to the photos. It uses exif.ui interface, and you have to add exif.py to the callable scripts for auick access. ''' import appex import photos import time import datetime from objc_util import ObjCInstance import ui import json def assets_for_attachments(attachments): ''' convert all photo attachments (from appex) to a list of photo assets it uses a precomputed dictionnary managed by load_assets_for_attachments() if the attachement is not found in dictionnary, the json file is update (once) this dramatically speeds up the process (json file is updated only when needed) ''' assetsUpdated = False localIds = load_assets_for_attachments() matching_assets = [] if type(attachments) != type([]): attachments = [attachments] for a in attachments: if (not a in localIds) and not assetsUpdated: save_assets_for_attachments() assetsUpdated = True localIds = load_assets_for_attachments() if a in localIds: id = localIds[a] matching_assets.append(photos.get_asset_with_local_id(id)) return matching_assets def save_assets_for_attachments(): ''' tricks from OMZ to link assets and attachements generates a dictionnary saved in a jason text file: dic[attachement]=asset.local_id ''' all_assets = photos.get_assets() asset_path = {} for a in all_assets: objc_asset = ObjCInstance(a) path_orig = str(objc_asset.pathForOriginalFile()) path_edit = str(objc_asset.pathForFullsizeRenderImageFile()) id = a.local_id asset_path[path_orig] = id asset_path[path_edit] = id f = open("exif.txt", "w", encoding="utf-8") json.dump(asset_path, f) f.close() def load_assets_for_attachments(): ''' loads a json text file of a dictionnary: dic[attachement]=asset.local_id ''' try: f = open("exif.txt", "r", encoding="utf-8") localIds = json.load(f) f.close() except: # it assumes the only error is when the file doesnt exist yet save_assets_for_attachments() f = open("exif.txt", "r", encoding="utf-8") localIds = json.load(f) f.close() return localIds def delta(s): ''' shift the asset creation time by the specified seconds ''' for asset in assets: asset.creation_date = asset.creation_date + datetime.timedelta(seconds = s) def plus10(sender): delta(10) def plus1(sender): delta(1) def minus10(sender): delta(-10) def minus1(sender): delta(-1) def minDate(sender): '''Find oldest date/time and Apply it to all selected photos ''' mini = assets[0].creation_date for asset in assets: current = asset.creation_date if current < mini: mini = current for asset in assets: asset.creation_date = mini def maxDate(sender): '''Find most recent date/time and Apply it to all selected photos ''' maxi = assets[0].creation_date for asset in assets: current = asset.creation_date if current > maxi: maxi = current for asset in assets: asset.creation_date = maxi def testTool(sender): # just for interactive testing pass def main(): global testing testing = False if not appex.is_running_extension() and not testing: print('This script is intended to be run from the sharing extension.') return global attachments attachments = appex.get_attachments() global assets assets = assets_for_attachments(attachments) v = ui.load_view('exif') v.present('sheet') if __name__ == '__main__': main()
-
the ui file (name it exit.ui)
[ { "selected" : false, "frame" : "{{0, 0}, {500, 472}}", "class" : "View", "nodes" : [ { "selected" : false, "frame" : "{{42, 54}, {186, 58}}", "class" : "Button", "nodes" : [ ], "attributes" : { "action" : "minDate", "frame" : "{{210, 220}, {80, 32}}", "title" : "min date & time", "uuid" : "37864F72-5867-429F-9B5B-EB37F9E8939D", "font_bold" : true, "class" : "Button", "name" : "button0", "font_size" : 15 } }, { "selected" : false, "frame" : "{{289, 137}, {186, 58}}", "class" : "Button", "nodes" : [ ], "attributes" : { "action" : "plus10", "frame" : "{{210, 220}, {80, 32}}", "title" : "plus 10 seconds", "uuid" : "37864F72-5867-429F-9B5B-EB37F9E8939D", "font_bold" : true, "class" : "Button", "name" : "plus10", "font_size" : 15 } }, { "selected" : false, "frame" : "{{42, 137}, {186, 58}}", "class" : "Button", "nodes" : [ ], "attributes" : { "action" : "minus10", "frame" : "{{210, 220}, {80, 32}}", "title" : "minus 10 seconds", "uuid" : "37864F72-5867-429F-9B5B-EB37F9E8939D", "font_bold" : true, "class" : "Button", "name" : "plus10", "font_size" : 15 } }, { "selected" : false, "frame" : "{{289, 203}, {186, 58}}", "class" : "Button", "nodes" : [ ], "attributes" : { "action" : "plus1", "frame" : "{{210, 220}, {80, 32}}", "title" : "plus 1 second", "uuid" : "37864F72-5867-429F-9B5B-EB37F9E8939D", "font_bold" : true, "class" : "Button", "name" : "plus10", "font_size" : 15 } }, { "selected" : false, "frame" : "{{289, 54}, {186, 58}}", "class" : "Button", "nodes" : [ ], "attributes" : { "action" : "maxDate", "frame" : "{{210, 220}, {80, 32}}", "title" : "max date & time", "uuid" : "37864F72-5867-429F-9B5B-EB37F9E8939D", "font_bold" : true, "class" : "Button", "name" : "button0", "font_size" : 15 } }, { "selected" : false, "frame" : "{{42, 203}, {186, 58}}", "class" : "Button", "nodes" : [ ], "attributes" : { "action" : "minus1", "frame" : "{{210, 220}, {80, 32}}", "title" : "minus 1 second", "uuid" : "37864F72-5867-429F-9B5B-EB37F9E8939D", "font_bold" : true, "class" : "Button", "name" : "plus10", "font_size" : 15 } }, { "selected" : true, "frame" : "{{161, 285.5}, {186, 58}}", "class" : "Button", "nodes" : [ ], "attributes" : { "action" : "testTool", "frame" : "{{210, 220}, {80, 32}}", "title" : "tests", "uuid" : "37864F72-5867-429F-9B5B-EB37F9E8939D", "font_bold" : true, "class" : "Button", "name" : "tester", "font_size" : 15 } } ], "attributes" : { "enabled" : true, "background_color" : "RGBA(1.000000,1.000000,1.000000,1.000000)", "tint_color" : "RGBA(0.000000,0.478000,1.000000,1.000000)", "border_color" : "RGBA(0.000000,0.000000,0.000000,1.000000)", "flex" : "" } } ]
-
it works fine with ios
but on flicker the image order is lost again
anyone knows how flicker orders the photos?
thanks! -
HELP!
I've used the script hundreds of times and it worked perfectly.
But now it doesnt work anymore. it doesnt do anything, No error, but nothing changes.
I've updated ios to lastest version, could have it changed something?
Thanks for your help. -
Clean reboot of Pythonista?
-
@ccc i've reboot pythonista, and suppressed all open apps, and it works again. Thanks for the advice!
-
cool but this doesn't seem to work fully. this changes the creation_date but how do you change the "taken date" as if the image was taken (not edited) just now?
import photos, dialogs asset = photos.pick_asset() new_date = dialogs.datetime_dialog('Set Creation Date') asset.creation_date = new_date
-
@average install
piexif.py
from here
and try this scriptfrom datetime import datetime import dialogs from objc_util import ObjCInstance import os import photos import piexif # https://github.com/hMatoba/Piexif/tree/master/piexif def exif_as_str(exif_val): exif_str = str(exif_val) if len(exif_str) >= 3: if exif_str[:2] == "b'" and exif_str[-1] == "'": exif_str = exif_str[2:-1] return exif_str def main(): # select photo asset = photos.pick_asset() # get its path path = str(ObjCInstance(asset).pathForOriginalFile()) #print(path) # get photo exifs exif_info = piexif.load(path) exif_str = exif_as_str(exif_info['Exif'][piexif.ExifIFD.DateTimeOriginal]) #print('photo taken date/time',exif_str) # get new taken date-time new_date = dialogs.datetime_dialog(title=str(exif_str)) dt_str = datetime.strftime(new_date,'%Y:%m:%d %H:%M:%S') # update exif of taken datetime exif_info['Exif'][piexif.ExifIFD.DateTimeOriginal] = dt_str exif_str = exif_as_str(exif_info['Exif'][piexif.ExifIFD.DateTimeOriginal]) #print('modified photo taken date/time',exif_str) # create new photo with modified taken date-time exif_bytes = piexif.dump(exif_info) new_path = 'temp.jpg' piexif.insert(exif_bytes,path,new_file=new_path) photos.create_image_asset(new_path) os.remove(new_path) #--- protect for import if __name__ == '__main__': main()
If any error, please forgive me and don't forget I'm in holidays In Bourgogne (🍷=>bugs)