2007-05-01

GPS-Koordinaten in EXIF-Infos nachführen

Angeregt durch ein Mail von Alexander Klein und einen Artikel in der aktuellsten c’t 10/2007 will ich jetzt doch noch erklären, wie die GPS-Positionen aus meinem ersten Artikel von der Karte in die Bilder gelangen.

In der Karten-Seite befindet sich ein HTML-Formular, das die Eingabe von Breite und Länge erlaubt:

<form method="post" action="exifupd.php">
    <input type="hidden" name="image" value="<?php echo $image ?>" />
    <label for="lat">Latitude:</label>
    <input type="text" id="lat" name="lat" /><br />
    <label for="lon">Longitude:</label>
    <input type="text" id="lon" name="lon" /><br />
    <button type="submit">Update</button>
</form>

Die Werte im Formular werden per JavaScript im onclick() Event der Karte gesetzt:

GEvent.addListener(gMap, 'click', function(obj, pos) {
    document.getElementById('lat').value = pos.lat();
    document.getElementById('lon').value = pos.lng();
});

exifupd.php setzt die Koordinaten via ein Python-Skript ins Bild hinein. Die relevante Zeile des Update-Skripts ist diese:

exec("sudo -u me /home/me/bin/gpstag '{$_POST['lat']}' '{$_POST['lon']}' '{$_SERVER['DOCUMENT_ROOT']}{$_POST['image']}'", $result, $rc);

Der Python-Skript gpstag benötigt seinerseits exiv2, ein Kommandozeilen-Tool zum Bearbeiten von EXIF-Infos.

Diese zwei Zeilen in /etc/sudoers ermöglichen sudo, dass www-data, der User, als der mein Webserver läuft, in meinem Namen das Programm gpstag ausführen darf:

# Apache may update GPS data in images
www-data        ALL = (me) NOPASSWD: /home/me/bin/gpstag

Das Bearbeiten von sudoers erfordert root-Rechte und sollte immer mit dem Befehl visudo geschehen.

Als letztes wird der Update-Skript noch mit einem Eintrag in .htaccess passwortgeschützt:

# Password protection for GPS updates

<Files exifupd.php>
    AuthType Basic
    AuthName "GPS Update"
    AuthUserFile /home/me/lib/htpasswd/php
    Require valid-user
</Files>

Wie man ein Passwort-File bereitstellt, ist unter man htpasswd nachzulesen.

20:27 [/software/php] Google Trackback
Tags:

2006-12-13

Fotos, EXIF-Infos, GPS, Google Maps etc…

Nachdem ich vor ca. zwei Wochen Mätthu geholfen hatte, einen Skript zu schreiben, mit dem er in Google Maps zeigen konnte, wo er ein bestimmtes Foto gemacht hat, habe ich diese Woche das selbe in mein Album eingebaut.

JPG-Dateien von Digital-Kameras können Meta-Daten enthalten, wie z.B. Belichtungszeit, Blende, ob der Blitz eingesetzt wurde, etc. Diese Daten werden innerhalb der JPG-Daten im EXIF-Format abgelegt. Seit Version 2.2 des EXIF-Standards ist möglich, in den Meta-Daten eine geografische Position und andere von einem GPS-Empfänger gelieferte Daten zu speichern.

Meine Skripts ermöglichen es nun, die Position auf einer Google Map darzustellen.

Zu Beginn stehen einige JavaScript-Funktionen:

// Convert a decimal degree value to degress, minutes and seconds
function decToDMS(dec) {
    var deg = Math.floor(dec);
    dec = (dec - deg) * 60;
    var min = Math.floor(dec);
    var sec = (dec - min) * 60;
    sec = Math.round(100 * sec) / 100;	// round to 1/100th precision
    return deg + "&#xB0; " + min + "&#x2032; " + sec + "&#x2033;";
}

// Convert latitude and longitude to a HTML string
function LatLngToHtml(lat, lng) {
    lat = decToDMS(Math.abs(lat)) + (lat < 0 ? ' S' : ' N');
    lng = decToDMS(Math.abs(lng)) + (lng < 0 ? ' W' : ' E');
    return lat + ', ' + lng;
}

// Create a marker at the given point with the given label
function GMcreateMarker(point, label) {
    var marker = new GMarker(point);
    GEvent.addListener(marker, "click", function() {
        marker.openInfoWindowHtml(label);
    });
    return marker;
}

