Skip to content

Commit 8efb825

Browse files
committed
ExifTool:
* Use `-unknown` command line parameter when executing `GetInfoAsJson` to add extra tags to the JSON output. * Add `Run` method to allow specifying custom command line arguments.
1 parent 574ca17 commit 8efb825

15 files changed

Lines changed: 131 additions & 64 deletions

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,9 +223,14 @@ WScript.Echo("Encoding: " + res.Encoding) // "utf-8"
223223
* The encoding doesn't show if a file has BOM or not.
224224

225225
## ExifTool
226-
This class uses [ExifTool](https://exiftool.org/) to read meta information from files. The ExifTool executable is included in the installation package, so you don't need to install it separately.<br>
226+
This class uses [ExifTool](https://exiftool.org/) to read metadata from files. The ExifTool executable is included in the installation package, so you don't need to install it separately.<br>
227+
228+
### How it works
229+
This class starts the ExifTool process only once in the background using `-stay_open true -@ -` command line arguments and communicates with it using stdOut and stdIn.<br>
230+
It is much faster than running ExifTool for each file separately.<br>
227231

228232
### Examples
233+
#### Getting all tags
229234
```javascript
230235
// Create the COM object. The first time the object is created, it starts the ExifTool process in the background.
231236
var exifTool = new ActiveXObject("DOpusScriptingExtensions.ExifTool")
@@ -280,13 +285,27 @@ for (var tagName in tags) {
280285
```
281286

282287
#### Getting only specific tags
283-
`GetInfoAsJson` method accepts the array of tag names in format `Group0:TagName`. The output will only contain these specific tags. The tag names are passed to the ExifTool executable as `-TAG` command line arguments.
288+
`GetInfoAsJson` method accepts the array of tag names in the format `Group0:TagName`. The output will only contain these specific tags. The tag names are passed to the ExifTool executable as `-TAG` command line arguments.
284289
```javascript
285290
var jsonString = exifTool.GetInfoAsJson(
286291
"C:/Windows/SystemResources/Windows.UI.SettingsAppThreshold/SystemSettings/Assets/HDRSample.mkv",
287292
["Matroska:TrackType", "Matroska:DocTypeVersion"])
288293
```
289294

295+
#### Advanced usage: run ExifTool with custom arguments
296+
This example shows how you can run ExifTool with custom arguments.
297+
You can also use it to set and modify tags.
298+
```javascript
299+
// The command line arguments array should contain one argument per element,
300+
// not one option per element - some options require additional arguments, and all arguments must be provided as separate elements
301+
var exifToolStdOut = exifTool.Run("C:/someFile.mkv", ["-xmlFormat", "-long", "-unknown"])
302+
WScript.Echo(exifToolStdOut) // prints tags as XML
303+
304+
// don't use quotes when setting tag values that contain spaces, otherwise the quotes will be added to the tag value
305+
exifToolStdOut = exifTool.Run("C:/someFile.png", ["-PNG:Copyright=Some copyright", "-PNG:Comment=Some comment"])
306+
WScript.Echo(exifToolStdOut) // contains: "1 image files updated"
307+
```
308+
290309
## UCharDet
291310
Detects the encoding of a text file using [UCharDet](https://www.freedesktop.org/wiki/Software/uchardet/).
292311
The list of supported encodings is described in the paragraph `Supported Languages/Encodings`.

src/DOpusScriptingExtensions/DOpusScriptingExtensions.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "FileMimeTypeDetector/FileMimeTypeDetector.h"
1313
#include "StringFormatter/StringFormatter.h"
1414
#include "MediaInfoRetriever/MediaInfoRetriever.h"
15+
#include "ExifTool/ExifToolCommandArgsGenerator.h"
1516
#include "ExifTool/ExifToolWrapper.h"
1617
#include "ExifTool/ExifTool.h"
1718
#include "UCharDet/UCharDetWrapper.h"

src/DOpusScriptingExtensions/DOpusScriptingExtensions.idl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ interface IMediaInfoRetriever : IDispatch
5555
interface IExifTool : IDispatch
5656
{
5757
[id(1)] HRESULT GetInfoAsJson([in] BSTR fileFullName, [in, defaultvalue(0)] IDispatch* tagNamesJsArray, [out, retval] BSTR* infoAsJson);
58+
[id(2)] HRESULT Run([in] BSTR fileFullName, [in, defaultvalue(0)] IDispatch* commandLineArgs, [out, retval] BSTR* result);
5859
};
5960

6061
[object, uuid(EA478603-BB0E-4024-9308-FC147D0443F4), dual, nonextensible, pointer_default(unique)]

src/DOpusScriptingExtensions/DOpusScriptingExtensions.vcxproj

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
<ItemGroup>
142142
<ClInclude Include="DOpusScriptingExtensions_i.h" />
143143
<ClInclude Include="ExifTool\ExifTool.h" />
144+
<ClInclude Include="ExifTool\ExifToolCommandArgsGenerator.h" />
144145
<ClInclude Include="ExifTool\ExifToolWrapper.h" />
145146
<ClInclude Include="FileMimeTypeDetector\FileMimeTypeDetector.h" />
146147
<ClInclude Include="FileMimeTypeDetector\FileMimeTypeDetectorResult.h" />
@@ -193,12 +194,10 @@
193194
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
194195
<ImportGroup Label="ExtensionTargets">
195196
</ImportGroup>
196-
197197
<!-- Copy magic.mgc file that is needed for LibMagic -->
198198
<Target Name="CopyMagicMgcFileToOutputFolder" AfterTargets="Build" Condition="!Exists('$(OutputPath)/magic.mgc')">
199199
<Copy SourceFiles="$(ProjectRoot)/vcpkg_installed/x64-windows-static/vcpkg/pkgs/libmagic_x64-windows/share/libmagic/misc/magic.mgc" DestinationFolder="$(OutputPath)" />
200200
</Target>
201-
202201
<!-- Download ExifTool binaries -->
203202
<Target Name="DownloadExifTool" AfterTargets="Build" Condition="!Exists('$(BuildFolder)exiftool.zip')">
204203
<DownloadFile SourceUrl="https://exiftool.org/exiftool-13.30_64.zip" DestinationFolder="$(BuildFolder)" DestinationFileName="exiftool.zip" />
@@ -208,7 +207,6 @@
208207
<Exec Command="robocopy &quot;$(BuildFolder)exiftool-13.30_64&quot; &quot;$(OutputPath)exiftool&quot; /e &gt; nul" ContinueOnError="True" />
209208
<Exec Command="ren &quot;$(OutputPath)exiftool\exiftool(-k).exe&quot; &quot;exiftool.exe&quot;" />
210209
</Target>
211-
212210
<!-- Download language files for MediaInfoLib -->
213211
<Target Name="DownloadMediaInfo" AfterTargets="Build" Condition="!Exists('$(BuildFolder)MediaInfo.zip')">
214212
<DownloadFile SourceUrl="https://github.com/MediaArea/MediaInfo/archive/refs/tags/v25.04.zip" DestinationFolder="$(BuildFolder)" DestinationFileName="MediaInfo.zip" />
@@ -217,4 +215,4 @@
217215
<Target Name="CopyMediaInfoLanguagesToOutputFolder" AfterTargets="DownloadMediaInfo" Condition="!Exists('$(OutputPath)MediaInfoLanguages')">
218216
<Exec Command="robocopy &quot;$(BuildFolder)MediaInfo-25.04/Source/Resource/Plugin/Language&quot; &quot;$(OutputPath)MediaInfoLanguages&quot; /e &gt; nul" ContinueOnError="True" />
219217
</Target>
220-
</Project>
218+
</Project>

src/DOpusScriptingExtensions/DOpusScriptingExtensions.vcxproj.filters

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@
7979
<ClInclude Include="UCharDet\UCharDetWrapper.h">
8080
<Filter>UCharDet</Filter>
8181
</ClInclude>
82+
<ClInclude Include="ExifTool\ExifToolCommandArgsGenerator.h">
83+
<Filter>ExifTool</Filter>
84+
</ClInclude>
8285
</ItemGroup>
8386
<ItemGroup>
8487
<ClCompile Include="DOpusScriptingExtensions.cpp" />

src/DOpusScriptingExtensions/ExifTool/ExifTool.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ class ATL_NO_VTABLE CExifTool :
2424
return S_OK;
2525
} CATCH_ALL_EXCEPTIONS()
2626

27+
STDMETHOD(Run)(BSTR fileFullName, IDispatch* commandLineArgs, BSTR* result) override try {
28+
*result = Copy(
29+
exifToolWrapper->Run(fileFullName, ToUtf8StringVector(JsStringArrayToVector(commandLineArgs))));
30+
return S_OK;
31+
} CATCH_ALL_EXCEPTIONS()
32+
2733
private:
2834
ExifToolWrapper* exifToolWrapper;
2935
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#pragma once
2+
3+
// Generates the command line arguments string that can be written to the ExifTool stdin pipe.
4+
// ExifTool requires so that each argument should be separated by a newline character.
5+
class ExifToolCommandArgsGenerator : boost::noncopyable {
6+
public:
7+
ExifToolCommandArgsGenerator(const std::wstring_view filePath) {
8+
if (!std::filesystem::exists(filePath)) {
9+
THROW_WEXCEPTION(L"File not found '{}'", filePath);
10+
}
11+
12+
if (std::filesystem::is_directory(filePath)) {
13+
THROW_WEXCEPTION(L"File is a directory '{}'", filePath);
14+
}
15+
16+
commandLineArgs = ToUtf8(filePath) + "\n";
17+
}
18+
19+
void AddTagNames(const std::vector<std::string>& tagNames) {
20+
for (const auto& tagName : tagNames) {
21+
// ExifTags must be in the format "Group0:TagName"
22+
static const std::regex exifTagFormat(R"(^[A-Za-z0-9_-]+:[A-Za-z0-9_-]+$)");
23+
24+
if (!std::regex_match(tagName, exifTagFormat)) {
25+
THROW_WEXCEPTION(L"ExifTool tag name '{}' is incorrect. The tag name should be in format 'Group0:TagName', for example 'AIFF:FormatVersionTime'", ToUtf16(tagName));
26+
}
27+
28+
commandLineArgs += std::format("-{}\n", tagName);
29+
}
30+
}
31+
32+
void AddCommandLineArgs(const std::vector<std::string>& args) {
33+
for(const auto& arg : args) {
34+
commandLineArgs += arg + "\n";
35+
}
36+
}
37+
38+
// Returns a string like:
39+
// "C:\\file.jpg\n-decimal\n-json\n-long\n-unknown\n-groupNames:0:1\n-tag1\n-tag2\n-echo4\n{readyErr}\n-execute\n"
40+
std::string GenerateExifToolInput() const {
41+
return commandLineArgs + "-echo4\n{readyErr}\n-execute\n";
42+
}
43+
44+
private:
45+
std::string commandLineArgs;
46+
};

src/DOpusScriptingExtensions/ExifTool/ExifToolWrapper.h

Lines changed: 38 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#pragma once
22

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.
35
class ExifToolWrapper : boost::noncopyable {
46
public:
57
static ExifToolWrapper* GetInstance() {
@@ -28,64 +30,55 @@ class ExifToolWrapper : boost::noncopyable {
2830
// },
2931
// ...
3032
// }]
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+
}
4145

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+
}
4351

52+
private:
53+
std::wstring Execute(const std::string_view exifToolCommands) {
4454
ioCtx.restart();
4555

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));
4857

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).
4960
static const std::string readyStr = "{ready}\r\n";
5061
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) {});
5566
ioCtx.run();
5667

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);
8473
}
74+
75+
const auto& lengthOfStdOutput = stdOutBuf.size() - readyStr.size(); // we ignore the "{ready}\r\n" part
76+
return ToUtf16({ GetDataPointer(stdOutBuf), lengthOfStdOutput });
8577
}
8678

