Merge lp:~schwann/gallery-app/gallery-edit-thread into lp:gallery-app
- gallery-edit-thread
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Gustavo Pichorim Boiko |
Approved revision: | 788 |
Merged at revision: | 787 |
Proposed branch: | lp:~schwann/gallery-app/gallery-edit-thread |
Merge into: | lp:gallery-app |
Diff against target: |
894 lines (+457/-261) 7 files modified
src/media/media-source.h (+3/-1) src/photo/CMakeLists.txt (+2/-0) src/photo/photo-edit-thread.cpp (+285/-0) src/photo/photo-edit-thread.h (+63/-0) src/photo/photo.cpp (+82/-253) src/photo/photo.h (+13/-7) tests/unittests/stubs/photo_stub.cpp (+9/-0) |
To merge this branch: | bzr merge lp:~schwann/gallery-app/gallery-edit-thread |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gustavo Pichorim Boiko (community) | Approve | ||
PS Jenkins bot | continuous-integration | Approve | |
Review via email: mp+177165@code.launchpad.net |
Commit message
Run edit operations in a background task
Description of the change
Run edit operations in a background task. So now one can see the spinner running, and is able to use the UI, while the editing is performed.
- 788. By Günter Schwann
-
Properly block undo/redo
PS Jenkins bot (ps-jenkins) wrote : | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:788
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Gustavo Pichorim Boiko (boiko) wrote : | # |
Looks good.
Preview Diff
1 | === modified file 'src/media/media-source.h' |
2 | --- src/media/media-source.h 2013-07-05 08:12:31 +0000 |
3 | +++ src/media/media-source.h 2013-07-26 16:08:26 +0000 |
4 | @@ -103,7 +103,6 @@ |
5 | void setFileTimestamp(const QDateTime& timestamp); |
6 | |
7 | const QSize& size(); |
8 | - void setSize(const QSize& size); |
9 | |
10 | qint64 id() const; |
11 | void setId(qint64 id); |
12 | @@ -112,6 +111,9 @@ |
13 | |
14 | void setMediaTable(MediaTable *mediaTable); |
15 | |
16 | +public Q_SLOTS: |
17 | + void setSize(const QSize& size); |
18 | + |
19 | protected: |
20 | bool isSizeSet() const; |
21 | |
22 | |
23 | === modified file 'src/photo/CMakeLists.txt' |
24 | --- src/photo/CMakeLists.txt 2013-07-03 11:53:48 +0000 |
25 | +++ src/photo/CMakeLists.txt 2013-07-26 16:08:26 +0000 |
26 | @@ -18,12 +18,14 @@ |
27 | photo.h |
28 | photo-caches.h |
29 | photo-edit-state.h |
30 | + photo-edit-thread.h |
31 | ) |
32 | |
33 | set(gallery_photo_SRCS |
34 | photo.cpp |
35 | photo-caches.cpp |
36 | photo-edit-state.cpp |
37 | + photo-edit-thread.cpp |
38 | ) |
39 | |
40 | add_library(${GALLERY_PHOTO_LIB} |
41 | |
42 | === added file 'src/photo/photo-edit-thread.cpp' |
43 | --- src/photo/photo-edit-thread.cpp 1970-01-01 00:00:00 +0000 |
44 | +++ src/photo/photo-edit-thread.cpp 2013-07-26 16:08:26 +0000 |
45 | @@ -0,0 +1,285 @@ |
46 | +/* |
47 | + * Copyright (C) 2013 Canonical Ltd |
48 | + * |
49 | + * This program is free software: you can redistribute it and/or modify |
50 | + * it under the terms of the GNU General Public License version 3 as |
51 | + * published by the Free Software Foundation. |
52 | + * |
53 | + * This program is distributed in the hope that it will be useful, |
54 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
55 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
56 | + * GNU General Public License for more details. |
57 | + * |
58 | + * You should have received a copy of the GNU General Public License |
59 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
60 | + */ |
61 | + |
62 | +#include "photo-edit-thread.h" |
63 | +#include "photo.h" |
64 | + |
65 | +// medialoader |
66 | +#include "photo-metadata.h" |
67 | + |
68 | +// util |
69 | +#include "imaging.h" |
70 | + |
71 | +#include <QDebug> |
72 | + |
73 | +/*! |
74 | + * \brief PhotoEditThread::PhotoEditThread |
75 | + */ |
76 | +PhotoEditThread::PhotoEditThread(Photo *photo, const PhotoEditState &editState) |
77 | + : QThread(), |
78 | + m_photo(photo), |
79 | + m_editState(editState), |
80 | + m_caches(photo->file()), |
81 | + m_oldOrientation(photo->orientation()) |
82 | +{ |
83 | +} |
84 | + |
85 | +/*! |
86 | + * \brief PhotoEditThread::editState resturns the editing stse used for this processing |
87 | + * \return |
88 | + */ |
89 | +const PhotoEditState &PhotoEditThread::editState() const |
90 | +{ |
91 | + return m_editState; |
92 | +} |
93 | + |
94 | +/*! |
95 | + * \brief PhotoEditThread::oldOrientation returns the orientation of the photo before the editing |
96 | + * \return |
97 | + */ |
98 | +Orientation PhotoEditThread::oldOrientation() const |
99 | +{ |
100 | + return m_oldOrientation; |
101 | +} |
102 | + |
103 | +/*! |
104 | + * \brief PhotoEditThread::run \reimp |
105 | + */ |
106 | +void PhotoEditThread::run() |
107 | +{ |
108 | + // As a special case, if editing to the original version, we simply restore |
109 | + // from the original and call it a day. |
110 | + if (m_editState.isOriginal()) { |
111 | + if (!m_caches.restoreOriginal()) |
112 | + qWarning() << "Error restoring original for" << m_photo->file().filePath(); |
113 | + else |
114 | + Q_EMIT resetToOriginalSize(); |
115 | + |
116 | + // As a courtesy, when the original goes away, we get rid of the other |
117 | + // cached files too. |
118 | + m_caches.discardCachedEnhanced(); |
119 | + return; |
120 | + } |
121 | + |
122 | + if (!m_caches.cacheOriginal()) |
123 | + qWarning() << "Error caching original for" << m_photo->file().filePath(); |
124 | + |
125 | + if (m_editState.is_enhanced_ && !m_caches.hasCachedEnhanced()) |
126 | + createCachedEnhanced(); |
127 | + |
128 | + if (!m_caches.overwriteFromCache(m_editState.is_enhanced_)) |
129 | + qWarning() << "Error overwriting" << m_photo->file().filePath() << "from cache"; |
130 | + |
131 | + // Have we been rotated and _not_ cropped? |
132 | + if (m_photo->fileFormatHasOrientation() && (!m_editState.crop_rectangle_.isValid()) && |
133 | + m_editState.exposureCompensation_ == 0 && |
134 | + (m_editState.orientation_ != PhotoEditState::ORIGINAL_ORIENTATION)) { |
135 | + // Yes; skip out on decoding and re-encoding the image. |
136 | + handleSimpleMetadataRotation(m_editState); |
137 | + return; |
138 | + } |
139 | + |
140 | + // TODO: we might be able to avoid reading/writing pixel data (and other |
141 | + // more general optimizations) under certain conditions here. Might be worth |
142 | + // doing if it doesn't make the code too much worse. |
143 | + // |
144 | + // At the moment, we are skipping at least one decode and one encode in cases |
145 | + // where a .jpeg file has been rotated, but not cropped, since rotation can be |
146 | + // controlled by manipulating its metadata without having to modify pixel data; |
147 | + // please see the method handle_simple_metadata_rotation() for details. |
148 | + |
149 | + QImage image(m_photo->file().filePath(), m_photo->fileFormat().toStdString().c_str()); |
150 | + if (image.isNull()) { |
151 | + qWarning() << "Error loading" << m_photo->file().filePath() << "for editing"; |
152 | + return; |
153 | + } |
154 | + PhotoMetadata* metadata = PhotoMetadata::fromFile(m_photo->file()); |
155 | + |
156 | + if (m_photo->fileFormatHasOrientation() && |
157 | + m_editState.orientation_ != PhotoEditState::ORIGINAL_ORIENTATION) |
158 | + metadata->setOrientation(m_editState.orientation_); |
159 | + |
160 | + if (m_photo->fileFormatHasOrientation() && |
161 | + metadata->orientation() != TOP_LEFT_ORIGIN) |
162 | + image = image.transformed(metadata->orientationTransform()); |
163 | + else if (m_editState.orientation_ != PhotoEditState::ORIGINAL_ORIENTATION && |
164 | + m_editState.orientation_ != TOP_LEFT_ORIGIN) |
165 | + image = image.transformed( |
166 | + OrientationCorrection::fromOrientation(m_editState.orientation_).toTransform()); |
167 | + |
168 | + if (m_editState.crop_rectangle_.isValid()) |
169 | + image = image.copy(m_editState.crop_rectangle_); |
170 | + |
171 | + // exposure compensation |
172 | + if (m_editState.exposureCompensation_ != 0.0) { |
173 | + image = compensateExposure(image, m_editState.exposureCompensation_); |
174 | + } |
175 | + |
176 | + // exposure compensation |
177 | + if (!m_editState.colorBalance_.isNull()) { |
178 | + const QVector4D &v = m_editState.colorBalance_; |
179 | + image = doColorBalance(image, v.x(), v.y(), v.z(), v.w()); |
180 | + } |
181 | + |
182 | + QSize new_size = image.size(); |
183 | + |
184 | + // We need to apply the reverse transformation so that when we reload the |
185 | + // file and reapply the transformation it comes out correctly. |
186 | + if (m_photo->fileFormatHasOrientation() && |
187 | + metadata->orientation() != TOP_LEFT_ORIGIN) |
188 | + image = image.transformed(metadata->orientationTransform().inverted()); |
189 | + |
190 | + bool saved = image.save(m_photo->file().filePath(), m_photo->fileFormat().toStdString().c_str(), 90); |
191 | + if (saved && m_photo->fileFormatHasMetadata()) |
192 | + saved = metadata->save(); |
193 | + if (!saved) |
194 | + qWarning() << "Error saving edited" << m_photo->file().filePath(); |
195 | + |
196 | + delete metadata; |
197 | + |
198 | + Q_EMIT newSize(new_size); |
199 | +} |
200 | + |
201 | +/*! |
202 | + * \brief PhotoEditThread::handleSimpleMetadataRotation |
203 | + * Handler for the case of an image whose only change is to its |
204 | + * orientation; used to skip re-encoding of JPEGs. |
205 | + * \param state |
206 | + */ |
207 | +void PhotoEditThread::handleSimpleMetadataRotation(const PhotoEditState& state) |
208 | +{ |
209 | + PhotoMetadata* metadata = PhotoMetadata::fromFile(m_photo->file()); |
210 | + metadata->setOrientation(state.orientation_); |
211 | + |
212 | + metadata->save(); |
213 | + delete(metadata); |
214 | + |
215 | + OrientationCorrection orig_correction = |
216 | + OrientationCorrection::fromOrientation(m_photo->originalOrientation()); |
217 | + OrientationCorrection dest_correction = |
218 | + OrientationCorrection::fromOrientation(state.orientation_); |
219 | + |
220 | + QSize new_size = m_photo->originalSize(); |
221 | + int angle = dest_correction.getNormalizedRotationDifference(orig_correction); |
222 | + |
223 | + if ((angle == 90) || (angle == 270)) { |
224 | + new_size = m_photo->originalSize().transposed(); |
225 | + } |
226 | + |
227 | + Q_EMIT newSize(new_size); |
228 | +} |
229 | + |
230 | +/*! |
231 | + * \brief PhotoEditThread::createCachedEnhanced |
232 | + */ |
233 | +void PhotoEditThread::createCachedEnhanced() |
234 | +{ |
235 | + if (!m_caches.cacheEnhancedFromOriginal()) { |
236 | + qWarning() << "Error creating enhanced file for" << m_photo->file().filePath(); |
237 | + return; |
238 | + } |
239 | + |
240 | + QFileInfo to_enhance = m_photo->enhancedFile(); |
241 | + PhotoMetadata* metadata = PhotoMetadata::fromFile(to_enhance); |
242 | + |
243 | + QImage unenhanced_img(to_enhance.filePath(), m_photo->fileFormat().toStdString().c_str()); |
244 | + int width = unenhanced_img.width(); |
245 | + int height = unenhanced_img.height(); |
246 | + |
247 | + QImage sample_img = (unenhanced_img.width() > 400) ? |
248 | + unenhanced_img.scaledToWidth(400) : unenhanced_img; |
249 | + |
250 | + AutoEnhanceTransformation enhance_txn = AutoEnhanceTransformation(sample_img); |
251 | + |
252 | + QImage::Format dest_format = unenhanced_img.format(); |
253 | + |
254 | + // Can't write into indexed images, due to a limitation in Qt. |
255 | + if (dest_format == QImage::Format_Indexed8) |
256 | + dest_format = QImage::Format_RGB32; |
257 | + |
258 | + QImage enhanced_image(width, height, dest_format); |
259 | + |
260 | + for (int j = 0; j < height; j++) { |
261 | + for (int i = 0; i < width; i++) { |
262 | + QColor px = enhance_txn.transformPixel( |
263 | + QColor(unenhanced_img.pixel(i, j))); |
264 | + enhanced_image.setPixel(i, j, px.rgb()); |
265 | + } |
266 | + } |
267 | + |
268 | + bool saved = enhanced_image.save(to_enhance.filePath(), |
269 | + m_photo->fileFormat().toStdString().c_str(), 90); |
270 | + if (saved && m_photo->fileFormatHasMetadata()) |
271 | + saved = metadata->save(); |
272 | + if (!saved) { |
273 | + qWarning() << "Error saving enhanced file for" << m_photo->file().filePath(); |
274 | + m_caches.discardCachedEnhanced(); |
275 | + } |
276 | + |
277 | + delete metadata; |
278 | +} |
279 | + |
280 | +/*! |
281 | + * \brief PhotoEditThread::compensateExposure Compensates the exposure |
282 | + * Compensating the exposure is a change in brightnes |
283 | + * \param image Image to change the brightnes |
284 | + * \param compansation -1.0 is total dark, +1.0 is total bright |
285 | + * \return The image with adjusted brightnes |
286 | + */ |
287 | +QImage PhotoEditThread::compensateExposure(const QImage &image, qreal compansation) |
288 | +{ |
289 | + int shift = qBound(-255, (int)(255*compansation), 255); |
290 | + QImage result(image.width(), image.height(), image.format()); |
291 | + |
292 | + for (int j = 0; j < image.height(); j++) { |
293 | + for (int i = 0; i <image.width(); i++) { |
294 | + QColor px = image.pixel(i, j); |
295 | + int red = qBound(0, px.red() + shift, 255); |
296 | + int green = qBound(0, px.green() + shift, 255); |
297 | + int blue = qBound(0, px.blue() + shift, 255); |
298 | + result.setPixel(i, j, qRgb(red, green, blue)); |
299 | + } |
300 | + } |
301 | + |
302 | + return result; |
303 | +} |
304 | + |
305 | +/*! |
306 | + * \brief PhotoEditThread::colorBalance |
307 | + * \param image |
308 | + * \param brightness 0 is total dark, 1 is as the original, grater than 1 is brigther |
309 | + * \param contrast from 0 maybe 5. 1 is as the original |
310 | + * \param saturation from 0 maybe 5. 1 is as the original |
311 | + * \param hue from 0 to 360. 0 and 360 is as the original |
312 | + * \return |
313 | + */ |
314 | +QImage PhotoEditThread::doColorBalance(const QImage &image, qreal brightness, qreal contrast, qreal saturation, qreal hue) |
315 | +{ |
316 | + QImage result(image.width(), image.height(), image.format()); |
317 | + |
318 | + ColorBalance cb(brightness, contrast, saturation, hue); |
319 | + |
320 | + for (int j = 0; j < image.height(); j++) { |
321 | + for (int i = 0; i <image.width(); i++) { |
322 | + QColor px = image.pixel(i, j); |
323 | + QColor tpx = cb.transformPixel(px); |
324 | + result.setPixel(i, j, tpx.rgb()); |
325 | + } |
326 | + } |
327 | + |
328 | + return result; |
329 | +} |
330 | + |
331 | |
332 | === added file 'src/photo/photo-edit-thread.h' |
333 | --- src/photo/photo-edit-thread.h 1970-01-01 00:00:00 +0000 |
334 | +++ src/photo/photo-edit-thread.h 2013-07-26 16:08:26 +0000 |
335 | @@ -0,0 +1,63 @@ |
336 | +/* |
337 | + * Copyright (C) 2013 Canonical Ltd |
338 | + * |
339 | + * This program is free software: you can redistribute it and/or modify |
340 | + * it under the terms of the GNU General Public License version 3 as |
341 | + * published by the Free Software Foundation. |
342 | + * |
343 | + * This program is distributed in the hope that it will be useful, |
344 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
345 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
346 | + * GNU General Public License for more details. |
347 | + * |
348 | + * You should have received a copy of the GNU General Public License |
349 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
350 | + */ |
351 | + |
352 | +#ifndef GALLERY_PHOTO_EDIT_THREAD_H_ |
353 | +#define GALLERY_PHOTO_EDIT_THREAD_H_ |
354 | + |
355 | +#include "photo-caches.h" |
356 | +#include "photo-edit-state.h" |
357 | + |
358 | +// util |
359 | +#include "orientation.h" |
360 | + |
361 | +#include <QImage> |
362 | +#include <QThread> |
363 | +#include <QUrl> |
364 | + |
365 | +class Photo; |
366 | + |
367 | +/*! |
368 | + * \brief The PhotoEditThread class |
369 | + */ |
370 | +class PhotoEditThread: public QThread |
371 | +{ |
372 | + Q_OBJECT |
373 | +public: |
374 | + PhotoEditThread(Photo *photo, const PhotoEditState& editState); |
375 | + |
376 | + const PhotoEditState& editState() const; |
377 | + Orientation oldOrientation() const; |
378 | + |
379 | +Q_SIGNALS: |
380 | + void newSize(QSize size); |
381 | + void resetToOriginalSize(); |
382 | + |
383 | +protected: |
384 | + void run() Q_DECL_OVERRIDE; |
385 | + |
386 | +private: |
387 | + void createCachedEnhanced(); |
388 | + QImage compensateExposure(const QImage& image, qreal compansation); |
389 | + QImage doColorBalance(const QImage& image, qreal brightness, qreal contrast, qreal saturation, qreal hue); |
390 | + void handleSimpleMetadataRotation(const PhotoEditState& state); |
391 | + |
392 | + Photo *m_photo; |
393 | + PhotoEditState m_editState; |
394 | + PhotoCaches m_caches; |
395 | + Orientation m_oldOrientation; |
396 | +}; |
397 | + |
398 | +#endif |
399 | |
400 | === modified file 'src/photo/photo.cpp' |
401 | --- src/photo/photo.cpp 2013-07-05 08:12:31 +0000 |
402 | +++ src/photo/photo.cpp 2013-07-26 16:08:26 +0000 |
403 | @@ -23,6 +23,7 @@ |
404 | |
405 | #include "photo.h" |
406 | #include "photo-edit-state.h" |
407 | +#include "photo-edit-thread.h" |
408 | |
409 | // database |
410 | #include "database.h" |
411 | @@ -185,6 +186,7 @@ |
412 | Photo::Photo(const QFileInfo& file) |
413 | : MediaSource(file), |
414 | m_editRevision(0), |
415 | + m_editThread(0), |
416 | m_caches(file), |
417 | m_originalSize(), |
418 | m_originalOrientation(TOP_LEFT_ORIGIN), |
419 | @@ -201,6 +203,10 @@ |
420 | */ |
421 | Photo::~Photo() |
422 | { |
423 | + if (m_editThread) { |
424 | + m_editThread->wait(); |
425 | + finishEditing(); |
426 | + } |
427 | delete(d_ptr); |
428 | } |
429 | |
430 | @@ -347,13 +353,15 @@ |
431 | void Photo::undo() |
432 | { |
433 | Q_D(Photo); |
434 | - Orientation old_orientation = orientation(); |
435 | + if (busy()) { |
436 | + qWarning() << "Don't start edit operation, while another one is running"; |
437 | + return; |
438 | + } |
439 | |
440 | PhotoEditState prev = d->editStack()->current(); |
441 | PhotoEditState next = d->editStack()->undo(); |
442 | if (next != prev) { |
443 | - save(next, old_orientation); |
444 | - emit editStackChanged(); |
445 | + asyncEdit(next); |
446 | } |
447 | } |
448 | |
449 | @@ -363,13 +371,15 @@ |
450 | void Photo::redo() |
451 | { |
452 | Q_D(Photo); |
453 | - Orientation old_orientation = orientation(); |
454 | + if (busy()) { |
455 | + qWarning() << "Don't start edit operation, while another one is running"; |
456 | + return; |
457 | + } |
458 | |
459 | PhotoEditState prev = d->editStack()->current(); |
460 | PhotoEditState next = d->editStack()->redo(); |
461 | if (next != prev) { |
462 | - save(next, old_orientation); |
463 | - emit editStackChanged(); |
464 | + asyncEdit(next); |
465 | } |
466 | } |
467 | |
468 | @@ -556,6 +566,14 @@ |
469 | } |
470 | |
471 | /*! |
472 | + * \brief Photo::resetToOriginalSize set the size to the one of the orifinal photo |
473 | + */ |
474 | +void Photo::resetToOriginalSize() |
475 | +{ |
476 | + setSize(originalSize(PhotoEditState::ORIGINAL_ORIENTATION)); |
477 | +} |
478 | + |
479 | +/*! |
480 | * \brief Photo::currentState |
481 | * \return |
482 | */ |
483 | @@ -607,26 +625,43 @@ |
484 | */ |
485 | void Photo::makeUndoableEdit(const PhotoEditState& state) |
486 | { |
487 | + if (busy()) { |
488 | + qWarning() << "Don't start edit operation, while another one is running"; |
489 | + return; |
490 | + } |
491 | + |
492 | Q_D(Photo); |
493 | - Orientation old_orientation = orientation(); |
494 | - |
495 | d->editStack()->pushEdit(state); |
496 | - save(state, old_orientation); |
497 | - emit editStackChanged(); |
498 | + asyncEdit(state); |
499 | } |
500 | |
501 | /*! |
502 | - * \brief Photo::save |
503 | - * \param state |
504 | - * \param oldOrientation |
505 | + * \brief Photo::asyncEdit does edit the photo according to the given state |
506 | + * in a background task |
507 | + * \param state the new editing state |
508 | */ |
509 | -void Photo::save(const PhotoEditState& state, Orientation oldOrientation) |
510 | +void Photo::asyncEdit(const PhotoEditState& state) |
511 | { |
512 | setBusy(true); |
513 | - editFile(state); |
514 | + m_editThread = new PhotoEditThread(this, state); |
515 | + connect(m_editThread, SIGNAL(finished()), this, SLOT(finishEditing())); |
516 | + connect(m_editThread, SIGNAL(newSize(QSize)), this, SLOT(setSize(QSize))); |
517 | + connect(m_editThread, SIGNAL(resetToOriginalSize()), this, SLOT(resetToOriginalSize())); |
518 | + m_editThread->start(); |
519 | +} |
520 | + |
521 | +/*! |
522 | + * \brief Photo::finishEditing do all the updates once the editing is done |
523 | + */ |
524 | +void Photo::finishEditing() |
525 | +{ |
526 | + if (!m_editThread || m_editThread->isRunning()) |
527 | + return; |
528 | + |
529 | + const PhotoEditState &state = m_editThread->editState(); |
530 | GalleryManager::instance()->database()->getPhotoEditTable()->setEditState(id(), state); |
531 | |
532 | - if (orientation() != oldOrientation) |
533 | + if (orientation() != m_editThread->oldOrientation()) |
534 | emit orientationChanged(); |
535 | notifyDataChanged(); |
536 | |
537 | @@ -635,244 +670,10 @@ |
538 | emit galleryPathChanged(); |
539 | emit galleryPreviewPathChanged(); |
540 | emit galleryThumbnailPathChanged(); |
541 | + m_editThread->deleteLater(); |
542 | + m_editThread = 0; |
543 | setBusy(false); |
544 | -} |
545 | - |
546 | -/*! |
547 | - * \brief Photo::handleSimpleMetadataRotation |
548 | - * Handler for the case of an image whose only change is to its |
549 | - * orientation; used to skip re-encoding of JPEGs. |
550 | - * \param state |
551 | - */ |
552 | -void Photo::handleSimpleMetadataRotation(const PhotoEditState& state) |
553 | -{ |
554 | - PhotoMetadata* metadata = PhotoMetadata::fromFile(file()); |
555 | - metadata->setOrientation(state.orientation_); |
556 | - |
557 | - metadata->save(); |
558 | - delete(metadata); |
559 | - |
560 | - OrientationCorrection orig_correction = |
561 | - OrientationCorrection::fromOrientation(m_originalOrientation); |
562 | - OrientationCorrection dest_correction = |
563 | - OrientationCorrection::fromOrientation(state.orientation_); |
564 | - |
565 | - QSize new_size = m_originalSize; |
566 | - int angle = dest_correction.getNormalizedRotationDifference(orig_correction); |
567 | - |
568 | - if ((angle == 90) || (angle == 270)) { |
569 | - new_size = m_originalSize.transposed(); |
570 | - } |
571 | - |
572 | - setSize(new_size); |
573 | -} |
574 | - |
575 | -/*! |
576 | - * \brief Photo::editFile |
577 | - * \param state |
578 | - */ |
579 | -void Photo::editFile(const PhotoEditState& state) |
580 | -{ |
581 | - // As a special case, if editing to the original version, we simply restore |
582 | - // from the original and call it a day. |
583 | - if (state.isOriginal()) { |
584 | - if (!m_caches.restoreOriginal()) |
585 | - qDebug("Error restoring original for %s", qPrintable(file().filePath())); |
586 | - else |
587 | - setSize(originalSize(PhotoEditState::ORIGINAL_ORIENTATION)); |
588 | - |
589 | - // As a courtesy, when the original goes away, we get rid of the other |
590 | - // cached files too. |
591 | - m_caches.discardCachedEnhanced(); |
592 | - return; |
593 | - } |
594 | - |
595 | - if (!m_caches.cacheOriginal()) |
596 | - qDebug("Error caching original for %s", qPrintable(file().filePath())); |
597 | - |
598 | - if (state.is_enhanced_ && !m_caches.hasCachedEnhanced()) |
599 | - createCachedEnhanced(); |
600 | - |
601 | - if (!m_caches.overwriteFromCache(state.is_enhanced_)) |
602 | - qDebug("Error overwriting %s from cache", qPrintable(file().filePath())); |
603 | - |
604 | - // Have we been rotated and _not_ cropped? |
605 | - if (fileFormatHasOrientation() && (!state.crop_rectangle_.isValid()) && |
606 | - state.exposureCompensation_ == 0 && |
607 | - (state.orientation_ != PhotoEditState::ORIGINAL_ORIENTATION)) { |
608 | - // Yes; skip out on decoding and re-encoding the image. |
609 | - handleSimpleMetadataRotation(state); |
610 | - return; |
611 | - } |
612 | - |
613 | - // TODO: we might be able to avoid reading/writing pixel data (and other |
614 | - // more general optimizations) under certain conditions here. Might be worth |
615 | - // doing if it doesn't make the code too much worse. |
616 | - // |
617 | - // At the moment, we are skipping at least one decode and one encode in cases |
618 | - // where a .jpeg file has been rotated, but not cropped, since rotation can be |
619 | - // controlled by manipulating its metadata without having to modify pixel data; |
620 | - // please see the method handle_simple_metadata_rotation() for details. |
621 | - |
622 | - QImage image(file().filePath(), m_fileFormat.toStdString().c_str()); |
623 | - if (image.isNull()) { |
624 | - qDebug("Error loading %s for editing", qPrintable(file().filePath())); |
625 | - return; |
626 | - } |
627 | - PhotoMetadata* metadata = PhotoMetadata::fromFile(file()); |
628 | - |
629 | - if (fileFormatHasOrientation() && |
630 | - state.orientation_ != PhotoEditState::ORIGINAL_ORIENTATION) |
631 | - metadata->setOrientation(state.orientation_); |
632 | - |
633 | - if (fileFormatHasOrientation() && |
634 | - metadata->orientation() != TOP_LEFT_ORIGIN) |
635 | - image = image.transformed(metadata->orientationTransform()); |
636 | - else if (state.orientation_ != PhotoEditState::ORIGINAL_ORIENTATION && |
637 | - state.orientation_ != TOP_LEFT_ORIGIN) |
638 | - image = image.transformed( |
639 | - OrientationCorrection::fromOrientation(state.orientation_).toTransform()); |
640 | - |
641 | - // Cache this here so we may be able to avoid another JPEG decode later just |
642 | - // to find the dimensions. |
643 | - if (!m_originalSize.isValid()) |
644 | - m_originalSize = image.size(); |
645 | - |
646 | - if (state.crop_rectangle_.isValid()) |
647 | - image = image.copy(state.crop_rectangle_); |
648 | - |
649 | - // exposure compensation |
650 | - if (state.exposureCompensation_ != 0.0) { |
651 | - image = compensateExposure(image, state.exposureCompensation_); |
652 | - } |
653 | - |
654 | - // exposure compensation |
655 | - if (!state.colorBalance_.isNull()) { |
656 | - const QVector4D &v = state.colorBalance_; |
657 | - image = doColorBalance(image, v.x(), v.y(), v.z(), v.w()); |
658 | - } |
659 | - |
660 | - QSize new_size = image.size(); |
661 | - |
662 | - // We need to apply the reverse transformation so that when we reload the |
663 | - // file and reapply the transformation it comes out correctly. |
664 | - if (fileFormatHasOrientation() && |
665 | - metadata->orientation() != TOP_LEFT_ORIGIN) |
666 | - image = image.transformed(metadata->orientationTransform().inverted()); |
667 | - |
668 | - bool saved = image.save(file().filePath(), m_fileFormat.toStdString().c_str(), 90); |
669 | - if (saved && fileFormatHasMetadata()) |
670 | - saved = metadata->save(); |
671 | - if (!saved) |
672 | - qDebug("Error saving edited %s", qPrintable(file().filePath())); |
673 | - |
674 | - delete metadata; |
675 | - |
676 | - setSize(new_size); |
677 | -} |
678 | - |
679 | -/*! |
680 | - * \brief Photo::createCachedEnhanced |
681 | - */ |
682 | -void Photo::createCachedEnhanced() |
683 | -{ |
684 | - if (!m_caches.cacheEnhancedFromOriginal()) { |
685 | - qDebug("Error creating enhanced file for %s", qPrintable(file().filePath())); |
686 | - return; |
687 | - } |
688 | - |
689 | - QFileInfo to_enhance = m_caches.enhancedFile(); |
690 | - PhotoMetadata* metadata = PhotoMetadata::fromFile(to_enhance); |
691 | - |
692 | - QImage unenhanced_img(to_enhance.filePath(), m_fileFormat.toStdString().c_str()); |
693 | - int width = unenhanced_img.width(); |
694 | - int height = unenhanced_img.height(); |
695 | - |
696 | - QImage sample_img = (unenhanced_img.width() > 400) ? |
697 | - unenhanced_img.scaledToWidth(400) : unenhanced_img; |
698 | - |
699 | - AutoEnhanceTransformation enhance_txn = AutoEnhanceTransformation(sample_img); |
700 | - |
701 | - QImage::Format dest_format = unenhanced_img.format(); |
702 | - |
703 | - // Can't write into indexed images, due to a limitation in Qt. |
704 | - if (dest_format == QImage::Format_Indexed8) |
705 | - dest_format = QImage::Format_RGB32; |
706 | - |
707 | - QImage enhanced_image(width, height, dest_format); |
708 | - |
709 | - for (int j = 0; j < height; j++) { |
710 | - //QApplication::processEvents(); |
711 | - for (int i = 0; i < width; i++) { |
712 | - QColor px = enhance_txn.transformPixel( |
713 | - QColor(unenhanced_img.pixel(i, j))); |
714 | - enhanced_image.setPixel(i, j, px.rgb()); |
715 | - } |
716 | - } |
717 | - |
718 | - bool saved = enhanced_image.save(to_enhance.filePath(), |
719 | - m_fileFormat.toStdString().c_str(), 99); |
720 | - if (saved && fileFormatHasMetadata()) |
721 | - saved = metadata->save(); |
722 | - if (!saved) { |
723 | - qDebug("Error saving enhanced file for %s", qPrintable(file().filePath())); |
724 | - m_caches.discardCachedEnhanced(); |
725 | - } |
726 | - |
727 | - delete metadata; |
728 | -} |
729 | - |
730 | -/*! |
731 | - * \brief Photo::compensateExposure Compensates the exposure |
732 | - * Compensating the exposure is a change in brightnes |
733 | - * \param image Image to change the brightnes |
734 | - * \param compansation -1.0 is total dark, +1.0 is total bright |
735 | - * \return The image with adjusted brightnes |
736 | - */ |
737 | -QImage Photo::compensateExposure(const QImage &image, qreal compansation) |
738 | -{ |
739 | - int shift = qBound(-255, (int)(255*compansation), 255); |
740 | - QImage result(image.width(), image.height(), image.format()); |
741 | - |
742 | - for (int j = 0; j < image.height(); j++) { |
743 | - QApplication::processEvents(); |
744 | - for (int i = 0; i <image.width(); i++) { |
745 | - QColor px = image.pixel(i, j); |
746 | - int red = qBound(0, px.red() + shift, 255); |
747 | - int green = qBound(0, px.green() + shift, 255); |
748 | - int blue = qBound(0, px.blue() + shift, 255); |
749 | - result.setPixel(i, j, qRgb(red, green, blue)); |
750 | - } |
751 | - } |
752 | - |
753 | - return result; |
754 | -} |
755 | - |
756 | -/*! |
757 | - * \brief Photo::colorBalance |
758 | - * \param image |
759 | - * \param brightness 0 is total dark, 1 is as the original, grater than 1 is brigther |
760 | - * \param contrast from 0 maybe 5. 1 is as the original |
761 | - * \param saturation from 0 maybe 5. 1 is as the original |
762 | - * \param hue from 0 to 360. 0 and 360 is as the original |
763 | - * \return |
764 | - */ |
765 | -QImage Photo::doColorBalance(const QImage &image, qreal brightness, qreal contrast, qreal saturation, qreal hue) |
766 | -{ |
767 | - QImage result(image.width(), image.height(), image.format()); |
768 | - |
769 | - ColorBalance cb(brightness, contrast, saturation, hue); |
770 | - |
771 | - for (int j = 0; j < image.height(); j++) { |
772 | - QApplication::processEvents(); |
773 | - for (int i = 0; i <image.width(); i++) { |
774 | - QColor px = image.pixel(i, j); |
775 | - QColor tpx = cb.transformPixel(px); |
776 | - result.setPixel(i, j, tpx.rgb()); |
777 | - } |
778 | - } |
779 | - |
780 | - return result; |
781 | + emit editStackChanged(); |
782 | } |
783 | |
784 | /*! |
785 | @@ -901,6 +702,15 @@ |
786 | } |
787 | |
788 | /*! |
789 | + * \brief Photo::fileFormat returns the file format as QString |
790 | + * \return |
791 | + */ |
792 | +const QString &Photo::fileFormat() const |
793 | +{ |
794 | + return m_fileFormat; |
795 | +} |
796 | + |
797 | +/*! |
798 | * \brief Photo::fileFormatHasMetadata |
799 | * \return |
800 | */ |
801 | @@ -927,3 +737,22 @@ |
802 | { |
803 | m_originalOrientation = orientation; |
804 | } |
805 | + |
806 | +/*! |
807 | + * \brief Photo::originalOrientation returns the original orientation |
808 | + * \return |
809 | + */ |
810 | +Orientation Photo::originalOrientation() const |
811 | +{ |
812 | + return m_originalOrientation; |
813 | +} |
814 | + |
815 | +/*! |
816 | + * \brief Photo::originalSize |
817 | + * \return |
818 | + */ |
819 | +const QSize &Photo::originalSize() |
820 | +{ |
821 | + originalSize(PhotoEditState::ORIGINAL_ORIENTATION); |
822 | + return m_originalSize; |
823 | +} |
824 | |
825 | === modified file 'src/photo/photo.h' |
826 | --- src/photo/photo.h 2013-06-17 12:57:21 +0000 |
827 | +++ src/photo/photo.h 2013-07-26 16:08:26 +0000 |
828 | @@ -31,6 +31,7 @@ |
829 | #include "orientation.h" |
830 | |
831 | class PhotoEditState; |
832 | +class PhotoEditThread; |
833 | class PhotoPrivate; |
834 | |
835 | /*! |
836 | @@ -81,6 +82,12 @@ |
837 | Q_INVOKABLE void crop(QVariant vrect); |
838 | |
839 | void setOriginalOrientation(Orientation orientation); |
840 | + Orientation originalOrientation() const; |
841 | + const QSize &originalSize(); |
842 | + |
843 | + const QString &fileFormat() const; |
844 | + bool fileFormatHasMetadata() const; |
845 | + bool fileFormatHasOrientation() const; |
846 | |
847 | signals: |
848 | void editStackChanged(); |
849 | @@ -88,22 +95,21 @@ |
850 | protected: |
851 | virtual void destroySource(bool destroyBacking, bool asOrphan); |
852 | |
853 | +private Q_SLOTS: |
854 | + void resetToOriginalSize(); |
855 | + void finishEditing(); |
856 | + |
857 | private: |
858 | const PhotoEditState& currentState() const; |
859 | QSize originalSize(Orientation orientation); |
860 | void makeUndoableEdit(const PhotoEditState& state); |
861 | - void save(const PhotoEditState& state, Orientation oldOrientation); |
862 | + void asyncEdit(const PhotoEditState& state); |
863 | void editFile(const PhotoEditState& state); |
864 | - void createCachedEnhanced(); |
865 | - QImage compensateExposure(const QImage& image, qreal compansation); |
866 | - QImage doColorBalance(const QImage& image, qreal brightness, qreal contrast, qreal saturation, qreal hue); |
867 | void appendPathParams(QUrl* url, Orientation orientation, const int sizeLevel) const; |
868 | - void handleSimpleMetadataRotation(const PhotoEditState& state); |
869 | - bool fileFormatHasMetadata() const; |
870 | - bool fileFormatHasOrientation() const; |
871 | |
872 | QString m_fileFormat; |
873 | int m_editRevision; // How many times the pixel data has been modified by us. |
874 | + PhotoEditThread *m_editThread; |
875 | PhotoCaches m_caches; |
876 | |
877 | // We cache this data to avoid an image read at various times. |
878 | |
879 | === modified file 'tests/unittests/stubs/photo_stub.cpp' |
880 | --- tests/unittests/stubs/photo_stub.cpp 2013-06-17 12:57:21 +0000 |
881 | +++ tests/unittests/stubs/photo_stub.cpp 2013-07-26 16:08:26 +0000 |
882 | @@ -159,3 +159,12 @@ |
883 | { |
884 | m_originalOrientation = orientation; |
885 | } |
886 | + |
887 | +void Photo::resetToOriginalSize() |
888 | +{ |
889 | +} |
890 | + |
891 | +void Photo::finishEditing() |
892 | +{ |
893 | +} |
894 | + |
PASSED: Continuous integration, rev:787 jenkins. qa.ubuntu. com/job/ gallery- app-ci/ 372/ jenkins. qa.ubuntu. com/job/ gallery- app-saucy- amd64-ci/ 182 jenkins. qa.ubuntu. com/job/ gallery- app-saucy- armhf-ci/ 182 jenkins. qa.ubuntu. com/job/ gallery- app-saucy- armhf-ci/ 182/artifact/ work/output/ *zip*/output. zip jenkins. qa.ubuntu. com/job/ gallery- app-saucy- i386-ci/ 182 jenkins. qa.ubuntu. com/job/ generic- mediumtests- saucy/1602 jenkins. qa.ubuntu. com/job/ generic- mediumtests- builder- saucy/1606 jenkins. qa.ubuntu. com/job/ generic- mediumtests- builder- saucy/1606/ artifact/ work/output/ *zip*/output. zip jenkins. qa.ubuntu. com/job/ generic- mediumtests- runner- saucy/1356
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild: s-jenkins: 8080/job/ gallery- app-ci/ 372/rebuild
http://