// Initialize a map and return it
function GMinit(mapid) {
    var div = document.getElementById(mapid);
    if (!div) {
        alert("Map region not found");
        return null;
    }

    if (!GBrowserIsCompatible()) {
        div.innerHTML = '<p>Sorry, your browser is not compatible with Google Maps.</p>';
        return null;
    }

    var map = new GMap2(div);

    if (parseInt(div.style.height) >= 350) {
        map.addControl(new GOverviewMapControl());
        map.addControl(new GLargeMapControl());
        map.addControl(new GScaleControl());
    }
    else
        map.addControl(new GSmallMapControl());
    map.addControl(new GMapTypeControl());
    // map.enableDoubleClickZoom();
    map.setCenter(new GLatLng(47.2, 7.5), 8);

    // Allow to click anywhere and display the clicked point's coordinates
    GEvent.addListener(map, 'click', function(obj, point) {
        map.lastPoint = point;  // Keep a copy for the main page
        if (point)
            this.openInfoWindowHtml(point, LatLngToHtml(point.lat(), point.lng()));
    });

    // Add a new member function that creates labeled markers
    map.addMarkerOverlay = function(lat, lng, label) {
        var point = new GLatLng(lat, lng);
        this.addOverlay(GMcreateMarker(point, label));
        return point;
    }

    // Register the cleanup function
    window.onunload = GUnload;

    return map;
}

Die Funktion GMinit wird aus dem onload() Event aufgerufen und gibt ein GMap2-Objekt aus dem Google Map API zurück, das um eine Methode addMarkerOverlay() erweitert ist. Diese Methode erleichtert das Anzeigen von Popup-Meldungen.

Weiter gehts mit einigen PHP-Funktionen zum Auslesen der EXIF-Informationen:

// Utility functions dealing with GPS coordinates from EXIF data

define(NO_ALTITUDE, -1024);

// Convert a lat or lon 3-array to decimal degrees
function exif_degrees($a) {
  @eval("$deg = $a[0]; $min = $a[1]; $sec = $a[2];");
  return $deg + $min / 60.0 + $sec / 3600.0;
}

// Extract lat, lon and altitude from an EXIF GPS array
function exif_gps_vars($exif, &$lat, &$lon, &$alt) {
  $lat = exif_degrees($exif['GPS']['GPSLatitude']);
  if ($exif['GPS']['GPSLatitudeRef'] == 'S')
    $lat *= -1;
  $lon = exif_degrees($exif['GPS']['GPSLongitude']);
  if ($exif['GPS']['GPSLongitudeRef'] == 'W')
    $lon *= -1;
  if (isset($exif['GPS']['GPSAltitude']))
    @eval("$alt = {$exif['GPS']['GPSAltitude']};");
  else
    $alt = NO_ALTITUDE;
}

// Convert decimal degrees to deg° min' sec" H
function exif_dec_to_DMS($dec, $hemi_pos, $hemi_neg) {
  $absdec = abs($dec);
  $deg = intval($absdec);
  $absdec = ($absdec - $deg) * 60;
  $min = intval($absdec);
  $sec = ($absdec - $min) * 60;
  return sprintf("%d° %02d′ %05.2f″ %s",
    $deg, $min, $sec, $dec >= 0 ? $hemi_pos : $hemi_neg
  );
}

// Convert an EXIF GPS array to HTML-usable text
function exif_to_html($exif) {
  exif_gps_vars($exif, $lat, $lon, $alt);
  $lat = exif_dec_to_DMS($lat, 'N', 'S');
  $lon = exif_dec_to_DMS($lon, 'E', 'W');
  $alt = intval($alt);
  return "$lat, $lon" . ($alt == NO_ALTITUDE ? '' : " ($alt m)");
}

Diese Funktionen werden in der Karten-Seite zusammengesetzt:

PHP-Code:

$def_image = '/pic/glasses.jpg';
$image = isset($_GET['image']) ? $_GET['image'] : $def_image;

function image_exif_gps($file) {
  if (
    ($exif = @exif_read_data($_SERVER['DOCUMENT_ROOT'] . '/' . $file, 0, true)) !== false &&
    $exif['GPS']
  ) {
    exif_gps_vars($exif, $lat, $lon, $alt);
    // Return a JavaScript dictionary containing all useful GPS info
    return "{isValid: true, lat: $lat, lon: $lon, alt: $alt, file: \"$file\"}";
  }
  else
    return "{isValid: false, file: \"$file\"}";
}

