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