#!/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
#       May 27, 2012            bar     doxygen namespace
#       May 12, 2013            bar     comment get/set
#                                       --test cmd line write to file name without the original ext
#       March 27, 2016          bar     get_gps_time()
#       November 26, 2016       bar     because of the nexus5, use first the GPS date/time rather than the others if the GPS timestamp is there
#       May 6, 2018             bar     get_orientation()
#       May 19, 2018            bar     able to be given file data to avoid the old CString kludge
#       July 14, 2018           bar     allow unicode file names to open
#       August 11, 2019         bar     jpeg_file_quality_estimate()
#       August 19, 2019         bar     jpeg_file_quality() and image_jpeg_quality()
#       May 20, 2020            bar     fix some weird, inconsistent, yet-another-fuss thing with new jpeg comments
#       July 18, 2020           bar     get_xml()
#                                       get google's panorama date/times as the best image date/time
#       --eodstamps--
##      \file
#       \namespace              tzpython.tz_jpg
#
#
#       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  re
import  struct
import  sys
import  time
import  traceback

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

import  replace_file
import  tz_parse_time
import  tzlib



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

REQUIRE_END_MARKER_AT_END   = 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"
JPG_COM             =   "\xff\xfe"

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
XML_MARKER                          =   0x02bc
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
EXIF_ORIENTATION_MARKER             =   0x0112          # uint16: 1=hz(normal) 2=mirror_hz 3=180 4=mirror_vt 5=mirror_hz_and_270CW 6=90CW 7=mirror_hz_and_90CW 8=270CW


