MythTV  master
thumbgenerator.cpp
Go to the documentation of this file.
1 /* ============================================================
2  * File : thumbgenerator.cpp
3  * Author: Renchi Raju <renchi@pooh.tam.uiuc.edu>
4  * Date : 2004-01-02
5  * Description :
6  *
7  * Copyright 2004 by Renchi Raju
8 
9  * This program is free software; you can redistribute it
10  * and/or modify it under the terms of the GNU General
11  * Public License as published bythe Free Software Foundation;
12  * either version 2, or (at your option)
13  * any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18  * GNU General Public License for more details.
19  *
20  * ============================================================ */
21 
22 // qt
23 #include <QApplication>
24 #include <QImage>
25 #include <QFileInfo>
26 #include <QDir>
27 #include <QEvent>
28 #include <QImageReader>
29 #include <QSet>
30 
31 // myth
32 #include <mythuihelper.h>
33 #include <mythcontext.h>
34 #include <mythdirs.h>
35 #include <mthread.h>
36 #include <mythdate.h>
37 
38 // mythgallery
39 #include "config.h"
40 #include "thumbgenerator.h"
41 #include "galleryutil.h"
42 #include "mythsystemlegacy.h"
43 #include "exitcodes.h"
44 #include "mythlogging.h"
45 
46 #ifdef DCRAW_SUPPORT
47 #include "../dcrawplugin/dcrawformats.h"
48 #include "../dcrawplugin/dcrawhandler.h"
49 #endif // DCRAW_SUPPORT
50 
51 #ifdef EXIF_SUPPORT
52 #include <libexif/exif-data.h>
53 #include <libexif/exif-entry.h>
54 #endif
55 
56 QEvent::Type ThumbGenEvent::kEventType =
57  (QEvent::Type) QEvent::registerEventType();
58 
60 {
61  cancel();
62  wait();
63 }
64 
65 void ThumbGenerator::setSize(int w, int h)
66 {
67  m_width = w;
68  m_height = h;
69 }
70 
71 void ThumbGenerator::setDirectory(const QString& directory, bool isGallery)
72 {
73  m_mutex.lock();
74  m_directory = directory;
75  m_isGallery = isGallery;
76  m_mutex.unlock();
77 }
78 
79 void ThumbGenerator::addFile(const QString& filePath)
80 {
81  // Add a file to the list of thumbs.
82  // Must remember to call start after adding all the files!
83  m_mutex.lock();
84  m_fileList.append(filePath);
85  m_mutex.unlock();
86 }
87 
89 {
90  m_mutex.lock();
91  m_fileList.clear();
92  m_cancel = true;
93  m_mutex.unlock();
94 }
95 
97 {
98  RunProlog();
99 
100  m_cancel = false;
101  while (moreWork() && !m_cancel)
102  {
103  QString file, dir;
104  bool isGallery;
105 
106  m_mutex.lock();
107  dir = m_directory;
108  isGallery = m_isGallery;
109  file = m_fileList.first();
110  if (!m_fileList.isEmpty())
111  m_fileList.pop_front();
112  m_mutex.unlock();
113  if (file.isEmpty())
114  continue;
115 
116  QString filePath = dir + QString("/") + file;
117  QFileInfo fileInfo(filePath);
118  if (!fileInfo.exists())
119  continue;
120 
121  if (isGallery)
122  {
123  if (fileInfo.isDir())
124  isGallery = checkGalleryDir(fileInfo);
125  else
126  isGallery = checkGalleryFile(fileInfo);
127  }
128 
129  if (!isGallery)
130  {
131  QString cachePath = QString("%1%2.jpg").arg(getThumbcacheDir(dir))
132  .arg(file);
133  QFileInfo cacheInfo(cachePath);
134 
135  if (cacheInfo.exists() &&
136  cacheInfo.lastModified() >= fileInfo.lastModified())
137  {
138  continue;
139  }
140 
141  // cached thumbnail not there or out of date
142  QImage image;
143 
144  // Remove the old one if it exists
145  if (cacheInfo.exists())
146  QFile::remove(cachePath);
147 
148  if (fileInfo.isDir())
149  loadDir(image, fileInfo);
150  else
151  loadFile(image, fileInfo);
152 
153  if (image.isNull())
154  continue; // give up;
155 
156  // if the file is a movie save the image to use as a screenshot
157  if (GalleryUtil::IsMovie(fileInfo.filePath()))
158  {
159  QString screenshotPath = QString("%1%2-screenshot.jpg")
160  .arg(getThumbcacheDir(dir))
161  .arg(file);
162  image.save(screenshotPath, "JPEG", 95);
163  }
164 
165  image = image.scaled(m_width,m_height,
166  Qt::KeepAspectRatio, Qt::SmoothTransformation);
167  image.save(cachePath, "JPEG", 95);
168 
169  ThumbData *td = new ThumbData;
170  td->directory = dir;
171  td->fileName = file;
172  td->thumb = image;
173 
174  // inform parent we have thumbnail ready for it
175  QApplication::postEvent(m_parent, new ThumbGenEvent(td));
176  }
177  }
178 
179  RunEpilog();
180 }
181 
183 {
184  bool result;
185  m_mutex.lock();
186  result = !m_fileList.isEmpty();
187  m_mutex.unlock();
188  return result;
189 }
190 
191 bool ThumbGenerator::checkGalleryDir(const QFileInfo& fi)
192 {
193  // try to find a highlight
194  QDir subdir(fi.absoluteFilePath(), "*.highlight.*", QDir::Name,
195  QDir::Files);
196 
197 
198  if (subdir.count() > 0)
199  {
200  // check if the image format is understood
201  QString path(subdir.entryInfoList().begin()->absoluteFilePath());
202  QImageReader testread(path);
203  return testread.canRead();
204  }
205  return false;
206 }
207 
208 bool ThumbGenerator::checkGalleryFile(const QFileInfo& fi)
209 {
210  // if the image name is xyz.jpg, then look
211  // for a file named xyz.thumb.jpg.
212  QString fn = fi.fileName();
213  int firstDot = fn.indexOf('.');
214  if (firstDot > 0)
215  {
216  fn.insert(firstDot, ".thumb");
217  QFileInfo galThumb(fi.absolutePath() + "/" + fn);
218  if (galThumb.exists())
219  {
220  QImageReader testread(galThumb.absoluteFilePath());
221  return testread.canRead();
222  }
223  return false;
224  }
225  return false;
226 }
227 
228 void ThumbGenerator::loadDir(QImage& image, const QFileInfo& fi)
229 {
230  QDir dir(fi.absoluteFilePath());
231  dir.setFilter(QDir::Files);
232 
233  QList<QByteArray> const fmts = QImageReader::supportedImageFormats();
234 
235  QFileInfoList list = dir.entryInfoList();
236 
237  for (QFileInfoList::const_iterator it = list.begin();
238  it != list.end() && !m_cancel; ++it)
239  {
240  const QFileInfo *f = &(*it);
241  if (fmts.contains(f->completeSuffix().toLower().toLatin1()))
242  {
243  loadFile(image, *f);
244  return;
245  }
246  }
247 
248  // If we are supposed to cancel, don't recurse into subdirs, just quit
249  if (m_cancel)
250  return;
251 
252  // if we didn't find the image yet
253  // go into subdirs and keep looking
254  dir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
255  QFileInfoList dirlist = dir.entryInfoList();
256  if (dirlist.isEmpty())
257  return;
258 
259  for (QFileInfoList::const_iterator it = dirlist.begin();
260  it != dirlist.end() && image.isNull() && !m_cancel; ++it)
261  {
262  const QFileInfo *f = &(*it);
263  loadDir(image, *f);
264  }
265 }
266 
267 void ThumbGenerator::loadFile(QImage& image, const QFileInfo& fi)
268 {
269  if (GalleryUtil::IsMovie(fi.filePath()))
270  {
271  bool thumbnailCreated = false;
272  QDir tmpDir("/tmp/mythgallery");
273  if (!tmpDir.exists())
274  {
275  if (!tmpDir.mkdir(tmpDir.absolutePath()))
276  {
277  LOG(VB_GENERAL, LOG_ERR,
278  "Unable to create temp dir for movie thumbnail creation: " +
279  tmpDir.absolutePath());
280  }
281  }
282 
283  if (tmpDir.exists())
284  {
285  static int sequence = 0;
286  QString thumbFile = QString("%1.png")
287  .arg(++sequence,8,10,QChar('0'));
288 
289  QString cmd = "mythpreviewgen";
290  QStringList args;
291  args << logPropagateArgs.split(" ", QString::SkipEmptyParts);
292  args << "--infile" << '"' + fi.absoluteFilePath() + '"';
293  args << "--outfile" << '"' + tmpDir.filePath(thumbFile) + '"';
294 
296  ms.SetDirectory(tmpDir.absolutePath());
297  ms.Run();
298  if (ms.Wait() == GENERIC_EXIT_OK)
299  {
300  QFileInfo thumb(tmpDir.filePath(thumbFile));
301  if (thumb.exists())
302  {
303  QImage img(thumb.absoluteFilePath());
304  image = img;
305  thumbnailCreated = true;
306  }
307  }
308  }
309 
310  if (!thumbnailCreated)
311  {
312  QString movie("gallery-moviethumb.png");
313  if (GetMythUI()->FindThemeFile(movie))
314  image.load(movie);
315  }
316  }
317  else
318  {
319  long rotateAngle = GalleryUtil::GetNaturalRotation(fi.absoluteFilePath());
320 #ifdef EXIF_SUPPORT
321  // Try to get thumbnail from exif data
322  ExifData *ed = exif_data_new_from_file(fi.absoluteFilePath()
323  .toLocal8Bit().constData());
324  if (ed && ed->data)
325  {
326  image.loadFromData(ed->data, ed->size);
327  // Strictly image orientation doesn't necessarily apply to thumbnail
328  // However it commonly does.
329  if (rotateAngle != 0)
330  {
331  QMatrix matrix;
332  matrix.rotate(rotateAngle);
333  image = image.transformed(matrix);
334  }
335  }
336 
337  if (ed)
338  exif_data_free(ed);
339 
340  if (image.width() > m_width && image.height() > m_height)
341  return;
342 #endif
343 
344 #ifdef DCRAW_SUPPORT
345  QString extension = fi.suffix();
346  QSet<QString> dcrawFormats = DcrawFormats::getFormats();
347 
348  if (dcrawFormats.contains(extension) &&
349  (rotateAngle = DcrawHandler::loadThumbnail(&image,
350  fi.absoluteFilePath())) != -1 &&
351  image.width() > m_width && image.height() > m_height)
352  {
353  if (rotateAngle != 0)
354  {
355  QMatrix matrix;
356  matrix.rotate(rotateAngle);
357  image = image.transformed(matrix);
358  }
359 
360  return;
361  }
362 #endif
363 
364  image.load(fi.absoluteFilePath());
365  if (rotateAngle != 0)
366  {
367  QMatrix matrix;
368  matrix.rotate(rotateAngle);
369  image = image.transformed(matrix, Qt::SmoothTransformation);
370  }
371  }
372 }
373 
374 // static function
375 QString ThumbGenerator::getThumbcacheDir(const QString& inDir)
376 {
377  QString galleryDir = gCoreContext->GetSetting("GalleryDir");
378 
379  // For directory "/my/images/january", this function either returns
380  // "/my/images/january/.thumbcache" or
381  // "~/.mythtv/mythgallery/january/.thumbcache"
382  QString aPath = inDir + QString("/.thumbcache/");
383  QDir dir(aPath);
384  if (gCoreContext->GetBoolSetting("GalleryThumbnailLocation") &&
385  !dir.exists() && inDir.startsWith(galleryDir))
386  {
387  dir.mkpath(aPath);
388  }
389 
390  if (!gCoreContext->GetBoolSetting("GalleryThumbnailLocation") ||
391  !dir.exists() || !inDir.startsWith(galleryDir))
392  {
393  // Arrive here if storing thumbs in home dir,
394  // OR failed to create thumb dir in gallery pics location
395  int prefixLen = galleryDir.length();
396  QString location = "";
397  if (prefixLen < inDir.length())
398  location = QString("%1/")
399  .arg(inDir.right(inDir.length() - prefixLen));
400  aPath = QString("%1/MythGallery/%2").arg(GetConfDir())
401  .arg(location);
402  dir.setPath(aPath);
403  dir.mkpath(aPath);
404  }
405 
406  return aPath;
407 }
408 
409 /*
410  * vim:ts=4:sw=4:ai:et:si:sts=4
411  */
void RunEpilog(void)
Cleans up a thread's resources, call this if you reimplement run().
Definition: mthread.cpp:215
QStringList m_fileList
void Run(time_t timeout=0)
Runs a command inside the /bin/sh shell. Returns immediately.
static long GetNaturalRotation(const unsigned char *buffer, int size)
QString fileName
static const AudioFormat fmts[]
QImage thumb
#define GENERIC_EXIT_OK
Exited with no error.
Definition: exitcodes.h:10
bool checkGalleryDir(const QFileInfo &fi)
bool wait(unsigned long time=ULONG_MAX)
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:311
void setDirectory(const QString &directory, bool isGallery=false)
QString logPropagateArgs
Definition: logging.cpp:89
QObject * m_parent
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
int matrix[4][2]
QString GetConfDir(void)
Definition: mythdirs.cpp:224
static bool IsMovie(const QString &filePath)
run process through shell
Definition: mythsystem.h:41
QString GetSetting(const QString &key, const QString &defaultval="")
void SetDirectory(const QString &)
static QString getThumbcacheDir(const QString &inDir)
static QSet< QString > getFormats()
void run() override
Runs the Qt event loop unless we have a QRunnable, in which case we run the runnable run instead.
bool checkGalleryFile(const QFileInfo &fi)
void loadDir(QImage &image, const QFileInfo &fi)
MythUIHelper * GetMythUI()
void loadFile(QImage &image, const QFileInfo &fi)
uint Wait(time_t timeout=0)
static Type kEventType
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
bool GetBoolSetting(const QString &key, bool defaultval=false)
void RunProlog(void)
Sets up a thread, call this if you reimplement run().
Definition: mthread.cpp:202
QString m_directory
void addFile(const QString &filePath)
QString directory
void setSize(int w, int h)
static int loadThumbnail(QImage *image, const QString &fileName)