#!/usr/bin/python

# tz_jpg.py
#       --copyright--                   Copyright 2007 (C) Tranzoa, Co. All rights reserved.    Warranty: You're free and on your own here. This code is not necessarily up-to-date or of public quality.
#       --url--                         http://www.tranzoa.net/tzpython/
#       --email--                       pycode is the name to send to. tranzoa.com is the place to send to.
#       --bodstamps--
#       June 26, 2007           bar
#       June 27, 2007           bar
#       June 28, 2007           bar
#       July 2, 2007            bar
#       July 2, 2007            bar     lat/long/altitude stuff
#       July 21, 2007           bar     set_file_date_time_from_exif()
#                                       change a marker names and use DATE_TIME_DIGITIZED_MARKER is the preferred date/time
#       July 27, 2007           bar     able to handle time offsets using tz_parse_time
#       July 28, 2007           bar     import os at top
#                                       fix a bad file name variable
#       November 18, 2007       bar     turn on doxygen
#       November 27, 2007       bar     insert boilerplate copyright
#       March 8, 2008           bar     jpeg_data
#       May 17, 2008            bar     email adr
#       August 28, 2008         bar     basestring instead of StringType because of unicode strings and others
#       September 29, 2008      bar     comment
#       October 11, 2008        bar     find why the deprecated warning in format (altitude data was float)
#       March 21, 2010          bar     play with exit time stuff, but leave it alone since we don't put date/time stamps in the file yet
#       May 14, 2010            bar     --test
#                                       allow exif items to be format zero if we're not strict (ImageMagick convert creates one)
#       June 15, 2011           bar     altitude is signed int
#       November 29, 2011       bar     pyflake cleanup
#       --eodstamps--
##      \file
#
#
#       Do what I need to do with .jpg files.
#
#       Specifically, this code can update GPS information (any exif information, in fact - including adding new ifd tables and trees)
#
#
#       See the test code to find out about looking up exif data.
#           Items are found by looking them up with a long number that gives their paths down through the IFD tables.
#
#       Data comes back from item.get_values(), which returns an array of values (which may only have one element).
#           ASCII strings have a trailing null stripped.
#           Likewise, if you set an ASCII string without a null, one will be added.
#           Ratios go in and out doubled up in       [ [ numerator, denominator ], ... ]        form.
#           Endian-ness is handled beneath the covers.
#
#
#       Bugs:
#
#           PIL won't read the updated GPS information unless there was already GPS info in the file.
#             There are two problems with the PIL code:
#               1) It doesn't read the 1st table's position. It just assumes the table is at 8.
#               2) It doesn't read through the daisy chain of tables. It only reads the 1st one.
#             PIL could be "fixed" by duping the GPS info to the SUBIFD_MARKER chain, if it's in the 1st ifd, I think.
#
#       Todo:
#
#           Spin the exif stuff off to a separate .py file.
#           Able to put date/time exif information in file.
#           Compute the ratios to put in values. Break them out when going back to the exif data.
#           Make a better way of indexing the exif items than the concatenated binary scheme used now.
#               Treeless, or partial tree, lookup would be very nice. So that the coder only needs to know the 16-bit tag and parent, if there is one. (e.g. Parent, GPS. Otherwise, GPS items are ambiguous.)
#               Use names.
#           Able to delete exif items.
#           Some of the standard sub-IFD tables aren't parsed/handled. (e.g. makers)
#           Attach a maker block for ourselves when we write out the file, and especially when we create the exif data.
#               Use tags like 'Vv' and use ascii data in "key = value" form. (Golly the exif format is a time-sink, bug-attracting crock!)
#
#           EXIF data in XML form.
#
#


import  copy
import  os
import  struct
import  sys
import  time
import  traceback

from    types           import  ListType, TupleType, IntType, LongType, FloatType

import  replace_file
import  tz_parse_time



KEEP_IFD0_AT_8      =   False                   # False allows the first IFD to move to another spot. But probably some programs won't like that. If they don't, they will show only overlaid data, not new or larger things that are linked off of IFD0


VERBOSE             =   0
WRITE_TO_FILE       =   False



JPG_BEGIN_MARKER    =   "\xff\xd8"
JPG_END_MARKER      =   "\xff\xd9"
JPG_START_OF_SCAN   =   "\xff\xda"
JPG_APP0            =   "\xff\xe0"
JPG_APP1            =   "\xff\xe1"

MARKER_LEN          =   len(JPG_BEGIN_MARKER)




NO_DATA_MARKERS     =   {
                            "\xff\xc8"          : True,
                            "\xff\xd0"          : True,
                            "\xff\xd1"          : True,
                            "\xff\xd2"          : True,
                            "\xff\xd3"          : True,
                            "\xff\xd4"          : True,
                            "\xff\xd5"          : True,
                            "\xff\xd6"          : True,
                            "\xff\xd7"          : True,
                            JPG_BEGIN_MARKER    : True,
                            JPG_END_MARKER      : True,
                            "\xff\xf0"          : True,
                            "\xff\xf1"          : True,
                            "\xff\xf2"          : True,
                            "\xff\xf3"          : True,
                            "\xff\xf4"          : True,
                            "\xff\xf5"          : True,
                            "\xff\xf6"          : True,
                            "\xff\xf7"          : True,
                            "\xff\xf8"          : True,
                            "\xff\xf9"          : True,
                            "\xff\xfa"          : True,
                            "\xff\xfb"          : True,
                            "\xff\xfc"          : True,
                            "\xff\xfd"          : True,
                        }

JFIF                =   "JFIF\x00"





EXIF_HDR            =   "Exif\x00\x00"


EXIF_BYTE_FORMAT                =  1
EXIF_STRING_FORMAT              =  2
EXIF_U16_FORMAT                 =  3
EXIF_U32_FORMAT                 =  4
EXIF_U32_RATIO_FORMAT           =  5
EXIF_CHAR_FORMAT                =  6
EXIF_UNDEF_FORMAT               =  7
EXIF_I16_FORMAT                 =  8
EXIF_I32_FORMAT                 =  9
EXIF_I32_RATIO_FORMAT           = 10
EXIF_FLOAT_FORMAT               = 11
EXIF_DOUBLE_FORMAT              = 12