#
#
#       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
GPS_DATE_STAMP_MARKER           =   0x001d



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_orientation(object) :

    def __init__(me, uint_v) :
        me.v    = uint_v        # remember the actual, numeric value
        me.hz   = False         # mirror horizontal (done before rotation)
        me.vt   = False         # mirror vertical   (done before rotation)
        me.rot  = 0             # clockwise rotation in degrees
        if   uint_v == 2 :
            me.hz   = True
        elif uint_v == 3 :
            me.rot  = 180
        elif uint_v == 4 :
            me.vt   = True
        elif uint_v == 5 :
            me.hz   = True
            me.rot  = 270
        elif uint_v == 6 :
            me.rot  = 90
        elif uint_v == 7 :
            me.hz   = True
            me.rot  = 90
        elif uint_v == 8 :
            me.rot  = 270
        pass

    def __str__(me) :
        """ String-ize our value like exiftool does. """
        s          = ""
        if  me.vt   :
            s      += " and mirror vertical"
        if  me.hz   :
            s      += " and mirror horizontal"
        if  me.rot  :
            s      += " and rotate %u CW" % me.rot
        if  not (0 < me.v <= 8) :
            s      += " and unknown (%u)" % me.v
        if  not s   :
            s       = " and horizontal (normal)"
        s           = s[5:]     # get rid of leading ' and '
        if  len(s)  :
            s       = s[0].upper() + s[1:]
        return(s)

    # an_exif_orientation





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.
        """

        xml = me.get_xml()
        if  xml     :
            g       = re.search(r'GPano:LastPhotoDate="([^"]+)"', xml)
            if  not g :
                g   = re.search(r'GPano:FirstPhotoDate="([^"]+)"', xml)
            if  g   :
                mtime       = tz_parse_time.parse_time(g.group(1))
                if  mtime   :
                    return(mtime)
                pass
            pass
        try         :
            mtime   = me.get_gps_time()
            if  mtime != None :
                return(mtime)
            pass
        except      :
            pass

        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 get_gps_time(me) :
        """
            Return the GPS time/date stamp.
        """
        it          = me.get((GPS_EXIF_MARKER << 16) | GPS_TIME_STAMP_MARKER)
        if  not it  :
            return(None)

        ta          = it.get_values()
        t           = (3600 * float(ta[0][0]) / float(ta[0][1])) + (60 * float(ta[1][0]) / float(ta[1][1])) + (float(ta[2][0]) / float(ta[2][1]))

        it          = me.get((GPS_EXIF_MARKER << 16) | GPS_DATE_STAMP_MARKER)
        if  (not it) or (not it.values) :
            return(None)

        d           = tz_parse_time.parse_time(it.values[0].replace(':', '/'))
        d           = tz_parse_time.parse_time(it.values[0])
        if  d is None :
            return(None)

        t          += d

        return(t)


    def get_orientation(me) :
        """ Return the exif orientations in a (possibly empty) array or an_exif_orientation's. """
        it  = me.get(EXIF_ORIENTATION_MARKER)
        if  not it :
            return([])
        va  = it.get_values() or []
        vaa = [ an_exif_orientation(v) for v in va ]
        return(vaa)


    def get_xml(me) :
        it  = me.get(XML_MARKER)
        if  not it :
            return(None)
        va  = it.get_values() or []
        vaa = "".join([ chr(c) for c in va ])
        return(vaa)



    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 comment(me) :
        """ If this chunk is a comment, return it, or None if this chunk is not a comment. """
        if  me.marker == JPG_COM :
            return(me.data)
        return(None)



    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) :
        """
            Make a JPEG object from the given file/file-name/jpeg-file-data.

        """

        me.file_name    = ""
        me.chunks       = []
        me.all          = ""        # all the file's data

        if  fi  :
            fn  = None
            fw  = -1
            es  = ""
            if isinstance(fi, basestring) :
                if  fi.startswith(unicode(JPG_BEGIN_MARKER + JPG_APP0 + '\x00\x10' + JFIF, 'latin1')) :     # for handiness, allow the caller to pass us the JPEG data. But we're strict about what it starts with.
                    me.all          = fi
                else                :
                    fn              = fi
                    fi              = open(fi, "rb")
                    me.file_name    = fn
                pass
            if  False and (getattr(fi, 'tell', None) != None) and (getattr(fi, 'seek', None) != None) :     # note: in case some old case expects to pass a file object and have us read to the end, where it will write more data, we'll not implement this logic. for now.
                fw      = fi.tell()
            me.all      = me.all or fi.read()       # read the whole file's data if we weren't given it directly
            if  fn      :
                fi.close()
                es      = fn + " "
            elif fw >=  0 :
                fi.seek(fw)                         # assume he gave us the file at the start, so take the pointer back to where we found it
            pass

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

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

            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 begin 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. Has %u!" % ( es, len(me.chunks) ) )

            #   See https://stackoverflow.com/questions/5413022/is-the-2nd-and-3rd-byte-of-a-jpeg-image-always-the-app0-or-app1-marker
            #       for more information about what's right.
            #   This filters out all but normal files.
            if  me.chunks[1].marker != JPG_APP0 :
                if  me.chunks[1].marker != JPG_APP1 :           # deal with APP1 files, but don't care about "JFIF" with them - seemed to work long ago when this code was written
                    raise ValueError("File %snot does not have starting APP0 or APP1 marker chunk. Has %s!" % ( es, me.chunks[1].marker_str() ) )
                pass
            elif me.chunks[1].data[0 : 5] != JFIF :
                raise ValueError("File %snot does not have starting APP0 JFIF marker chunk 2!" % ( es ) )

            pass

        pass


    def add_chunk(me, marker, where = None) :
        """ Return existing chunk of the given marker, or add a new chunk and return it. """
        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 comment(me, comment = None) :
        """ Get/set comment. Return current comment, None if there is none. Remove comment with empty string for 'comment'. """

        cmt = ([ c.comment() for c in me.chunks if c.comment() != None ] + [ None ])[0]

        if  comment != None :
            if  not len(comment) :
                me.chunks   = [ c for c in me.chunks if c.comment() is None ]           # remove all the comments
            else    :
                c           = me.add_chunk(JPG_COM)
                c.data      = str(comment)                                              # the str seems to fix a unicode decode of 0xff problem with a string that had no 0xff's
            pass

        return(cmt)




    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 get_gps_time(me) :
        """
            Return None or the GPS (no time zone) time from an image file with such EXIF data.
        """

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

        return(exif.get_gps_time())



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

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

        return("".join(sa))




    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)



#   From https://stackoverflow.com/questions/17738276/detect-jpeg-image-quality which references https://tools.ietf.org/html/rfc2435#section-4.2
#   I notice the estimates are high - squashed against 100
#   E.g. My 95% photos are 94..100, mostly 99 and 100
#   But the really low quality images that caused this code ( 17% from http://fotoforensics.com/ ) come out at anywhere from 33 to the 90's
#   And, anyway, the RFC doesn't match up with this code, really. In practice, the table seems to be monotonically increasing but they don't match the two tables in the RFC's appendix A, scaled or not.
#   ImageMajik's "identify -verbose file_name" doesn't work on the video images I tried it on. It reports nothing.
#       So it's not just storing at various quality levels and sensing at which level the quality is unaffected. Which is the proper way to do things.
def jpeg_file_quality_estimate(fn) :
    """ Return a guess at the JPG quality 0..100 of this file. Or None if the file isn't JPEG or something went wrong. """
    from    PIL import Image                # this is here to keep this module independent of PIL
    q   = None
    jpg = Image.open(fn)
    qt  = getattr(jpg, 'quantization', {})
    if  len(qt)         :
        try             :
            qa          = qt[0]
            if  qa[58] <= 100 :
                q       = int(100 - (qa[58] / 2))
            else        :
                q       = int(5000.0 / 2.5 / (qa[15] or 1))
            pass
        except IndexError :
            pass
        except KeyError   :
            pass
        pass
    return(q)