8779
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 };
8982
boost::asio::writable_pipe stdInPipe{ ioCtx };
9083
boost::process::v2::process proc{
9184
ioCtx,

src/DOpusScriptingExtensions/FileMimeTypeDetector/LibMagicWrapper.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class LibMagicWrapper : boost::noncopyable {
1212
if (!mimeCStr) {
1313
THROW_WEXCEPTION(L"Failed to detect type of a file '{}'. Error message: {}", fileFullName, GetError());
1414
}
15-
return ToWide(mimeCStr);
15+
return ToUtf16(mimeCStr);
1616
}
1717

1818
~LibMagicWrapper() {
@@ -40,7 +40,7 @@ class LibMagicWrapper : boost::noncopyable {
4040
if (err == nullptr) {
4141
return L"";
4242
}
43-
return ToWide(magic_error(magicCookie));
43+
return ToUtf16(magic_error(magicCookie));
4444
}
4545

4646
magic_t magicCookie;

src/DOpusScriptingExtensions/MediaInfoRetriever/MediaInfoRetriever.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,10 @@ class ATL_NO_VTABLE CMediaInfoRetriever :
8989
std::ifstream file(languageFilePath.c_str());
9090
if (!file) {
9191
std::error_code ec(errno, std::generic_category());
92-
THROW_WEXCEPTION(L"Failed to open language file '{}'. Error message: {}", languageFilePath, ToWide(ec.message()));
92+
THROW_WEXCEPTION(L"Failed to open language file '{}'. Error message: {}", languageFilePath, ToUtf16(ec.message()));
9393
}
9494

95-
return ToWide(std::string{ std::istreambuf_iterator<char>(file), {} });
95+
return ToUtf16(std::string{ std::istreambuf_iterator<char>(file), {} });
9696
}
9797

9898
inline static const auto& mediaInfoLanguagesPath = boost::dll::this_line_location().parent_path() / L"MediaInfoLanguages";

0 commit comments

Comments
 (0)