EXIF_LE_ITEM_FORMATS    =   [
                                [ None, 1, 1 ],
                                [ 'B',  1, 1 ],
                                [ "",   1, 1 ],
                                [ 'H',  2, 1 ],
                                [ 'I',  4, 1 ],
                                [ 'I',  8, 2 ],
                                [ 'b',  1, 1 ],
                                [ None, 1, 1 ],
                                [ 'h',  2, 1 ],
                                [ 'i',  4, 1 ],
                                [ 'i',  8, 2 ],
                                [ 'f',  4, 1 ],
                                [ 'd',  8, 1 ],
                            ]

def write_exif_val(efc, fc, values) :
    """
        Return a string with the values converted to file-format according to the given format.
    """

    try :
        values[0]
    except :
        values  = [ values ]

    efc    += fc

    s       = ""
    for i in xrange(len(values)) :
        try :
            s  += struct.pack(efc, values[i])           # got deprecation warning float-instead-of-int from geocoding img_0974 thru img_1417
        except :
            if  VERBOSE :
                print "whoa!!!", str(type(values[i]))
            raise
        pass

    return(s)


def read_exif_val(efc, fc, data, di = 0, cnt = 1) :
    """
        Return an single value or an array of values from file data.
    """

    efc    += fc * cnt
    ln      = struct.calcsize(efc)

    r       = struct.unpack(efc, data[di : di + ln])
    if  cnt != 1 :
        return(r)

    return(r[0])




def data_length_info(format, values) :
    """
        Return complete information of how many bytes of the file the exif item's values will take.
    """

    fc          = EXIF_LE_ITEM_FORMATS[format][0]
    iln         = EXIF_LE_ITEM_FORMATS[format][1]

    cnt         = len(values)
    length      = iln * cnt

    values      = copy.deepcopy(values)

    if  fc == ""    :
        if  not values[0].endswith("\x00") :
            values[0]  += "\x00"
        length  = cnt   = len(values[0])

    return( ( length, fc, cnt, values ) )


def data_length(format, values) :
    """
        Return how many bytes of the file the exif item's values will take.
    """

    return(data_length_info(format, values)[0])



def make_exif_data(efc, format, values) :
    """
        Return file-format data and the count value (used in the IFD entry for this data) for an exif item's values.
    """


    ( length, fc, cnt, values ) = data_length_info(format, values)

    if not cnt      :
        data        = ""
    elif fc == ""   :
        data        = values[0]
    elif not fc     :
        data        = "".join(values)
    else :
        if  EXIF_LE_ITEM_FORMATS[format][2] == 2 :
            va      = []
            for vv in values :
                va += vv
            values  = va

        data = write_exif_val(efc, fc, values)

        if  len(data) != length :
            raise ValueError("Bug: exif length problem len(data):%u != length:%u!" % ( len(data), length ) )
        pass

    return( ( data, cnt ) )





#
#
#   Handy page with list of exif tags:
#
#           http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/EXIF.html
#
#


MAKE_MARKER                         =   0x010f
SOFTWARE_MARKER                     =   0x0131
SUBIFD_MARKER                       =   0x8769
INTEROP_MARKER                      =   0xa005
GPS_EXIF_MARKER                     =   0x8825          # note: this is put in the high word of GPS items' tags so that they don't collide with any other tag
EXIF_VERSION_MARKER                 =   0x9000
DATE_TIME_MODIFIED_MARKER           =   0x0132
DATE_TIME_MODIFIED_TIME_ZONE_MARKER =   0x882a          # 1 or 2 values 1stvalue: time zone offset of DateTimeOriginal (0x9003) from GMT in hours   2ndvalue: time zone offset of ModifyDate (0x0132)
DATE_TIME_ORIGINAL_MARKER           =   0x9003
DATE_TIME_DIGITIZED_MARKER          =   0x9004


#
#
#       Seems to have good info:
#           http://www.kanzaki.com/ns/exif
#
#
GPS_VERSION_MARKER              =   0x0000
GPS_LATITUDE_REF_MARKER         =   0x0001
GPS_LATITUDE_MARKER             =   0x0002
GPS_LONGITUDE_REF_MARKER        =   0x0003
GPS_LONGITUDE_MARKER            =   0x0004
GPS_ALTITUDE_REF_MARKER         =   0x0005
GPS_ALTITUDE_MARKER             =   0x0006
GPS_TIME_STAMP_MARKER           =   0x0007
GPS_SATELLITES_MARKER           =   0x0008
GPS_STATUS_MARKER               =   0x0009
GPS_MEASURE_MODE_MARKER         =   0x000a
GPS_DOP_MARKER                  =   0x000b
GPS_SPEED_REF_MARKER            =   0x000c
GPS_SPEED_MARKER                =   0x000d
GPS_TRACK_REF_MARKER            =   0x000e
GPS_TRACK_MARKER                =   0x000f
GPS_IMG_DIRECTION_REF_MARKER    =   0x0010
GPS_IMG_DIRECTION_MARKER        =   0x0011
GPS_MAP_DATUM_MARKER            =   0x0012
GPS_DEST_LATITUDE_REF_MARKER    =   0x0013
GPS_DEST_LATITUDE_MARKER        =   0x0014
GPS_DEST_LONGITUDE_REF_MARKER   =   0x0015
GPS_DEST_LONGITUDE_MARKER       =   0x0016
GPS_DEST_BEARING_REF_MARKER     =   0x0017
GPS_DEST_BEARING_MARKER         =   0x0018
GPS_DEST_DISTANCE_REF_MARKER    =   0x0019
GPS_DEST_DISTANCE_MARKER        =   0x001a



def tree_tag_to_tag(tree, tag) :
    """
        We track the items using a combination of the "tree" (which IFD in the top-level chain) that the item is in - and a 'tag' that contains a chain of marker values.
        The low word of 'tag' is the actually marker for the item.
        The rest of the information - the tree and the high words - are there to make the tag unique and locatable.
    """

    if  tree :
        tree        = long(tree)
        m           = 0xffffL
        while tag & m :
            tree    = tree << 16
            m       = m    << 16
        pass

    return(tree | tag)



class   a_new_exif_item(object) :

    def __init__(me, tree, tag, format, values, di = 0, do = 0, allow_zero_format = True) :

        me.do           = do                                # the TIFF data may be offset in a larger chunk of data (in JPG files, it's 6-in). This value allows everything to adjust for that.

        me.offset       = di                                # where the item's data is in the file data (for IFD items, this points to the table)
        me.length       = 0                                 # how many bytes in the file data this item takes (for IFD items, it's the table length in bytes)

        me.items        = []                                # in case we're an IFD table item, track new items to be written
        me.next         = None                              # not none?  it's an ifd item


        me.tag          = tree_tag_to_tag(tree, tag)        # the complete, unique tag, including the tree information
        me.format       = format                            # the format of the data (for IFD items, it's EXIF_U32_FORMAT - for the table offset value, stuffed in the parent table)

        if  (not isinstance(values, ListType)) and (not isinstance(values, TupleType)) :
            values      = [ values ]
        me.values       = values                            # where we keep the usable information
        me.is_new       = True                              # keep track of whether this item must be written back to the file data

        if  not ((((not allow_zero_format) and 1) or 0) <= format < len(EXIF_LE_ITEM_FORMATS)) :
            raise ValueError("Bad format in exif item (%04x %u)!" % ( me.tag, me.format ) )

        pass


    def get_values(me) :
        """
            Return an array of the item's values.
        """

        return(me.values)


    def get_first_value(me) :
        """
            Return the first value in the array of values. (Legacy routine, but will return None if the item was read with a zero count.)
        """

        try :
            return(me.values[0])
        except :
            pass

        return(me.values)


    def set_values(me, format, values) :
        """
            Put new values in to the item.
            Later, they may be flushed out to the file data.
        """

        if  (not isinstance(values, ListType)) and (not isinstance(values, TupleType)) :
            values  = [ values ]

        me.values   = values
        me.format   = format
        me.is_new   = True

        if  not (1 <= format < len(EXIF_LE_ITEM_FORMATS)) :
            raise ValueError("Bad format set (%04x %u)!" % ( me.tag, me.format ) )

        pass



    def add_item(me, it) :
        """
            Add another item to this IFD item's list of sub-items.
            Don't replace an existing item. (That should never happen, as higher level update logic handles such cases.)
        """

        if  not it in me.items :
            me.items.append(it)
            me.is_new   = True
        pass


    def ifd_length(me) :
        """
            Return the IFD table length in bytes for this item.
        """

        return(2 + (12 * len(me.items)) + 4)


    def __str__(me) :

        is_ifd       = False
        if  me.next != None :
            is_ifd   = True

        return("%08lx fmt=%u next=%s icnt=%u %s" % ( me.tag, me.format, str(is_ifd), len(me.items), str(me.values)[0:100] ) )



    pass    # a_new_exif_item




class   an_exif_item(a_new_exif_item) :

    def __init__(me, tree, efc, data, di = 0, do = 0, parent_tag = 0, allow_zero_format = True) :

        tag         = read_exif_val(efc, 'H', data, di) | (long(parent_tag) << 16)
        format      = read_exif_val(efc, 'H', data, di + 2)

        if  VERBOSE > 2 :
            print "an_exif_item", di, "%04x" % ( tag )

        a_new_exif_item.__init__(me, tree, tag, format, None, di, do, allow_zero_format = True)

        me.is_new   = False

        cnt         = read_exif_val(efc, 'I', data, di + 4)
        od          = data[di + 8 : di + 12]
        offset      = do + read_exif_val(efc, 'I', od)

        fc          = EXIF_LE_ITEM_FORMATS[me.format][0]
        iln         = EXIF_LE_ITEM_FORMATS[me.format][1]
        pack_cnt    = EXIF_LE_ITEM_FORMATS[me.format][2]

        me.offset   = offset                    # where the data is
        me.length   = iln * cnt                 # how many bytes are in the data

        d               = data
        if  me.length  <= 4 :
            d           = od
            offset      = 0
            me.offset   = 0

        if  not fc :
            me.values   = [ d[offset : offset + me.length] ]
            if  fc == "" :
                if  me.values[0].endswith('\0') :
                    me.values[0]    = me.values[0][:-1]
                pass
            pass
        elif not cnt :
            me.values   = None
        else :
            me.values   = read_exif_val(efc, fc, d, offset, cnt * pack_cnt)

            if  pack_cnt != 1 :
                dd      = []
                i       = 0
                while i < len(me.values) :
                    dd.append( [ me.values[i + dti] for dti in xrange(pack_cnt) ] )          # double up the ratio values as pairs
                    i      += pack_cnt
                me.values   = dd
            elif    cnt    == 1 :
                me.values   = [ me.values ]                                                  # make 'em all arrays or tuples
            pass

        pass



    pass    # an_exif_ifd




