Add check for empty <LabelText> in <ContentVersion>
[libdcp.git] / src / types.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 /** @file  src/types.cc
36  *  @brief Miscellaneous types
37  */
38
39
40 #include "compose.hpp"
41 #include "dcp_assert.h"
42 #include "exceptions.h"
43 #include "raw_convert.h"
44 #include "types.h"
45 #include "warnings.h"
46 LIBDCP_DISABLE_WARNINGS
47 #include <libxml++/libxml++.h>
48 LIBDCP_ENABLE_WARNINGS
49 #include <boost/algorithm/string.hpp>
50 #include <string>
51 #include <vector>
52 #include <cmath>
53 #include <cstdio>
54 #include <iomanip>
55
56
57 using std::string;
58 using std::ostream;
59 using std::vector;
60 using namespace dcp;
61 using namespace boost;
62
63
64 bool dcp::operator== (dcp::Size const & a, dcp::Size const & b)
65 {
66         return (a.width == b.width && a.height == b.height);
67 }
68
69
70 bool dcp::operator!= (dcp::Size const & a, dcp::Size const & b)
71 {
72         return !(a == b);
73 }
74
75
76 /** Construct a Fraction from a string of the form "numerator denominator"
77  *  e.g. "1 3".
78  */
79 Fraction::Fraction (string s)
80 {
81         vector<string> b;
82         split (b, s, is_any_of (" "));
83         if (b.size() != 2) {
84                 boost::throw_exception (XMLError("malformed fraction " + s + " in XML node"));
85         }
86         numerator = raw_convert<int> (b[0]);
87         denominator = raw_convert<int> (b[1]);
88 }
89
90
91 string
92 Fraction::as_string () const
93 {
94         return String::compose ("%1 %2", numerator, denominator);
95 }
96
97
98 bool
99 dcp::operator== (Fraction const & a, Fraction const & b)
100 {
101         return (a.numerator == b.numerator && a.denominator == b.denominator);
102 }
103
104
105 bool
106 dcp::operator!= (Fraction const & a, Fraction const & b)
107 {
108         return (a.numerator != b.numerator || a.denominator != b.denominator);
109 }
110
111
112 Colour::Colour (int r_, int g_, int b_)
113         : r (r_)
114         , g (g_)
115         , b (b_)
116 {
117
118 }
119
120
121 Colour::Colour (string argb_hex)
122 {
123         int alpha;
124         if (sscanf (argb_hex.c_str(), "%2x%2x%2x%2x", &alpha, &r, &g, &b) != 4) {
125                 boost::throw_exception (XMLError ("could not parse colour string"));
126         }
127 }
128
129
130 string
131 Colour::to_argb_string () const
132 {
133         char buffer[9];
134         snprintf (buffer, sizeof(buffer), "FF%02X%02X%02X", r, g, b);
135         return buffer;
136 }
137
138
139 string
140 Colour::to_rgb_string () const
141 {
142         char buffer[7];
143         snprintf (buffer, sizeof(buffer), "%02X%02X%02X", r, g, b);
144         return buffer;
145 }
146
147
148 bool
149 dcp::operator== (Colour const & a, Colour const & b)
150 {
151         return (a.r == b.r && a.g == b.g && a.b == b.b);
152 }
153
154
155 bool
156 dcp::operator!= (Colour const & a, Colour const & b)
157 {
158         return !(a == b);
159 }
160
161
162 string
163 dcp::effect_to_string (Effect e)
164 {
165         switch (e) {
166         case Effect::NONE:
167                 return "none";
168         case Effect::BORDER:
169                 return "border";
170         case Effect::SHADOW:
171                 return "shadow";
172         }
173
174         boost::throw_exception (MiscError("unknown effect type"));
175 }
176
177
178 Effect
179 dcp::string_to_effect (string s)
180 {
181         if (s == "none") {
182                 return Effect::NONE;
183         } else if (s == "border") {
184                 return Effect::BORDER;
185         } else if (s == "shadow") {
186                 return Effect::SHADOW;
187         }
188
189         boost::throw_exception (ReadError("unknown subtitle effect type"));
190 }
191
192
193 string
194 dcp::halign_to_string (HAlign h)
195 {
196         switch (h) {
197         case HAlign::LEFT:
198                 return "left";
199         case HAlign::CENTER:
200                 return "center";
201         case HAlign::RIGHT:
202                 return "right";
203         }
204
205         boost::throw_exception (MiscError("unknown subtitle halign type"));
206 }
207
208
209 HAlign
210 dcp::string_to_halign (string s)
211 {
212         if (s == "left") {
213                 return HAlign::LEFT;
214         } else if (s == "center") {
215                 return HAlign::CENTER;
216         } else if (s == "right") {
217                 return HAlign::RIGHT;
218         }
219
220         boost::throw_exception (ReadError("unknown subtitle halign type"));
221 }
222
223
224 string
225 dcp::valign_to_string (VAlign v)
226 {
227         switch (v) {
228         case VAlign::TOP:
229                 return "top";
230         case VAlign::CENTER:
231                 return "center";
232         case VAlign::BOTTOM:
233                 return "bottom";
234         }
235
236         boost::throw_exception (MiscError("unknown subtitle valign type"));
237 }
238
239
240 VAlign
241 dcp::string_to_valign (string s)
242 {
243         if (s == "top") {
244                 return VAlign::TOP;
245         } else if (s == "center") {
246                 return VAlign::CENTER;
247         } else if (s == "bottom") {
248                 return VAlign::BOTTOM;
249         }
250
251         boost::throw_exception (ReadError("unknown subtitle valign type"));
252 }
253
254
255 string
256 dcp::direction_to_string (Direction v)
257 {
258         switch (v) {
259         case Direction::LTR:
260                 return "ltr";
261         case Direction::RTL:
262                 return "rtl";
263         case Direction::TTB:
264                 return "ttb";
265         case Direction::BTT:
266                 return "btt";
267         }
268
269         boost::throw_exception (MiscError("unknown subtitle direction type"));
270 }
271
272
273 Direction
274 dcp::string_to_direction (string s)
275 {
276         if (s == "ltr" || s == "horizontal") {
277                 return Direction::LTR;
278         } else if (s == "rtl") {
279                 return Direction::RTL;
280         } else if (s == "ttb" || s == "vertical") {
281                 return Direction::TTB;
282         } else if (s == "btt") {
283                 return Direction::BTT;
284         }
285
286         boost::throw_exception (ReadError("unknown subtitle direction type"));
287 }
288
289
290 string
291 dcp::marker_to_string (dcp::Marker m)
292 {
293         switch (m) {
294         case Marker::FFOC:
295                 return "FFOC";
296         case Marker::LFOC:
297                 return "LFOC";
298         case Marker::FFTC:
299                 return "FFTC";
300         case Marker::LFTC:
301                 return "LFTC";
302         case Marker::FFOI:
303                 return "FFOI";
304         case Marker::LFOI:
305                 return "LFOI";
306         case Marker::FFEC:
307                 return "FFEC";
308         case Marker::LFEC:
309                 return "LFEC";
310         case Marker::FFMC:
311                 return "FFMC";
312         case Marker::LFMC:
313                 return "LFMC";
314         }
315
316         DCP_ASSERT (false);
317 }
318
319
320 dcp::Marker
321 dcp::marker_from_string (string s)
322 {
323         if (s == "FFOC") {
324                 return Marker::FFOC;
325         } else if (s == "LFOC") {
326                 return Marker::LFOC;
327         } else if (s == "FFTC") {
328                 return Marker::FFTC;
329         } else if (s == "LFTC") {
330                 return Marker::LFTC;
331         } else if (s == "FFOI") {
332                 return Marker::FFOI;
333         } else if (s == "LFOI") {
334                 return Marker::LFOI;
335         } else if (s == "FFEC") {
336                 return Marker::FFEC;
337         } else if (s == "LFEC") {
338                 return Marker::LFEC;
339         } else if (s == "FFMC") {
340                 return Marker::FFMC;
341         } else if (s == "LFMC") {
342                 return Marker::LFMC;
343         }
344
345         DCP_ASSERT (false);
346 }
347
348
349 ContentVersion::ContentVersion ()
350         : id ("urn:uuid:" + make_uuid())
351 {
352
353 }
354
355
356 ContentVersion::ContentVersion (cxml::ConstNodePtr node)
357         : id(node->string_child("Id"))
358         , label_text(node->string_child("LabelText"))
359 {
360
361 }
362
363
364 ContentVersion::ContentVersion (string label_text_)
365         : id ("urn:uuid:" + make_uuid())
366         , label_text (label_text_)
367 {
368
369 }
370
371
372 void
373 ContentVersion::as_xml (xmlpp::Element* parent) const
374 {
375         auto cv = parent->add_child("ContentVersion");
376         cv->add_child("Id")->add_child_text(id);
377         cv->add_child("LabelText")->add_child_text(label_text);
378 }
379
380
381 Luminance::Luminance (cxml::ConstNodePtr node)
382         : _value(raw_convert<float>(node->content()))
383         , _unit(string_to_unit(node->string_attribute("units")))
384 {
385
386 }
387
388
389 Luminance::Luminance (float value, Unit unit)
390         : _unit (unit)
391 {
392         set_value (value);
393 }
394
395
396 void
397 Luminance::set_value (float v)
398 {
399         if (v < 0) {
400                 throw dcp::MiscError (String::compose("Invalid luminance value %1", v));
401         }
402
403         _value = v;
404 }
405
406
407 void
408 Luminance::as_xml (xmlpp::Element* parent, string ns) const
409 {
410         auto lum = parent->add_child("Luminance", ns);
411         lum->set_attribute("units", unit_to_string(_unit));
412         lum->add_child_text(raw_convert<string>(_value, 3));
413 }
414
415
416 string
417 Luminance::unit_to_string (Unit u)
418 {
419         switch (u) {
420         case Unit::CANDELA_PER_SQUARE_METRE:
421                 return "candela-per-square-metre";
422         case Unit::FOOT_LAMBERT:
423                 return "foot-lambert";
424         default:
425                 DCP_ASSERT (false);
426         }
427
428         return {};
429 }
430
431
432 Luminance::Unit
433 Luminance::string_to_unit (string u)
434 {
435         if (u == "candela-per-square-metre") {
436                 return Unit::CANDELA_PER_SQUARE_METRE;
437         } else if (u == "foot-lambert") {
438                 return Unit::FOOT_LAMBERT;
439         }
440
441         throw XMLError (String::compose("Invalid luminance unit %1", u));
442 }
443
444
445 float
446 Luminance::value_in_foot_lamberts () const
447 {
448         switch (_unit) {
449         case Unit::CANDELA_PER_SQUARE_METRE:
450                 return _value / 3.426;
451         case Unit::FOOT_LAMBERT:
452                 return _value;
453         default:
454                 DCP_ASSERT (false);
455         }
456 }
457
458
459 bool
460 dcp::operator== (Luminance const& a, Luminance const& b)
461 {
462         return fabs(a.value() - b.value()) < 0.001 && a.unit() == b.unit();
463 }
464
465
466 MainSoundConfiguration::MainSoundConfiguration (string s)
467 {
468         vector<string> parts;
469         boost::split (parts, s, boost::is_any_of("/"));
470         if (parts.empty()) {
471                 throw MainSoundConfigurationError(s);
472         }
473
474         if (parts[0] == "51") {
475                 _field = MCASoundField::FIVE_POINT_ONE;
476         } else if (parts[0] == "71") {
477                 _field = MCASoundField::SEVEN_POINT_ONE;
478         } else {
479                 _field = MCASoundField::OTHER;
480         }
481
482         if (parts.size() < 2) {
483                 /* I think it's OK to just have the sound field descriptor with no channels, though
484                  * to me it's not clear and I might be wrong.
485                  */
486                 return;
487         }
488
489         vector<string> channels;
490         boost::split (channels, parts[1], boost::is_any_of(","));
491
492         if (channels.size() > 16) {
493                 throw MainSoundConfigurationError (s);
494         }
495
496         for (auto i: channels) {
497                 if (i == "-") {
498                         _channels.push_back(optional<Channel>());
499                 } else {
500                         _channels.push_back(mca_id_to_channel(i));
501                 }
502         }
503 }
504
505
506 MainSoundConfiguration::MainSoundConfiguration (MCASoundField field, int channels)
507         : _field (field)
508 {
509         _channels.resize (channels);
510 }
511
512
513 string
514 MainSoundConfiguration::to_string () const
515 {
516         string c;
517         switch (_field) {
518         case MCASoundField::FIVE_POINT_ONE:
519                 c = "51/";
520                 break;
521         case MCASoundField::SEVEN_POINT_ONE:
522                 c = "71/";
523                 break;
524         default:
525                 DCP_ASSERT(false);
526         }
527
528         for (auto i: _channels) {
529                 if (!i) {
530                         c += "-,";
531                 } else {
532                         c += channel_to_mca_id(*i, _field) + ",";
533                 }
534         }
535
536         if (c.length() > 0) {
537                 c = c.substr(0, c.length() - 1);
538         }
539
540         return c;
541 }
542
543
544 optional<Channel>
545 MainSoundConfiguration::mapping (int index) const
546 {
547         DCP_ASSERT (static_cast<size_t>(index) < _channels.size());
548         return _channels[index];
549 }
550
551
552 void
553 MainSoundConfiguration::set_mapping (int index, Channel c)
554 {
555         DCP_ASSERT (static_cast<size_t>(index) < _channels.size());
556         _channels[index] = c;
557 }
558
559
560 string
561 dcp::status_to_string (Status s)
562 {
563         switch (s) {
564         case Status::FINAL:
565                 return "final";
566         case Status::TEMP:
567                 return "temp";
568         case Status::PRE:
569                 return "pre";
570         default:
571                 DCP_ASSERT (false);
572         }
573 }
574
575
576 Status
577 dcp::string_to_status (string s)
578 {
579         if (s == "final") {
580                 return Status::FINAL;
581         } else if (s == "temp") {
582                 return Status::TEMP;
583         } else if (s == "pre") {
584                 return Status::PRE;
585         }
586
587         DCP_ASSERT (false);
588 }
589
590
591 Channel
592 dcp::mca_id_to_channel (string id)
593 {
594         transform(id.begin(), id.end(), id.begin(), ::tolower);
595
596         if (id == "l") {
597                 return Channel::LEFT;
598         } else if (id == "r") {
599                 return Channel::RIGHT;
600         } else if (id == "c") {
601                 return Channel::CENTRE;
602         } else if (id == "lfe") {
603                 return Channel::LFE;
604         } else if (id == "ls" || id == "lss") {
605                 return Channel::LS;
606         } else if (id == "rs" || id == "rss") {
607                 return Channel::RS;
608         } else if (id == "hi") {
609                 return Channel::HI;
610         } else if (id == "vin") {
611                 return Channel::VI;
612         } else if (id == "lrs") {
613                 return Channel::BSL;
614         } else if (id == "rrs") {
615                 return Channel::BSR;
616         } else if (id == "dbox") {
617                 return Channel::MOTION_DATA;
618         } else if (id == "sync" || id == "fsksync") {
619                 return Channel::SYNC_SIGNAL;
620         } else if (id == "slvs") {
621                 return Channel::SIGN_LANGUAGE;
622         }
623
624         throw UnknownChannelIdError (id);
625 }
626
627
628 string
629 dcp::channel_to_mca_id (Channel c, MCASoundField field)
630 {
631         switch (c) {
632         case Channel::LEFT:
633                 return "L";
634         case Channel::RIGHT:
635                 return "R";
636         case Channel::CENTRE:
637                 return "C";
638         case Channel::LFE:
639                 return "LFE";
640         case Channel::LS:
641                 return field == MCASoundField::FIVE_POINT_ONE ? "Ls" : "Lss";
642         case Channel::RS:
643                 return field == MCASoundField::FIVE_POINT_ONE ? "Rs" : "Rss";
644         case Channel::HI:
645                 return "HI";
646         case Channel::VI:
647                 return "VIN";
648         case Channel::BSL:
649                 return "Lrs";
650         case Channel::BSR:
651                 return "Rrs";
652         case Channel::MOTION_DATA:
653                 return "DBOX";
654         case Channel::SYNC_SIGNAL:
655                 return "FSKSync";
656         case Channel::SIGN_LANGUAGE:
657                 return "SLVS";
658         default:
659                 break;
660         }
661
662         DCP_ASSERT (false);
663 }
664
665
666 string
667 dcp::channel_to_mca_name (Channel c, MCASoundField field)
668 {
669         switch (c) {
670         case Channel::LEFT:
671                 return "Left";
672         case Channel::RIGHT:
673                 return "Right";
674         case Channel::CENTRE:
675                 return "Center";
676         case Channel::LFE:
677                 return "LFE";
678         case Channel::LS:
679                 return field == MCASoundField::FIVE_POINT_ONE ? "Left Surround" : "Left Side Surround";
680         case Channel::RS:
681                 return field == MCASoundField::FIVE_POINT_ONE ? "Right Surround" : "Right Side Surround";
682         case Channel::HI:
683                 return "Hearing Impaired";
684         case Channel::VI:
685                 return "Visually Impaired-Narrative";
686         case Channel::BSL:
687                 return "Left Rear Surround";
688         case Channel::BSR:
689                 return "Right Rear Surround";
690         case Channel::MOTION_DATA:
691                 return "D-BOX Motion Code Primary Stream";
692         case Channel::SYNC_SIGNAL:
693                 return "FSK Sync";
694         case Channel::SIGN_LANGUAGE:
695                 return "Sign Language Video Stream";
696         default:
697                 break;
698         }
699
700         DCP_ASSERT (false);
701 }
702
703
704 ASDCP::UL
705 dcp::channel_to_mca_universal_label (Channel c, MCASoundField field, ASDCP::Dictionary const* dict)
706 {
707         static byte_t sync_signal[] = {
708                 0x06, 0x0e, 0x2b, 0x34, 0x04, 0x01, 0x01, 0x0d, 0x03, 0x02, 0x01, 0x10, 0x00, 0x00, 0x00, 0x00
709         };
710
711         static byte_t sign_language[] = {
712                 0x06, 0x0e, 0x2b, 0x34, 0x04, 0x01, 0x01, 0x0d, 0x0d, 0x0f, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00
713         };
714
715         switch (c) {
716         case Channel::LEFT:
717                 return dict->ul(ASDCP::MDD_DCAudioChannel_L);
718         case Channel::RIGHT:
719                 return dict->ul(ASDCP::MDD_DCAudioChannel_R);
720         case Channel::CENTRE:
721                 return dict->ul(ASDCP::MDD_DCAudioChannel_C);
722         case Channel::LFE:
723                 return dict->ul(ASDCP::MDD_DCAudioChannel_LFE);
724         case Channel::LS:
725                 return dict->ul(field == MCASoundField::FIVE_POINT_ONE ? ASDCP::MDD_DCAudioChannel_Ls : ASDCP::MDD_DCAudioChannel_Lss);
726         case Channel::RS:
727                 return dict->ul(field == MCASoundField::FIVE_POINT_ONE ? ASDCP::MDD_DCAudioChannel_Rs : ASDCP::MDD_DCAudioChannel_Rss);
728         case Channel::HI:
729                 return dict->ul(ASDCP::MDD_DCAudioChannel_HI);
730         case Channel::VI:
731                 return dict->ul(ASDCP::MDD_DCAudioChannel_VIN);
732         case Channel::BSL:
733                 return dict->ul(ASDCP::MDD_DCAudioChannel_Lrs);
734         case Channel::BSR:
735                 return dict->ul(ASDCP::MDD_DCAudioChannel_Rrs);
736         case Channel::MOTION_DATA:
737                 return dict->ul(ASDCP::MDD_DBOXMotionCodePrimaryStream);
738         case Channel::SYNC_SIGNAL:
739                 return ASDCP::UL(sync_signal);
740         case Channel::SIGN_LANGUAGE:
741                 return ASDCP::UL(sign_language);
742         default:
743                 break;
744         }
745
746         DCP_ASSERT (false);
747 }
748
749
750 vector<dcp::Channel>
751 dcp::used_audio_channels ()
752 {
753         return {
754                 Channel::LEFT,
755                 Channel::RIGHT,
756                 Channel::CENTRE,
757                 Channel::LFE,
758                 Channel::LS,
759                 Channel::RS,
760                 Channel::HI,
761                 Channel::VI,
762                 Channel::BSL,
763                 Channel::BSR,
764                 Channel::MOTION_DATA,
765                 Channel::SYNC_SIGNAL,
766                 Channel::SIGN_LANGUAGE
767         };
768 }
769
770
771 string
772 dcp::formulation_to_string (dcp::Formulation formulation)
773 {
774         switch (formulation) {
775         case Formulation::MODIFIED_TRANSITIONAL_1:
776                 return "modified-transitional-1";
777         case Formulation::MULTIPLE_MODIFIED_TRANSITIONAL_1:
778                 return "multiple-modified-transitional-1";
779         case Formulation::DCI_ANY:
780                 return "dci-any";
781         case Formulation::DCI_SPECIFIC:
782                 return "dci-specific";
783         }
784
785         DCP_ASSERT (false);
786 }
787
788
789 dcp::Formulation
790 dcp::string_to_formulation (string formulation)
791 {
792         if (formulation == "modified-transitional-1") {
793                 return Formulation::MODIFIED_TRANSITIONAL_1;
794         } else if (formulation == "multiple-modified-transitional-1") {
795                 return Formulation::MULTIPLE_MODIFIED_TRANSITIONAL_1;
796         } else if (formulation == "dci-any") {
797                 return Formulation::DCI_ANY;
798         } else if (formulation == "dci-specific") {
799                 return Formulation::DCI_SPECIFIC;
800         }
801
802         DCP_ASSERT (false);
803 }
804