diff options
-rw-r--r-- | dive.h | 9 | ||||
-rw-r--r-- | dives/test31.xml | 23 | ||||
-rw-r--r-- | file.c | 2 | ||||
-rw-r--r-- | file.h | 8 | ||||
-rw-r--r-- | qt-ui/divelistview.cpp | 89 | ||||
-rw-r--r-- | qt-ui/divelistview.h | 3 | ||||
-rw-r--r-- | qt-ui/exif.cpp | 559 | ||||
-rw-r--r-- | qt-ui/exif.h | 143 | ||||
-rw-r--r-- | qt-ui/profilegraphics.cpp | 7 | ||||
-rw-r--r-- | qt-ui/shiftimagetimes.ui | 137 | ||||
-rw-r--r-- | qt-ui/simplewidgets.cpp | 20 | ||||
-rw-r--r-- | qt-ui/simplewidgets.h | 13 | ||||
-rw-r--r-- | subsurface.pro | 3 | ||||
-rw-r--r-- | wreck.jpg | bin | 0 -> 116727 bytes |
14 files changed, 1015 insertions, 1 deletions
@@ -614,6 +614,11 @@ static inline struct dive *getDiveById(int id) } return dive; } + +#ifdef __cplusplus +extern "C" { +#endif + extern struct dive *find_dive_including(timestamp_t when); extern bool dive_within_time_range(struct dive *dive, timestamp_t when, timestamp_t offset); struct dive *find_dive_n_near(timestamp_t when, int n, timestamp_t offset); @@ -692,6 +697,10 @@ extern void set_dc_nickname(struct dive *dive); extern void set_autogroup(bool value); extern int total_weight(struct dive *); +#ifdef __cplusplus +} +#endif + #define DIVE_ERROR_PARSE 1 #define DIVE_ERROR_PLAN 2 diff --git a/dives/test31.xml b/dives/test31.xml new file mode 100644 index 000000000..a93155432 --- /dev/null +++ b/dives/test31.xml @@ -0,0 +1,23 @@ +<divelog program='subsurface' version='2'> +<settings> +<divecomputerid model='Suunto Vyper' deviceid='7fffffff' serial='00522075' firmware='0.0.33'/> +<divecomputerid model='Suunto Vyper' deviceid='e9237b0a' nickname='Suunto Vyper (e9237b0a)'/> +<autogroup state='1' /> +</settings> +<dives> +<trip date='2012-01-08' time='15:30:03'> +<dive number='1' date='2012-01-08' time='15:30:03' duration='46:06 min'> + <cylinder size='24.0 l' workpressure='232.0 bar' description='D12 232 bar' /> + <divecomputer model='manually added dive' date='2014-01-09' time='14:22:03'> + <depth max='15.0 m' mean='13.671 m' /> + <sample time='0:00 min' depth='0.0 m' /> + <sample time='1:00 min' depth='15.0 m' /> + <sample time='40:00 min' depth='15.0 m' /> + <sample time='42:00 min' depth='5.0 m' /> + <sample time='45:00 min' depth='5.0 m' /> + <sample time='46:06 min' depth='0.0 m' /> + </divecomputer> +</dive> +</trip> +</dives> +</divelog> @@ -156,7 +156,7 @@ static int try_to_open_db(const char *filename, struct memblock *mem, char **err return parse_dm4_buffer(filename, mem->buffer, mem->size, &dive_table, error); } -static timestamp_t parse_date(const char *date) +timestamp_t parse_date(const char *date) { int hour, min, sec; struct tm tm; @@ -9,6 +9,14 @@ struct memblock { #if 0 extern int try_to_open_cochran(const char *filename, struct memblock *mem, GError **error); #endif + +#ifdef __cplusplus +extern "C" { +#endif extern int readfile(const char *filename, struct memblock *mem); +extern timestamp_t parse_date(const char *date); +#ifdef __cplusplus +} +#endif #endif diff --git a/qt-ui/divelistview.cpp b/qt-ui/divelistview.cpp index 86a3b95fd..1b19d06fd 100644 --- a/qt-ui/divelistview.cpp +++ b/qt-ui/divelistview.cpp @@ -10,6 +10,8 @@ #include "mainwindow.h" #include "subsurfacewebservices.h" #include "../display.h" +#include "exif.h" +#include "../file.h" #include <QApplication> #include <QHeaderView> #include <QDebug> @@ -21,6 +23,9 @@ #include <QKeyEvent> #include <QMenu> #include <QFileDialog> +#include <string> +#include <iostream> + DiveListView::DiveListView(QWidget *parent) : QTreeView(parent), mouseClickSelection(false), sortColumn(0), currentOrder(Qt::DescendingOrder), searchBox(new QLineEdit(this)) @@ -735,6 +740,7 @@ void DiveListView::contextMenuEvent(QContextMenuEvent *event) popup.addAction(tr("save As"), this, SLOT(saveSelectedDivesAs())); popup.addAction(tr("export As UDDF"), this, SLOT(exportSelectedDivesAsUDDF())); popup.addAction(tr("shift times"), this, SLOT(shiftTimes())); + popup.addAction(tr("load images"), this, SLOT(loadImages())); } if (d) popup.addAction(tr("upload dive(s) to divelogs.de"), this, SLOT(uploadToDivelogsDE())); @@ -794,7 +800,90 @@ void DiveListView::shiftTimes() ShiftTimesDialog::instance()->show(); } +void DiveListView::loadImages() +{ + struct memblock mem; + EXIFInfo exif; + int code; + time_t imagetime; + QStringList fileNames = QFileDialog::getOpenFileNames(this, tr("Open Image Files"), lastUsedImageDir(), tr("Image Files (*.jpg *.jpeg *.pnm *.tif *.tiff)")); + + if (fileNames.isEmpty()) + return; + + updateLastUsedImageDir(QFileInfo(fileNames[0]).dir().path()); + + ShiftImageTimesDialog* shiftDialog = ShiftImageTimesDialog::instance(); + shiftDialog->exec(); + + for (int i = 0; i < fileNames.size(); ++i) { + struct tm tm; + int year, month, day, hour, min, sec; + readfile(fileNames.at(i).toUtf8().data(), &mem); + code = exif.parseFrom((const unsigned char *) mem.buffer, (unsigned) mem.size); + free(mem.buffer); + sscanf(exif.DateTime.c_str(), "%d:%d:%d %d:%d:%d", &year, &month, &day, &hour, &min, &sec); + tm.tm_year = year; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_hour = hour; + tm.tm_min = min; + tm.tm_sec = sec; + imagetime = utc_mktime(&tm) + shiftDialog->amount; + int j = 0; + struct dive *dive; + for_each_dive(j, dive){ + if (!dive->selected) + continue; + if (dive->when - 3600 < imagetime && dive->when + dive->duration.seconds + 3600 > imagetime){ + if (dive->when > imagetime) { + ; // Before dive + add_event(&(dive->dc), 0, 123, 0, 0, fileNames.at(i).toUtf8().data()); + mainWindow()->refreshDisplay(); + mark_divelist_changed(true); + } + else if (dive->when + dive->duration.seconds < imagetime){ + ; // After dive + add_event(&(dive->dc), dive->duration.seconds, 123, 0, 0, fileNames.at(i).toUtf8().data()); + mainWindow()->refreshDisplay(); + mark_divelist_changed(true); + } + else { + add_event(&(dive->dc), imagetime - dive->when, 123, 0, 0, fileNames.at(i).toUtf8().data()); + mainWindow()->refreshDisplay(); + mark_divelist_changed(true); + } + if (!dive->latitude.udeg && !IS_FP_SAME(exif.GeoLocation.Latitude, 0.0)){ + dive->latitude.udeg = lrint(1000000.0 * exif.GeoLocation.Latitude); + dive->longitude.udeg = lrint(1000000.0 * exif.GeoLocation.Longitude); + mark_divelist_changed(true); + mainWindow()->refreshDisplay(); + } + } + } + } +} + void DiveListView::uploadToDivelogsDE() { DivelogsDeWebServices::instance()->prepareDivesForUpload(); } + +QString DiveListView::lastUsedImageDir() +{ + QSettings settings; + QString lastImageDir = QDir::homePath(); + + settings.beginGroup("FileDialog"); + if (settings.contains("LastImageDir")) + if (QDir::setCurrent(settings.value("LastImageDir").toString())) + lastImageDir = settings.value("LastIamgeDir").toString(); + return lastImageDir; +} + +void DiveListView::updateLastUsedImageDir(const QString& dir) +{ + QSettings s; + s.beginGroup("FileDialog"); + s.setValue("LastImageDir", dir); +} diff --git a/qt-ui/divelistview.h b/qt-ui/divelistview.h index f65a6780b..99b8fce23 100644 --- a/qt-ui/divelistview.h +++ b/qt-ui/divelistview.h @@ -49,6 +49,7 @@ public slots: void saveSelectedDivesAs(); void exportSelectedDivesAsUDDF(); void shiftTimes(); + void loadImages(); void uploadToDivelogsDE(); signals: @@ -71,6 +72,8 @@ private: void restoreExpandedRows(); int lastVisibleColumn(); void selectTrip ( dive_trip_t* trip ); + QString lastUsedImageDir(); + void updateLastUsedImageDir(const QString& s); }; #endif // DIVELISTVIEW_H diff --git a/qt-ui/exif.cpp b/qt-ui/exif.cpp new file mode 100644 index 000000000..c122567cb --- /dev/null +++ b/qt-ui/exif.cpp @@ -0,0 +1,559 @@ +#include <stdio.h> +/************************************************************************** + exif.cpp -- A simple ISO C++ library to parse basic EXIF + information from a JPEG file. + + Copyright (c) 2010-2013 Mayank Lahiri + mlahiri@gmail.com + All rights reserved (BSD License). + + See exif.h for version history. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + -- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + -- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESS + OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#include <algorithm> +#include "exif.h" + +using std::string; + +namespace { + // IF Entry + struct IFEntry { + // Raw fields + unsigned short tag; + unsigned short format; + unsigned data; + unsigned length; + + // Parsed fields + string val_string; + unsigned short val_16; + unsigned val_32; + double val_rational; + unsigned char val_byte; + }; + + // Helper functions + unsigned int parse32(const unsigned char *buf, bool intel) { + if (intel) + return ((unsigned)buf[3]<<24) | + ((unsigned)buf[2]<<16) | + ((unsigned)buf[1]<<8) | + buf[0]; + + return ((unsigned)buf[0]<<24) | + ((unsigned)buf[1]<<16) | + ((unsigned)buf[2]<<8) | + buf[3]; + } + + unsigned short parse16(const unsigned char *buf, bool intel) { + if (intel) + return ((unsigned) buf[1]<<8) | buf[0]; + return ((unsigned) buf[0]<<8) | buf[1]; + } + + string parseEXIFString(const unsigned char *buf, + const unsigned num_components, + const unsigned data, + const unsigned base, + const unsigned len) { + string value; + if (num_components <= 4) + value.assign( (const char*)&data, num_components ); + else { + if (base+data+num_components <= len) + value.assign( (const char*)(buf+base+data), num_components ); + } + return value; + } + + double parseEXIFRational(const unsigned char *buf, bool intel) { + double numerator = 0; + double denominator = 1; + + numerator = (double) parse32(buf, intel); + denominator= (double) parse32(buf+4, intel); + if(denominator < 1e-20) + return 0; + return numerator/denominator; + } + + IFEntry parseIFEntry(const unsigned char *buf, + const unsigned offs, + const bool alignIntel, + const unsigned base, + const unsigned len) { + IFEntry result; + + // Each directory entry is composed of: + // 2 bytes: tag number (data field) + // 2 bytes: data format + // 4 bytes: number of components + // 4 bytes: data value or offset to data value + result.tag = parse16(buf + offs, alignIntel); + result.format = parse16(buf + offs + 2, alignIntel); + result.length = parse32(buf + offs + 4, alignIntel); + result.data = parse32(buf + offs + 8, alignIntel); + + // Parse value in specified format + switch (result.format) { + case 1: + result.val_byte = (unsigned char) *(buf + offs + 8); + break; + case 2: + result.val_string = parseEXIFString(buf, result.length, result.data, base, len); + break; + case 3: + result.val_16 = parse16((const unsigned char *) buf + offs + 8, alignIntel); + break; + case 4: + result.val_32 = result.data; + break; + case 5: + if (base + result.data + 8 <= len) + result.val_rational = parseEXIFRational(buf + base + result.data, alignIntel); + break; + case 7: + case 9: + case 10: + break; + default: + result.tag = 0xFF; + } + return result; + } +} + +// +// Locates the EXIF segment and parses it using parseFromEXIFSegment +// +int EXIFInfo::parseFrom(const unsigned char *buf, unsigned len) { + // Sanity check: all JPEG files start with 0xFFD8 and end with 0xFFD9 + // This check also ensures that the user has supplied a correct value for len. + if (!buf || len < 4) + return PARSE_EXIF_ERROR_NO_EXIF; + if (buf[0] != 0xFF || buf[1] != 0xD8) + return PARSE_EXIF_ERROR_NO_JPEG; + if (buf[len-2] != 0xFF || buf[len-1] != 0xD9) + return PARSE_EXIF_ERROR_NO_JPEG; + clear(); + + // Scan for EXIF header (bytes 0xFF 0xE1) and do a sanity check by + // looking for bytes "Exif\0\0". The marker length data is in Motorola + // byte order, which results in the 'false' parameter to parse16(). + // The marker has to contain at least the TIFF header, otherwise the + // EXIF data is corrupt. So the minimum length specified here has to be: + // 2 bytes: section size + // 6 bytes: "Exif\0\0" string + // 2 bytes: TIFF header (either "II" or "MM" string) + // 2 bytes: TIFF magic (short 0x2a00 in Motorola byte order) + // 4 bytes: Offset to first IFD + // ========= + // 16 bytes + unsigned offs = 0; // current offset into buffer + for (offs = 0; offs < len-1; offs++) + if (buf[offs] == 0xFF && buf[offs+1] == 0xE1) + break; + if (offs + 4 > len) + return PARSE_EXIF_ERROR_NO_EXIF; + offs += 2; + unsigned short section_length = parse16(buf + offs, false); + if (offs + section_length > len || section_length < 16) + return PARSE_EXIF_ERROR_CORRUPT; + offs += 2; + + return parseFromEXIFSegment(buf + offs, len - offs); +} + +int EXIFInfo::parseFrom(const string &data) { + return parseFrom((const unsigned char *)data.data(), data.length()); +} + +// +// Main parsing function for an EXIF segment. +// +// PARAM: 'buf' start of the EXIF TIFF, which must be the bytes "Exif\0\0". +// PARAM: 'len' length of buffer +// +int EXIFInfo::parseFromEXIFSegment(const unsigned char *buf, unsigned len) { + bool alignIntel = true; // byte alignment (defined in EXIF header) + unsigned offs = 0; // current offset into buffer + if (!buf || len < 6) + return PARSE_EXIF_ERROR_NO_EXIF; + + if (!std::equal(buf, buf+6, "Exif\0\0")) + return PARSE_EXIF_ERROR_NO_EXIF; + offs += 6; + + // Now parsing the TIFF header. The first two bytes are either "II" or + // "MM" for Intel or Motorola byte alignment. Sanity check by parsing + // the unsigned short that follows, making sure it equals 0x2a. The + // last 4 bytes are an offset into the first IFD, which are added to + // the global offset counter. For this block, we expect the following + // minimum size: + // 2 bytes: 'II' or 'MM' + // 2 bytes: 0x002a + // 4 bytes: offset to first IDF + // ----------------------------- + // 8 bytes + if (offs + 8 > len) + return PARSE_EXIF_ERROR_CORRUPT; + unsigned tiff_header_start = offs; + if (buf[offs] == 'I' && buf[offs+1] == 'I') + alignIntel = true; + else { + if(buf[offs] == 'M' && buf[offs+1] == 'M') + alignIntel = false; + else + return PARSE_EXIF_ERROR_UNKNOWN_BYTEALIGN; + } + this->ByteAlign = alignIntel; + offs += 2; + if (0x2a != parse16(buf+offs, alignIntel)) + return PARSE_EXIF_ERROR_CORRUPT; + offs += 2; + unsigned first_ifd_offset = parse32(buf + offs, alignIntel); + offs += first_ifd_offset - 4; + if (offs >= len) + return PARSE_EXIF_ERROR_CORRUPT; + + // Now parsing the first Image File Directory (IFD0, for the main image). + // An IFD consists of a variable number of 12-byte directory entries. The + // first two bytes of the IFD section contain the number of directory + // entries in the section. The last 4 bytes of the IFD contain an offset + // to the next IFD, which means this IFD must contain exactly 6 + 12 * num + // bytes of data. + if (offs + 2 > len) + return PARSE_EXIF_ERROR_CORRUPT; + int num_entries = parse16(buf + offs, alignIntel); + if (offs + 6 + 12 * num_entries > len) + return PARSE_EXIF_ERROR_CORRUPT; + offs += 2; + unsigned exif_sub_ifd_offset = len; + unsigned gps_sub_ifd_offset = len; + while (--num_entries >= 0) { + IFEntry result = parseIFEntry(buf, offs, alignIntel, tiff_header_start, len); + offs += 12; + switch(result.tag) { + case 0x102: + // Bits per sample + if (result.format == 3) + this->BitsPerSample = result.val_16; + break; + + case 0x10E: + // Image description + if (result.format == 2) + this->ImageDescription = result.val_string; + break; + + case 0x10F: + // Digicam make + if (result.format == 2) + this->Make = result.val_string; + break; + + case 0x110: + // Digicam model + if (result.format == 2) + this->Model = result.val_string; + break; + + case 0x112: + // Orientation of image + if (result.format == 3) + this->Orientation = result.val_16; + break; + + case 0x131: + // Software used for image + if (result.format == 2) + this->Software = result.val_string; + break; + + case 0x132: + // EXIF/TIFF date/time of image modification + if (result.format == 2) + this->DateTime = result.val_string; + break; + + case 0x8298: + // Copyright information + if (result.format == 2) + this->Copyright = result.val_string; + break; + + case 0x8825: + // GPS IFS offset + gps_sub_ifd_offset = tiff_header_start + result.data; + break; + + case 0x8769: + // EXIF SubIFD offset + exif_sub_ifd_offset = tiff_header_start + result.data; + break; + } + } + + // Jump to the EXIF SubIFD if it exists and parse all the information + // there. Note that it's possible that the EXIF SubIFD doesn't exist. + // The EXIF SubIFD contains most of the interesting information that a + // typical user might want. + if (exif_sub_ifd_offset + 4 <= len) { + offs = exif_sub_ifd_offset; + int num_entries = parse16(buf + offs, alignIntel); + if (offs + 6 + 12 * num_entries > len) + return PARSE_EXIF_ERROR_CORRUPT; + offs += 2; + while (--num_entries >= 0) { + IFEntry result = parseIFEntry(buf, offs, alignIntel, tiff_header_start, len); + switch(result.tag) { + case 0x829a: + // Exposure time in seconds + if (result.format == 5) + this->ExposureTime = result.val_rational; + break; + + case 0x829d: + // FNumber + if (result.format == 5) + this->FNumber = result.val_rational; + break; + + case 0x8827: + // ISO Speed Rating + if (result.format == 3) + this->ISOSpeedRatings = result.val_16; + break; + + case 0x9003: + // Original date and time + if (result.format == 2) + this->DateTimeOriginal = result.val_string; + break; + + case 0x9004: + // Digitization date and time + if (result.format == 2) + this->DateTimeDigitized = result.val_string; + break; + + case 0x9201: + // Shutter speed value + if (result.format == 5) + this->ShutterSpeedValue = result.val_rational; + break; + + case 0x9204: + // Exposure bias value + if (result.format == 5) + this->ExposureBiasValue = result.val_rational; + break; + + case 0x9206: + // Subject distance + if (result.format == 5) + this->SubjectDistance = result.val_rational; + break; + + case 0x9209: + // Flash used + if (result.format == 3) + this->Flash = result.data ? 1 : 0; + break; + + case 0x920a: + // Focal length + if (result.format == 5) + this->FocalLength = result.val_rational; + break; + + case 0x9207: + // Metering mode + if (result.format == 3) + this->MeteringMode = result.val_16; + break; + + case 0x9291: + // Subsecond original time + if (result.format == 2) + this->SubSecTimeOriginal = result.val_string; + break; + + case 0xa002: + // EXIF Image width + if (result.format == 4) + this->ImageWidth = result.val_32; + if (result.format == 3) + this->ImageWidth = result.val_16; + break; + + case 0xa003: + // EXIF Image height + if (result.format == 4) + this->ImageHeight = result.val_32; + if (result.format == 3) + this->ImageHeight = result.val_16; + break; + + case 0xa405: + // Focal length in 35mm film + if (result.format == 3) + this->FocalLengthIn35mm = result.val_16; + break; + } + offs += 12; + } + } + + // Jump to the GPS SubIFD if it exists and parse all the information + // there. Note that it's possible that the GPS SubIFD doesn't exist. + if (gps_sub_ifd_offset + 4 <= len) { + offs = gps_sub_ifd_offset; + int num_entries = parse16(buf + offs, alignIntel); + if (offs + 6 + 12 * num_entries > len) + return PARSE_EXIF_ERROR_CORRUPT; + offs += 2; + while (--num_entries >= 0) { + unsigned short tag = parse16(buf + offs, alignIntel); + unsigned short format = parse16(buf + offs + 2, alignIntel); + unsigned length = parse32(buf + offs + 4, alignIntel); + unsigned data = parse32(buf + offs + 8, alignIntel); + switch(tag) { + case 1: + // GPS north or south + this->GeoLocation.LatComponents.direction = *(buf + offs + 8); + if ('S' == this->GeoLocation.LatComponents.direction) + this->GeoLocation.Latitude = -this->GeoLocation.Latitude; + break; + + case 2: + // GPS latitude + if (format == 5 && length == 3) { + this->GeoLocation.LatComponents.degrees = + parseEXIFRational(buf + data + tiff_header_start, alignIntel); + this->GeoLocation.LatComponents.minutes = + parseEXIFRational(buf + data + tiff_header_start + 8, alignIntel); + this->GeoLocation.LatComponents.seconds = + parseEXIFRational(buf + data + tiff_header_start + 16, alignIntel); + this->GeoLocation.Latitude = + this->GeoLocation.LatComponents.degrees + + this->GeoLocation.LatComponents.minutes / 60 + + this->GeoLocation.LatComponents.seconds / 3600; + if ('S' == this->GeoLocation.LatComponents.direction) + this->GeoLocation.Latitude = -this->GeoLocation.Latitude; + } + break; + + case 3: + // GPS east or west + this->GeoLocation.LonComponents.direction = *(buf + offs + 8); + if ('W' == this->GeoLocation.LonComponents.direction) + this->GeoLocation.Longitude = -this->GeoLocation.Longitude; + break; + + case 4: + // GPS longitude + if (format == 5 && length == 3) { + this->GeoLocation.LonComponents.degrees = + parseEXIFRational(buf + data + tiff_header_start, alignIntel); + this->GeoLocation.LonComponents.minutes = + parseEXIFRational(buf + data + tiff_header_start + 8, alignIntel); + this->GeoLocation.LonComponents.seconds = + parseEXIFRational(buf + data + tiff_header_start + 16, alignIntel); + this->GeoLocation.Longitude = + this->GeoLocation.LonComponents.degrees + + this->GeoLocation.LonComponents.minutes / 60 + + this->GeoLocation.LonComponents.seconds / 3600; + if ('W' == this->GeoLocation.LonComponents.direction) + this->GeoLocation.Longitude = -this->GeoLocation.Longitude; + } + break; + + case 5: + // GPS altitude reference (below or above sea level) + this->GeoLocation.AltitudeRef = *(buf + offs + 8); + if (1 == this->GeoLocation.AltitudeRef) + this->GeoLocation.Altitude = -this->GeoLocation.Altitude; + break; + + case 6: + // GPS altitude reference + if (format == 5) { + this->GeoLocation.Altitude = + parseEXIFRational(buf + data + tiff_header_start, alignIntel); + if (1 == this->GeoLocation.AltitudeRef) + this->GeoLocation.Altitude = -this->GeoLocation.Altitude; + } + break; + } + offs += 12; + } + } + + return PARSE_EXIF_SUCCESS; +} + +void EXIFInfo::clear() { + // Strings + ImageDescription = ""; + Make = ""; + Model = ""; + Software = ""; + DateTime = ""; + DateTimeOriginal = ""; + DateTimeDigitized = ""; + SubSecTimeOriginal= ""; + Copyright = ""; + + // Shorts / unsigned / double + ByteAlign = 0; + Orientation = 0; + + BitsPerSample = 0; + ExposureTime = 0; + FNumber = 0; + ISOSpeedRatings = 0; + ShutterSpeedValue = 0; + ExposureBiasValue = 0; + SubjectDistance = 0; + FocalLength = 0; + FocalLengthIn35mm = 0; + Flash = 0; + MeteringMode = 0; + ImageWidth = 0; + ImageHeight = 0; + + // Geolocation + GeoLocation.Latitude = 0; + GeoLocation.Longitude = 0; + GeoLocation.Altitude = 0; + GeoLocation.AltitudeRef = 0; + GeoLocation.LatComponents.degrees = 0; + GeoLocation.LatComponents.minutes = 0; + GeoLocation.LatComponents.seconds = 0; + GeoLocation.LatComponents.direction = 0; + GeoLocation.LonComponents.degrees = 0; + GeoLocation.LonComponents.minutes = 0; + GeoLocation.LonComponents.seconds = 0; + GeoLocation.LonComponents.direction = 0; +} diff --git a/qt-ui/exif.h b/qt-ui/exif.h new file mode 100644 index 000000000..a05399f92 --- /dev/null +++ b/qt-ui/exif.h @@ -0,0 +1,143 @@ +/************************************************************************** + exif.h -- A simple ISO C++ library to parse basic EXIF + information from a JPEG file. + + Based on the description of the EXIF file format at: + -- http://park2.wakwak.com/~tsuruzoh/Computer/Digicams/exif-e.html + -- http://www.media.mit.edu/pia/Research/deepview/exif.html + -- http://www.exif.org/Exif2-2.PDF + + Copyright (c) 2010-2013 Mayank Lahiri + mlahiri@gmail.com + All rights reserved. + + VERSION HISTORY: + ================ + + 2.1: Released July 2013 + -- fixed a bug where JPEGs without an EXIF SubIFD would not be parsed + -- fixed a bug in parsing GPS coordinate seconds + -- fixed makefile bug + -- added two pathological test images from Matt Galloway + http://www.galloway.me.uk/2012/01/uiimageorientation-exif-orientation-sample-images/ + -- split main parsing routine for easier integration into Firefox + + 2.0: Released February 2013 + -- complete rewrite + -- no new/delete + -- added GPS support + + 1.0: Released 2010 + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + -- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + -- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESS + OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#ifndef __EXIF_H +#define __EXIF_H + +#include <string> + +// +// Class responsible for storing and parsing EXIF information from a JPEG blob +// +class EXIFInfo { + public: + // Parsing function for an entire JPEG image buffer. + // + // PARAM 'data': A pointer to a JPEG image. + // PARAM 'length': The length of the JPEG image. + // RETURN: PARSE_EXIF_SUCCESS (0) on succes with 'result' filled out + // error code otherwise, as defined by the PARSE_EXIF_ERROR_* macros + int parseFrom(const unsigned char *data, unsigned length); + int parseFrom(const std::string &data); + + // Parsing function for an EXIF segment. This is used internally by parseFrom() + // but can be called for special cases where only the EXIF section is + // available (i.e., a blob starting with the bytes "Exif\0\0"). + int parseFromEXIFSegment(const unsigned char *buf, unsigned len); + + // Set all data members to default values. + void clear(); + + // Data fields filled out by parseFrom() + char ByteAlign; // 0 = Motorola byte alignment, 1 = Intel + std::string ImageDescription; // Image description + std::string Make; // Camera manufacturer's name + std::string Model; // Camera model + unsigned short Orientation; // Image orientation, start of data corresponds to + // 0: unspecified in EXIF data + // 1: upper left of image + // 3: lower right of image + // 6: upper right of image + // 8: lower left of image + // 9: undefined + unsigned short BitsPerSample; // Number of bits per component + std::string Software; // Software used + std::string DateTime; // File change date and time + std::string DateTimeOriginal; // Original file date and time (may not exist) + std::string DateTimeDigitized; // Digitization date and time (may not exist) + std::string SubSecTimeOriginal; // Sub-second time that original picture was taken + std::string Copyright; // File copyright information + double ExposureTime; // Exposure time in seconds + double FNumber; // F/stop + unsigned short ISOSpeedRatings; // ISO speed + double ShutterSpeedValue; // Shutter speed (reciprocal of exposure time) + double ExposureBiasValue; // Exposure bias value in EV + double SubjectDistance; // Distance to focus point in meters + double FocalLength; // Focal length of lens in millimeters + unsigned short FocalLengthIn35mm; // Focal length in 35mm film + char Flash; // 0 = no flash, 1 = flash used + unsigned short MeteringMode; // Metering mode + // 1: average + // 2: center weighted average + // 3: spot + // 4: multi-spot + // 5: multi-segment + unsigned ImageWidth; // Image width reported in EXIF data + unsigned ImageHeight; // Image height reported in EXIF data + struct Geolocation_t { // GPS information embedded in file + double Latitude; // Image latitude expressed as decimal + double Longitude; // Image longitude expressed as decimal + double Altitude; // Altitude in meters, relative to sea level + char AltitudeRef; // 0 = above sea level, -1 = below sea level + struct Coord_t { + double degrees; + double minutes; + double seconds; + char direction; + } LatComponents, LonComponents; // Latitude, Longitude expressed in deg/min/sec + } GeoLocation; + EXIFInfo() { + clear(); + } +}; + +// Parse was successful +#define PARSE_EXIF_SUCCESS 0 +// No JPEG markers found in buffer, possibly invalid JPEG file +#define PARSE_EXIF_ERROR_NO_JPEG 1982 +// No EXIF header found in JPEG file. +#define PARSE_EXIF_ERROR_NO_EXIF 1983 +// Byte alignment specified in EXIF file was unknown (not Motorola or Intel). +#define PARSE_EXIF_ERROR_UNKNOWN_BYTEALIGN 1984 +// EXIF header was found, but data was corrupted. +#define PARSE_EXIF_ERROR_CORRUPT 1985 + +#endif diff --git a/qt-ui/profilegraphics.cpp b/qt-ui/profilegraphics.cpp index d500b42e8..65266d31e 100644 --- a/qt-ui/profilegraphics.cpp +++ b/qt-ui/profilegraphics.cpp @@ -1018,6 +1018,13 @@ void ProfileGraphicsView::plot_one_event(struct event *ev) item->setPos(x, y); scene()->addItem(item); + if (ev->type == 123){ + QPixmap picture; + picture.load(ev->name); + scene()->addPixmap(picture.scaledToHeight(100, Qt::SmoothTransformation))->setPos(x, y + 10); + } + + /* we display the event on screen - so translate (with the correct context for events) */ QString name = gettextFromC::instance()->tr(ev->name); if (ev->value) { diff --git a/qt-ui/shiftimagetimes.ui b/qt-ui/shiftimagetimes.ui new file mode 100644 index 000000000..653103b53 --- /dev/null +++ b/qt-ui/shiftimagetimes.ui @@ -0,0 +1,137 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ShiftImageTimesDialog</class> + <widget class="QDialog" name="ShiftImageTimesDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>343</width> + <height>177</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Shift selected times</string> + </property> + <property name="windowIcon"> + <iconset> + <normalon>:/subsurface-icon</normalon> + </iconset> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item alignment="Qt::AlignTop"> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Shift times of image(s) by</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTimeEdit" name="timeEdit"> + <property name="date"> + <date> + <year>2000</year> + <month>1</month> + <day>1</day> + </date> + </property> + <property name="minimumDateTime"> + <datetime> + <hour>0</hour> + <minute>0</minute> + <second>0</second> + <year>2000</year> + <month>1</month> + <day>1</day> + </datetime> + </property> + <property name="maximumDate"> + <date> + <year>2000</year> + <month>1</month> + <day>1</day> + </date> + </property> + <property name="displayFormat"> + <string>h:mm</string> + </property> + <property name="timeSpec"> + <enum>Qt::LocalTime</enum> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="backwards"> + <property name="text"> + <string>earlier</string> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="forward"> + <property name="text"> + <string>later</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources> + <include location="../subsurface.qrc"/> + </resources> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ShiftImageTimesDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ShiftImageTimesDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/qt-ui/simplewidgets.cpp b/qt-ui/simplewidgets.cpp index f3555e376..c76921c32 100644 --- a/qt-ui/simplewidgets.cpp +++ b/qt-ui/simplewidgets.cpp @@ -156,6 +156,26 @@ ShiftTimesDialog::ShiftTimesDialog(QWidget *parent): QDialog(parent) ui.setupUi(this); connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(buttonClicked(QAbstractButton*))); } +ShiftImageTimesDialog* ShiftImageTimesDialog::instance() +{ + static ShiftImageTimesDialog* self = new ShiftImageTimesDialog(mainWindow()); + return self; +} + +void ShiftImageTimesDialog::buttonClicked(QAbstractButton* button) +{ + if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { + amount = ui.timeEdit->time().hour() * 3600 + ui.timeEdit->time().minute() * 60; + if (ui.backwards->isChecked()) + amount *= -1; + } +} + +ShiftImageTimesDialog::ShiftImageTimesDialog(QWidget *parent): QDialog(parent) +{ + ui.setupUi(this); + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(buttonClicked(QAbstractButton*))); +} bool isGnome3Session() { diff --git a/qt-ui/simplewidgets.h b/qt-ui/simplewidgets.h index e2ad195e6..b8097b55a 100644 --- a/qt-ui/simplewidgets.h +++ b/qt-ui/simplewidgets.h @@ -9,6 +9,7 @@ class QAbstractButton; #include "ui_renumber.h" #include "ui_shifttimes.h" +#include "ui_shiftimagetimes.h" class MinMaxAvgWidget : public QWidget{ Q_OBJECT @@ -54,6 +55,18 @@ private: Ui::ShiftTimesDialog ui; }; +class ShiftImageTimesDialog : public QDialog { + Q_OBJECT +public: + static ShiftImageTimesDialog *instance(); + int amount; +private slots: + void buttonClicked(QAbstractButton *button); +private: + explicit ShiftImageTimesDialog(QWidget *parent); + Ui::ShiftImageTimesDialog ui; +}; + bool isGnome3Session(); #endif diff --git a/subsurface.pro b/subsurface.pro index ea5dd3488..148b40047 100644 --- a/subsurface.pro +++ b/subsurface.pro @@ -54,6 +54,7 @@ HEADERS = \ qt-ui/starwidget.h \ qt-ui/subsurfacewebservices.h \ qt-ui/tableview.h \ + qt-ui/exif.h \ sha1.h \ statistics.h \ subsurface-icon.h \ @@ -114,6 +115,7 @@ SOURCES = \ qt-ui/starwidget.cpp \ qt-ui/subsurfacewebservices.cpp \ qt-ui/tableview.cpp \ + qt-ui/exif.cpp \ save-xml.c \ sha1.c \ statistics.c \ @@ -153,6 +155,7 @@ FORMS = \ qt-ui/printoptions.ui \ qt-ui/renumber.ui \ qt-ui/shifttimes.ui \ + qt-ui/shiftimagetimes.ui \ qt-ui/webservices.ui \ qt-ui/tableview.ui \ qt-ui/divelogimportdialog.ui \ diff --git a/wreck.jpg b/wreck.jpg Binary files differnew file mode 100644 index 000000000..1ebfde9f0 --- /dev/null +++ b/wreck.jpg |