def image_jpeg_quality(img) :
    """
        Return an estimate of the JPEG quality (0..98-ish) level of this PIL image or numpy array.

        Use PIL to test-save the images, so the quality level returned will be similar to what PIL might use.

        Returns None if the quality can't be guessed or if the image isn't acceptable.

    """
    import  cStringIO
    import  math

    try :
        numpy   = __import__('numpy')
        Image   = __import__('PIL.Image', fromlist = 'PIL')         # pull in PIL without letting modulefinder.py know about it.
    except ImportError :
        return(None)

    cv2         = None
    try :
        cv2     = __import__('cv2')                                 # their absdiff routine is faster than hacking it with numpy
    except ImportError :
        pass

    def _try(pil_img, q) :
        fo      = cStringIO.StringIO()
        pil_img.save(fo, format = 'JPEG', quality = q)
        fo.seek(0)
        smg     = numpy.array(Image.open(fo))
        img     = numpy.array(pil_img)
        if  cv2 :
            img = numpy.asarray(cv2.absdiff(smg, img), dtype = numpy.int64)     # absdiff is faster to get the difference, but the conversion swamps savings. 1 second in 20-something saved, maybe, after all is said and done
        else    :
            d   = img - smg
            s   = numpy.uint8(img < smg) * 254 + 1                  # this cute trick is from https://stackoverflow.com/questions/35777830/fast-absolute-difference-of-two-uint8-arrays
            img = numpy.asarray(d * s, dtype = numpy.int64)
        img     = img * img
        return(math.sqrt(numpy.sum(img)))

    if      not tzlib.is_pil_image(img)   :
        if  not tzlib.is_numpy_array(img) :
            return(None)                                            # sorry, only PIL images and numpy arrays are handled
        img     = Image.fromarray(img)


    qa  = [ _try(img, q) for q in xrange(1, 101) ]
    qd  = [ qa[q] - qa[q + 1] for q in xrange(len(qa) - 1) ]
    if  False   :
        for q in xrange(len(qd)) :
            print "%3u %7.1f %7.1f" % ( q + 1, qa[q], qd[q], )
        pass
    q   = tzlib.argmin(qd)
    if  qd[q] < 0 :
        return(q + 1)                                               # all but pretty high qualities seem to have a notch in our match-measure at about the Q level we saved the file as

    q   = tzlib.argmin(qd[75: -2]) + 75                             # when the Q level is high (or maybe a bit fuzzy), it turns out that the 1st differential is in a valley somewhere high where the image is probably at (note: the diff numbers get weird at the very top and roughly below from 75 down to 60-ish)
    return(q + 1)


def jpeg_file_quality(fn) :
    """
        Return an estimate of the JPEG quality (0..98-ish) level of this image file.

        Use PIL to test-save the images, so the quality level returned will be similar to what PIL might use.

        Returns None if the quality can't be guessed or if the image isn't acceptable.

    """
    try :
        Image   = __import__('PIL.Image', fromlist = 'PIL')         # pull in PIL without letting modulefinder.py know about it.
    except ImportError :
        return(None)

    img         = Image.open(fn)
    return(image_jpeg_quality(img))



if  __name__ == '__main__' :

    import  TZCommandLineAtFile


    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)

            cm  = jpg.comment()
            if  cm != None :
                print "Comment:[%s]" % cm
            jpg.comment('@@@@ too bad')

            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(),
                    t   = exif.get_gps_time()
                    if  t != None :
                        print t, "-", time.asctime(time.localtime(t)), "-", time.asctime(time.gmtime(t)),
                    print

                    print "eo gps"
                    print


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

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

                _show_gps(exif)

                print "Orientation: ", [ [ ov.v, str(ov), ] for ov in exif.get_orientation() ]

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

                        jpg.add_exif_item(0, EXIF_ORIENTATION_MARKER, EXIF_U16_FORMAT, [ 7 ] )

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

                    _show_gps(exif)

                    print "Orientation: ", [ [ ov.v, str(ov), ] for ov in exif.get_orientation() ]

                    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(os.path.splitext(fn)[0] + "_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
