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