Forum Archive

using appex to modify a photo creation date directly

jmv38

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!

zipit

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.

[1] https://pypi.python.org/pypi/piexif

cvp

With piexif.py, you can change some Exif of a PIL image and of course you can save a PIL image, thus..

omz

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
omz

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)
jmv38

thanks a lot for the quick answer! I'll try your code asap and feedback. Thanks!

jmv38

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.

jmv38

@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!

omz

@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...
jmv38

looks great! I'll try it and feedback. Thanks!
EDIT: seems to work as expected. Thanks!

jmv38

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()
jmv38

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" : ""
    }
  }
]
jmv38

it works fine with ios
but on flicker the image order is lost again
anyone knows how flicker orders the photos?
thanks!

jmv38

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.

ccc

Clean reboot of Pythonista?

jmv38

@ccc i've reboot pythonista, and suppressed all open apps, and it works again. Thanks for the advice!

average

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
cvp

@average install piexif.py from here
and try this script

from   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)