newlines are valid inside elements!
[asdcplib.git] / src / TimedText_Parser.cpp
1 /*
2 Copyright (c) 2007-2014, 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 #ifndef KM_WIN32
74   // TODO, fix this for win32 (needs regex)
75   FindInPath(PathMatchRegex(RID.EncodeHex(buf, 64)), m_Dirname, found_list);
76 #endif
77
78   if ( found_list.size() == 1 )
79     {
80       FileReader Reader;
81       DefaultLogSink().Debug("retrieving resource %s from file %s\n", buf, found_list.front().c_str());
82
83       result = Reader.OpenRead(found_list.front().c_str());
84
85       if ( KM_SUCCESS(result) )
86         {
87           ui32_t read_count, read_size = Reader.Size();
88           result = FrameBuf.Capacity(read_size);
89       
90           if ( KM_SUCCESS(result) )
91             result = Reader.Read(FrameBuf.Data(), read_size, &read_count);
92
93           if ( KM_SUCCESS(result) )
94             FrameBuf.Size(read_count);
95         }
96     }
97   else if ( ! found_list.empty() )
98     {
99       DefaultLogSink().Error("More than one file in %s matches %s.\n", m_Dirname.c_str(), buf);
100       result = RESULT_RAW_FORMAT;
101     }
102
103   return result;
104 }
105
106 //------------------------------------------------------------------------------------------
107
108 typedef std::map<Kumu::UUID, TimedText::MIMEType_t> ResourceTypeMap_t;
109
110 class ASDCP::TimedText::DCSubtitleParser::h__SubtitleParser
111 {
112   XMLElement  m_Root;
113   ResourceTypeMap_t m_ResourceTypes;
114   Result_t OpenRead();
115
116   ASDCP_NO_COPY_CONSTRUCT(h__SubtitleParser);
117
118 public:
119   std::string m_Filename;
120   std::string m_XMLDoc;
121   TimedTextDescriptor  m_TDesc;
122   mem_ptr<LocalFilenameResolver> m_DefaultResolver;
123
124   h__SubtitleParser() : m_Root("**ParserRoot**")
125   {
126     memset(&m_TDesc.AssetID, 0, UUIDlen);
127   }
128
129   ~h__SubtitleParser() {}
130
131   TimedText::IResourceResolver* GetDefaultResolver()
132   {
133     if ( m_DefaultResolver.empty() )
134       {
135         m_DefaultResolver = new LocalFilenameResolver();
136         m_DefaultResolver->OpenRead(PathDirname(m_Filename));
137       }
138
139     return m_DefaultResolver;
140   }
141
142   Result_t OpenRead(const std::string& filename);
143   Result_t OpenRead(const std::string& xml_doc, const std::string& filename);
144   Result_t ReadAncillaryResource(const byte_t* uuid, FrameBuffer& FrameBuf, const IResourceResolver& Resolver) const;
145 };
146
147 //
148 bool
149 get_UUID_from_element(XMLElement* Element, UUID& ID)
150 {
151   assert(Element);
152   const char* p = Element->GetBody().c_str();
153   if ( strncmp(p, "urn:uuid:", 9) == 0 )    p += 9;
154   return ID.DecodeHex(p);
155 }
156
157 //
158 bool
159 get_UUID_from_child_element(const char* name, XMLElement* Parent, UUID& outID)
160 {
161   assert(name); assert(Parent);
162   XMLElement* Child = Parent->GetChildWithName(name);
163   if ( Child == 0 )    return false;
164   return get_UUID_from_element(Child, outID);
165 }
166
167 //
168 static ASDCP::Rational
169 decode_rational(const char* str_rat)
170 {
171   assert(str_rat);
172   ui32_t Num = atoi(str_rat);
173   ui32_t Den = 0;
174
175   const char* den_str = strrchr(str_rat, ' ');
176   if ( den_str != 0 )
177     Den = atoi(den_str+1);
178
179   return ASDCP::Rational(Num, Den);
180 }
181
182 //
183 Result_t
184 ASDCP::TimedText::DCSubtitleParser::h__SubtitleParser::OpenRead(const std::string& filename)
185 {
186   Result_t result = ReadFileIntoString(filename, m_XMLDoc);
187
188   if ( KM_SUCCESS(result) )
189     result = OpenRead();
190
191   m_Filename = filename;
192   return result;
193 }
194
195 //
196 Result_t
197 ASDCP::TimedText::DCSubtitleParser::h__SubtitleParser::OpenRead(const std::string& xml_doc, const std::string& filename)
198 {
199   m_XMLDoc = xml_doc;
200
201   if ( filename.empty() )
202     {
203       m_Filename = "<string>";
204     }
205   else
206     {
207       m_Filename = filename;
208     }
209
210   return OpenRead();
211 }
212
213 //
214 Result_t
215 ASDCP::TimedText::DCSubtitleParser::h__SubtitleParser::OpenRead()
216 {
217   if ( ! m_Root.ParseString(m_XMLDoc.c_str()) )
218     return RESULT_FORMAT;
219
220   m_TDesc.EncodingName = "UTF-8"; // the XML parser demands UTF-8
221   m_TDesc.ResourceList.clear();
222   m_TDesc.ContainerDuration = 0;
223   const XMLNamespace* ns = m_Root.Namespace();
224
225   if ( ns == 0 )
226     {
227       DefaultLogSink(). Warn("Document has no namespace name, assuming %s\n", c_dcst_namespace_name);
228       m_TDesc.NamespaceName = c_dcst_namespace_name;
229     }
230   else
231     {
232       m_TDesc.NamespaceName = ns->Name();
233     }
234
235   UUID DocID;
236   if ( ! get_UUID_from_child_element("Id", &m_Root, DocID) )
237     {
238       DefaultLogSink(). Error("Id element missing from input document\n");
239       return RESULT_FORMAT;
240     }
241
242   memcpy(m_TDesc.AssetID, DocID.Value(), DocID.Size());
243   XMLElement* EditRate = m_Root.GetChildWithName("EditRate");
244
245   if ( EditRate == 0 )
246     {
247       DefaultLogSink(). Error("EditRate element missing from input document\n");
248       return RESULT_FORMAT;
249     }
250
251   m_TDesc.EditRate = decode_rational(EditRate->GetBody().c_str());
252
253   if ( m_TDesc.EditRate != EditRate_23_98
254        && m_TDesc.EditRate != EditRate_24
255        && m_TDesc.EditRate != EditRate_25
256        && m_TDesc.EditRate != EditRate_30
257        && m_TDesc.EditRate != EditRate_48
258        && m_TDesc.EditRate != EditRate_50
259        && m_TDesc.EditRate != EditRate_60 )
260     {
261       DefaultLogSink(). Error("Unexpected EditRate: %d/%d\n",
262                               m_TDesc.EditRate.Numerator, m_TDesc.EditRate.Denominator);
263       return RESULT_FORMAT;
264     }
265
266   // list of fonts
267   ElementList FontList;
268   m_Root.GetChildrenWithName("LoadFont", FontList);
269
270   for ( Elem_i i = FontList.begin(); i != FontList.end(); i++ )
271     {
272       UUID AssetID;
273       if ( ! get_UUID_from_element(*i, AssetID) )
274         {
275           DefaultLogSink(). Error("LoadFont element does not contain a urn:uuid value as expected.\n");
276           return RESULT_FORMAT;
277         }
278
279       TimedTextResourceDescriptor TmpResource;
280       memcpy(TmpResource.ResourceID, AssetID.Value(), UUIDlen);
281       TmpResource.Type = MT_OPENTYPE;
282       m_TDesc.ResourceList.push_back(TmpResource);
283       m_ResourceTypes.insert(ResourceTypeMap_t::value_type(UUID(TmpResource.ResourceID), MT_OPENTYPE));
284     }
285
286   // list of images
287   ElementList ImageList;
288   m_Root.GetChildrenWithName("Image", ImageList);
289   std::set<Kumu::UUID> visited_items;
290
291   for ( Elem_i i = ImageList.begin(); i != ImageList.end(); i++ )
292     {
293       UUID AssetID;
294       if ( ! get_UUID_from_element(*i, AssetID) )
295         {
296           DefaultLogSink(). Error("Image element does not contain a urn:uuid value as expected.\n");
297           return RESULT_FORMAT;
298         }
299
300       if ( visited_items.find(AssetID) == visited_items.end() )
301         {
302           TimedTextResourceDescriptor TmpResource;
303           memcpy(TmpResource.ResourceID, AssetID.Value(), UUIDlen);
304           TmpResource.Type = MT_PNG;
305           m_TDesc.ResourceList.push_back(TmpResource);
306           m_ResourceTypes.insert(ResourceTypeMap_t::value_type(UUID(TmpResource.ResourceID), MT_PNG));
307           visited_items.insert(AssetID);
308         }
309     }
310
311   // Calculate the timeline duration.
312   // This is a little ugly because the last element in the file is not necessarily
313   // the last instance to be displayed, e.g., element n and element n-1 may have the
314   // same start time but n-1 may have a greater duration making it the last to be seen.
315   // We must scan the list to accumulate the latest TimeOut value.
316   ElementList InstanceList;
317   ElementList::const_iterator ei;
318   ui32_t end_count = 0;
319   
320   m_Root.GetChildrenWithName("Subtitle", InstanceList);
321
322   if ( InstanceList.empty() )
323     {
324       DefaultLogSink(). Error("XML document contains no Subtitle elements.\n");
325       return RESULT_FORMAT;
326     }
327
328   // assumes edit rate is constrained above
329   ui32_t TCFrameRate = ( m_TDesc.EditRate == EditRate_23_98  ) ? 24 : m_TDesc.EditRate.Numerator;
330
331   S12MTimecode beginTC;
332   beginTC.SetFPS(TCFrameRate);
333   XMLElement* StartTime = m_Root.GetChildWithName("StartTime");
334
335   if ( StartTime != 0 )
336     beginTC.DecodeString(StartTime->GetBody());
337
338   for ( ei = InstanceList.begin(); ei != InstanceList.end(); ei++ )
339     {
340       S12MTimecode tmpTC((*ei)->GetAttrWithName("TimeOut"), TCFrameRate);
341       if ( end_count < tmpTC.GetFrames() )
342         end_count = tmpTC.GetFrames();
343     }
344
345   if ( end_count <= beginTC.GetFrames() )
346     {
347       DefaultLogSink(). Error("Timed Text file has zero-length timeline.\n");
348       return RESULT_FORMAT;
349     }
350
351   m_TDesc.ContainerDuration = end_count - beginTC.GetFrames();
352
353   return RESULT_OK;
354 }
355
356
357 //
358 Result_t
359 ASDCP::TimedText::DCSubtitleParser::h__SubtitleParser::ReadAncillaryResource(const byte_t* uuid, FrameBuffer& FrameBuf,
360                                                                              const IResourceResolver& Resolver) const
361 {
362   FrameBuf.AssetID(uuid);
363   UUID TmpID(uuid);
364   char buf[64];
365
366   ResourceTypeMap_t::const_iterator rmi = m_ResourceTypes.find(TmpID);
367
368   if ( rmi == m_ResourceTypes.end() )
369     {
370       DefaultLogSink().Error("Unknown ancillary resource id: %s\n", TmpID.EncodeHex(buf, 64));
371       return RESULT_RANGE;
372     }
373
374   Result_t result = Resolver.ResolveRID(uuid, FrameBuf);
375
376   if ( KM_SUCCESS(result) )
377     {
378       if ( (*rmi).second == MT_PNG )
379         FrameBuf.MIMEType("image/png");
380               
381       else if ( (*rmi).second == MT_OPENTYPE )
382         FrameBuf.MIMEType("application/x-font-opentype");
383
384       else
385         FrameBuf.MIMEType("application/octet-stream");
386     }
387
388   return result;
389 }
390
391 //------------------------------------------------------------------------------------------
392
393 ASDCP::TimedText::DCSubtitleParser::DCSubtitleParser()
394 {
395 }
396
397 ASDCP::TimedText::DCSubtitleParser::~DCSubtitleParser()
398 {
399 }
400
401 // Opens the stream for reading, parses enough data to provide a complete
402 // set of stream metadata for the MXFWriter below.
403 ASDCP::Result_t
404 ASDCP::TimedText::DCSubtitleParser::OpenRead(const std::string& filename) const
405 {
406   const_cast<ASDCP::TimedText::DCSubtitleParser*>(this)->m_Parser = new h__SubtitleParser;
407
408   Result_t result = m_Parser->OpenRead(filename);
409
410   if ( ASDCP_FAILURE(result) )
411     const_cast<ASDCP::TimedText::DCSubtitleParser*>(this)->m_Parser = 0;
412
413   return result;
414 }
415
416 // Parses an XML document to provide a complete set of stream metadata for the MXFWriter below.
417 Result_t
418 ASDCP::TimedText::DCSubtitleParser::OpenRead(const std::string& xml_doc, const std::string& filename) const
419 {
420   const_cast<ASDCP::TimedText::DCSubtitleParser*>(this)->m_Parser = new h__SubtitleParser;
421
422   Result_t result = m_Parser->OpenRead(xml_doc, filename);
423
424   if ( ASDCP_FAILURE(result) )
425     const_cast<ASDCP::TimedText::DCSubtitleParser*>(this)->m_Parser = 0;
426
427   return result;
428 }
429
430 //
431 ASDCP::Result_t
432 ASDCP::TimedText::DCSubtitleParser::FillTimedTextDescriptor(TimedTextDescriptor& TDesc) const
433 {
434   if ( m_Parser.empty() )
435     return RESULT_INIT;
436
437   TDesc = m_Parser->m_TDesc;
438   return RESULT_OK;
439 }
440
441 // Reads the complete Timed Text Resource into the given string.
442 ASDCP::Result_t
443 ASDCP::TimedText::DCSubtitleParser::ReadTimedTextResource(std::string& s) const
444 {
445   if ( m_Parser.empty() )
446     return RESULT_INIT;
447
448   s = m_Parser->m_XMLDoc;
449   return RESULT_OK;
450 }
451
452 //
453 ASDCP::Result_t
454 ASDCP::TimedText::DCSubtitleParser::ReadAncillaryResource(const byte_t* uuid, FrameBuffer& FrameBuf,
455                                                           const IResourceResolver* Resolver) const
456 {
457   if ( m_Parser.empty() )
458     return RESULT_INIT;
459
460   if ( Resolver == 0 )
461     Resolver = m_Parser->GetDefaultResolver();
462
463   return m_Parser->ReadAncillaryResource(uuid, FrameBuf, *Resolver);
464 }
465
466
467 //
468 // end AS_DCP_TimedTextParser.cpp
469 //