DEV Community

Cover image for Use Exiv2 to extract GPS data from Images using CFML
James Moberg
James Moberg

Posted on

3 2

Use Exiv2 to extract GPS data from Images using CFML

I mentioned Exiv2 on a blog post from 2 years ago regarding Supporting ColdFusion with Command Line Programs. Someone in the Adobe ColdFusion Forum recently inquired how to "use ImageGetEXIFMetaData to try to get gps coordinates of an image".

I've been down that road before and have found the built-in function (BIF) ImageGetEXIFMetaData to be both non-performant and lacking when it comes to retrieving image metadata. The BIF only returns Exif data which explains the extremely specific function name. Sometimes the metadata you want is accessible as IPTC, XMP metadata or ICC Profile.

Disclaimer: I stopped using many BIFs years ago in favor of dropping down to the command line and either using CFExecute or CFX_Exec[http://adiabata.com/cfx_exec.cfm] to run apps like WKHTMLTOPDF (for PDF generation) and GraphicsMagick (for image manipulation). The result has been faster performance, higher quality results (data or display-wise) and the ability to perform tasks offline and/or concurrently. I also prefer it because Adobe has historically changed what some functions return (may be a bug, may be intentional, who really knows), whereas I haven't really encountered this issue when using portable command-line apps.

Performance: This Exiv2 UDF takes 40-135ms to return everything (exif, IPTC, XMP metadata & ICC profile) whereas ImageGetEXIFMetaData takes 280-340ms to return only exif data. The BIF's GPS data is also only returned in a sexagesimal display format rather than a useful decimal format. I've also ensured that the keys generated by the UDF are CF-variable-compatible (ie, isValid("variablename")) for easy reuse and/or direct output to an API. Dates are also outputted as valid yyyy-mm-dd hh:mm:ss dates rather than non-normalized yyyy:mm:dd HH:mm:ss format.

WRITING METADATA: If you need to write EXIF, IPTC, XMP or IPTC metadata to an image, I highly recommend using ExifTool. We use ExifTool to embed copyright information into images for the automatic display of the license information whenever thumbnails are displayed via Google Photos. (I find this approach to be superior than adding lots of hidden XML to webpages where the photos are displayed.)

Exiv2 (freeware)

Exiv is a Cross-platform C++ library and a command line utility to manage image metadata. It provides fast and easy read and write access to the Exif, IPTC and XMP metadata and the ICC Profile embedded within digital images in various formats.

Exiv2 UDF Usage

imageData = exiv2(filePath="c:\myImage", exePath="C:\Exiv2\Exiv2.exe");
writeDump(imageData);
Enter fullscreen mode Exit fullscreen mode

Source Code

https://gist.github.com/JamoCA/308df6f2bc927203330a23c1c6678c39

<cfscript>
/* 2016-02-16 Exiv2 (shared on 2022-07-06)
By James Moberg - SunStar Media
Requires command-line version of Exiv2 http://www.exiv2.org/
All data returned in a struct. Based on different filetypes & available metadata, keys may not always exist.
Requirements: CF2016u3+ or Lucee 4.5+
Blog: https://dev.to/gamesover/use-exiv2-to-extract-gps-data-from-images-using-cfml-3maa
Tweet: https://twitter.com/gamesover/status/1544747532881903616
Forum: https://community.adobe.com/t5/coldfusion-discussions/using-imagegetexifmetadata-to-try-to-get-gps-coordinates-of-an-image/m-p/13053016
*/
public struct function exiv2(required string filepath, string exePath="") output=false hint="Extract image metadata (Exif, IPTC, XMP metadata and ICC Profile) using Exiv2" {
local.data = [:];
if ( fileExists(arguments.filepath)) {
// -Pnt = Print Report (P) w/tag name (t) and interpreted (translated) human readable data (t)
cfexecute(arguments="-Pnt #arguments.filepath#", variable="local.raw", name=arguments.exePath, timeout=15);
// 4 Space delimited values. key, datatype,
for (local.line in listToArray(local.raw, chr(10))){
local.line = trim(local.line);
if (listLen(local.line, " ") > 1) {
local.temp = trim(listFirst(local.line, " "));
local.data["#local.temp#"] = trim(listRest(local.line, " "));
}
}
}
// DEBUGGING local.data["raw"] = local.Raw;
if (structCount(local.data)) {
// Add decimal Latitude/Longitude
local.data["Latitude"] = 0;
local.data["Longitude"] = 0;
// Parse sexagesimal latitude/longitude and convert to decimal
if (structKeyExists(local.data, "GPSLatitude") && len(local.data.GPSLatitude) && val(local.data.GPSLatitude)
and structKeyExists(local.data, "GPSLatitudeRef") && len(local.data.GPSLatitudeRef)
and structKeyExists(local.data, "GPSLongitude") && len(local.data.GPSLongitude) && val(local.data.GPSLongitude)
and structKeyExists(local.data, "GPSLongitudeRef") && len(local.data.GPSLongitudeRef)
){
local.latitudeParts = listToArray(replace(replace(local.data["GPSLatitude"],"""",""), "deg",""""), "'""");
local.data["Latitude"] = (local.latitudeParts[1] + (local.latitudeParts[2] / 60) + (local.latitudeParts[3] / 3600));
if ((local.data.GPSLatitudeRef == "South")){
local.data["Latitude"] *= -1;
}
local.longitudeParts = listToArray(replace(replace(local.data["GPSLongitude"],"""",""), "deg",""""), "'""");
local.data["Longitude"] = (local.longitudeParts[1] + (local.longitudeParts[2] / 60) + (local.longitudeParts[3] / 3600));
if ((local.data.GPSLongitudeRef == "West")){
local.data["Longitude"] *= -1;
}
}
// Parse date Exif values to dates. Consolidate GPSDateStamp & GPSTimeStamp (UTC)
for (local.key in local.data) {
if (local.key == "GPSDateStamp" && listLen(local.data[local.key],":") == 3 && isDate(replace(local.data[local.key],":","-", "all"))) {
if (structKeyExists(local.data, "GPSTimeStamp") && listLen(local.data["GPSTimeStamp"],":") == 3) {
local.data["GPSDateStamp"] = dateFormat(parseDateTime(replace(local.data[local.key],":","-", "all")), "yyyy-mm-dd") & " " & local.data["GPSTimeStamp"];
} else {
local.data[local.key] = dateFormat(parseDateTime(replace(local.data[local.key],":","-", "all")), "yyyy-mm-dd") & "00:00:00";
}
} else if (find("DateTime", local.key) && listLen(local.data[local.key],":") == 5 && isDate(replace(replace(local.data[local.key],":","-"),":","-"))) {
local.tempDate = parseDateTime(replace(replace(local.data[local.key],":","-"),":","-"));
local.data[local.key] = dateTimeFormat(local.tempDate,"yyyy-mm-dd") & " " & timeFormat(local.tempDate,"HH:nn:ss");
}
}
} else {
local.data = [
"Error": "Error accessing file"
,"Warning": "Unable to analyze #arguments.filepath#"
,"input": "#arguments.exePath# -Pnt #arguments.filepath#"
,"output": (local.keyExists("raw")) ? local.raw : ""
];
}
return local.data;
}
</cfscript>
view raw exiv2.cfm hosted with ❤ by GitHub

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Engage with a sea of insights in this enlightening article, highly esteemed within the encouraging DEV Community. Programmers of every skill level are invited to participate and enrich our shared knowledge.

A simple "thank you" can uplift someone's spirits. Express your appreciation in the comments section!

On DEV, sharing knowledge smooths our journey and strengthens our community bonds. Found this useful? A brief thank you to the author can mean a lot.

Okay