class   an_exif(object) :


    def _make_new_ifd(me, tree, it, data, allow_zero_format = True) :

        di          = me.do + it.get_first_value()
        it.offset   = di

        cnt         = read_exif_val(me.endian_fc, 'H', data, di)
        di         += 2

        it.length   = 2 + (cnt * 12) + 4

        for i in xrange(cnt) :
            nit     = an_exif_item(0, me.endian_fc, data, di, me.do, it.tag, allow_zero_format = allow_zero_format)
            it.add_item(nit)

            di     += 12

        me.items   += it.items                                                              # remember all the exif items under the ifd items

        it.next     = 0                                                                     # remember that we are an ifd item

        return(me.do + read_exif_val(me.endian_fc, 'I', data, di))                          # tell caller where to go next



    def _make_new_sub_ifd(me, tree, it, data, allow_zero_format = True) :

        nxt         =   me._make_new_ifd(tree, it, data, allow_zero_format = allow_zero_format)

        if  (nxt >  me.do) and me.strict :
            raise ValueError("SubIFD with 2nd IFD %08x (%u)!" % ( it.tag, me.do + it.get_first_value() ) )

        return(nxt)




    def hash_this_item(me, it) :
        me.items[it.tag]                = it



    def __init__(me, chunk, strict = False, allow_zero_format = None) :

        me.strict       = strict or False
        me.chunk        = chunk

        if  allow_zero_format  == None :
            allow_zero_format   = not strict

        data            = me.chunk.data

        if  len(data) < 12 + 6 :
            raise ValueError("Exif data too short (%u)!" % ( len(data) ) )

        if  data[0:6] != EXIF_HDR :
            raise ValueError("Exif data unheadered [0x%02x...]!" % ( ord(data[0]) ) )

        di  =   me.do  = 6
        s   = data[di : di + 4]
        if  s == "II*\x00" :
            me.endian_fc    = "<"
        elif s == "MM\x00*" :
            me.endian_fc    = ">"
        else :
            raise ValueError("Exif data not TIFF [0x%02x%02x%02x%02x]!" % ( ord(s[0]), ord(s[1]), ord(s[2]), ord(s[3]) ) )

        di  = me.do + read_exif_val(me.endian_fc, 'I', data, di + 4)            # start at the first IFD

        #
        #   Run through the chain of ifd's (amazingly, their order matters! and it matters which ifd an item is under. Sheesh.)
        #
        me.trees    = []
        me.items    = []
        ifd_cnt     = 0
        while ifd_cnt < 10000 :
            if  VERBOSE > 2 :
                print "di", di
            ifd     = a_new_exif_item(0, ifd_cnt, EXIF_U32_FORMAT, di - me.do, di, me.do)
            nxt     = me._make_new_ifd(0, ifd, data, allow_zero_format = allow_zero_format)

            me.trees.append(ifd)
            me.items.append(ifd)

            for it in ifd.items :
                if  (it.tag & 0xffff) == SUBIFD_MARKER :
                    me._make_new_sub_ifd(ifd_cnt, it, data, allow_zero_format = allow_zero_format)
                    me.items   += it.items

                    for sit in it.items :
                        if  (sit.tag & 0xffff) == INTEROP_MARKER :
                            me._make_new_sub_ifd(ifd_cnt, sit, data, allow_zero_format = allow_zero_format)
                            me.items   += sit.items
                        pass
                    pass

                if  (it.tag & 0xffff) == GPS_EXIF_MARKER :
                    me._make_new_sub_ifd(ifd_cnt, it, data, allow_zero_format = allow_zero_format)
                    me.items   += it.items
                pass

            me.items   += ifd.items

            if  nxt    <= me.do :
                break

            di          = nxt
            ifd_cnt    += 1
        if  ifd_cnt    >= 10000 :
            raise ValueError("Too many IFDs!")

        for i in xrange(len(me.trees) - 1) :
            me.trees[i].next = me.trees[i + 1]

        items           = me.items
        me.items        = {}
        for it in items :
            me.hash_this_item(it)

        pass


    def get(me, tag, dflt = None) :
        """
            Get an exif item by unique tag (including the top level value).
        """

        return(me.items.get(tag, dflt))


    def keys(me) :
        """
            Return an array of all known tags.
        """

        return(me.items.keys())



    def values(me) :
        """
            Return an array of all known items.
        """

        return(me.items.values())



    def get_a_time(me, tag, dflt = None, time_func = time.gmtime) :
        """
            Return None or the UTC time (assuming the EXIF date/time is in local time) from an EXIF-formatted date/time string.
            Note: If the camera is in Zulu time, then the calls to localtime() need to be changed to gmtime().
        """


        t   = dflt

        #   print "tie", [ "%08x" % k for k in me.items.keys() ]
        it  = me.get(tag)
        if  it :
            s   = it.get_first_value()

            if  isinstance(s, IntType) or isinstance(s, LongType) or isinstance(s, FloatType) :
                return(s)

            t   = dflt
            if  s :
                t   = tz_parse_time.parse_time(str(s))
                if  t == None :
                    t   = dflt
                pass
            pass

        return(t)



    def picture_taken_time(me, dflt = None, time_func = time.gmtime) :
        """
            Return None or the UTC time (assuming the EXIF date/time is in local time) from an image file with EXIF data.
        """

        try :
            mtime   = me.get_a_time(                        DATE_TIME_MODIFIED_MARKER,  dflt,  time_func = time_func)
            mtime   = me.get_a_time((SUBIFD_MARKER << 16) | DATE_TIME_ORIGINAL_MARKER,  mtime, time_func = time_func)
            mtime   = me.get_a_time((SUBIFD_MARKER << 16) | DATE_TIME_DIGITIZED_MARKER, mtime, time_func = time_func)
        except :
            mtime   =   dflt

        return(mtime)



    def get_latitude_or_longitude(me, tag) :
        """
            Return a latitude or longitude.
        """

        if  not (tag >> 16) :
            tag    |= (GPS_EXIF_MARKER << 16)
        it          = me.get(tag)

        if  not it :
            return(None)

        ll          = it.get_values()
        ll          = float(ll[0][0]) / float(ll[0][1]) + ((float(ll[1][0]) / float(ll[1][1])) / 60.0) + ((float(ll[2][0]) / float(ll[2][1])) / 3600.0)

        tag        -= 1
        it          = me.get(tag)

        if  it == None :
            return(None)

        if  it.get_first_value().lower().startswith('w') or it.get_first_value().lower().startswith('s') :
            ll      = -ll

        return(ll)


    def get_latitude(me) :
        """
            Return a latitude.
        """

        return(me.get_latitude_or_longitude(GPS_LATITUDE_MARKER))


    def get_longitude(me) :
        """
            Return a longitude.
        """

        return(me.get_latitude_or_longitude(GPS_LONGITUDE_MARKER))


    def get_altitude(me) :
        """
            Return the altitude (in meters).
        """

        tag     = (GPS_EXIF_MARKER << 16) | GPS_ALTITUDE_MARKER

        it      = me.get(tag)
        if  not it :
            return(None)

        alt     = it.get_first_value()
        if  isinstance(alt, ListType) or isinstance(alt, TupleType) :
            if  not alt[1] :
                return(None)

            alt = float(alt[0]) / float(alt[1])

        it      = me.get(tag - 1)
        if  not it :
            return(None)

        if  it.get_first_value() :
            alt = -alt

        return(alt)




    def add_data(me, data) :
        retval          = len(me.chunk.data)
        me.chunk.data  += data

        return(retval)


    def replace_data(me, data, offset) :
        me.chunk.data   = me.chunk.data[0 : offset] + data + me.chunk.data[offset + len(data) : ]

        return(offset)


    def _add_new_ifd_item(me, tag) :
        """
            Make sure that the whole path up to the top level exists for the given tag (item).
            The top, tree item must exist already.
        """

        it              = a_new_exif_item(0, tag, EXIF_U32_FORMAT, 0)
        me.hash_this_item(it)

        it.next         = 0                                 # mark this thing as an IFD item

        if  VERBOSE > 2 :
            print "New IFD %08x" % ( it.tag )

        tag             = tag >> 16
        if  tag :                                           # note that the top level item will already be created by code below
            ifd_it      = me.get(tag)
            if  not ifd_it :
                ifd_it  = me._add_new_ifd_item(tag)
            ifd_it.add_item(it)                             # at the first level of recursion, this is where the top level/tree ifd item is updated if the driving item is off a non-zero tree
        else :
            me.trees[0].add_item(it)                        # we assume that the top level/tree IFD items are already created, so this code catches IFD items that are direct, off the first IFD table

        return(it)


    def add_item(me, tree, tag, format, values) :
        """
            Add or update the given item with the new format/values.
        """

        if  tree > 100 :
            raise ValueError("Tree too high (%08x)!" % ( tag ) )

        if  not tag and not tree :
            raise ValueError("Tree and tag cannot be zero!")


        #
        #   Fix an existing item, if there is one
        #

        t   = tree_tag_to_tag(tree, tag)
        it  = me.get(t)
        if  it :
            if  it.next != None :
                raise ValueError("Trying to add/update an IFD item %08x!" % ( tag ) )

            it.set_values(format, values)

            return(it)


        #
        #   If the chunk has more than a 1st IFD stub, we can't update the first IFD 'cause we're scared of programs that don't read where the first ifd table is
        #

        if  (not it) and (not tree) and (len(me.chunk.data) > 20) and KEEP_IFD0_AT_8 :
            tree    = 1


        if  VERBOSE > 1 :
            print "adding %0x" % ( tree_tag_to_tag(tree, tag)), str(it), str(values), str(tree)


        #
        #   Make sure we have enough trees to reference this item
        #
        while len(me.trees)    <= tree :
            t                   = len(me.trees)
            pit                 = a_new_exif_item(0, t, EXIF_U32_FORMAT, 0)
            pit.next            = 0                             # mark this thing as an IFD item
            me.hash_this_item(pit)
            me.trees.append(pit)
            me.trees[-2].next   = pit
            if  VERBOSE > 2 :
                print "New tree %0x" % ( pit.tag )
            pass


        #
        #       Construct the path of IFD items from this one up to the top level
        #

        t16         = tree_tag_to_tag(tree, tag) >> 16
        if  not me.get(t16) :
            me._add_new_ifd_item(t16)


        it          = it or a_new_exif_item(tree, tag, format, values)
        me.hash_this_item(it)

        me.get(t16).add_item(it)

        return(it)



    def construct_ifd_table_data(me, it) :

        da  = []
        for sit in it.items :
            ( offset, od, cnt ) = me.prepare_item_for_write(sit)

            if  od != None :
                da.append( [ sit, od, cnt ] )
            else :
                da.append( [ sit, write_exif_val(me.endian_fc, 'I', offset - me.do), cnt ] )
            pass


        def _cmp_tags(a, b) :
            return(cmp(a[0].tag & 0xffff, b[0].tag & 0xffff))

        da.sort(_cmp_tags)

        data        = write_exif_val(me.endian_fc, 'H', len(it.items))
        for d in da :
            sit     = d[0]
            data   += write_exif_val(me.endian_fc, 'H', sit.tag & 0xffff)
            data   += write_exif_val(me.endian_fc, 'H', sit.format)
            data   += write_exif_val(me.endian_fc, 'I', d[2])
            data   += d[1]

        return(data)


    def prepare_ifd_item_for_write(me, it) :
        tl              = it.ifd_length()
        if  tl > it.length :
            it.offset   =   me.add_data('\0' * tl)
            it.length   =   tl

        tbl         = me.construct_ifd_table_data(it)

        if  not it.next :
            nxt     = 0
        else :
            nxt     = me.prepare_item_for_write(it.next)[0] - me.do
        tbl        += write_exif_val(me.endian_fc, 'I', nxt)

        if  tbl != me.chunk.data[it.offset : it.offset + len(tbl) ] :
            me.replace_data(tbl, it.offset)

        it.is_new   = False

        return( ( it.offset, None, 1 ) )



    def prepare_item_for_write(me, it) :
        if  not it :
            return( ( 0, None, 0 ) )

        if  it.next != None :
            return(me.prepare_ifd_item_for_write(it))                           # Do the IFD items seprately from normal items


        ln  = data_length(it.format, it.values)

        if  VERBOSE > 1 :
            print "xx %08x %s %s %u" % ( it.tag, str(it.values)[0:100], str(it.is_new), ln )

        ( data, cnt )   = make_exif_data(me.endian_fc, it.format, it.values)    # we need the cnt, 'cause i got rid of the code that saved it in the item
        if  ln <= 4 :
            od          = data + "\0\0\0\0"
            od          = od[0:4]

            return( ( it.offset, od, cnt ) )


        if  it.offset and not it.is_new :
            return( ( it.offset, None, cnt ) )


        it.is_new       = False

        if  it.offset and (ln <= it.length) :
            me.replace_data(data, it.offset)
        else :
            it.offset   = me.add_data(data)

        it.length       = ln

        return( ( it.offset, None, cnt ) )



    def prepare_to_write(me) :
        """
            Before writing the exif data, call this routine to make it all up-to-date with respect to any added/modified items.
        """

        it  = me.trees[0]
        if  len(me.chunk.data) <= 20 :                                          # is this exif data a stub?  If so, take room for the whole table so it can be put at offset 8

            dummy_table     = "\0" * it.ifd_length()
            me.chunk.data   = me.chunk.data[0 : it.offset]
            it.offset       = me.add_data(dummy_table)
            it.length       = len(dummy_table)

        ( offset, od, cnt ) = me.prepare_item_for_write(me.trees[0])
        me.replace_data(write_exif_val(me.endian_fc, 'I', offset - me.do), 10)  # we may have moved the first IFD

        if  WRITE_TO_FILE :
            fo  = open("x.d", "wb")
            fo.write(me.chunk.data)
            fo.close()

        me.chunk.exif       = an_exif(me.chunk, me.strict)                      # validates our logic if we can read it anew



    pass    # an_exif








