Skip to content

Instantly share code, notes, and snippets.

@danzek
Last active January 25, 2024 22:38
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save danzek/deb2760e345bbd0a2404 to your computer and use it in GitHub Desktop.
Save danzek/deb2760e345bbd0a2404 to your computer and use it in GitHub Desktop.
X-Ways Python X-Tension: Plot EXIF location data in a KML file
# Extracts GPS coordinates from images in X-Ways Forensic software and creates a KML file plotting
# the location data that can be opened in Google Earth.
#
# Using public code for extracting GPS EXIF data from https://gist.github.com/moshekaplan/5330395
# based on original code at https://gist.github.com/erans/983821 using PIL 1.1.7 library
#
# Copyright (c) 2013 Dan O'Day. All rights reserved. https://code.google.com/p/digital0day/
# This software distributed under the Eclipse Public License 1.0 (EPL-1.0)
# http://www.opensource.org/licenses/EPL-1.0
#
# Feel free to use as you please, I've heavily commented API-specific code - I assume you can
# already understand the Python code. I've also included non-essential functions to give some
# basic information about how they could be used. More information is available in the API
# documentation: http://www.x-ways.net/forensics/x-tensions/api.html
"""
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
import XWF, sys
from tempfile import TemporaryFile
from PIL import Image # non-standard library, must be installed in Python path (PIL 1.1.7 http://www.pythonware.com/products/pil/ )
from PIL.ExifTags import TAGS, GPSTAGS
# This class provided by X-Ways OutputRedirector sample script; I included it in this script to prevent needing two files
class redirector:
def write(self, text):
# flag=1: Python calls print a second time to print the line feed
# so we don't need to do this here
XWF.OutputMessage(text, 1)
return
gps_data = {} # global dictionary for storing filenames and GPS coordinates
# The first function that is called when a Python X-Tension is called
def XT_Init(nVersion, nFlags, hMainWnd, lpReserved):
sys.stderr = sys.stdout = redirector() # instantiates redirector class object provided by X-Ways (so print statements output to messages window)
print('Initialized')
return
# Describes X-Tension, required function
def XT_About(hParentWnd, lpReserved):
print('Creates virtual KML file containing GPS data from images.')
return
# Called before items or search hits are processed individually, required function
def XT_Prepare(hVolume, hEvidence, nOpType, lpReserved):
return
# Use for search X-Tensions (loaded from search dialog within X-Ways), required function
def XT_ProcessSearchHit(iSize, nItemID, nRelOfs, nAbsOfs, lpOptionalHitPtr, lpSearchTermID, nLength, nCodePage, nFlags):
return
# Implement and export this function if you merely need to retrieve information about the file but don't need to read its contents (performance benefit)
def XT_ProcessItem(nItem, reserved):
return
def get_exif_data(image):
exif_data = {}
try:
info = image._getexif()
except AttributeError:
info = None
if info:
for tag, value in info.items():
decoded = TAGS.get(tag, tag)
if decoded == "GPSInfo":
gps_data = {}
for gps_tag in value:
sub_decoded = GPSTAGS.get(gps_tag, gps_tag)
gps_data[sub_decoded] = value[gps_tag]
exif_data[decoded] = gps_data
else:
exif_data[decoded] = value
return exif_data
def _convert_to_degrees(value):
deg_num, deg_denom = value[0]
d = float(deg_num) / float(deg_denom)
min_num, min_denom = value[1]
m = float(min_num) / float(min_denom)
sec_num, sec_denom = value[1]
s = float(sec_num) / float(sec_denom)
return d + (m / 60.0) + (s / 3600.0)
def get_lat_lon(exif_data):
lat = None
lon = None
if "GPSInfo" in exif_data:
gps_info = exif_data["GPSInfo"]
gps_latitude = gps_info.get("GPSLatitude")
gps_latitude_ref = gps_info.get('GPSLatitudeRef')
gps_longitude = gps_info.get('GPSLongitude')
gps_longitude_ref = gps_info.get('GPSLongitudeRef')
if gps_latitude and gps_latitude_ref and gps_longitude and gps_longitude_ref:
lat = _convert_to_degrees(gps_latitude)
if gps_latitude_ref != "N":
lat *= -1
lon = _convert_to_degrees(gps_longitude)
if gps_longitude_ref != "E":
lon *= -1
return lat, lon
def writeKML():
print('Specify the location and filename where you wish to save the GPS report file. WARNING: If you select an existing file, it will be overwritten without warning!')
try:
filename = XWF.GetSaveFileName() # shows save file dialog within X-Ways (I haven't figured out how to pass any parameters to this that it recognizes)
except SystemError:
print('You did not select a valid report path and file name.')
return
if filename[-4:] != '.kml':
filename += '.kml'
with open(filename, "w+") as kml:
kml.write('<?xml version="1.0" encoding="UTF-8"?>\n<kml xmlns="http://www.opengis.net/kml/2.2">\n<Document>\n<name>Embedded GPS EXIF Data</name>')
for fn, (lat, lon) in gps_data.items():
kml.write('\n<Placemark>\n\t<name>%s</name>' % fn)
kml.write('\n\t<Point>\n\t\t<coordinates>%s,%s</coordinates>\n\t</Point>\n</Placemark>' % (lon, lat))
kml.write("\n</Document>\n</kml>")
print('KML report generated at %s. View report using Google Earth.' % filename)
# Implement and export this function if you need to read the item's contents, which you can do using the hItem parameter (file handle)
def XT_ProcessItemEx(nItem, hItem, reserved):
global gps_data
offset = 0
size = XWF.GetItemSize(nItem)
fn = str(nItem) + '__' + XWF.GetItemName(nItem)
if offset < size:
with TemporaryFile(prefix=fn) as tmpFile:
tmpFile.write(XWF.Read(hItem, offset, size))
tmpFile.seek(0)
try:
image = Image.open(tmpFile)
exif_data = get_exif_data(image)
except IOError:
print('%s is not an image' % fn)
return
gps = get_lat_lon(exif_data)
if gps[0]:
gps_data[fn] = (repr(gps[0]), repr(gps[1]))
print('Found GPS data in %s' % fn)
else:
print('No GPS data in image %s' % fn)
del image
else:
print('%s is too small to contain GPS data.' % fn)
return
# Called when other operations have completed, required function
def XT_Finalize(hVolume, hEvidence, nOpType, lpReserved):
if gps_data:
writeKML()
else:
print('No GPS data in any specified images.')
return
# Called just before the DLL is unloaded to give you a chance to dispose any allocated memory, save certain data permanently etc., required function
def XT_Done(lpReserved):
print('Finished processing files.')
return
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment