In-line run of subs_in_out so that it gets the environment more easily.
[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
35 #include "common.h"
36 #include "compose.hpp"
37 #include "cpl.h"
38 #include "dcp.h"
39 #include "decrypted_kdm.h"
40 #include "encrypted_kdm.h"
41 #include "exceptions.h"
42 #include "filesystem.h"
43 #include "interop_subtitle_asset.h"
44 #include "mono_picture_asset.h"
45 #include "picture_asset.h"
46 #include "reel.h"
47 #include "reel_picture_asset.h"
48 #include "reel_sound_asset.h"
49 #include "reel_subtitle_asset.h"
50 #include "smpte_subtitle_asset.h"
51 #include "sound_asset.h"
52 #include "subtitle_asset.h"
53 #include "subtitle_image.h"
54 #include "subtitle_string.h"
55 #include <getopt.h>
56 #include <boost/filesystem.hpp>
57 #include <boost/algorithm/string.hpp>
58 #include <iostream>
59 #include <cstdlib>
60 #include <sstream>
61 #include <inttypes.h>
62
63
64 using std::cerr;
65 using std::cout;
66 using std::dynamic_pointer_cast;
67 using std::exception;
68 using std::list;
69 using std::max;
70 using std::min;
71 using std::pair;
72 using std::shared_ptr;
73 using std::string;
74 using std::stringstream;
75 using std::vector;
76 using boost::optional;
77 using namespace dcp;
78
79
80 static void
81 help (string n)
82 {
83         cerr << "Syntax: " << n << " [options] [<DCP>] [<CPL>]\n"
84              << "  -s, --subtitles              list all subtitles\n"
85              << "  -p, --picture                analyse picture\n"
86              << "  -d, --decompress             decompress picture when analysing (this is slow)\n"
87              << "  -o, --only                   only output certain pieces of information; see below.\n"
88              << "      --kdm                    KDM to decrypt DCP\n"
89              << "      --private-key            private key for the certificate that the KDM is targeted at\n"
90              << "      --ignore-missing-assets  ignore missing asset files\n";
91
92         cerr << "--only takes a comma-separated list of strings, one or more of:\n"
93                 "    dcp-path     DCP path\n"
94                 "    cpl-name-id  CPL name and ID\n"
95                 "    picture      picture information\n"
96                 "    sound        sound information\n"
97                 "    subtitle     picture information\n"
98                 "    total-time   total DCP time\n";
99 }
100
101 static double
102 mbits_per_second (int size, Fraction frame_rate)
103 {
104         return size * 8 * frame_rate.as_float() / 1e6;
105 }
106
107 #define OUTPUT_DCP_PATH(...)    maybe_output(only, "dcp-path", String::compose(__VA_ARGS__));
108 #define OUTPUT_CPL_NAME_ID(...) maybe_output(only, "cpl-name-id", String::compose(__VA_ARGS__));
109 #define OUTPUT_PICTURE(...)     maybe_output(only, "picture", String::compose(__VA_ARGS__));
110 #define OUTPUT_PICTURE_NC(x)    maybe_output(only, "picture", (x));
111 #define SHOULD_PICTURE          should_output(only, "picture")
112 #define OUTPUT_SOUND(...)       maybe_output(only, "sound", String::compose(__VA_ARGS__));
113 #define OUTPUT_SOUND_NC(x)      maybe_output(only, "sound", (x));
114 #define OUTPUT_SUBTITLE(...)    maybe_output(only, "subtitle", String::compose(__VA_ARGS__));
115 #define OUTPUT_SUBTITLE_NC(x)   maybe_output(only, "subtitle", (x));
116 #define OUTPUT_TOTAL_TIME(...)  maybe_output(only, "total-time", String::compose(__VA_ARGS__));
117
118 static bool
119 should_output(vector<string> const& only, string t)
120 {
121         return only.empty() || find(only.begin(), only.end(), t) != only.end();
122 }
123
124 static void
125 maybe_output(vector<string> const& only, string t, string s)
126 {
127         if (should_output(only, t)) {
128                 cout << s;
129         }
130 }
131
132 static
133 dcp::Time
134 main_picture (vector<string> const& only, shared_ptr<Reel> reel, bool analyse, bool decompress)
135 {
136         shared_ptr<dcp::ReelPictureAsset> mp = reel->main_picture ();
137         if (!mp) {
138                 return dcp::Time();
139         }
140
141         OUTPUT_PICTURE("      Picture ID:  %1", mp->id());
142         if (mp->entry_point()) {
143                 OUTPUT_PICTURE(" entry %1", *mp->entry_point());
144         }
145         if (mp->duration()) {
146                 OUTPUT_PICTURE(
147                         " duration %1 (%2) intrinsic %3",
148                         *mp->duration(),
149                         dcp::Time(*mp->duration(), mp->frame_rate().as_float(), mp->frame_rate().as_float()).as_string(dcp::Standard::SMPTE),
150                         mp->intrinsic_duration()
151                         );
152         } else {
153                 OUTPUT_PICTURE(" intrinsic duration %1", mp->intrinsic_duration());
154         }
155
156         if (mp->asset_ref().resolved()) {
157                 if (mp->asset()) {
158                         OUTPUT_PICTURE("\n      Picture:     %1x%2\n", mp->asset()->size().width, mp->asset()->size().height);
159                 }
160
161                 shared_ptr<MonoPictureAsset> ma = dynamic_pointer_cast<MonoPictureAsset>(mp->asset());
162                 if (analyse && ma) {
163                         shared_ptr<MonoPictureAssetReader> reader = ma->start_read ();
164                         pair<int, int> j2k_size_range (INT_MAX, 0);
165                         for (int64_t i = 0; i < ma->intrinsic_duration(); ++i) {
166                                 shared_ptr<const MonoPictureFrame> frame = reader->get_frame (i);
167                                 if (SHOULD_PICTURE) {
168                                         printf("Frame %" PRId64 " J2K size %7d", i, frame->size());
169                                 }
170                                 j2k_size_range.first = min(j2k_size_range.first, frame->size());
171                                 j2k_size_range.second = max(j2k_size_range.second, frame->size());
172
173                                 if (decompress) {
174                                         try {
175                                                 frame->xyz_image();
176                                                 if (SHOULD_PICTURE) {
177                                                         printf(" decrypted OK");
178                                                 }
179                                         } catch (exception& e) {
180                                                 if (SHOULD_PICTURE) {
181                                                         printf(" decryption FAILED");
182                                                 }
183                                         }
184                                 }
185
186                                 if (SHOULD_PICTURE) {
187                                         printf("\n");
188                                 }
189
190                         }
191                         if (SHOULD_PICTURE) {
192                                 printf(
193                                                 "J2K size ranges from %d (%.1f Mbit/s) to %d (%.1f Mbit/s)\n",
194                                                 j2k_size_range.first, mbits_per_second(j2k_size_range.first, ma->frame_rate()),
195                                                 j2k_size_range.second, mbits_per_second(j2k_size_range.second, ma->frame_rate())
196                                       );
197                         }
198                 }
199         } else {
200                 OUTPUT_PICTURE_NC(" - not present in this DCP.\n");
201         }
202
203         return dcp::Time (
204                         mp->duration().get_value_or(mp->intrinsic_duration()),
205                         mp->frame_rate().as_float(),
206                         mp->frame_rate().as_float()
207                         );
208 }
209
210 static
211 void
212 main_sound (vector<string> const& only, shared_ptr<Reel> reel)
213 {
214         shared_ptr<dcp::ReelSoundAsset> ms = reel->main_sound ();
215         if (!ms) {
216                 return;
217         }
218
219         OUTPUT_SOUND("      Sound ID:    %1", ms->id());
220         if (ms->entry_point()) {
221                 OUTPUT_SOUND(" entry %1", *ms->entry_point());
222         }
223         if (ms->duration()) {
224                 OUTPUT_SOUND(" duration %1 intrinsic %2", *ms->duration(), ms->intrinsic_duration());
225         } else {
226                 OUTPUT_SOUND(" intrinsic duration %1", ms->intrinsic_duration());
227         }
228
229         if (ms->asset_ref().resolved()) {
230                 if (ms->asset()) {
231                         OUTPUT_SOUND(
232                                 "\n      Sound:       %1 channels at %2Hz\n",
233                                 ms->asset()->channels(),
234                                 ms->asset()->sampling_rate()
235                                 );
236                 }
237         } else {
238                 OUTPUT_SOUND_NC(" - not present in this DCP.\n");
239         }
240 }
241
242 static
243 void
244 main_subtitle (vector<string> const& only, shared_ptr<Reel> reel, bool list_subtitles)
245 {
246         shared_ptr<dcp::ReelSubtitleAsset> ms = reel->main_subtitle ();
247         if (!ms) {
248                 return;
249         }
250
251         OUTPUT_SUBTITLE("      Subtitle ID: %1", ms->id());
252
253         if (ms->asset_ref().resolved()) {
254                 auto subs = ms->asset()->subtitles ();
255                 OUTPUT_SUBTITLE("\n      Subtitle:    %1 subtitles", subs.size());
256                 shared_ptr<InteropSubtitleAsset> iop = dynamic_pointer_cast<InteropSubtitleAsset> (ms->asset());
257                 if (iop) {
258                         OUTPUT_SUBTITLE(" in %1\n", iop->language());
259                 }
260                 shared_ptr<SMPTESubtitleAsset> smpte = dynamic_pointer_cast<SMPTESubtitleAsset> (ms->asset());
261                 if (smpte && smpte->language ()) {
262                         OUTPUT_SUBTITLE(" in %1\n", smpte->language().get());
263                 }
264                 if (list_subtitles) {
265                         for (auto k: subs) {
266                                 auto ks = dynamic_pointer_cast<const SubtitleString>(k);
267                                 if (ks) {
268                                         stringstream s;
269                                         s << *ks;
270                                         OUTPUT_SUBTITLE("%1\n", s.str());
271                                 }
272                                 auto is = dynamic_pointer_cast<const SubtitleImage>(k);
273                                 if (is) {
274                                         stringstream s;
275                                         s << *is;
276                                         OUTPUT_SUBTITLE("%1\n", s.str());
277                                 }
278                         }
279                 }
280         } else {
281                 OUTPUT_SUBTITLE_NC(" - not present in this DCP.\n");
282         }
283 }
284
285
286 int
287 main (int argc, char* argv[])
288 {
289         dcp::init ();
290
291         bool subtitles = false;
292         bool picture = 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                         { "decompress", no_argument, 0, 'd' },
307                         { "only", required_argument, 0, 'o' },
308                         { "ignore-missing-assets", no_argument, 0, 'A' },
309                         { "kdm", required_argument, 0, 'B' },
310                         { "private-key", required_argument, 0, 'C' },
311                         { 0, 0, 0, 0 }
312                 };
313
314                 int c = getopt_long (argc, argv, "vhspdo:AB:C:", long_options, &option_index);
315
316                 if (c == -1) {
317                         break;
318                 }
319
320                 switch (c) {
321                 case 'v':
322                         cout << "libdcp version " << dcp::version << "\n";
323                         exit (EXIT_SUCCESS);
324                 case 'h':
325                         help (argv[0]);
326                         exit (EXIT_SUCCESS);
327                 case 's':
328                         subtitles = true;
329                         break;
330                 case 'p':
331                         picture = true;
332                         break;
333                 case 'd':
334                         decompress = true;
335                         break;
336                 case 'o':
337                         only_string = optarg;
338                         break;
339                 case 'A':
340                         ignore_missing_assets = true;
341                         break;
342                 case 'B':
343                         kdm = optarg;
344                         break;
345                 case 'C':
346                         private_key = optarg;
347                         break;
348                 }
349         }
350
351         if (argc <= optind || argc > (optind + 1)) {
352                 help (argv[0]);
353                 exit (EXIT_FAILURE);
354         }
355
356         if (!filesystem::exists(argv[optind])) {
357                 cerr << argv[0] << ": DCP or CPL " << argv[optind] << " not found.\n";
358                 exit (EXIT_FAILURE);
359         }
360
361         vector<string> only;
362         if (only_string) {
363                 only = boost::split(only, *only_string, boost::is_any_of(","));
364         }
365
366         vector<shared_ptr<CPL>> cpls;
367         if (boost::filesystem::is_directory(argv[optind])) {
368                 DCP* dcp = 0;
369                 vector<dcp::VerificationNote> notes;
370                 try {
371                         dcp = new DCP (argv[optind]);
372                         dcp->read (&notes);
373                         if (kdm && private_key) {
374                                 dcp->add(DecryptedKDM(EncryptedKDM(file_to_string(*kdm)), file_to_string(*private_key)));
375                         }
376                 } catch (FileError& e) {
377                         cerr << "Could not read DCP " << argv[optind] << "; " << e.what() << "\n";
378                         exit (EXIT_FAILURE);
379                 } catch (ReadError& e) {
380                         cerr << "Could not read DCP " << argv[optind] << "; " << e.what() << "\n";
381                         exit (EXIT_FAILURE);
382                 } catch (KDMDecryptionError& e) {
383                         cerr << e.what() << "\n";
384                         exit (EXIT_FAILURE);
385                 }
386
387                 OUTPUT_DCP_PATH("DCP: %1\n", boost::filesystem::path(argv[optind]).string());
388
389                 dcp::filter_notes (notes, ignore_missing_assets);
390                 for (auto i: notes) {
391                         cerr << "Error: " << note_to_string(i) << "\n";
392                 }
393
394                 cpls = dcp->cpls ();
395         } else {
396                 cpls.push_back (shared_ptr<CPL>(new CPL(boost::filesystem::path(argv[optind]))));
397                 ignore_missing_assets = true;
398         }
399
400         dcp::Time total_time;
401
402         for (auto i: cpls) {
403                 OUTPUT_CPL_NAME_ID("  CPL: %1 %2\n", i->annotation_text().get_value_or(""), i->id());
404
405                 int R = 1;
406                 for (auto j: i->reels()) {
407                         if (should_output(only, "picture") || should_output(only, "sound") || should_output(only, "subtitle")) {
408                                 cout << "    Reel " << R << "\n";
409                         }
410
411                         try {
412                                 total_time += main_picture(only, j, picture, decompress);
413                         } catch (UnresolvedRefError& e) {
414                                 if (!ignore_missing_assets) {
415                                         cerr << e.what() << " (for main picture)\n";
416                                 }
417                         }
418
419                         try {
420                                 main_sound(only, j);
421                         } catch (UnresolvedRefError& e) {
422                                 if (!ignore_missing_assets) {
423                                         cerr << e.what() << " (for main sound)\n";
424                                 }
425                         }
426
427                         try {
428                                 main_subtitle (only, j, subtitles);
429                         } catch (UnresolvedRefError& e) {
430                                 if (!ignore_missing_assets) {
431                                         cerr << e.what() << " (for main subtitle)\n";
432                                 }
433                         }
434
435                         ++R;
436                 }
437         }
438
439         OUTPUT_TOTAL_TIME("Total: %1\n", total_time.as_string(dcp::Standard::SMPTE));
440
441         return 0;
442 }