JavaScript-Code:

var gmap;

window.onload = function() {
    if (gmap = GMinit('map')) {
        gmap.addMarkerOverlay(47.0452, 7.2713, 'Home, sweet home!');
        var gps = gmap.gps = <?php echo image_exif_gps($image) ?>;
        if (gps.isValid)
            gmap.setCenter(
                gmap.addMarkerOverlay(gps.lat, gps.lon, LatLngToHtml(gps.lat, gps.lon)),
                14, G_SATELLITE_MAP
            );
    }
}

Das Besondere dabei ist, dass die PHP-Funktion image_exif_gps() dynamisch ein JavaScript-Objekt generiert, das die GPS-Koordinaten enthält.

Den Update-Mechanismus beschreibe ich in einem nächsten Post.

Update: Hier gibts den versprochenen Update der GPS-Position im Bild.

23:33 [/software/php] Google Trackback
Tags:

2005-09-15

Mehrsprachige Webseiten

Kürzlich hatte ich mal ein Flashback in die Zeit, in der alle Software, die ich schrieb, mehrsprachig sein musste. Also habe ich mich an eine standardkompatible und semantisch korrekte PHP- und CSS-Implementation gemacht, die ich jetzt hier mal poste, in der Hoffnung, dass das vielleicht einmal jemandem mit ähnlichen Problemen etwas weiter hilft: trilingual.php, Source.

22:43 [/software/php] Google Trackback
Tags:

2005-03-26

Album-Update

Ich habe im Album einige access keys eingeführt:

  • In einem Menu können die ersten zehn Unter-Alben mit Alt-1 bis Alt-0 gewählt werden
  • Innerhalb eines Albums geht
    • Alt-n zum nächsten Bild
    • Alt-p zum vorangehenden Bild
    • Alt-0 zurück in die Album-Übersicht

Die davon betroffenen Links sind jetzt auch mit einem passenden rel-Attribut versehen.

19:10 [/software/php] Google Trackback
Tags:

2005-01-13

Album-Update

Ab heute sind zwei neue Features im Album:

  • Unterstützung für Album-Informationen analog Bild-Informationen. Name der Album-Info-Datei: .info
  • Der Name des Zufallsbildes auf der Einstiegsseite ist in einen Album- und einen Bild-Link unterteilt

23:21 [/software/php] Google Trackback

2004-06-06

Nicht der Kaffee, sondern das Album ist fertig! [Update]

oder «YAPA: Yet another PHP album»

Jetzt, da mein Foto-Album (alias Galerie) auch Vorschau-Bilder anzeigen kann, ist die Zeit gekommen, es der Öffentlichkeit vorzustellen. Von der Albumauswahl an sollten eigentlich die weiteren Schritte offensichtlich sein… wenn nicht: sofort melden!

Konzept

Die Grundidee war—ganz dem Prinzip von Unix folgend—, diejenige hierarchische Datenbank für meine Zwecke zu benützen, die jedes halbwegs vernünftige Betriebssystem von Hause aus bereits zur Verfügung stellt, nämlich das Filesystem! Ein Album ist ganz einfach ein Verzeichnis unter $HOME/public_html/album, in dem Bilddateien der Typen PNG, JPEG oder GIF abgelegt werden können. Das schöne am Filesystem ist natürlich, dass ein Album wieder Unter-Albums (-Albümmer?) enthalten kann, und diese wieder weitere Unter-Unter-Albums, etc, etc, bis der Disk-Space ausgeht. Da die Verzeichnisse bei jedem Request dynamisch ausgelesen werden, stimmt die Anzeige immer mit der Verzeichnisstruktur überein. Und nein, es gibt kein Performance-Problem, sofern ein Album nicht mehr als ein paar Dutzend Bilder umfasst.

Zusatzinformationen zu Bildern und Alben

