Don't look for ImageMagick/GraphicsMagick if not building examples.
[libdcp.git] / tools / dcpinfo.cc
1 /*
2     Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
6     libdcp is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     libdcp is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with libdcp.  If not, see <http://www.gnu.org/licenses/>.
18
19     In addition, as a special exception, the copyright holders give
20     permission to link the code of portions of this program with the
21     OpenSSL library under certain conditions as described in each
22     individual source file, and distribute linked combinations
23     including the two.
24
25     You must obey the GNU General Public License in all respects
26     for all of the code used other than OpenSSL.  If you modify
27     file(s) with this exception, you may extend this exception to your
28     version of the file(s), but you are not obligated to do so.  If you
29     do not wish to do so, delete this exception statement from your
30     version.  If you delete this exception statement from all source
31     files in the program, then also delete it here.
32 */
33
34 #include "dcp.h"
35 #include "exceptions.h"
36 #include "reel.h"
37 #include "sound_asset.h"
38 #include "picture_asset.h"
39 #include "subtitle_asset.h"
40 #include "reel_picture_asset.h"
41 #include "reel_sound_asset.h"
42 #include "reel_subtitle_asset.h"
43 #include "subtitle_string.h"
44 #include "subtitle_image.h"
45 #include "interop_subtitle_asset.h"
46 #include "smpte_subtitle_asset.h"
47 #include "mono_picture_asset.h"
48 #include "encrypted_kdm.h"
49 #include "decrypted_kdm.h"
50 #include "cpl.h"
51 #include "common.h"
52 #include "compose.hpp"
53 #include <getopt.h>
54 #include <boost/filesystem.hpp>
55 #include <boost/algorithm/string.hpp>
56 #include <iostream>
57 #include <cstdlib>
58 #include <sstream>
59 #include <inttypes.h>
60
61 using std::cerr;
62 using std::cout;
63 using std::dynamic_pointer_cast;
64 using std::exception;
65 using std::list;
66 using std::make_shared;
67 using std::max;
68 using std::min;
69 using std::pair;
70 using std::shared_ptr;
71 using std::string;
72 using std::stringstream;
73 using std::vector;
74 using boost::optional;
75 using namespace dcp;
76
77 static void
78 help (string n)
79 {
80         cerr << "Syntax: " << n << " [options] [<DCP>] [<CPL>]\n"
81              << "  -s, --subtitles              list all subtitles\n"
82              << "  -p, --picture                analyse picture\n"
83              << "  -f, --frame                  show details of individual picture frames\n"
84              << "  -d, --decompress             decompress picture when analysing (this is slow)\n"
85              << "  -o, --only                   only output certain pieces of information; see below.\n"
86              << "      --kdm                    KDM to decrypt DCP\n"
87              << "      --private-key            private key for the certificate that the KDM is targeted at\n"
88              << "      --ignore-missing-assets  ignore missing asset files\n";
89
90         cerr << "--only takes a comma-separated list of strings, one or more of:\n"
91                 "    dcp-path     DCP path\n"
92                 "    cpl-name-id  CPL name and ID\n"
93                 "    picture      picture information\n"
94                 "    sound        sound information\n"
95                 "    subtitle     picture information\n"
96                 "    total-time   total DCP time\n";
97 }
98
99 static double
100 mbits_per_second (int size, Fraction frame_rate)
101 {
102         return size * 8 * frame_rate.as_float() / 1e6;
103 }
104
105 #define OUTPUT_DCP_PATH(...)    maybe_output(only, "dcp-path", String::compose(__VA_ARGS__));
106 #define OUTPUT_CPL_NAME_ID(...) maybe_output(only, "cpl-name-id", String::compose(__VA_ARGS__));
107 #define OUTPUT_PICTURE(...)     maybe_output(only, "picture", String::compose(__VA_ARGS__));
108 #define OUTPUT_PICTURE_NC(x)    maybe_output(only, "picture", (x));
109 #define SHOULD_PICTURE          should_output(only, "picture")
110 #define OUTPUT_SOUND(...)       maybe_output(only, "sound", String::compose(__VA_ARGS__));
111 #define OUTPUT_SOUND_NC(x)      maybe_output(only, "sound", (x));
112 #define OUTPUT_SUBTITLE(...)    maybe_output(only, "subtitle", String::compose(__VA_ARGS__));
113 #define OUTPUT_SUBTITLE_NC(x)   maybe_output(only, "subtitle", (x));
114 #define OUTPUT_TOTAL_TIME(...)  maybe_output(only, "total-time", String::compose(__VA_ARGS__));
115
116 static bool
117 should_output(vector<string> const& only, string t)
118 {
119         return only.empty() || find(only.begin(), only.end(), t) != only.end();
120 }
121
122 static void
123 maybe_output(vector<string> const& only, string t, string s)
124 {
125         if (should_output(only, t)) {
126                 cout << s;
127         }
128 }
129
130 static
131 dcp::Time
132 main_picture (vector<string> const& only, shared_ptr<Reel> reel, bool analyse, bool frame_detail, bool decompress)
133 {
134         auto mp = reel->main_picture ();
135         if (!mp) {
136                 return {};
137         }
138
139         OUTPUT_PICTURE("      Picture ID:         %1", mp->id());
140         if (mp->entry_point()) {
141                 OUTPUT_PICTURE(" entry %1", *mp->entry_point());
142         }
143         if (mp->duration()) {
144                 OUTPUT_PICTURE(
145                         " duration %1 (%2) intrinsic %3",
146                         *mp->duration(),
147                         dcp::Time(*mp->duration(), mp->frame_rate().as_float(), mp->frame_rate().as_float()).as_string(dcp::Standard::SMPTE),
148                         mp->intrinsic_duration()
149                         );
150         } else {
151                 OUTPUT_PICTURE(" intrinsic duration %1", mp->intrinsic_duration());
152         }
153
154         if (mp->asset_ref().resolved()) {
155                 if (mp->asset()) {
156                         OUTPUT_PICTURE("\n      Picture:            %1x%2\n", mp->asset()->size().width, mp->asset()->size().height);
157                 }
158
159                 auto ma = dynamic_pointer_cast<MonoPictureAsset>(mp->asset());
160                 if (analyse && ma) {
161                         auto reader = ma->start_read ();
162                         pair<int, int> j2k_size_range (INT_MAX, 0);
163                         for (int64_t i = 0; i < ma->intrinsic_duration(); ++i) {
164                                 auto frame = reader->get_frame (i);
165                                 if (SHOULD_PICTURE && frame_detail) {
166                                         printf("Frame %" PRId64 " J2K size %7d", i, frame->size());
167                                 }
168                                 j2k_size_range.first = min(j2k_size_range.first, frame->size());
169                                 j2k_size_range.second = max(j2k_size_range.second, frame->size());
170
171                                 if (decompress) {
172                                         try {
173                                                 frame->xyz_image();
174                                                 if (SHOULD_PICTURE && frame_detail) {
175                                                         printf(" decrypted OK");
176                                                 }
177                                         } catch (exception& e) {
178                                                 if (SHOULD_PICTURE && frame_detail) {
179                                                         printf(" decryption FAILED");
180                                                 }
181                                         }
182                                 }
183
184                                 if (SHOULD_PICTURE && frame_detail) {
185                                         printf("\n");
186                                 }
187
188                         }
189                         if (SHOULD_PICTURE) {
190                                 printf(
191                                                 "      Minimum frame size: %5.1f MBit/s (%6d)\n"
192                                                 "      Maximum frame size: %5.1f MBit/s (%6d)\n",
193                                                 mbits_per_second(j2k_size_range.first, ma->frame_rate()), j2k_size_range.first,
194                                                 mbits_per_second(j2k_size_range.second, ma->frame_rate()), j2k_size_range.second
195                                       );
196                         }
197                 }
198         } else {
199                 OUTPUT_PICTURE_NC(" - not present in this DCP.\n");
200         }
201
202         return dcp::Time (
203                         mp->duration().get_value_or(mp->intrinsic_duration()),
204                         mp->frame_rate().as_float(),
205                         mp->frame_rate().as_float()
206                         );
207 }
208
209 static
210 void
211 main_sound (vector<string> const& only, shared_ptr<Reel> reel)
212 {
213         auto ms = reel->main_sound ();
214         if (!ms) {
215                 return;
216         }
217
218         OUTPUT_SOUND("      Sound ID:           %1", ms->id());
219         if (ms->entry_point()) {
220                 OUTPUT_SOUND(" entry %1", *ms->entry_point());
221         }
222         if (ms->duration()) {
223                 OUTPUT_SOUND(" duration %1 intrinsic %2", *ms->duration(), ms->intrinsic_duration());
224         } else {
225                 OUTPUT_SOUND(" intrinsic duration %1", ms->intrinsic_duration());
226         }
227
228         if (ms->asset_ref().resolved()) {
229                 if (ms->asset()) {
230                         OUTPUT_SOUND(
231                                 "\n      Sound:              %1 channels at %2Hz\n",
232                                 ms->asset()->channels(),
233                                 ms->asset()->sampling_rate()
234                                 );
235                 }
236         } else {
237                 OUTPUT_SOUND_NC(" - not present in this DCP.\n");
238         }
239 }
240
241 static
242 void
243 main_subtitle (vector<string> const& only, shared_ptr<Reel> reel, bool list_subtitles)
244 {
245         auto ms = reel->main_subtitle ();
246         if (!ms) {
247                 return;
248         }
249
250         OUTPUT_SUBTITLE("      Subtitle ID: %1", ms->id());
251
252         if (ms->asset_ref().resolved()) {
253                 auto subs = ms->asset()->subtitles ();
254                 OUTPUT_SUBTITLE("\n      Subtitle:    %1 subtitles", subs.size());
255                 auto iop = dynamic_pointer_cast<InteropSubtitleAsset> (ms->asset());
256                 if (iop) {
257                         OUTPUT_SUBTITLE(" in %1\n", iop->language());
258                 }
259                 auto smpte = dynamic_pointer_cast<SMPTESubtitleAsset> (ms->asset());
260                 if (smpte && smpte->language ()) {
261                         OUTPUT_SUBTITLE(" in %1\n", smpte->language().get());
262                 }
263                 if (list_subtitles) {
264                         for (auto k: subs) {
265                                 auto ks = dynamic_pointer_cast<const SubtitleString>(k);
266                                 if (ks) {
267                                         stringstream s;
268                                         s << *ks;
269                                         OUTPUT_SUBTITLE("%1\n", s.str());
270                                 }
271                                 auto is = dynamic_pointer_cast<const SubtitleImage>(k);
272                                 if (is) {
273                                         stringstream s;
274                                         s << *is;
275                                         OUTPUT_SUBTITLE("%1\n", s.str());
276                                 }
277                         }
278                 }
279         } else {
280                 OUTPUT_SUBTITLE_NC(" - not present in this DCP.\n");
281         }
282 }
283
284
285 int
286 main (int argc, char* argv[])
287 {
288         dcp::init ();
289
290         bool subtitles = false;
291         bool picture = false;
292         bool frame = false;
293         bool decompress = false;
294         bool ignore_missing_assets = false;
295         optional<boost::filesystem::path> kdm;
296         optional<boost::filesystem::path> private_key;
297         optional<string> only_string;
298
299         int option_index = 0;
300         while (true) {
301                 static struct option long_options[] = {
302                         { "version", no_argument, 0, 'v' },
303                         { "help", no_argument, 0, 'h' },
304                         { "subtitles", no_argument, 0, 's' },
305                         { "picture", no_argument, 0, 'p' },
306                         { "frame", no_argument, 0, 'f' },
307                         { "decompress", no_argument, 0, 'd' },
308                         { "only", required_argument, 0, 'o' },
309                         { "ignore-missing-assets", no_argument, 0, 'A' },
310                         { "kdm", required_argument, 0, 'B' },
311                         { "private-key", required_argument, 0, 'C' },
312                         { 0, 0, 0, 0 }
313                 };
314
315                 int c = getopt_long (argc, argv, "vhspfdo:AB:C:", long_options, &option_index);
316
317                 if (c == -1) {
318                         break;
319                 }
320
321                 switch (c) {
322                 case 'v':
323                         cout << "libdcp version " << LIBDCP_VERSION << "\n";
324                         exit (EXIT_SUCCESS);
325                 case 'h':
326                         help (argv[0]);
327                         exit (EXIT_SUCCESS);
328                 case 's':
329                         subtitles = true;
330                         break;
331                 case 'p':
332                         picture = true;
333                         break;
334                 case 'f':
335                         frame = true;
336                         break;
337                 case 'd':
338                         decompress = true;
339                         break;
340                 case 'o':
341                         only_string = optarg;
342                         break;
343                 case 'A':
344                         ignore_missing_assets = true;
345                         break;
346                 case 'B':
347                         kdm = optarg;
348                         break;
349                 case 'C':
350                         private_key = optarg;
351                         break;
352                 }
353         }
354
355         if (argc <= optind || argc > (optind + 1)) {
356                 help (argv[0]);
357                 exit (EXIT_FAILURE);
358         }
359
360         if (!boost::filesystem::exists (argv[optind])) {
361                 cerr << argv[0] << ": DCP or CPL " << argv[optind] << " not found.\n";
362                 exit (EXIT_FAILURE);
363         }
364
365         vector<string> only;
366         if (only_string) {
367                 only = boost::split(only, *only_string, boost::is_any_of(","));
368         }
369
370         vector<shared_ptr<CPL>> cpls;
371         if (boost::filesystem::is_directory(argv[optind])) {
372                 DCP* dcp = 0;
373                 vector<dcp::VerificationNote> notes;
374                 try {
375                         dcp = new DCP (argv[optind]);
376                         dcp->read (&notes);
377                         if (kdm && private_key) {
378                                 dcp->add(DecryptedKDM(EncryptedKDM(file_to_string(*kdm)), file_to_string(*private_key)));
379                         }
380                 } catch (FileError& e) {
381                         cerr << "Could not read DCP " << argv[optind] << "; " << e.what() << "\n";
382                         exit (EXIT_FAILURE);
383                 } catch (ReadError& e) {
384                         cerr << "Could not read DCP " << argv[optind] << "; " << e.what() << "\n";
385                         exit (EXIT_FAILURE);
386                 } catch (KDMDecryptionError& e) {
387                         cerr << e.what() << "\n";
388                         exit (EXIT_FAILURE);
389                 }
390
391                 OUTPUT_DCP_PATH("DCP: %1\n", boost::filesystem::path(argv[optind]).string());
392
393                 dcp::filter_notes (notes, ignore_missing_assets);
394                 for (auto i: notes) {
395                         cerr << "Error: " << note_to_string(i) << "\n";
396                 }
397
398                 cpls = dcp->cpls ();
399         } else {
400                 cpls.push_back (std::make_shared<CPL>(boost::filesystem::path(argv[optind])));
401                 ignore_missing_assets = true;
402         }
403
404         dcp::Time total_time;
405
406         for (auto i: cpls) {
407                 OUTPUT_CPL_NAME_ID("  CPL: %1 %2\n", i->annotation_text().get_value_or(""), i->id());
408
409                 int R = 1;
410                 for (auto j: i->reels()) {
411                         if (should_output(only, "picture") || should_output(only, "sound") || should_output(only, "subtitle")) {
412                                 cout << "    Reel " << R << "\n";
413                         }
414
415                         try {
416                                 total_time += main_picture(only, j, picture, frame, decompress);
417                         } catch (UnresolvedRefError& e) {
418                                 if (!ignore_missing_assets) {
419                                         cerr << e.what() << " (for main picture)\n";
420                                 }
421                         }
422
423                         try {
424                                 main_sound(only, j);
425                         } catch (UnresolvedRefError& e) {
426                                 if (!ignore_missing_assets) {
427                                         cerr << e.what() << " (for main sound)\n";
428                                 }
429                         }
430
431                         try {
432                                 main_subtitle (only, j, subtitles);
433                         } catch (UnresolvedRefError& e) {
434                                 if (!ignore_missing_assets) {
435                                         cerr << e.what() << " (for main subtitle)\n";
436                                 }
437                         }
438
439                         ++R;
440                 }
441         }
442
443         OUTPUT_TOTAL_TIME("Total: %1\n", total_time.as_string(dcp::Standard::SMPTE));
444
445         return 0;
446 }