Merge lp:~fboucault/thumbnailer/new_qml_api into lp:thumbnailer

Proposed by Florian Boucault
Status: Rejected
Rejected by: Michi Henning
Proposed branch: lp:~fboucault/thumbnailer/new_qml_api
Merge into: lp:thumbnailer
Diff against target: 777 lines (+631/-14)
13 files modified
debian/libthumbnailer0.symbols (+1/-0)
include/thumbnailer.h (+9/-0)
plugins/Ubuntu/Thumbnailer/CMakeLists.txt (+2/-0)
plugins/Ubuntu/Thumbnailer/plugin.cpp (+3/-0)
plugins/Ubuntu/Thumbnailer/qthumbnailer.cpp (+260/-0)
plugins/Ubuntu/Thumbnailer/qthumbnailer.h (+114/-0)
plugins/Ubuntu/Thumbnailer/thumbnailqueue.cpp (+45/-0)
plugins/Ubuntu/Thumbnailer/thumbnailqueue.h (+41/-0)
src/libthumbnailer.map (+1/-0)
src/thumbnailer.cpp (+33/-12)
tests/basic.cpp (+23/-0)
tests/qml/tst_image_provider.qml (+2/-2)
tests/qml/tst_thumbnailer.qml (+97/-0)
To merge this branch: bzr merge lp:~fboucault/thumbnailer/new_qml_api
Reviewer Review Type Date Requested Status
Michi Henning (community) Disapprove
Florian Boucault (community) Needs Fixing
Jussi Pakkanen (community) Approve
PS Jenkins bot (community) continuous-integration Approve
Xavi Garcia (community) Needs Fixing
Review via email: mp+250832@code.launchpad.net

Commit message

New QML Thumbnailer API allowing custom thumbnailing schedule.

Videos are queued thumbnailed independently from pictures resulting in a better user experience.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
138. By Florian Boucault

added unit test

139. By Florian Boucault

Clearer test

140. By Florian Boucault

Added missing symbol

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
141. By Florian Boucault

Added QML tests

142. By Florian Boucault

Added more QML tests

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Jussi Pakkanen (jpakkane) wrote :

I didn't get to review the functional bits quite yet, but here are some simpler issues in the mean time.

- the qml tests don't seem to be run
- the bit that converts relative file names to absolute ones is duplicated in get_thumbnail and needs_generation, please move it to a shared function in an anonymous namespace
- the name needs_generation seems a bit backwards, would it be better to call it is_cached instead

review: Needs Fixing
Revision history for this message
Florian Boucault (fboucault) wrote :

Qml tests not being run automatically is not a new issue. Should be fixed
separately.
Le 25 févr. 2015 07:52, "Jussi Pakkanen" <email address hidden> a
écrit :

> Review: Needs Fixing
>
> I didn't get to review the functional bits quite yet, but here are some
> simpler issues in the mean time.
>
> - the qml tests don't seem to be run
> - the bit that converts relative file names to absolute ones is duplicated
> in get_thumbnail and needs_generation, please move it to a shared function
> in an anonymous namespace
> - the name needs_generation seems a bit backwards, would it be better to
> call it is_cached instead
>
> --
> https://code.launchpad.net/~fboucault/thumbnailer/new_qml_api/+merge/250832
> You are the owner of lp:~fboucault/thumbnailer/new_qml_api.
>

Revision history for this message
Xavi Garcia (xavi-garcia-mena) wrote :

Other minor changes:

 - The new added symbol:
+ (c++)"Thumbnailer::thumbnail_needs_generation(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, ThumbnailSize)@Base" 1.1

The version should be 0replaceme instead of 1.1, so the debian machinery sets the right value.

 - In the newly created file qthumbnailer.h members should follow the same rules. Either to have the m_/s_ prefix or none. The first class have prefixes while the second doesn't.

review: Needs Fixing
143. By Florian Boucault

Use proper generic version for new symbol

144. By Florian Boucault

ThumbnailTask to follow the m_/s_ syntax convention used in other classes.

145. By Florian Boucault

Renamed thumbnail_needs_generation into thumbnail_is_cached

Revision history for this message
Florian Boucault (fboucault) wrote :

> Other minor changes:
>
> - The new added symbol:
> + (c++)"Thumbnailer::thumbnail_needs_generation(std::basic_string<char,
> std::char_traits<char>, std::allocator<char> > const&, ThumbnailSize)@Base"
> 1.1
>
> The version should be 0replaceme instead of 1.1, so the debian machinery sets
> the right value.
>

Fixed.

> - In the newly created file qthumbnailer.h members should follow the same
> rules. Either to have the m_/s_ prefix or none. The first class have prefixes
> while the second doesn't.

Fixed.

146. By Florian Boucault

Factorised common code between thumbnail_is_cached and get_thumbnail into a private function.

Revision history for this message
Florian Boucault (fboucault) wrote :