Jetzt habe ich also meine Bilder schön strukturiert in ihren Verzeichnissen abgelegt, aber woher nehme ich jetzt die Zusatzinformationen? Vielleicht möchte ich ja zu einem Bild noch eine Beschreibung anzeigen! Auch hier habe ich wieder dass Prinzip «KISS» angewendet: zu einem Bild namens blabla.jpg kann es eine Datei namens blabla.info geben, die diese Zusatzinformationen enthält. Die Info-Datei für ein ganzes Album heisst einfach .info. Und damit diesen Infos auch eine gewisse Ordnung gegeben werden kann, haben diese .info-Dateien einen bestimmten Aufbau, den ich schamlos im RFC 822 (STANDARD FOR THE FORMAT OF ARPA INTERNET TEXT MESSAGES) abgekupfert habe: Auf eine beliebige Anzahl Headerzeilen der Form Name: Wert folgt eine Leerzeile, dann folgt der unstrukturierte Rest.

Die folgenden Headerzeilen sind momentan definiert:

Title:
Der Bildtitel; meistens eine kurze Beschreibung des Inhalts. Default: der Dateiname des Bildes.
Subtitle:
Eine Unterüberschrift, Tagline, etc. Default: leer.
Alt:
Der Text, der als alt-Attribut des Bildes verwendet wird. Default: der Dateiname des Bildes.
Width:
Die gewünschte Bildbreite. Default: die effektive Bildbreite.
Height:
Die gewünschte Bildhöhe. Default: die effektive Bildhöhe.
EXIF:
Erzwingt die Anzeite der EXIF-Informationen, sofern vorhanden. Default: keine EXIF-Anzeige.

Bemerkungen:

  • Die Bildgrösse kann mit Width: und Height: gezielt übersteuert werden, falls das nötig sein sollte.
  • Die Anzeige der EXIF-Informationen kann auch in der URI durch das Anhängen des Query-Parameters &exif=1 erzwungen werden.
  • Der unstrukturierte Rest, von dem oben die Rede war, ist die eigentliche, unter Umständen lange Bildbeschreibung, die natürlich wieder alle HTML-Tags beinhalten kann. Der Schreiblust sind hier keine Grenzen gesetzt!
  • Da alle Kopfzeilen fakultativ sind, muss die Info-Datei mit einer Leerzeile beginnen, wenn sie ausschliesslich eine Bildbeschreibung enthält.

Vorschau, alias thumbnails, alias preview

Was jetzt noch fehlt, ist die Bildvorschau, im Jargon so schön thumbnail genannt. Hier habe ich mich für zwei verschiedene Möglichkeiten entschieden:

  1. JPEG-Bilder von Digitalkameras enthalten meistens in den EXIF-Informationen eine brauchbare Vorschau von ca. 160x120 Pixeln. Wenn diese Vorschau vorhanden ist, wird sie verwendet.
  2. Wenn Bilder vor der Veröffentlichung bearbeitet werden, geht in vielen Fällen die Vorschau verloren, oder zumindest gibt sie nicht mehr das bearbeitete Bild wieder. In diesen Fällen, wie auch bei den anderen Bildtypen GIF und PNG, die keine integrierte Vorschau kennen, kann die Vorschau als eigenes Bild namens tn_Bild.Typ abgelegt werden («tn» wie thumbnail, capisce?). Diese Datei, sofern sie vorhanden ist, übersteuert die EXIF-Vorschau.

Damit das Layout der Bildauswahl nicht völlig auseinander fällt, sollten die Thumbnails nicht grösser als 160x160 Pixel sein. À propos Layout: der sich der Browserbreite anpassende Umbruch, der nota bene mit einem Tabellen-basierten Seitenaufbau schlicht und ergreifend nicht möglich ist (dies nur ein kleiner Seitenhieb am Rande! ;-) habe ich von A list apart, der ausgezeichneten Site des Web-Standard-Experten Jeffrey Zeldman.

Todo-Liste

  • In der Vorschau anstelle des Dateinamens den Titel aus der Info-Datei anzeigen
  • Eine Info-Datei für das ganze Album ermöglichen
  • Ein schöneres Bild als aktuelle «No Preview» machen
  • Soll ein Bild ein Link auf das nächste Bild des Albums sein?
  • Andere Medientypen ermöglichen (MP3, MOV, AVI, …)

Geeks only

Der Source-Code des Albums besteht aus ca. 350 Zeilen (für meine Verhältnisse ausgiebig kommentierten) PHP-Code und ein paar Zeilen CSS.

17:01 [/software/php] Google Trackback