class   a_jpg_chunk(object) :

    def __init__(me, data, di = 0, es = "", strict = False) :

        me.length       = 0
        me.ei           = di
        me.skipped_ff   = me.skipped_zero   = False
        me.data         = ""

        if  len(data) - me.ei < MARKER_LEN :
            raise ValueError("Bad chunk - not chunk header at %u: %s!" % ( me.ei, es ) )

        while me.ei < len(data) - 2 :
            if  data[me.ei] == "\xff" :
                if  data[me.ei + 1] != "\xff" :
                    break
                me.skipped_ff   = True
            elif  data[me.ei] != "\x00" :
                break
            else :
                me.skipped_zero = True

            me.ei      += 1


        if  me.skipped_zero :
            me.marker   = "\xff" + data[me.ei]
            me.ei      += 1
        else :
            me.marker   = data[me.ei : me.ei + MARKER_LEN]
            me.ei      += MARKER_LEN


        if  me.marker[0] != JPG_BEGIN_MARKER[0] :
            raise ValueError("Bad chunk - marker 1st byte is not FF %u:0x%02x %s!" % ( di, ord(data[me.ei]), es ) )


        if  me.marker in NO_DATA_MARKERS :
            me.data     = ""
            me.length   = 0
        elif len(data)  < me.ei + 2 :
            raise ValueError("Bad chunk - no length in chunk header at %u:%s %s!" % ( di, me.marker_str(), es ) )
        else :
            me.length   = (ord(data[me.ei]) * 256) + ord(data[me.ei + 1])
            if  len(data) < me.ei + me.length :
                raise ValueError("Bad chunk - chunk length too long at %u:%s %u %s!" % ( di, me.marker_str(), me.length, es ) )

            di          = me.ei + me.length
            if me.marker == JPG_START_OF_SCAN :
                di      = len(data)

            me.ei      += 2

            me.data     = data[me.ei : di]

            me.ei       = di


        me.exif         = None
        if  me.data[0:6] == EXIF_HDR :

            try :
                me.exif     = an_exif(me, strict)
            except :
                if  False :
                    e       = sys.exc_info()
                    traceback.print_exception(e[0], e[1], e[2])
                    me.exif = None
                pass
            pass

        pass


    def add_exif(me) :
        """
            Force this chunk to contain a valid EXIF information block.
            If there is no exif block, this routine wipes out any old data in the chunk and creates a new exif block.
        """

        if  not me.exif :
            if  VERBOSE > 3 :
                print "Creating exif"

            maker       = "tz_jpg.py\0"

            me.data     = "Exif\0\0"

            me.data    += "II*\0"
            me.data    += "\x08\0\0\0"

            me.data    += "\0\0"
            me.data    += "\0\0\0\0"

            if  VERBOSE > 3 :
                print "raw datalen", len(me.data)

            me.length   = len(me.data)
            me.ei       = 0                                                                                                     # now meaningless, used when we are creating ourself to tell a_jpg who long we are
            me.exif     = an_exif(me, True)

            if  VERBOSE > 3 :
                print "Adding stuff"

            me.exif.add_item(0,                           SOFTWARE_MARKER,      EXIF_STRING_FORMAT, maker                   )
            me.exif.add_item(0, (SUBIFD_MARKER   << 16) | EXIF_VERSION_MARKER,  EXIF_UNDEF_FORMAT,  [ '0', '2', '2', '0' ]  )   # this is the screwy data, but it's what II type files have, at least. Don't know about MM files.
            me.exif.add_item(0, (GPS_EXIF_MARKER << 16) | GPS_VERSION_MARKER,   EXIF_BYTE_FORMAT,   [ 0, 0, 2, 2 ]          )   # if big-endian, they go the other way, I've read, but we don't care (or maybe that's the badly done EXIF_VERSION_MARKER item)

        return(me.exif)


    def add_exif_item(me, tree, tag, format, values) :
        """
            Make sure the given exif item is in this chunk.
            If the chunk does not contain exif data, the chunk's data is replaced by the exif information.
        """

        return(me.add_exif().add_item(tree, tag, format, values))


    def prepare_to_write(me) :
        """
            Before writing the chunk, call this routine to make it any exif information up-to-date with respect to any added/modified items.
        """


        if  me.exif :
            me.exif.prepare_to_write()
            if  len(me.data) >= 0xfffe :
                raise ValueError("Exif data too large (%u)!" % ( len(me.data) ) )
            pass
        pass



    def marker_str(me) :
        return("0x%02x%02x" % ( ord(me.marker[0]), ord(me.marker[1]) ) )



    pass        # a_jpg_chunk