> I didn't get to review the functional bits quite yet, but here are some
> simpler issues in the mean time.
>
> - the qml tests don't seem to be run
> - the bit that converts relative file names to absolute ones is duplicated in
> get_thumbnail and needs_generation, please move it to a shared function in an
> anonymous namespace

Fixed.

> - the name needs_generation seems a bit backwards, would it be better to call
> it is_cached instead

Fixed.

147. By Florian Boucault

Removed manual example

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Albert Astals Cid (aacid) wrote :

To me this seems a fix on the wrong level, that is, it'll mean people have to use Thumbnail {} instead of Image {}, ideally we could still use Image {} and have parallization at the Qt level, though obviously that means convincing the Qt people that this makes sense and getting it accepted upstream.

Revision history for this message
Michał Sawicz (saviq) wrote :

Right, +1 to the above. Can you mark this as being a workaround for QTBUG-37988 please.

Revision history for this message
Florian Boucault (fboucault) wrote :

> To me this seems a fix on the wrong level, that is, it'll mean people have to
> use Thumbnail {} instead of Image {}, ideally we could still use Image {} and
> have parallization at the Qt level, though obviously that means convincing the
> Qt people that this makes sense and getting it accepted upstream.

No matter how much we patch Qt it will never do the right thing, ie. when a thumbnail is being generated, loading thumbnails that have already been generated previously should not be blocked.

Revision history for this message
Jussi Pakkanen (jpakkane) wrote :

Let me try to condense Florian's concerns for those who have not had to deal with the process of thumbnailing.

Creating a video screenshot thumbnail takes several seconds (up to 5) on the device.

Creating a thumbnail for other types (mostly images) takes a fraction of a second. Loading a preexisting thumbnail takes roughly the same.

Suppose you have n threads for thumbnailing and want to display n videos and m images. If you submit the videos first, the images are stuck until the first video gets processed.

What we want is to be able to submit both images and videos and have the system prioritise those thumbnails that are quick to load (like, say, reserving one thread for them and one for video screenshots).

Qt's image provider does not have priorisation capabilities and to fix our use case something like that needs to be added to Qt. This is the Correct thing to do, but on the other hand it is a lot more work than just increasing the amount of threads for QQuickImageProviders.

Revision history for this message
Florian Boucault (fboucault) wrote :

> Let me try to condense Florian's concerns for those who have not had to deal
> with the process of thumbnailing.
>
> Creating a video screenshot thumbnail takes several seconds (up to 5) on the
> device.
>
> Creating a thumbnail for other types (mostly images) takes a fraction of a
> second. Loading a preexisting thumbnail takes roughly the same.
>
> Suppose you have n threads for thumbnailing and want to display n videos and m
> images. If you submit the videos first, the images are stuck until the first
> video gets processed.
>
> What we want is to be able to submit both images and videos and have the
> system prioritise those thumbnails that are quick to load (like, say,
> reserving one thread for them and one for video screenshots).
>
> Qt's image provider does not have priorisation capabilities and to fix our use
> case something like that needs to be added to Qt. This is the Correct thing to
> do, but on the other hand it is a lot more work than just increasing the
> amount of threads for QQuickImageProviders.

Yes and no. I am also fixing a case that you merged with another one as being the same:
case 1) image with non existing thumbnail
case 2) video with non existing thumbnail
case 3) any media with already existing thumbnail

1) is fast but not instant
2) is slow
3) is instant

But because of the queuing, if a video is being thumbnailed, 1) and 3) become slow.
Adding N more threads to the image provider infrastructure would fix the issue of 1) becoming slow but 3) would still not be instant in some cases.

Revision history for this message
Jussi Pakkanen (jpakkane) wrote :

LGTM. One thing to note is that there are two queues called video and image. The image queue deals with other image types besides images, such as audio files (and possibly in the future also pdf files, libreoffice docs etc). Thus it might make sense to rename it s_otherQueue, s_basicQueue or something like that. This is totally up to you, though, and not a requirement.

review: Approve
Revision history for this message
Florian Boucault (fboucault) wrote :

Will be reworked in 1-2 weeks to make use of the new Qt QQuickImageProvider async API that aacid is working on. That will allow us to not have that new Thmubnailer {} QML API.

review: Needs Fixing
Revision history for this message
Michi Henning (michihenning) wrote :

Rejecting this, seeing it is obsolete.

review: Disapprove

Unmerged revisions

147. By Florian Boucault

Removed manual example

146. By Florian Boucault

Factorised common code between thumbnail_is_cached and get_thumbnail into a private function.

145. By Florian Boucault

Renamed thumbnail_needs_generation into thumbnail_is_cached

144. By Florian Boucault

ThumbnailTask to follow the m_/s_ syntax convention used in other classes.

143. By Florian Boucault

Use proper generic version for new symbol

142. By Florian Boucault

Added more QML tests

141. By Florian Boucault

Added QML tests

140. By Florian Boucault

Added missing symbol

139. By Florian Boucault

Clearer test

138. By Florian Boucault

