|
1 | 1 | #pragma once |
2 | 2 |
|
| 3 | +// When instance of this class is created it starts ExifTool.exe process in the background mode. |
| 4 | +// In this mode ExifTool accepts commands via stdin and prints results to stdout or stderr in case of an error. |
3 | 5 | class ExifToolWrapper : boost::noncopyable { |
4 | 6 | public: |
5 | 7 | static ExifToolWrapper* GetInstance() { |
@@ -28,64 +30,55 @@ class ExifToolWrapper : boost::noncopyable { |
28 | 30 | // }, |
29 | 31 | // ... |
30 | 32 | // }] |
31 | | - std::wstring GetTagInfosJson(std::wstring_view filePath, const std::vector<std::string>& tagNames) { |
32 | | - if (!std::filesystem::exists(filePath)) |
33 | | - { |
34 | | - THROW_WEXCEPTION(L"File not found '{}'", filePath); |
35 | | - } |
36 | | - |
37 | | - if (std::filesystem::is_directory(filePath)) |
38 | | - { |
39 | | - THROW_WEXCEPTION(L"File is a directory '{}'", filePath); |
40 | | - } |
| 33 | + std::wstring GetTagInfosJson(const std::wstring_view filePath, const std::vector<std::string>& tagNames) { |
| 34 | + ExifToolCommandArgsGenerator argsGenerator(filePath); |
| 35 | + argsGenerator.AddCommandLineArgs({ |
| 36 | + "-decimal", |
| 37 | + "-json", |
| 38 | + "-long", |
| 39 | + "-unknown", |
| 40 | + "-groupNames:0:1" |
| 41 | + }); |
| 42 | + argsGenerator.AddTagNames(tagNames); |
| 43 | + return Execute(argsGenerator.GenerateExifToolInput()); |
| 44 | + } |
41 | 45 |
|
42 | | - ValidateExifToolTagNames(tagNames); |
| 46 | + std::wstring Run(const std::wstring_view filePath, const std::vector<std::string>& commandLineArgs) { |
| 47 | + ExifToolCommandArgsGenerator argsGenerator(filePath); |
| 48 | + argsGenerator.AddCommandLineArgs(commandLineArgs); |
| 49 | + return Execute(argsGenerator.GenerateExifToolInput()); |
| 50 | + } |
43 | 51 |
|
| 52 | +private: |
| 53 | + std::wstring Execute(const std::string_view exifToolCommands) { |
44 | 54 | ioCtx.restart(); |
45 | 55 |
|
46 | | - boost::asio::write(stdInPipe, boost::asio::buffer( |
47 | | - ToUtf8(filePath) + "\n-decimal\n-json\n-long\n-groupNames:0:1\n-echo4\n{readyErr}\n" + ToCommandLineArgs(tagNames) + "-execute\n")); |
| 56 | + boost::asio::write(stdInPipe, boost::asio::buffer(exifToolCommands)); |
48 | 57 |
|
| 58 | + // After the write is completed, we need to read from stdOut and stdErr in parallel, to avoid ExifTool from deadlocking in case one of the pipes gets full. |
| 59 | + // When ExifTool is ready, it prints "{ready}\r\n" to stdOut and "{readyErr}\r\n" to stdErr (even if there are no errors). |
49 | 60 | static const std::string readyStr = "{ready}\r\n"; |
50 | 61 | static const std::string readyErrStr = "{readyErr}\r\n"; |
51 | | - boost::asio::streambuf outBuf, errBuf; |
52 | | - boost::asio::async_read_until(stdOutPipe, outBuf, readyStr, [&](auto, std::size_t) { }); |
53 | | - boost::asio::async_read_until(stdErrPipe, errBuf, readyErrStr, [&](auto, std::size_t) { }); |
54 | | - |
| 62 | + boost::asio::streambuf stdOutBuf; |
| 63 | + boost::asio::streambuf stdErrBuf; |
| 64 | + boost::asio::async_read_until(stdOutPipe, stdOutBuf, readyStr, [&](auto, std::size_t) {}); |
| 65 | + boost::asio::async_read_until(stdErrPipe, stdErrBuf, readyErrStr, [&](auto, std::size_t) {}); |
55 | 66 | ioCtx.run(); |
56 | 67 |
|
57 | | - const auto& stdErr = ToWide({ GetData(errBuf), errBuf.size() - readyErrStr.size() }); |
58 | | - if (!stdErr.empty()) { |
59 | | - THROW_WEXCEPTION(L"ExifTool.exe failed to get information from the file: '{}'. Error message: {}", filePath, stdErr); |
60 | | - } |
61 | | - |
62 | | - return ToWide({ GetData(outBuf), outBuf.size() - readyStr.size() }); |
63 | | - } |
64 | | - |
65 | | -private: |
66 | | - ExifToolWrapper() = default; |
67 | | - |
68 | | - std::string ToCommandLineArgs(const std::vector<std::string>& tagNames) { |
69 | | - std::string result; |
70 | | - for (const auto& tag : tagNames) { |
71 | | - result += std::format("-{}\n", tag); |
72 | | - } |
73 | | - return result; |
74 | | - } |
75 | | - |
76 | | - void ValidateExifToolTagNames(const std::vector<std::string>& tagNames) { |
77 | | - // ExifTags must be in the format "Group0:TagName" |
78 | | - static const std::regex exifTagFormat(R"(^[A-Za-z0-9_-]+:[A-Za-z0-9_-]+$)"); |
79 | | - |
80 | | - for (const auto& tagName : tagNames) { |
81 | | - if(!std::regex_match(tagName, exifTagFormat)) { |
82 | | - THROW_WEXCEPTION(L"ExifTool tag name '{}' is incorrect. The tag name should be in format 'Group0:TagName', for example 'AIFF:FormatVersionTime'", ToWide(tagName)); |
83 | | - } |
| 68 | + // If there is anything written to stdErr, it means that ExifTool failed to process the file. |
| 69 | + const auto& lengthOfErrorMessage = stdErrBuf.size() - readyErrStr.size(); // we ignore the "{readyErr}\r\n" part |
| 70 | + if(lengthOfErrorMessage != 0) { |
| 71 | + const auto& errorMessage = ToUtf16({ GetDataPointer(stdErrBuf), lengthOfErrorMessage }); |
| 72 | + THROW_WEXCEPTION(L"ExifTool.exe failed to get information using the following parameters:\n{}\n\nError message:\n{}", ToUtf16(exifToolCommands), errorMessage); |
84 | 73 | } |
| 74 | + |
| 75 | + const auto& lengthOfStdOutput = stdOutBuf.size() - readyStr.size(); // we ignore the "{ready}\r\n" part |
| 76 | + return ToUtf16({ GetDataPointer(stdOutBuf), lengthOfStdOutput }); |
85 | 77 | } |
86 | 78 |
|
87 | 79 | boost::asio::io_context ioCtx; |
88 | | - boost::asio::readable_pipe stdOutPipe{ ioCtx }, stdErrPipe{ ioCtx }; |
| 80 | + boost::asio::readable_pipe stdOutPipe{ ioCtx }; |
| 81 | + boost::asio::readable_pipe stdErrPipe{ ioCtx }; |
89 | 82 | boost::asio::writable_pipe stdInPipe{ ioCtx }; |
90 | 83 | boost::process::v2::process proc{ |
91 | 84 | ioCtx, |
|
0 commit comments