1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2021-02-18
7  * Description : Qt5 and Qt6 interface for exiftool.
8  *               Based on ZExifTool Qt interface published at 18 Feb 2021
9  *               https://github.com/philvl/ZExifTool
10  *
11  * Copyright (C) 2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
12  * Copyright (c) 2021 by Philippe Vianney Liaud <philvl dot dev at gmail dot com>
13  *
14  * This program is free software; you can redistribute it
15  * and/or modify it under the terms of the GNU General
16  * Public License as published by the Free Software Foundation;
17  * either version 2, or (at your option)
18  * any later version.
19  *
20  * This program is distributed in the hope that it will be useful,
21  * but WITHOUT ANY WARRANTY; without even the implied warranty of
22  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23  * GNU General Public License for more details.
24  *
25  * ============================================================ */
26 
27 #include "exiftoolprocess_p.h"
28 
29 namespace Digikam
30 {
31 
ExifToolProcess(QObject * const parent)32 ExifToolProcess::ExifToolProcess(QObject* const parent)
33     : QObject(parent),
34       d      (new Private(this))
35 {
36     d->process = new QProcess(this);
37     d->process->setProcessEnvironment(adjustedEnvironmentForAppImage());
38 
39     connect(d->process, &QProcess::started,
40             this, &ExifToolProcess::slotStarted);
41 
42 #if QT_VERSION >= 0x060000
43 
44     connect(d->process, &QProcess::finished,
45             this, &ExifToolProcess::slotFinished);
46 
47 #else
48 
49     connect(d->process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
50             this, &ExifToolProcess::slotFinished);
51 
52 #endif
53 
54     connect(d->process, &QProcess::stateChanged,
55             this, &ExifToolProcess::slotStateChanged);
56 
57     connect(d->process, &QProcess::errorOccurred,
58             this, &ExifToolProcess::slotErrorOccurred);
59 
60     connect(d->process, &QProcess::readyReadStandardOutput,
61             this, &ExifToolProcess::slotReadyReadStandardOutput);
62 
63     connect(d->process, &QProcess::readyReadStandardError,
64             this, &ExifToolProcess::slotReadyReadStandardError);
65 }
66 
~ExifToolProcess()67 ExifToolProcess::~ExifToolProcess()
68 {
69     terminate();
70 
71     delete d;
72 }
73 
setProgram(const QString & etExePath,const QString & perlExePath)74 void ExifToolProcess::setProgram(const QString& etExePath, const QString& perlExePath)
75 {
76     // Check if ExifTool is starting or running
77 
78     if (d->process->state() != QProcess::NotRunning)
79     {
80         qCWarning(DIGIKAM_METAENGINE_LOG) << "ExifToolProcess::setProgram(): ExifTool is already running";
81 
82         return;
83     }
84 
85     d->etExePath   = etExePath;
86     d->perlExePath = perlExePath;
87 
88     if      (d->etExePath.isEmpty())
89     {
90         d->etExePath = exifToolBin();
91     }
92     else if (QFileInfo(d->etExePath).isDir())
93     {
94         d->etExePath.append(QLatin1Char('/'));
95         d->etExePath.append(exifToolBin());
96     }
97 }
98 
program() const99 QString ExifToolProcess::program() const
100 {
101     return d->etExePath;
102 }
103 
start()104 bool ExifToolProcess::start()
105 {
106     // Check if ExifTool is starting or running
107 
108     if (d->process->state() != QProcess::NotRunning)
109     {
110         return true;
111     }
112 
113     if (!checkExifToolProgram())
114     {
115         return false;
116     }
117 
118     // Prepare command for ExifTool
119 
120     QString program = d->etExePath;
121     QStringList args;
122 
123     if (!d->perlExePath.isEmpty())
124     {
125         program = d->perlExePath;
126         args << d->etExePath;
127     }
128 
129     //-- Advanced options
130 
131     args << QLatin1String("-stay_open");
132     args << QLatin1String("true");
133 
134     //-- Other options
135 
136     args << QLatin1String("-@");
137     args << QLatin1String("-");
138 
139     //-- Define common arguments
140 
141     args << QLatin1String("-common_args");
142 
143     //-- Use UTF-8 for file names
144 
145     args << QLatin1String("-charset");
146     args << QLatin1String("filename=UTF8");
147 
148     args << QLatin1String("-charset");
149     args << QLatin1String("iptc=UTF8");
150 
151     // Clear queue before start
152 
153     d->cmdQueue.clear();
154     d->cmdRunning           = 0;
155     d->cmdAction            = NO_ACTION;
156 
157     // Clear errors
158 
159     d->processError         = QProcess::UnknownError;
160     d->errorString.clear();
161 
162     // Start ExifTool process
163 
164     d->writeChannelIsClosed = false;
165 
166     qCDebug(DIGIKAM_METAENGINE_LOG) << "ExifToolProcess::start(): create new ExifTool instance:" << program << args;
167 
168     d->process->start(program, args, QProcess::ReadWrite);
169 
170     return d->process->waitForStarted(1000);
171 }
172 
terminate()173 void ExifToolProcess::terminate()
174 {
175     if (d->process->state() == QProcess::Running)
176     {
177         // If process is in running state, close ExifTool normally
178 
179         qCDebug(DIGIKAM_METAENGINE_LOG) << "ExifToolProcess::terminate(): send ExifTool shutdown command...";
180 
181         d->cmdQueue.clear();
182         d->process->write(QByteArray("-stay_open\nfalse\n"));
183         d->process->closeWriteChannel();
184         d->writeChannelIsClosed = true;
185 
186         if (!d->process->waitForFinished(5000))
187         {
188             // Otherwise, close ExifTool using OS system call
189             // (WM_CLOSE [Windows] or SIGTERM [Unix])
190 
191             // Console applications on Windows that do not run an event loop,
192             // or whose event loop does not handle the WM_CLOSE message,
193             // can only be terminated by calling kill().
194 
195 #ifdef Q_OS_WIN
196 
197             kill();
198 
199 #else
200 
201             qCDebug(DIGIKAM_METAENGINE_LOG) << "ExifToolProcess::terminate(): closing ExifTool instance...";
202 
203             d->process->terminate();
204 
205 #endif
206 
207         }
208     }
209 }
210 
kill()211 void ExifToolProcess::kill()
212 {
213     qCDebug(DIGIKAM_METAENGINE_LOG) << "ExifToolProcess::kill(): shutdown ExifTool instance...";
214 
215     d->process->kill();
216 }
217 
isRunning() const218 bool ExifToolProcess::isRunning() const
219 {
220     return (d->process->state() == QProcess::Running);
221 }
222 
isBusy() const223 bool ExifToolProcess::isBusy() const
224 {
225     return (d->cmdRunning ? true : false);
226 }
227 
processId() const228 qint64 ExifToolProcess::processId() const
229 {
230     return d->process->processId();
231 }
232 
state() const233 QProcess::ProcessState ExifToolProcess::state() const
234 {
235     return d->process->state();
236 }
237 
error() const238 QProcess::ProcessError ExifToolProcess::error() const
239 {
240     return d->processError;
241 }
242 
errorString() const243 QString ExifToolProcess::errorString() const
244 {
245     return d->errorString;
246 }
247 
exitStatus() const248 QProcess::ExitStatus ExifToolProcess::exitStatus() const
249 {
250     return d->process->exitStatus();
251 }
252 
waitForStarted(int msecs) const253 bool ExifToolProcess::waitForStarted(int msecs) const
254 {
255     return d->process->waitForStarted(msecs);
256 }
257 
waitForFinished(int msecs) const258 bool ExifToolProcess::waitForFinished(int msecs) const
259 {
260     return d->process->waitForFinished(msecs);
261 }
262 
command(const QByteArrayList & args,Action ac)263 int ExifToolProcess::command(const QByteArrayList& args, Action ac)
264 {
265     if (
266         (d->process->state() != QProcess::Running) ||
267         d->writeChannelIsClosed                    ||
268         args.isEmpty()
269        )
270     {
271         qCWarning(DIGIKAM_METAENGINE_LOG) << "ExifToolProcess::command(): cannot process command with ExifTool" << args;
272 
273         return 0;
274     }
275 
276     // ThreadSafe incrementation of d->nextCmdId
277 
278     Private::s_cmdIdMutex.lock();
279     const int cmdId = Private::s_nextCmdId;
280 
281     if (Private::s_nextCmdId++ >= Private::CMD_ID_MAX)
282     {
283         Private::s_nextCmdId = Private::CMD_ID_MIN;
284     }
285 
286     Private::s_cmdIdMutex.unlock();
287 
288     // String representation of d->cmdId with leading zero -> constant size: 10 char
289 
290     const QByteArray cmdIdStr = QByteArray::number(cmdId).rightJustified(10, '0');
291 
292     // Build command string from args
293 
294     QByteArray cmdStr;
295 
296     for (const QByteArray& arg : args)
297     {
298         cmdStr.append(arg + '\n');
299     }
300 
301     //-- Advanced options
302 
303     cmdStr.append(QByteArray("-echo1\n{await") + cmdIdStr + QByteArray("}\n"));     // Echo text to stdout before processing is complete
304     cmdStr.append(QByteArray("-echo2\n{await") + cmdIdStr + QByteArray("}\n"));     // Echo text to stderr before processing is complete
305 
306     if (
307         cmdStr.contains(QByteArray("-q"))               ||
308         cmdStr.toLower().contains(QByteArray("-quiet")) ||
309         cmdStr.contains(QByteArray("-T"))               ||
310         cmdStr.toLower().contains(QByteArray("-table"))
311        )
312     {
313         cmdStr.append(QByteArray("-echo3\n{ready}\n"));                 // Echo text to stdout after processing is complete
314     }
315 
316     cmdStr.append(QByteArray("-echo4\n{ready}\n"));                     // Echo text to stderr after processing is complete
317     cmdStr.append(QByteArray("-execute\n"));                            // Execute command and echo {ready} to stdout after processing is complete
318 
319     // TODO: if -binary user, {ready} can not be present in the new line
320 
321     // Add command to queue
322 
323     Private::Command command;
324     command.id      = cmdId;
325     command.argsStr = cmdStr;
326     command.ac      = ac;
327     d->cmdQueue.append(command);
328 
329     // Exec cmd queue
330 
331     d->execNextCmd();
332 
333     return cmdId;
334 }
335 
slotStarted()336 void ExifToolProcess::slotStarted()
337 {
338     qCDebug(DIGIKAM_METAENGINE_LOG) << "ExifTool process started";
339 
340     emit signalStarted(d->cmdAction);
341 }
342 
slotFinished(int exitCode,QProcess::ExitStatus exitStatus)343 void ExifToolProcess::slotFinished(int exitCode, QProcess::ExitStatus exitStatus)
344 {
345     qCDebug(DIGIKAM_METAENGINE_LOG) << "ExifTool process finished" << exitCode << exitStatus;
346 
347     emit signalFinished(d->cmdAction, exitCode, exitStatus);
348 
349     d->cmdRunning = 0;
350     d->cmdAction  = NO_ACTION;
351 }
352 
slotStateChanged(QProcess::ProcessState newState)353 void ExifToolProcess::slotStateChanged(QProcess::ProcessState newState)
354 {
355     emit signalStateChanged(d->cmdAction, newState);
356 }
357 
slotErrorOccurred(QProcess::ProcessError error)358 void ExifToolProcess::slotErrorOccurred(QProcess::ProcessError error)
359 {
360     d->setProcessErrorAndEmit(error, d->process->errorString());
361 }
362 
slotReadyReadStandardOutput()363 void ExifToolProcess::slotReadyReadStandardOutput()
364 {
365     d->readOutput(QProcess::StandardOutput);
366 }
367 
slotReadyReadStandardError()368 void ExifToolProcess::slotReadyReadStandardError()
369 {
370     d->readOutput(QProcess::StandardError);
371 }
372 
exifToolBin() const373 QString ExifToolProcess::exifToolBin() const
374 {
375 
376 #ifdef Q_OS_WIN
377 
378     return QLatin1String("exiftool.exe");
379 
380 #else
381 
382     return QLatin1String("exiftool");
383 
384 #endif
385 
386 }
387 
checkExifToolProgram()388 bool ExifToolProcess::checkExifToolProgram()
389 {
390     // Check if Exiftool program exists and have execution permissions
391 
392     qCDebug(DIGIKAM_METAENGINE_LOG) << "Path to ExifTool:" << d->etExePath;
393 
394     if (
395         (d->etExePath != exifToolBin())                    &&
396         (!QFile::exists(d->etExePath)                      ||
397         !(QFile::permissions(d->etExePath) & QFile::ExeUser))
398        )
399     {
400         d->setProcessErrorAndEmit(QProcess::FailedToStart,
401                                   QString::fromLatin1("ExifTool does not exists or exec permission is missing"));
402         return false;
403     }
404 
405     // If perl path is defined, check if Perl program exists and have execution permissions
406 
407     if (
408         !d->perlExePath.isEmpty()                            &&
409         (!QFile::exists(d->perlExePath)                      ||
410         !(QFile::permissions(d->perlExePath) & QFile::ExeUser))
411        )
412     {
413         d->setProcessErrorAndEmit(QProcess::FailedToStart,
414                                   QString::fromLatin1("Perl does not exists or exec permission is missing"));
415         return false;
416     }
417 
418     return true;
419 }
420 
421 } // namespace Digikam
422