Changeset View
Changeset View
Standalone View
Standalone View
lib/exiv2imageloader.cpp
Context not available. | |||||
21 | // Self | 21 | // Self | ||
---|---|---|---|---|---|
22 | #include "exiv2imageloader.h" | 22 | #include "exiv2imageloader.h" | ||
23 | 23 | | |||
24 | // System | ||||
25 | #include <cmath> | ||||
26 | | ||||
24 | // Qt | 27 | // Qt | ||
28 | #include <QImageWriter> | ||||
25 | #include <QByteArray> | 29 | #include <QByteArray> | ||
30 | #include <QBuffer> | ||||
26 | #include <QString> | 31 | #include <QString> | ||
27 | #include <QFile> | 32 | #include <QFile> | ||
28 | 33 | | |||
29 | // KDE | 34 | // KDE | ||
35 | #include <KLocalizedString> | ||||
30 | 36 | | |||
31 | // Exiv2 | 37 | // Exiv2 | ||
32 | #include <exiv2/exiv2.hpp> | 38 | #include <exiv2/exiv2.hpp> | ||
33 | 39 | | |||
34 | // Local | 40 | // Local | ||
41 | #include "gwenviewconfig.h" | ||||
42 | #include "imageutils.h" | ||||
35 | 43 | | |||
36 | namespace Gwenview | 44 | namespace Gwenview | ||
37 | { | 45 | { | ||
38 | 46 | | |||
39 | struct Exiv2ImageLoaderPrivate | 47 | struct Exiv2ImageLoaderPrivate | ||
40 | { | 48 | { | ||
41 | std::unique_ptr<Exiv2::Image> mImage; | 49 | std::unique_ptr<Exiv2::Image> mExivHandle; | ||
42 | QString mErrorMessage; | 50 | QString mErrorMessage; | ||
43 | }; | 51 | }; | ||
44 | 52 | | |||
45 | Exiv2ImageLoader::Exiv2ImageLoader() | 53 | Exiv2ImageLoader::Exiv2ImageLoader() | ||
46 | : d(new Exiv2ImageLoaderPrivate) | 54 | : d(new Exiv2ImageLoaderPrivate()) | ||
47 | { | 55 | { | ||
48 | } | 56 | } | ||
49 | 57 | | |||
Context not available. | |||||
56 | { | 64 | { | ||
57 | QByteArray filePathByteArray = QFile::encodeName(filePath); | 65 | QByteArray filePathByteArray = QFile::encodeName(filePath); | ||
58 | try { | 66 | try { | ||
59 | d->mImage.reset(Exiv2::ImageFactory::open(filePathByteArray.constData()).release()); | 67 | d->mExivHandle.reset(Exiv2::ImageFactory::open(filePathByteArray.constData()).release()); | ||
60 | d->mImage->readMetadata(); | 68 | d->mExivHandle->readMetadata(); | ||
61 | } catch (const Exiv2::Error& error) { | 69 | } catch (const Exiv2::Error& error) { | ||
62 | d->mErrorMessage = QString::fromUtf8(error.what()); | 70 | d->mErrorMessage = QString::fromUtf8(error.what()); | ||
63 | return false; | 71 | return false; | ||
64 | } | 72 | } | ||
73 | d->mErrorMessage = ""; | ||||
65 | return true; | 74 | return true; | ||
66 | } | 75 | } | ||
67 | 76 | | |||
68 | bool Exiv2ImageLoader::load(const QByteArray& data) | 77 | // NOTE: Exiv2 takes ownership of data, so the caller must keep a reference to it to avoid use-after-free! | ||
78 | bool Exiv2ImageLoader::loadFromData(const QByteArray& data) | ||||
69 | { | 79 | { | ||
70 | try { | 80 | try { | ||
71 | d->mImage.reset(Exiv2::ImageFactory::open((unsigned char*)data.constData(), data.size()).release()); | 81 | // cast needed here, otherwise Exiv2::ImageFactory::open(const std::string&, bool) will be called | ||
72 | d->mImage->readMetadata(); | 82 | d->mExivHandle.reset(Exiv2::ImageFactory::open(reinterpret_cast<const Exiv2::byte*>(data.constData()), data.size()).release()); | ||
83 | d->mExivHandle->readMetadata(); | ||||
73 | } catch (const Exiv2::Error& error) { | 84 | } catch (const Exiv2::Error& error) { | ||
74 | d->mErrorMessage = QString::fromUtf8(error.what()); | 85 | d->mErrorMessage = QString::fromUtf8(error.what()); | ||
75 | return false; | 86 | return false; | ||
76 | } | 87 | } | ||
88 | d->mErrorMessage = ""; | ||||
77 | return true; | 89 | return true; | ||
78 | } | 90 | } | ||
79 | 91 | | |||
92 | bool Exiv2ImageLoader::save(const QString& path) | ||||
93 | { | ||||
94 | QFile file(path); | ||||
95 | if (!file.open(QIODevice::WriteOnly)) { | ||||
96 | d->mErrorMessage = i18nc("@info", "Could not open file for writing."); | ||||
97 | return false; | ||||
98 | } | ||||
99 | | ||||
100 | return save(&file); | ||||
101 | } | ||||
102 | | ||||
103 | bool Exiv2ImageLoader::save(QIODevice* device) | ||||
104 | { | ||||
105 | try { | ||||
106 | QByteArray rawData; | ||||
107 | { | ||||
108 | // read in the old image content | ||||
109 | Exiv2::BasicIo& io = d->mExivHandle->io(); | ||||
110 | if (io.open() != 0) { | ||||
111 | d->mErrorMessage = "Failed to open Exiv2::BasicIo during Exiv2ImageLoader::save()."; | ||||
112 | return false; | ||||
113 | } | ||||
114 | | ||||
115 | Exiv2::IoCloser closer(io); | ||||
116 | rawData.resize(io.size()); | ||||
117 | io.read((unsigned char*)rawData.data(), io.size()); | ||||
118 | } | ||||
119 | | ||||
120 | // read its metadata | ||||
121 | std::unique_ptr<Exiv2::Image> image; | ||||
122 | image.reset(Exiv2::ImageFactory::open((unsigned char*)rawData.data(), rawData.size()).release()); | ||||
123 | | ||||
124 | // copy all the potentially modified metadata to the new image | ||||
125 | image->setExifData(d->mExivHandle->exifData()); | ||||
126 | image->setIptcData(d->mExivHandle->iptcData()); | ||||
127 | image->setXmpData(d->mExivHandle->xmpData()); | ||||
128 | image->setComment(d->mExivHandle->comment()); | ||||
129 | | ||||
130 | // this does NOT write to rawData, it allocates its own intermediate buffer instead! | ||||
131 | image->writeMetadata(); | ||||
132 | | ||||
133 | // read the new data from the intermediate buffer | ||||
134 | Exiv2::BasicIo& io = image->io(); | ||||
135 | if (io.open() != 0) { | ||||
136 | d->mErrorMessage = "Failed to open Exiv2::BasicIo during Exiv2ImageLoader::save()."; | ||||
137 | return false; | ||||
138 | } | ||||
139 | | ||||
140 | // write buffer to IODevice | ||||
141 | QDataStream stream(device); | ||||
142 | stream.writeRawData(reinterpret_cast<char*>(io.mmap()), io.size()); | ||||
143 | | ||||
144 | return true; | ||||
145 | } catch(const std::bad_alloc&) { | ||||
146 | d->mErrorMessage = "Out of memory during Exiv2ImageLoader::save()."; | ||||
147 | } catch (const Exiv2::Error& error) { | ||||
148 | d->mErrorMessage = QString::fromUtf8(error.what()); | ||||
149 | } | ||||
150 | return false; | ||||
151 | } | ||||
152 | | ||||
153 | const Exiv2::Image* Exiv2ImageLoader::handle() const | ||||
154 | { | ||||
155 | return d->mExivHandle.get(); | ||||
156 | } | ||||
157 | | ||||
80 | QString Exiv2ImageLoader::errorMessage() const | 158 | QString Exiv2ImageLoader::errorMessage() const | ||
81 | { | 159 | { | ||
82 | return d->mErrorMessage; | 160 | return d->mErrorMessage; | ||
Context not available. | |||||
84 | 162 | | |||
85 | std::unique_ptr<Exiv2::Image> Exiv2ImageLoader::popImage() | 163 | std::unique_ptr<Exiv2::Image> Exiv2ImageLoader::popImage() | ||
86 | { | 164 | { | ||
87 | return std::move(d->mImage); | 165 | return std::move(d->mExivHandle); | ||
166 | } | ||||
167 | | ||||
168 | Orientation Exiv2ImageLoader::orientation() const | ||||
169 | { | ||||
170 | Exiv2::ExifKey key("Exif.Image.Orientation"); | ||||
171 | Exiv2::ExifData::iterator it = d->mExivHandle->exifData().findKey(key); | ||||
172 | | ||||
173 | // We do the same checks as in libexiv2's src/crwimage.cpp: | ||||
174 | // http://dev.exiv2.org/projects/exiv2/repository/entry/trunk/src/crwimage.cpp?rev=2681#L1336 | ||||
175 | if (it == d->mExivHandle->exifData().end() || it->count() == 0 || it->typeId() != Exiv2::unsignedShort) { | ||||
176 | return NOT_AVAILABLE; | ||||
177 | } | ||||
178 | return Orientation(it->toLong()); | ||||
179 | } | ||||
180 | | ||||
181 | int Exiv2ImageLoader::dotsPerMeterX() const | ||||
182 | { | ||||
183 | return dotsPerMeter(QStringLiteral("XResolution")); | ||||
184 | } | ||||
185 | | ||||
186 | int Exiv2ImageLoader::dotsPerMeterY() const | ||||
187 | { | ||||
188 | return dotsPerMeter(QStringLiteral("YResolution")); | ||||
189 | } | ||||
190 | | ||||
191 | int Exiv2ImageLoader::dotsPerMeter(const QString& keyName) const | ||||
192 | { | ||||
193 | Exiv2::ExifKey keyResUnit("Exif.Image.ResolutionUnit"); | ||||
194 | Exiv2::ExifData::iterator it = d->mExivHandle->exifData().findKey(keyResUnit); | ||||
195 | if (it == d->mExivHandle->exifData().end()) { | ||||
196 | return 0; | ||||
197 | } | ||||
198 | int res = it->toLong(); | ||||
199 | QString keyVal = QStringLiteral("Exif.Image.") + keyName; | ||||
200 | Exiv2::ExifKey keyResolution(keyVal.toLocal8Bit().data()); | ||||
201 | it = d->mExivHandle->exifData().findKey(keyResolution); | ||||
202 | if (it == d->mExivHandle->exifData().end()) { | ||||
203 | return 0; | ||||
204 | } | ||||
205 | // The unit for measuring XResolution and YResolution. The same unit is used for both XResolution and YResolution. | ||||
206 | // If the image resolution in unknown, 2 (inches) is designated. | ||||
207 | // Default = 2 | ||||
208 | // 2 = inches | ||||
209 | // 3 = centimeters | ||||
210 | // Other = reserved | ||||
211 | constexpr float INCHESPERMETER = (100. / 2.54); | ||||
212 | switch (res) { | ||||
213 | case 3: // dots per cm | ||||
214 | return int(it->toLong() * 100); | ||||
215 | default: // dots per inch | ||||
216 | return int(it->toLong() * INCHESPERMETER); | ||||
217 | } | ||||
218 | | ||||
219 | return 0; | ||||
220 | } | ||||
221 | | ||||
222 | void Exiv2ImageLoader::transform(Orientation orientation) | ||||
223 | { | ||||
224 | if (orientation != NOT_AVAILABLE) { | ||||
225 | Exiv2::ExifKey key("Exif.Image.Orientation"); | ||||
226 | Exiv2::ExifData::iterator it = d->mExivHandle->exifData().findKey(key); | ||||
227 | if (it != d->mExivHandle->exifData().end()) { | ||||
228 | *it = uint16_t(orientation); | ||||
229 | } | ||||
230 | } | ||||
231 | } | ||||
232 | | ||||
233 | void Exiv2ImageLoader::resetOrientation() | ||||
234 | { | ||||
235 | transform(NORMAL); | ||||
236 | } | ||||
237 | | ||||
238 | QSize Exiv2ImageLoader::size() const | ||||
239 | { | ||||
240 | QSize size(d->mExivHandle->pixelWidth(), d->mExivHandle->pixelHeight()); | ||||
241 | | ||||
242 | if (GwenviewConfig::applyExifOrientation()) { | ||||
243 | // Adjust the size according to the orientation | ||||
244 | switch (orientation()) { | ||||
245 | case TRANSPOSE: | ||||
246 | case ROT_90: | ||||
247 | case TRANSVERSE: | ||||
248 | case ROT_270: | ||||
249 | size.transpose(); | ||||
250 | break; | ||||
251 | default: | ||||
252 | break; | ||||
253 | } | ||||
254 | } | ||||
255 | | ||||
256 | return size; | ||||
257 | } | ||||
258 | | ||||
259 | QString Exiv2ImageLoader::comment() const | ||||
260 | { | ||||
261 | return QString::fromUtf8(d->mExivHandle->comment().c_str()); | ||||
262 | } | ||||
263 | | ||||
264 | void Exiv2ImageLoader::setComment(const QString& comment) | ||||
265 | { | ||||
266 | d->mExivHandle->setComment(comment.toUtf8().toStdString()); | ||||
267 | } | ||||
268 | | ||||
269 | QImage Exiv2ImageLoader::thumbnail() const | ||||
270 | { | ||||
271 | QImage image; | ||||
272 | if (!d->mExivHandle->exifData().empty()) { | ||||
273 | #if (EXIV2_TEST_VERSION(0,17,91)) | ||||
274 | Exiv2::ExifThumbC thumb(d->mExivHandle->exifData()); | ||||
275 | Exiv2::DataBuf thumbnail = thumb.copy(); | ||||
276 | #else | ||||
277 | Exiv2::DataBuf thumbnail = d->mExivHandle->exifData().copyThumbnail(); | ||||
278 | #endif | ||||
279 | image.loadFromData(thumbnail.pData_, thumbnail.size_); | ||||
280 | | ||||
281 | Exiv2::ExifData::iterator it = d->mExivHandle->exifData().findKey(Exiv2::ExifKey("Exif.Canon.ThumbnailImageValidArea")); | ||||
282 | // ensure ThumbnailImageValidArea actually specifies a rectangle, i.e. there must be 4 coordinates | ||||
283 | if (it != d->mExivHandle->exifData().end() && it->count() == 4) { | ||||
284 | QRect validArea(QPoint(it->toLong(0), it->toLong(2)), QPoint(it->toLong(1), it->toLong(3))); | ||||
285 | image = image.copy(validArea); | ||||
286 | } else { | ||||
287 | // Unfortunately, Sony does not provide an exif tag that specifies the valid area of the | ||||
288 | // embedded thumbnail. Need to derive it from the size of the preview image instead. | ||||
289 | it = d->mExivHandle->exifData().findKey(Exiv2::ExifKey("Exif.Sony1.PreviewImageSize")); | ||||
290 | if (it != d->mExivHandle->exifData().end() && it->count() == 2) { | ||||
291 | const long prevHeight = it->toLong(0); | ||||
292 | const long prevWidth = it->toLong(1); | ||||
293 | | ||||
294 | const double scale = prevWidth / image.width(); | ||||
295 | | ||||
296 | // the embedded thumb only needs to be cropped vertically | ||||
297 | const long validThumbAreaHeight = ceil(prevHeight / scale); | ||||
298 | const long totalHeightOfBlackArea = image.height() - validThumbAreaHeight; | ||||
299 | // black bars on top and bottom should be equal in height | ||||
300 | const long offsetFromTop = totalHeightOfBlackArea / 2; | ||||
301 | | ||||
302 | const QRect validArea(QPoint(0, offsetFromTop), QSize(image.width(), validThumbAreaHeight)); | ||||
303 | image = image.copy(validArea); | ||||
304 | } | ||||
305 | } | ||||
306 | | ||||
307 | Orientation o = orientation(); | ||||
308 | if (GwenviewConfig::applyExifOrientation() && o != NORMAL && o != NOT_AVAILABLE) { | ||||
309 | image = image.transformed(ImageUtils::transformMatrix(o)); | ||||
310 | } | ||||
311 | } | ||||
312 | return image; | ||||
313 | } | ||||
314 | | ||||
315 | void Exiv2ImageLoader::setThumbnail(const QImage& thumbnail) | ||||
316 | { | ||||
317 | if (d->mExivHandle->exifData().empty()) { | ||||
318 | return; | ||||
319 | } | ||||
320 | | ||||
321 | QByteArray array; | ||||
322 | QBuffer buffer(&array); | ||||
323 | buffer.open(QIODevice::WriteOnly); | ||||
324 | QImageWriter writer(&buffer, "JPEG"); | ||||
325 | if (!writer.write(thumbnail)) { | ||||
326 | qCritical() << "Could not write thumbnail\n"; | ||||
327 | return; | ||||
328 | } | ||||
329 | | ||||
330 | #if (EXIV2_TEST_VERSION(0,17,91)) | ||||
331 | Exiv2::ExifThumb thumb(d->mExivHandle->exifData()); | ||||
332 | thumb.setJpegThumbnail((unsigned char*)array.data(), array.size()); | ||||
333 | #else | ||||
334 | d->mExivHandle->exifData().setJpegThumbnail((unsigned char*)array.data(), array.size()); | ||||
335 | #endif | ||||
336 | } | ||||
337 | | ||||
338 | | ||||
339 | void Exiv2ImageLoader::setImage(const QImage& image) | ||||
340 | { | ||||
341 | d->mExivHandle->exifData()["Exif.Photo.PixelXDimension"] = image.width(); | ||||
342 | d->mExivHandle->exifData()["Exif.Photo.PixelYDimension"] = image.height(); | ||||
343 | | ||||
344 | QImage thumbnail = image.scaled(128, 128, Qt::KeepAspectRatio); | ||||
345 | setThumbnail(thumbnail); | ||||
346 | | ||||
347 | resetOrientation(); | ||||
348 | } | ||||
349 | | ||||
350 | QByteArray Exiv2ImageLoader::interColorProfile() const | ||||
351 | { | ||||
352 | QByteArray data; | ||||
353 | | ||||
354 | const Exiv2::ExifData& exifData = d->mExivHandle->exifData(); | ||||
355 | static const Exiv2::ExifKey key("Exif.Image.InterColorProfile"); | ||||
356 | Exiv2::ExifData::const_iterator it = exifData.findKey(key); | ||||
357 | if (it == exifData.end()) { | ||||
358 | qWarning() << "No profile found"; | ||||
359 | return data; | ||||
360 | } | ||||
361 | | ||||
362 | data.resize(it->size()); | ||||
363 | it->copy(reinterpret_cast<Exiv2::byte*>(data.data()), Exiv2::invalidByteOrder); | ||||
364 | | ||||
365 | return data; | ||||
88 | } | 366 | } | ||
89 | 367 | | |||
90 | } // namespace | 368 | } // namespace | ||
Context not available. |