removed nascent ST2052-1 support pending completion
[asdcplib.git] / src / TimedText_Parser.cpp
1 /*
2 Copyright (c) 2007-2013, John Hurst
3 All rights reserved.
4
5 Redistribution and use in source and binary forms, with or without
6 modification, are permitted provided that the following conditions
7 are met:
8 1. Redistributions of source code must retain the above copyright
9    notice, this list of conditions and the following disclaimer.
10 2. Redistributions in binary form must reproduce the above copyright
11    notice, this list of conditions and the following disclaimer in the
12    documentation and/or other materials provided with the distribution.
13 3. The name of the author may not be used to endorse or promote products
14    derived from this software without specific prior written permission.
15
16 THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
17 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
18 OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
19 IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
20 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
21 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
25 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27 /*! \file    AS_DCP_TimedText.cpp
28     \version $Id$       
29     \brief   AS-DCP library, PCM essence reader and writer implementation
30 */
31
32
33 #include "AS_DCP_internal.h"
34 #include "S12MTimecode.h"
35 #include "KM_xml.h"
36
37 using namespace Kumu;
38 using namespace ASDCP;
39
40 using Kumu::DefaultLogSink;
41
42 const char* c_dcst_namespace_name = "http://www.smpte-ra.org/schemas/428-7/2007/DCST";
43
44 //------------------------------------------------------------------------------------------
45
46
47 ASDCP::TimedText::LocalFilenameResolver::LocalFilenameResolver() {}
48
49 //
50 Result_t
51 ASDCP::TimedText::LocalFilenameResolver::OpenRead(const std::string& dirname)
52 {
53   if ( PathIsDirectory(dirname) )
54     {
55       m_Dirname = dirname;
56       return RESULT_OK;
57     }
58
59   DefaultLogSink().Error("Path '%s' is not a directory, defaulting to '.'\n", dirname.c_str());
60   m_Dirname = ".";
61   return RESULT_FALSE;
62 }
63
64 //
65 Result_t
66 ASDCP::TimedText::LocalFilenameResolver::ResolveRID(const byte_t* uuid, TimedText::FrameBuffer& FrameBuf) const
67 {
68   Result_t result = RESULT_NOT_FOUND;
69   char buf[64];
70   UUID RID(uuid);
71   PathList_t found_list;
72
73   FindInPath(PathMatchRegex(RID.EncodeHex(buf, 64)), m_Dirname, found_list);
74
75   if ( found_list.size() == 1 )
76     {
77       FileReader Reader;
78       DefaultLogSink().Debug("retrieving resource %s from file %s\n", buf, found_list.front().c_str());
79
80       result = Reader.OpenRead(found_list.front().c_str());
81
82       if ( KM_SUCCESS(result) )
83         {
84           ui32_t read_count, read_size = Reader.Size();
85           result = FrameBuf.Capacity(read_size);
86       
87           if ( KM_SUCCESS(result) )
88             result = Reader.Read(FrameBuf.Data(), read_size, &read_count);
89
90           if ( KM_SUCCESS(result) )
91             FrameBuf.Size(read_count);
92         }
93     }
94   else if ( ! found_list.empty() )
95     {
96       DefaultLogSink().Error("More than one file in %s matches %s.\n", m_Dirname.c_str(), buf);
97       result = RESULT_RAW_FORMAT;
98     }
99
100   return result;
101 }
102
103 //------------------------------------------------------------------------------------------
104
105 typedef std::map<Kumu::UUID, TimedText::MIMEType_t> ResourceTypeMap_t;
106
107 class ASDCP::TimedText::DCSubtitleParser::h__SubtitleParser
108 {
109   XMLElement  m_Root;
110   ResourceTypeMap_t m_ResourceTypes;
111   Result_t OpenRead();
112
113   ASDCP_NO_COPY_CONSTRUCT(h__SubtitleParser);
114
115 public:
116   std::string m_Filename;
117   std::string m_XMLDoc;
118   TimedTextDescriptor  m_TDesc;
119   mem_ptr<LocalFilenameResolver> m_DefaultResolver;
120
121   h__SubtitleParser() : m_Root("**ParserRoot**")
122   {
123     memset(&m_TDesc.AssetID, 0, UUIDlen);
124   }
125
126   ~h__SubtitleParser() {}
127
128   TimedText::IResourceResolver* GetDefaultResolver()
129   {
130     if ( m_DefaultResolver.empty() )
131       {
132         m_DefaultResolver = new LocalFilenameResolver();
133         m_DefaultResolver->OpenRead(PathDirname(m_Filename));
134       }
135
136     return m_DefaultResolver;
137   }
138
139   Result_t OpenRead(const std::string& filename);
140   Result_t OpenRead(const std::string& xml_doc, const std::string& filename);
141   Result_t ReadAncillaryResource(const byte_t* uuid, FrameBuffer& FrameBuf, const IResourceResolver& Resolver) const;
142 };
143
144 //
145 bool
146 get_UUID_from_element(XMLElement* Element, UUID& ID)
147 {
148   assert(Element);
149   const char* p = Element->GetBody().c_str();
150   if ( strncmp(p, "urn:uuid:", 9) == 0 )    p += 9;
151   return ID.DecodeHex(p);
152 }
153
154 //
155 bool
156 get_UUID_from_child_element(const char* name, XMLElement* Parent, UUID& outID)
157 {
158   assert(name); assert(Parent);
159   XMLElement* Child = Parent->GetChildWithName(name);
160   if ( Child == 0 )    return false;
161   return get_UUID_from_element(Child, outID);
162 }
163
164 //
165 static ASDCP::Rational
166 decode_rational(const char* str_rat)
167 {
168   assert(str_rat);
169   ui32_t Num = atoi(str_rat);
170   ui32_t Den = 0;
171
172   const char* den_str = strrchr(str_rat, ' ');
173   if ( den_str != 0 )
174     Den = atoi(den_str+1);
175
176   return ASDCP::Rational(Num, Den);
177 }
178
179 //
180 Result_t
181 ASDCP::TimedText::DCSubtitleParser::h__SubtitleParser::OpenRead(const std::string& filename)
182 {
183   Result_t result = ReadFileIntoString(filename, m_XMLDoc);
184
185   if ( KM_SUCCESS(result) )
186     result = OpenRead();
187
188   m_Filename = filename;
189   return result;
190 }
191
192 //
193 Result_t
194 ASDCP::TimedText::DCSubtitleParser::h__SubtitleParser::OpenRead(const std::string& xml_doc, const std::string& filename)
195 {
196   m_XMLDoc = xml_doc;
197
198   if ( filename.empty() )
199     {
200       m_Filename = "<string>";
201     }
202   else
203     {
204       m_Filename = filename;
205     }
206
207   return OpenRead();
208 }
209
210 //
211 Result_t
212 ASDCP::TimedText::DCSubtitleParser::h__SubtitleParser::OpenRead()
213 {
214   if ( ! m_Root.ParseString(m_XMLDoc.c_str()) )
215     return RESULT_FORMAT;
216
217   m_TDesc.EncodingName = "UTF-8"; // the XML parser demands UTF-8
218   m_TDesc.ResourceList.clear();
219   m_TDesc.ContainerDuration = 0;
220   const XMLNamespace* ns = m_Root.Namespace();
221
222   if ( ns == 0 )
223     {
224       DefaultLogSink(). Warn("Document has no namespace name, assuming %s\n", c_dcst_namespace_name);
225       m_TDesc.NamespaceName = c_dcst_namespace_name;
226     }
227   else
228     {
229       m_TDesc.NamespaceName = ns->Name();
230     }
231
232   UUID DocID;
233   if ( ! get_UUID_from_child_element("Id", &m_Root, DocID) )
234     {
235       DefaultLogSink(). Error("Id element missing from input document\n");
236       return RESULT_FORMAT;
237     }
238
239   memcpy(m_TDesc.AssetID, DocID.Value(), DocID.Size());
240   XMLElement* EditRate = m_Root.GetChildWithName("EditRate");
241
242   if ( EditRate == 0 )
243     {
244       DefaultLogSink(). Error("EditRate element missing from input document\n");
245       return RESULT_FORMAT;
246     }
247
248   m_TDesc.EditRate = decode_rational(EditRate->GetBody().c_str());
249
250   if ( m_TDesc.EditRate != EditRate_23_98
251        && m_TDesc.EditRate != EditRate_24
252        && m_TDesc.EditRate != EditRate_25
253        && m_TDesc.EditRate != EditRate_30
254        && m_TDesc.EditRate != EditRate_48
255        && m_TDesc.EditRate != EditRate_50
256        && m_TDesc.EditRate != EditRate_60 )
257     {
258       DefaultLogSink(). Error("Unexpected EditRate: %d/%d\n",
259                               m_TDesc.EditRate.Numerator, m_TDesc.EditRate.Denominator);
260       return RESULT_FORMAT;
261     }
262
263   // list of fonts
264   ElementList FontList;
265   m_Root.GetChildrenWithName("LoadFont", FontList);
266
267   for ( Elem_i i = FontList.begin(); i != FontList.end(); i++ )
268     {
269       UUID AssetID;
270       if ( ! get_UUID_from_element(*i, AssetID) )
271         {
272           DefaultLogSink(). Error("LoadFont element does not contain a urn:uuid value as expected.\n");
273           return RESULT_FORMAT;
274         }
275
276       TimedTextResourceDescriptor TmpResource;
277       memcpy(TmpResource.ResourceID, AssetID.Value(), UUIDlen);
278       TmpResource.Type = MT_OPENTYPE;
279       m_TDesc.ResourceList.push_back(TmpResource);
280       m_ResourceTypes.insert(ResourceTypeMap_t::value_type(UUID(TmpResource.ResourceID), MT_OPENTYPE));
281     }
282
283   // list of images
284   ElementList ImageList;
285   m_Root.GetChildrenWithName("Image", ImageList);
286   std::set<Kumu::UUID> visited_items;
287
288   for ( Elem_i i = ImageList.begin(); i != ImageList.end(); i++ )
289     {
290       UUID AssetID;
291       if ( ! get_UUID_from_element(*i, AssetID) )
292         {
293           DefaultLogSink(). Error("Image element does not contain a urn:uuid value as expected.\n");
294           return RESULT_FORMAT;
295         }
296
297       if ( visited_items.find(AssetID) == visited_items.end() )
298         {
299           TimedTextResourceDescriptor TmpResource;
300           memcpy(TmpResource.ResourceID, AssetID.Value(), UUIDlen);
301           TmpResource.Type = MT_PNG;
302           m_TDesc.ResourceList.push_back(TmpResource);
303           m_ResourceTypes.insert(ResourceTypeMap_t::value_type(UUID(TmpResource.ResourceID), MT_PNG));
304           visited_items.insert(AssetID);
305         }
306     }
307
308   // Calculate the timeline duration.
309   // This is a little ugly because the last element in the file is not necessarily
310   // the last instance to be displayed, e.g., element n and element n-1 may have the
311   // same start time but n-1 may have a greater duration making it the last to be seen.
312   // We must scan the list to accumulate the latest TimeOut value.
313   ElementList InstanceList;
314   ElementList::const_iterator ei;
315   ui32_t end_count = 0;
316   
317   m_Root.GetChildrenWithName("Subtitle", InstanceList);
318
319   if ( InstanceList.empty() )
320     {
321       DefaultLogSink(). Error("XML document contains no Subtitle elements.\n");
322       return RESULT_FORMAT;
323     }
324
325   // assumes edit rate is constrained above
326   ui32_t TCFrameRate = ( m_TDesc.EditRate == EditRate_23_98  ) ? 24 : m_TDesc.EditRate.Numerator;
327
328   S12MTimecode beginTC;
329   beginTC.SetFPS(TCFrameRate);
330   XMLElement* StartTime = m_Root.GetChildWithName("StartTime");
331
332   if ( StartTime != 0 )
333     beginTC.DecodeString(StartTime->GetBody());
334
335   for ( ei = InstanceList.begin(); ei != InstanceList.end(); ei++ )
336     {
337       S12MTimecode tmpTC((*ei)->GetAttrWithName("TimeOut"), TCFrameRate);
338       if ( end_count < tmpTC.GetFrames() )
339         end_count = tmpTC.GetFrames();
340     }
341
342   if ( end_count <= beginTC.GetFrames() )
343     {
344       DefaultLogSink(). Error("Timed Text file has zero-length timeline.\n");
345       return RESULT_FORMAT;
346     }
347
348   m_TDesc.ContainerDuration = end_count - beginTC.GetFrames();
349
350   return RESULT_OK;
351 }
352
353
354 //
355 Result_t
356 ASDCP::TimedText::DCSubtitleParser::h__SubtitleParser::ReadAncillaryResource(const byte_t* uuid, FrameBuffer& FrameBuf,
357                                                                              const IResourceResolver& Resolver) const
358 {
359   FrameBuf.AssetID(uuid);
360   UUID TmpID(uuid);
361   char buf[64];
362
363   ResourceTypeMap_t::const_iterator rmi = m_ResourceTypes.find(TmpID);
364
365   if ( rmi == m_ResourceTypes.end() )
366     {
367       DefaultLogSink().Error("Unknown ancillary resource id: %s\n", TmpID.EncodeHex(buf, 64));
368       return RESULT_RANGE;
369     }
370
371   Result_t result = Resolver.ResolveRID(uuid, FrameBuf);
372
373   if ( KM_SUCCESS(result) )
374     {
375       if ( (*rmi).second == MT_PNG )
376         FrameBuf.MIMEType("image/png");
377               
378       else if ( (*rmi).second == MT_OPENTYPE )
379         FrameBuf.MIMEType("application/x-font-opentype");
380
381       else
382         FrameBuf.MIMEType("application/octet-stream");
383     }
384
385   return result;
386 }
387
388 //------------------------------------------------------------------------------------------
389
390 ASDCP::TimedText::DCSubtitleParser::DCSubtitleParser()
391 {
392 }
393
394 ASDCP::TimedText::DCSubtitleParser::~DCSubtitleParser()
395 {
396 }
397
398 // Opens the stream for reading, parses enough data to provide a complete
399 // set of stream metadata for the MXFWriter below.
400 ASDCP::Result_t
401 ASDCP::TimedText::DCSubtitleParser::OpenRead(const std::string& filename) const
402 {
403   const_cast<ASDCP::TimedText::DCSubtitleParser*>(this)->m_Parser = new h__SubtitleParser;
404
405   Result_t result = m_Parser->OpenRead(filename);
406
407   if ( ASDCP_FAILURE(result) )
408     const_cast<ASDCP::TimedText::DCSubtitleParser*>(this)->m_Parser = 0;
409
410   return result;
411 }
412
413 // Parses an XML document to provide a complete set of stream metadata for the MXFWriter below.
414 Result_t
415 ASDCP::TimedText::DCSubtitleParser::OpenRead(const std::string& xml_doc, const std::string& filename) const
416 {
417   const_cast<ASDCP::TimedText::DCSubtitleParser*>(this)->m_Parser = new h__SubtitleParser;
418
419   Result_t result = m_Parser->OpenRead(xml_doc, filename);
420
421   if ( ASDCP_FAILURE(result) )
422     const_cast<ASDCP::TimedText::DCSubtitleParser*>(this)->m_Parser = 0;
423
424   return result;
425 }
426
427 //
428 ASDCP::Result_t
429 ASDCP::TimedText::DCSubtitleParser::FillTimedTextDescriptor(TimedTextDescriptor& TDesc) const
430 {
431   if ( m_Parser.empty() )
432     return RESULT_INIT;
433
434   TDesc = m_Parser->m_TDesc;
435   return RESULT_OK;
436 }
437
438 // Reads the complete Timed Text Resource into the given string.
439 ASDCP::Result_t
440 ASDCP::TimedText::DCSubtitleParser::ReadTimedTextResource(std::string& s) const
441 {
442   if ( m_Parser.empty() )
443     return RESULT_INIT;
444
445   s = m_Parser->m_XMLDoc;
446   return RESULT_OK;
447 }
448
449 //
450 ASDCP::Result_t
451 ASDCP::TimedText::DCSubtitleParser::ReadAncillaryResource(const byte_t* uuid, FrameBuffer& FrameBuf,
452                                                           const IResourceResolver* Resolver) const
453 {
454   if ( m_Parser.empty() )
455     return RESULT_INIT;
456
457   if ( Resolver == 0 )
458     Resolver = m_Parser->GetDefaultResolver();
459
460   return m_Parser->ReadAncillaryResource(uuid, FrameBuf, *Resolver);
461 }
462
463
464 //
465 // end AS_DCP_TimedTextParser.cpp
466 //