class a_jpg(object) :

    def __init__(me, fi, strict = False) :

        me.file_name    = ""
        me.chunks       = []

        if  fi :

            fn  = None
            if  isinstance(fi, basestring) :
                fn              = fi
                fi              = open(fi, "rb")
                me.file_name    = fn

            es   = ""
            me.all  = fi.read()
            if  fn :
                fi.close()
                es  = fn + " "

            if  not me.all.startswith(JPG_BEGIN_MARKER) :
                raise ValueError("File %snot a .jpg file at the start!" % ( es ) )

            if  not me.all.endswith(JPG_END_MARKER) :
                raise ValueError("File %snot a .jpg file at the end!" % ( es ) )

            i   = 0
            while i < len(me.all) :
                me.chunks.append(a_jpg_chunk(me.all, i, es = es, strict = strict))
                i   = me.chunks[-1].ei
                # print me.chunks[-1].marker_str(), me.chunks[-1].length, me.chunks[-1].ei

            if  me.chunks[0].marker != JPG_BEGIN_MARKER :
                raise ValueError("File %snot does not end with .jpg file marker!" % ( es ) )

            if  (me.chunks[-1].marker != JPG_END_MARKER) and (me.chunks[-1].marker != JPG_START_OF_SCAN) :
                raise ValueError("File %snot does not end with .jpg file marker (%s)!" % ( es, me.chunks[-1].marker_str() ) )

            if  len(me.chunks) < 3 :
                raise ValueError("File %snot does not have enough marker chunks (%u)!" % ( es, len(me.chunks) ) )

            if  me.chunks[1].marker != JPG_APP0 :
                if me.chunks[1].marker != JPG_APP1 :
                    raise ValueError("File %snot does not have APP0 or APP1 marker chunk (%s)!" % ( es, me.chunks[1].marker_str() ) )
                pass
            elif me.chunks[1].data[0 : 5] != JFIF :
                raise ValueError("File %snot does not have APP0 JFIF marker chunk!" % ( es ) )

            pass

        pass


    def add_chunk(me, marker, where = None) :
        for c in me.chunks :
            if  c.marker == marker :
                return(c)
            pass

        if  where  == None :
            where   = len(me.chunks) - 1

        data    = marker + "\x00\x00"
        c       = a_jpg_chunk(data, 0, strict = True)
        me.chunks.insert(where, c)

        # print "added chunk"

        return(c)



    def get_exif(me) :
        """
            Return an_exif, if there is one in the file.
            Otherwise, return None.
        """

        for c in me.chunks :
            if  c.exif :
                return(c.exif)

            pass

        return(None)



    def add_exif(me) :
        """
            Make sure this file contains an_exif.
            Returns the an_exit.
        """

        exif    = me.get_exif()
        if  not exif :
            # print "no exif"
            w   = 1
            if  me.chunks[1].marker == JPG_APP0 :
                w   = 2

            return(me.add_chunk(JPG_APP1, w).add_exif())

        return(exif)



    def add_exif_item(me, tree, tag, format, values) :
        """
            Make sure the given exif item is in this file.
            If the file does not contain exif data, this routine creates some to put the item in.
        """

        return(me.add_exif().add_item(tree, tag, format, values))



    def get_time_from_exif_item(me, tag, dflt = None, time_func = time.gmtime) :
        """
            Return None or the UTC time (assuming the EXIF date/time is in local time) from an EXIF-formatted date/time string.
            Note: If the camera is in Zulu time, then the calls to localtime() need to be changed to gmtime().
        """

        exif    = me.get_exif()
        if  not exif :
            return(dflt)

        return(exif.get_a_time(tag, dflt = dflt, time_func = time_func))



    def picture_taken_time(me, dflt = None, time_func = time.gmtime) :
        """
            Return None or the UTC time (assuming the EXIF date/time is in local time) from an image file with EXIF data.
        """

        exif    = me.get_exif()
        if  not exif :
            return(dflt)

        return(exif.picture_taken_time(dflt = dflt, time_func = time_func))



    def prepare_to_write(me) :
        """
            Update any (exif) information before writing it out.
        """

        for c in me.chunks :
            c.prepare_to_write()
        pass



    def jpeg_data(me) :
        """
            Return a string with the jpeg image data, such as would be written to a file.
        """

        me.prepare_to_write()

        s           = ""
        for c in me.chunks :
            if  c.marker in NO_DATA_MARKERS :
                s  += c.marker
            elif c.marker == JPG_START_OF_SCAN :
                s  += c.marker
                ln  = c.length
                s  += chr((ln >> 8) & 0xff) + chr(ln & 0xff)
                s  += c.data
            else :
                s  += c.marker
                ln  = 2 + len(c.data)
                s  += chr((ln >> 8) & 0xff) + chr(ln & 0xff)
                s  += c.data
            pass

        return(s)




    def save(me, fn) :
        """
            Save the file to the given file name.
            If exif information has changed, this routine handles it.
            If the target file already exists, it is renamed with ".bak" appended to its name. (And any file of that name is replaced.)
            The target file is written to a  [ fn + ".tmp" ]  name and renamed after it is fully written and closed.
        """

        tfn = fn + ".tmp"
        fo  = open(tfn, "wb")

        fo.write(me.jpeg_data())

        fo.close()
        del(fo)

        replace_file.replace_file(fn, tfn, fn + ".bak")



    def set_file_date_time_from_exif(me, tzone = 0) :
        """
            Set the file date/time from its exif data, if present, and if we know our file name.
            Return -1 if there was no exif date/time or we don't know our file name (and file date/time is not set).
            Otherwise, return the date/time.
        """

        mtime   = -1

        if  me.file_name :
            mtime   = me.picture_taken_time(dflt = -1)      # Note: no point in finding the file date/time as we're only setting it
            if  mtime > 0 :
                mtime   -= tzone
                os.utime(me.file_name, ( os.path.getatime(me.file_name), int(mtime) ) )
            pass

        return(mtime)



    pass        #       a_jpg




def set_file_date_time_from_exif(fn, tzone = 0) :
    """
        Set the file date/time from its exif data, if present.
        Return -1 if there was no exif date/time (and file date/time is not set).
    """

    jpg     = a_jpg(fn)

    mtime   = jpg.set_file_date_time_from_exif(tzone)

    return(mtime)




if  __name__ == '__main__' :

    import  TZCommandLineAtFile
    import  tzlib


    del(sys.argv[0])
    TZCommandLineAtFile.expand_at_sign_command_line_files(sys.argv)

    test        = 0

    while True :
        oi  = tzlib.array_find(sys.argv, "--test")
        if  oi < 0 :    break
        del sys.argv[oi]
        test   += 1


    if  test    :
        while len(sys.argv) :
            fn  = sys.argv.pop(0)

            jpg = a_jpg(fn)

            print fn, "chunk count", len(jpg.chunks)

            exif    = jpg.get_exif()
            if  not exif :
                print "Creating new exif"
                exif    = jpg.add_exif()

            if  exif :



                def _show_g(exif, g, bs = "") :
                    try :
                        ln  = g.length
                    except :
                        ln  = None
                    print bs + "%08x" % ( g.tag ), g.format, g.get_values(), g.offset, ln, ord(exif.chunk.data[g.offset])

                def _show_gps(exif) :
                    print "Taken time         ", time.asctime(time.gmtime(jpg.picture_taken_time()))
                    print "Date Time Modified ", time.asctime(time.gmtime(jpg.get_time_from_exif_item(DATE_TIME_MODIFIED_MARKER)))
                    print "Date Time Original ", time.asctime(time.gmtime(jpg.get_time_from_exif_item((SUBIFD_MARKER << 16) | DATE_TIME_ORIGINAL_MARKER)))
                    print "Date Time Digitized", time.asctime(time.gmtime(jpg.get_time_from_exif_item((SUBIFD_MARKER << 16) | DATE_TIME_DIGITIZED_MARKER)))

                    print "make and softwares"
                    for i in xrange(10) :
                        g   = exif.get((i << 16) | MAKE_MARKER, None)
                        if  g :
                            _show_g(exif, g, "  ")

                        g   = exif.get((i << 16) | SOFTWARE_MARKER, None)
                        if  g :
                            _show_g(exif, g, "  ")
                        pass

                    print "gps info", len(exif.chunk.data)
                    kys     = exif.keys()
                    kys.sort()

                    vals    = [ exif.items[k] for k in kys if (((k >> 16) & 0xffff) == GPS_EXIF_MARKER) or ((k >> 32) == GPS_EXIF_MARKER) ]
                    for g in vals :
                        _show_g(exif, g, "  ")

                    print "  ", exif.get_latitude(), exif.get_longitude(), exif.get_altitude()

                    print "eo gps"
                    print


                # for it in exif.values() :
                #     print "%08x" % ( it.tag )

                print "We read this    --------------------------------------"

                _show_gps(exif)

                if  test > 1 :
                    print "Making updates ----------------------------------"

                    if  True :
                        jpg.add_exif_item(0,                         MAKE_MARKER,               EXIF_STRING_FORMAT,    "tz_jpg_fooling_around ------------- 0"             )
                        jpg.add_exif_item(1,                         MAKE_MARKER,               EXIF_STRING_FORMAT,    "tz_jpg_fooling_around ------------- 1"             )
                        jpg.add_exif_item(2,                         MAKE_MARKER,               EXIF_STRING_FORMAT,    "tz_jpg_fooling_around ------------- 2"             )
                        jpg.add_exif_item(9,                         MAKE_MARKER,               EXIF_STRING_FORMAT,    "tz_jpg_fooling_around ------------- 9"             )
                        jpg.add_exif_item(0, GPS_EXIF_MARKER << 16 | GPS_LATITUDE_MARKER,       EXIF_U32_RATIO_FORMAT, [[ 23, 1], [ 5, 1], [0x6566, 100] ] )
                        jpg.add_exif_item(0, GPS_EXIF_MARKER << 16 | GPS_LATITUDE_REF_MARKER,   EXIF_STRING_FORMAT,    "N")
                        jpg.add_exif_item(0, GPS_EXIF_MARKER << 16 | GPS_LONGITUDE_MARKER,      EXIF_U32_RATIO_FORMAT, [[151, 1], [12, 1], [0x6566, 100] ] )
                        jpg.add_exif_item(0, GPS_EXIF_MARKER << 16 | GPS_LONGITUDE_REF_MARKER,  EXIF_STRING_FORMAT,    "W")
                        jpg.add_exif_item(0, GPS_EXIF_MARKER << 16 | GPS_ALTITUDE_MARKER,       EXIF_I32_RATIO_FORMAT, [ [ 234, 2 ] ] )
                        jpg.add_exif_item(0, GPS_EXIF_MARKER << 16 | GPS_ALTITUDE_REF_MARKER,   EXIF_BYTE_FORMAT,      [ 0 ] )
                        # jpg.add_exif_item(0, GPS_EXIF_MARKER << 16 | GPS_VERSION_MARKER,       EXIF_BYTE_FORMAT,      [ 1, 2, 3 ] )

                    print "Made updates ----------------------------------"

                    _show_gps(exif)

                    print "Locking them in ----------------------------------"

                    jpg.prepare_to_write()

                    exif    = jpg.get_exif()

                    print "After prep to write --------------------------"
                    _show_gps(exif)

                    pass
                pass


            if  test > 1 :
                jpg.save(fn + "_n.jpg")
            pass

        pass

    else :

        import  glob

        import  tzlib


        tzone   = 0


        while True :
            oi  = tzlib.array_find(sys.argv, [ "--tzone", "--time_offset", "-t"] )
            if  oi < 0 :    break
            del sys.argv[oi]
            tzone   = tz_parse_time.parse_time_zone(sys.argv.pop(oi))
            if  tzone == None :
                print "I cannot understand the timezone/offset!"
                sys.exit(102)
            pass



        fnames  = {}
        while len(sys.argv) :
            fn  = sys.argv.pop(0)

            fns = tzlib.make_dictionary(glob.glob(fn))
            if  not fns :
                print "Cannot find file(s):", fn
                sys.exit(103)
            fnames.update(fns)

        if  not fnames :
            print "Tell me ambiguous .jpg file name(s) to set the modification date/time according to exif date/time."
            sys.exit(101)

        kys     = fnames.keys()
        kys.sort()

        for fn in kys :
            if  set_file_date_time_from_exif(fn, tzone) <= 0 :
                print "Could not set date/time for", fn
        pass
    pass



#
#
#
# eof

