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