added unit test

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/libthumbnailer0.symbols'
2--- debian/libthumbnailer0.symbols 2014-08-11 14:20:31 +0000
3+++ debian/libthumbnailer0.symbols 2015-02-25 18:25:10 +0000
4@@ -5,3 +5,4 @@
5 (c++)"Thumbnailer::Thumbnailer()@Base" 1.0
6 (c++)"Thumbnailer::~Thumbnailer()@Base" 1.0
7 (c++)"Thumbnailer::get_artist_art(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, ThumbnailSize, ThumbnailPolicy)@Base" 1.1+14.10.20140811
8+ (c++)"Thumbnailer::thumbnail_is_cached(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, ThumbnailSize)@Base" 0replaceme
9
10=== modified file 'include/thumbnailer.h'
11--- include/thumbnailer.h 2014-08-01 13:22:43 +0000
12+++ include/thumbnailer.h 2015-02-25 18:25:10 +0000
13@@ -76,6 +76,15 @@
14 std::string get_artist_art(const std::string &artist, const std::string &album,
15 ThumbnailSize desiredSize, ThumbnailPolicy policy);
16
17+ /**
18+ * Returns whether or not the thumbnail was previously generated and is readily
19+ * available.
20+ *
21+ * If true the next call to get_thumbnail would lead to generating the
22+ * thumbnail and could potentially take a long time.
23+ */
24+ bool thumbnail_is_cached(const std::string &filename, ThumbnailSize desired_size);
25+
26 private:
27 Thumbnailer(const Thumbnailer &t);
28 Thumbnailer & operator=(const Thumbnailer &t);
29
30=== modified file 'plugins/Ubuntu/Thumbnailer/CMakeLists.txt'
31--- plugins/Ubuntu/Thumbnailer/CMakeLists.txt 2014-08-01 11:02:12 +0000
32+++ plugins/Ubuntu/Thumbnailer/CMakeLists.txt 2015-02-25 18:25:10 +0000
33@@ -7,6 +7,8 @@
34 artgeneratorcommon.cpp
35 artistartgenerator.cpp
36 thumbnailgenerator.cpp
37+ qthumbnailer.cpp
38+ thumbnailqueue.cpp
39 )
40
41 set_target_properties(thumbnailer-qml PROPERTIES AUTOMOC TRUE)
42
43=== modified file 'plugins/Ubuntu/Thumbnailer/plugin.cpp'
44--- plugins/Ubuntu/Thumbnailer/plugin.cpp 2014-08-01 10:21:31 +0000
45+++ plugins/Ubuntu/Thumbnailer/plugin.cpp 2015-02-25 18:25:10 +0000
46@@ -20,11 +20,14 @@
47 #include "albumartgenerator.h"
48 #include "artistartgenerator.h"
49 #include "thumbnailgenerator.h"
50+#include "qthumbnailer.h"
51
52 void ThumbnailerPlugin::registerTypes(const char *uri) {
53 qmlRegisterTypeNotAvailable(
54 uri, 0, 1, "__ThumbnailerIgnoreMe",
55 "Ignore this: QML plugins must contain at least one type");
56+ qmlRegisterType<QThumbnailer>(uri, 0, 1, "Thumbnailer");
57+
58 }
59
60 void ThumbnailerPlugin::initializeEngine(QQmlEngine *engine, const char *uri) {
61
62=== added file 'plugins/Ubuntu/Thumbnailer/qthumbnailer.cpp'
63--- plugins/Ubuntu/Thumbnailer/qthumbnailer.cpp 1970-01-01 00:00:00 +0000
64+++ plugins/Ubuntu/Thumbnailer/qthumbnailer.cpp 2015-02-25 18:25:10 +0000
65@@ -0,0 +1,260 @@
66+/*
67+ * Copyright 2015 Canonical Ltd.
68+ *
69+ * This program is free software; you can redistribute it and/or modify
70+ * it under the terms of the GNU Lesser General Public License as published by
71+ * the Free Software Foundation; version 3.
72+ *
73+ * This program is distributed in the hope that it will be useful,
74+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
75+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
76+ * GNU Lesser General Public License for more details.
77+ *
78+ * You should have received a copy of the GNU Lesser General Public License
79+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
80+ *
81+ * Authors: Florian Boucault <florian.boucault@canonical.com>
82+*/
83+
84+#include "qthumbnailer.h"
85+#include <stdexcept>
86+#include <QtCore/QDebug>
87+
88+/*!
89+ \qmltype Thumbnailer
90+ \instantiates QThumbnailer
91+ \inqmlmodule Ubuntu.Thumbnailer 0.1
92+ \ingroup ubuntu
93+ \brief Thumbnailer provides a way to load thumbnails of any media (image, video, etc.)
94+
95+ When an image representation of a media, e.g. a video, is needed a thumbnail can be
96+ generated and retrieved by Thumbnailer. Once generated a thumbnail is cached on
97+ disk and reused when needed.
98+
99+ Thumbnails generated have a size always greater or equal to \l size.
100+
101+ In the following example a thumbnail of a media located at 'pathToMediaFile' is
102+ generated and then loaded by a standard QML Image object. Its size is set so that
103+ it matches exactly the size required by the Image as to minimize the computation
104+ and memory used while still looking as good as possible.
105+
106+ \qml
107+ import QtQuick 2.0
108+ import Ubuntu.Thumbnailer 0.1
109+
110+ Item {
111+ Thumbnailer {
112+ id: thumbnailer
113+ source: pathToMediaFile
114+ size: image.sourceSize
115+ }
116+
117+ Image {
118+ id: image
119+
120+ fillMode: Image.PreserveAspectFit
121+ asynchronous: true
122+ source: thumbnailer.thumbnail
123+ sourceSize {
124+ width: image.width
125+ height: image.height
126+ }
127+ }
128+ }
129+ \endqml
130+
131+*/
132+
133+QThumbnailer::QThumbnailer(QObject* parent) :
134+ QObject(parent),
135+ m_componentCompleted(false),
136+ m_source(""),
137+ m_thumbnail(""),
138+ m_thumbnailsize(TN_SIZE_SMALL),
139+ m_currentTask(NULL)
140+{
141+}
142+
143+QThumbnailer::~QThumbnailer()
144+{
145+ cancelUpdateThumbnail();
146+}
147+
148+void QThumbnailer::classBegin()
149+{
150+}
151+
152+void QThumbnailer::componentComplete()
153+{
154+ m_componentCompleted = true;
155+ updateThumbnail();
156+}
157+
158+/*!
159+ \qmlproperty url Thumbnailer::source
160+
161+ URL of the media to be thumbnailed. Once set \l thumbnail will eventually
162+ hold the URL of the thumbnail.
163+ */
164+QUrl QThumbnailer::source() const
165+{
166+ return m_source;
167+}
168+
169+void QThumbnailer::setSource(QUrl source)
170+{
171+ if (source != m_source) {
172+ m_source = source;
173+ Q_EMIT sourceChanged();
174+ updateThumbnail();
175+ }
176+}
177+
178+/*!
179+ \qmlproperty size Thumbnailer::size
180+
181+ Size requested for the thumbnail. The resulting thumbnail's size will be
182+ at least \l size.
183+
184+ Warning: the thumbnail's size can be much greater than \l size.
185+ */
186+QSize QThumbnailer::size() const
187+{
188+ return m_size;
189+}
190+
191+ThumbnailSize thumbnailSizeFromSize(QSize size)
192+{
193+ const int xlarge_cutoff = 512;
194+ const int large_cutoff = 256;
195+ const int small_cutoff = 128;
196+ if(size.width() > xlarge_cutoff || size.height() > xlarge_cutoff) {
197+ return TN_SIZE_ORIGINAL;
198+ } else if(size.width() > large_cutoff || size.height() > large_cutoff) {
199+ return TN_SIZE_XLARGE;
200+ } else if(size.width() > small_cutoff || size.height() > small_cutoff) {
201+ return TN_SIZE_LARGE;
202+ } else {
203+ return TN_SIZE_SMALL;
204+ }
205+}
206+
207+void QThumbnailer::setSize(QSize size)
208+{
209+ if (size != m_size) {
210+ m_size = size;
211+ m_thumbnailsize = thumbnailSizeFromSize(m_size);
212+ Q_EMIT sizeChanged();
213+ updateThumbnail();
214+ }
215+}
216+
217+/*!
218+ \qmlproperty url Thumbnailer::thumbnail
219+
220+ URL of the thumbnail generated once \l source and \l size are set.
221+ */
222+QUrl QThumbnailer::thumbnail() const
223+{
224+ return m_thumbnail;
225+}
226+
227+void QThumbnailer::setThumbnail(QString thumbnail)
228+{
229+ QUrl thumbnailUrl;
230+ if (thumbnail.isEmpty()) {
231+ thumbnailUrl = QUrl();
232+ } else {
233+ thumbnailUrl = QUrl::fromLocalFile(thumbnail);
234+ }
235+ if (thumbnailUrl != m_thumbnail) {
236+ m_thumbnail = thumbnailUrl;
237+ Q_EMIT thumbnailChanged();
238+ }
239+}
240+
241+void QThumbnailer::updateThumbnail()
242+{
243+ if (!m_componentCompleted) {
244+ return;
245+ }
246+
247+ cancelUpdateThumbnail();
248+
249+ if (m_size.isEmpty() || m_source.isEmpty()) {
250+ setThumbnail("");
251+ return;
252+ }
253+
254+ bool slowUpdate = s_thumbnailer.thumbnail_is_cached(m_source.path().toStdString(),
255+ m_thumbnailsize);
256+ if (!slowUpdate) {
257+ // if we know retrieving the thumbnail is fast because it is readily
258+ // available on disk, set it immediately
259+ QString thumbnail = thumbnailPathForMedia(m_source.path(), m_thumbnailsize);
260+ setThumbnail(thumbnail);
261+ } else {
262+ // otherwise enqueue the thumbnailing task that will then get processed
263+ // in a background thread and will end up setting the thumbnail URL
264+ ThumbnailTask* task = new ThumbnailTask;
265+ task->m_source = m_source;
266+ task->m_size = m_thumbnailsize;
267+ task->m_caller = this;
268+ m_currentTask = task;
269+ enqueueThumbnailTask(task);
270+ }
271+}
272+
273+void QThumbnailer::cancelUpdateThumbnail()
274+{
275+ if (!m_currentTask.isNull()) {
276+ if (s_imageQueue.removeTask(m_currentTask.data()) ||
277+ s_videoQueue.removeTask(m_currentTask.data()) ) {
278+ delete m_currentTask;
279+ m_currentTask.clear();
280+ }
281+ }
282+}
283+
284+
285+/* Static methods implementation */
286+
287+static const QString DEFAULT_VIDEO_ART("/usr/share/thumbnailer/icons/video_missing.png");
288+static const QString DEFAULT_ALBUM_ART("/usr/share/thumbnailer/icons/album_missing.png");
289+
290+ThumbnailQueue QThumbnailer::s_videoQueue;
291+ThumbnailQueue QThumbnailer::s_imageQueue;
292+Thumbnailer QThumbnailer::s_thumbnailer;
293+QMimeDatabase QThumbnailer::s_mimeDatabase;
294+
295+QString QThumbnailer::thumbnailPathForMedia(QString mediaPath, ThumbnailSize size)
296+{
297+ QString thumbnailPath;
298+ try {
299+ thumbnailPath = QString::fromStdString(s_thumbnailer.get_thumbnail(mediaPath.toStdString(),
300+ (ThumbnailSize)size));
301+ } catch(std::runtime_error &e) {
302+ qWarning() << QString("Thumbnail retrieval for %1 failed:").arg(mediaPath).toLocal8Bit().constData() << e.what();
303+ }
304+
305+ if(thumbnailPath.isEmpty()) {
306+ QMimeType mime = s_mimeDatabase.mimeTypeForFile(mediaPath);
307+ if(mime.name().contains("audio")) {
308+ thumbnailPath = DEFAULT_ALBUM_ART;
309+ } else if(mime.name().contains("video")) {
310+ thumbnailPath = DEFAULT_VIDEO_ART;
311+ }
312+ }
313+
314+ return thumbnailPath;
315+}
316+
317+void QThumbnailer::enqueueThumbnailTask(ThumbnailTask* task)
318+{
319+ QMimeType mime = s_mimeDatabase.mimeTypeForFile(task->m_source.path());
320+ if(mime.name().contains("image")) {
321+ s_imageQueue.appendTask(task);
322+ } else {
323+ s_videoQueue.appendTask(task);
324+ }
325+}
326
327=== added file 'plugins/Ubuntu/Thumbnailer/qthumbnailer.h'
328--- plugins/Ubuntu/Thumbnailer/qthumbnailer.h 1970-01-01 00:00:00 +0000
329+++ plugins/Ubuntu/Thumbnailer/qthumbnailer.h 2015-02-25 18:25:10 +0000
330@@ -0,0 +1,114 @@
331+/*
332+ * Copyright 2015 Canonical Ltd.
333+ *
334+ * This program is free software; you can redistribute it and/or modify
335+ * it under the terms of the GNU Lesser General Public License as published by
336+ * the Free Software Foundation; version 3.
337+ *
338+ * This program is distributed in the hope that it will be useful,
339+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
340+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
341+ * GNU Lesser General Public License for more details.
342+ *
343+ * You should have received a copy of the GNU Lesser General Public License
344+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
345+ *
346+ * Authors: Florian Boucault <florian.boucault@canonical.com>
347+*/
348+
349+#ifndef THUMBNAILER_H
350+#define THUMBNAILER_H
351+
352+#include <QtCore/QObject>
353+#include <QtCore/QUrl>
354+#include <QtCore/QMimeDatabase>
355+#include <QtCore/QPointer>
356+#include <QtCore/QSize>
357+#include <QtQml/QQmlParserStatus>
358+#include <thumbnailer.h>
359+#include "thumbnailqueue.h"
360+
361+class ThumbnailTask;
362+
363+class QThumbnailer : public QObject, public QQmlParserStatus
364+{
365+ Q_OBJECT
366+ Q_INTERFACES(QQmlParserStatus)
367+ Q_DISABLE_COPY(QThumbnailer)
368+
369+ Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
370+ Q_PROPERTY(QSize size READ size WRITE setSize NOTIFY sizeChanged)
371+ Q_PROPERTY(QUrl thumbnail READ thumbnail NOTIFY thumbnailChanged)
372+
373+ friend class ThumbnailTask;
374+
375+public:
376+ QThumbnailer(QObject* parent=0);
377+ ~QThumbnailer();
378+
379+ // inherited from QQmlParserStatus
380+ void classBegin();
381+ void componentComplete();
382+
383+ // getters and setters
384+ QUrl source() const;
385+ void setSource(QUrl source);
386+ QSize size() const;
387+ void setSize(QSize size);
388+ QUrl thumbnail() const;
389+
390+Q_SIGNALS:
391+ void sourceChanged();
392+ void sizeChanged();
393+ void thumbnailChanged();
394+
395+protected:
396+ void updateThumbnail();
397+ void cancelUpdateThumbnail();
398+ static QString thumbnailPathForMedia(QString mediaPath, ThumbnailSize size);
399+ static void enqueueThumbnailTask(ThumbnailTask* task);
400+
401+protected Q_SLOTS:
402+ void setThumbnail(QString thumbnail);
403+
404+private:
405+ bool m_componentCompleted;
406+ QUrl m_source;
407+ QUrl m_thumbnail;
408+ QSize m_size;
409+ ThumbnailSize m_thumbnailsize;
410+ QPointer<ThumbnailTask> m_currentTask;
411+
412+ // static class members shared accross all instances of QThumbnailer
413+ static ThumbnailQueue s_videoQueue;
414+ static ThumbnailQueue s_imageQueue;
415+ static Thumbnailer s_thumbnailer;
416+ static QMimeDatabase s_mimeDatabase;
417+};
418+
419+class ThumbnailTask : public QObject, public QRunnable
420+{
421+ Q_OBJECT
422+
423+public:
424+ virtual void run() {
425+ try {
426+ QString thumbnail = QThumbnailer::thumbnailPathForMedia(m_source.path(), m_size);
427+ Q_EMIT thumbnailPathRetrieved(thumbnail);
428+ } catch (std::exception &e) {
429+ // catch all exceptions cause it is not allowed to have unhandled
430+ // exceptions thrown from a worker thread
431+ }
432+ Q_EMIT finished();
433+ }
434+
435+ QUrl m_source;
436+ ThumbnailSize m_size;
437+ QPointer<QThumbnailer> m_caller;
438+
439+Q_SIGNALS:
440+ void thumbnailPathRetrieved(QString thumbnail);
441+ void finished();
442+};
443+
444+#endif
445
446=== added file 'plugins/Ubuntu/Thumbnailer/thumbnailqueue.cpp'
447--- plugins/Ubuntu/Thumbnailer/thumbnailqueue.cpp 1970-01-01 00:00:00 +0000
448+++ plugins/Ubuntu/Thumbnailer/thumbnailqueue.cpp 2015-02-25 18:25:10 +0000
449@@ -0,0 +1,45 @@
450+/*
451+ * Copyright 2015 Canonical Ltd.
452+ *
453+ * This program is free software; you can redistribute it and/or modify
454+ * it under the terms of the GNU Lesser General Public License as published by
455+ * the Free Software Foundation; version 3.
456+ *
457+ * This program is distributed in the hope that it will be useful,
458+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
459+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
460+ * GNU Lesser General Public License for more details.
461+ *
462+ * You should have received a copy of the GNU Lesser General Public License
463+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
464+ *
465+ * Authors: Florian Boucault <florian.boucault@canonical.com>
466+*/
467+
468+#include "thumbnailqueue.h"
469+#include "qthumbnailer.h"
470+
471+void ThumbnailQueue::appendTask(ThumbnailTask* task)
472+{
473+ connect(task, SIGNAL(thumbnailPathRetrieved(QString)), task->m_caller, SLOT(setThumbnail(QString)));
474+ connect(task, SIGNAL(finished()), this, SLOT(processNext()));
475+ m_queue.append(task);
476+ processNext();
477+}
478+
479+bool ThumbnailQueue::removeTask(ThumbnailTask* task)
480+{
481+ return m_queue.removeOne(task);
482+}
483+
484+void ThumbnailQueue::processNext()
485+{
486+ if (m_queue.isEmpty()) {
487+ return;
488+ }
489+
490+ ThumbnailTask* task = m_queue.takeFirst();
491+ if (!m_threadPool.tryStart(task)) {
492+ m_queue.prepend(task);
493+ }
494+}
495
496=== added file 'plugins/Ubuntu/Thumbnailer/thumbnailqueue.h'
497--- plugins/Ubuntu/Thumbnailer/thumbnailqueue.h 1970-01-01 00:00:00 +0000
498+++ plugins/Ubuntu/Thumbnailer/thumbnailqueue.h 2015-02-25 18:25:10 +0000
499@@ -0,0 +1,41 @@
500+/*
501+ * Copyright 2015 Canonical Ltd.
502+ *
503+ * This program is free software; you can redistribute it and/or modify
504+ * it under the terms of the GNU Lesser General Public License as published by
505+ * the Free Software Foundation; version 3.
506+ *
507+ * This program is distributed in the hope that it will be useful,
508+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
509+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
510+ * GNU Lesser General Public License for more details.
511+ *
512+ * You should have received a copy of the GNU Lesser General Public License
513+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
514+ *
515+ * Authors: Florian Boucault <florian.boucault@canonical.com>
516+*/
517+
518+#ifndef THUMBNAILQUEUE_H
519+#define THUMBNAILQUEUE_H
520+
521+#include <QtCore/QObject>
522+#include <QtCore/QThreadPool>
523+
524+class ThumbnailTask;
525+
526+class ThumbnailQueue : public QObject
527+{
528+ Q_OBJECT
529+
530+public:
531+ void appendTask(ThumbnailTask* task);
532+ bool removeTask(ThumbnailTask* task);
533+ Q_SLOT void processNext();
534+
535+private:
536+ QThreadPool m_threadPool;
537+ QList<ThumbnailTask*> m_queue;
538+};
539+
540+#endif
541
542=== modified file 'src/libthumbnailer.map'
543--- src/libthumbnailer.map 2014-08-01 13:22:43 +0000
544+++ src/libthumbnailer.map 2015-02-25 18:25:10 +0000
545@@ -7,6 +7,7 @@
546 "Thumbnailer::get_thumbnail(std::string const&, ThumbnailSize, ThumbnailPolicy)";
547 "Thumbnailer::get_album_art(std::string const&, std::string const&, ThumbnailSize, ThumbnailPolicy)";
548 "Thumbnailer::get_artist_art(std::string const&, std::string const&, ThumbnailSize, ThumbnailPolicy)";
549+ "Thumbnailer::thumbnail_is_cached(std::string const&, ThumbnailSize)";
550 };
551 local:
552 extern "C++" {
553
554=== modified file 'src/thumbnailer.cpp'
555--- src/thumbnailer.cpp 2015-01-20 15:29:10 +0000
556+++ src/thumbnailer.cpp 2015-02-25 18:25:10 +0000
557@@ -37,6 +37,25 @@
558
559 using namespace std;
560
561+namespace {
562+ std::string absolute_path_from_filename(const std::string &filename) {
563+ string abspath;
564+ if(filename.empty()) {
565+ return "";
566+ }
567+ if(filename[0] != '/') {
568+ auto cwd = getcwd(nullptr, 0);
569+ abspath += cwd;
570+ free(cwd);
571+ abspath += "/" + filename;
572+ } else {
573+ abspath = filename;
574+ }
575+ return abspath;
576+ }
577+}
578+
579+
580 class ThumbnailerPrivate {
581 private:
582 random_device rnd;
583@@ -214,18 +233,7 @@
584 }
585 std::string Thumbnailer::get_thumbnail(const std::string &filename, ThumbnailSize desired_size,
586 ThumbnailPolicy policy) {
587- string abspath;
588- if(filename.empty()) {
589- return "";
590- }
591- if(filename[0] != '/') {
592- auto cwd = getcwd(nullptr, 0);
593- abspath += cwd;
594- free(cwd);
595- abspath += "/" + filename;
596- } else {
597- abspath = filename;
598- }
599+ std::string abspath = absolute_path_from_filename(filename);
600 std::string estimate = p->cache.get_if_exists(abspath, desired_size);
601 if(!estimate.empty())
602 return estimate;
603@@ -322,3 +330,16 @@
604
605 return "";
606 }
607+
608+bool Thumbnailer::thumbnail_is_cached(const std::string &filename, ThumbnailSize desired_size) {
609+ std::string abspath = absolute_path_from_filename(filename);
610+ std::string estimate = p->cache.get_if_exists(abspath, desired_size);
611+ if(!estimate.empty())
612+ return false;
613+
614+ if (p->cache.has_failure(abspath)) {
615+ return false;
616+ }
617+
618+ return true;
619+}
620
621=== modified file 'tests/basic.cpp'
622--- tests/basic.cpp 2014-09-25 09:43:53 +0000
623+++ tests/basic.cpp 2015-02-25 18:25:10 +0000
624@@ -168,6 +168,29 @@
625 ASSERT_FALSE(file_exists(thumbfile3));
626 }
627
628+static void is_cached_before_after_test(Thumbnailer &tn, string &imfile, ThumbnailSize size, bool always_needed) {
629+ bool is_cached;
630+ string thumbfile = tn.get_thumbnail(imfile, size);
631+ is_cached = tn.thumbnail_is_cached(imfile, size);
632+ ASSERT_EQ(is_cached, always_needed);
633+ unlink(thumbfile.c_str());
634+ is_cached = tn.thumbnail_is_cached(imfile, size);
635+ ASSERT_TRUE(is_cached);
636+}
637+
638+TEST(Thumbnailer, thumbnail_is_cached_size) {
639+ Thumbnailer tn;
640+ string srcimg(TESTIMAGE);
641+ string imfile("working_image.jpg");
642+ copy_file(srcimg, imfile);
643+
644+ is_cached_before_after_test(tn, imfile, TN_SIZE_SMALL, false);
645+ is_cached_before_after_test(tn, imfile, TN_SIZE_LARGE, false);
646+ is_cached_before_after_test(tn, imfile, TN_SIZE_XLARGE, false);
647+ /* when size is TN_SIZE_ORIGINAL generation will always happen */
648+ is_cached_before_after_test(tn, imfile, TN_SIZE_ORIGINAL, true);
649+}
650+
651 TEST(Thumbnailer, no_image_cache) {
652 Thumbnailer tn;
653 string srcimg(TESTIMAGE);
654
655=== added file 'tests/qml/testimage.jpg'
656Binary files tests/qml/testimage.jpg 1970-01-01 00:00:00 +0000 and tests/qml/testimage.jpg 2015-02-25 18:25:10 +0000 differ
657=== modified file 'tests/qml/tst_image_provider.qml'
658--- tests/qml/tst_image_provider.qml 2014-08-04 09:39:59 +0000
659+++ tests/qml/tst_image_provider.qml 2015-02-25 18:25:10 +0000
660@@ -30,13 +30,13 @@
661 function test_albumart() {
662 var ctx = loadImage(
663 "image://albumart/artist=Gotye&album=Making%20Mirrors");
664- comparePixel(ctx, 0, 0, 242, 228, 209, 255);
665+ comparePixel(ctx, 0, 0, 242, 229, 212, 255);
666 }
667
668 function test_artistart() {
669 var ctx = loadImage(
670 "image://artistart/artist=Gotye&album=Making%20Mirrors");
671- comparePixel(ctx, 0, 0, 242, 228, 209, 255);
672+ comparePixel(ctx, 0, 0, 249, 242, 236, 255);
673 }
674
675 function loadImage(uri) {
676
677=== added file 'tests/qml/tst_thumbnailer.qml'
678--- tests/qml/tst_thumbnailer.qml 1970-01-01 00:00:00 +0000
679+++ tests/qml/tst_thumbnailer.qml 2015-02-25 18:25:10 +0000
680@@ -0,0 +1,97 @@
681+import QtQuick 2.0
682+import QtTest 1.0
683+import Ubuntu.Thumbnailer 0.1
684+
685+Item {
686+ Image {
687+ id: image
688+ width: 200
689+ height: 200
690+ source: thumbnailer.thumbnail
691+
692+ SignalSpy {
693+ id: imageSpy
694+ target: image
695+ signalName: "statusChanged"
696+ }
697+ }
698+
699+ Thumbnailer {
700+ id: thumbnailer
701+ size {
702+ width: image.width
703+ height: image.height
704+ }
705+ }
706+
707+ SignalSpy {
708+ id: thumbnailerSpy
709+ target: thumbnailer
710+ signalName: "thumbnailChanged"
711+ }
712+
713+ Canvas {
714+ id: canvas
715+ width: image.width
716+ height: image.height
717+ renderStrategy: Canvas.Immediate
718+ renderTarget: Canvas.Image
719+ }
720+
721+ TestCase {
722+ name: "ThumbnailerTests"
723+ when: windowShown
724+
725+ function cleanup() {
726+ thumbnailer.source = "";
727+ thumbnailerSpy.clear();
728+ imageSpy.clear();
729+ }
730+
731+ function test_initialState() {
732+ compare(thumbnailerSpy.count, 0);
733+ compare(thumbnailer.size.width, 200);
734+ compare(thumbnailer.size.height, 200);
735+ compare(thumbnailer.thumbnail, "");
736+ }
737+
738+ function test_localjpeg() {
739+ thumbnailer.source = "testimage.jpg";
740+ thumbnailerSpy.wait();
741+ compare(thumbnailerSpy.count, 1);
742+ verify(thumbnailer.thumbnail != "");
743+ tryCompare(image, "status", Image.Ready)
744+
745+ var ctx = getContextForImage(image);
746+ comparePixel(ctx, 0, 0, 255, 1, 1, 255);
747+ comparePixel(ctx, 100, 100, 4, 2, 1, 255);
748+ }
749+
750+ function test_nonExistingFile() {
751+ thumbnailer.source = "idontexist.jpg";
752+ compare(thumbnailerSpy.count, 0);
753+ compare(thumbnailer.thumbnail, "");
754+ tryCompare(image, "status", Image.Null)
755+ }
756+
757+ function getContextForImage(image) {
758+ var ctx = canvas.getContext("2d");
759+ ctx.drawImage(image, 0, 0);
760+ return ctx;
761+ }
762+
763+ function comparePixel(ctx,x,y,r,g,b,a, d) {
764+ var c = ctx.getImageData(x,y,1,1).data;
765+ if (d === undefined)
766+ d = 0;
767+ r = Math.round(r);
768+ g = Math.round(g);
769+ b = Math.round(b);
770+ a = Math.round(a);
771+ var notSame = Math.abs(c[0]-r)>d || Math.abs(c[1]-g)>d || Math.abs(c[2]-b)>d || Math.abs(c[3]-a)>d;
772+ if (notSame)
773+ qtest_fail('Pixel compare fail:\nactual :[' + c[0]+','+c[1]+','+c[2]+','+c[3] + ']\nexpected:['+r+','+g+','+b+','+a+'] +/- '+d, 1);
774+
775+ }
776+ }
777+}

Subscribers

People subscribed via source and target branches

to all changes: