b6b2cc7c52897322b2a52760cbd91ef505ac99ae
[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     {
251       DefaultLogSink(). Error("Unexpected EditRate: %d/%d\n",
252                               m_TDesc.EditRate.Numerator, m_TDesc.EditRate.Denominator);
253       return RESULT_FORMAT;
254     }
255
256   // list of fonts
257   ElementList FontList;
258   m_Root.GetChildrenWithName("LoadFont", FontList);
259
260   for ( Elem_i i = FontList.begin(); i != FontList.end(); i++ )
261     {
262       UUID AssetID;
263       if ( ! get_UUID_from_element(*i, AssetID) )
264         {
265           DefaultLogSink(). Error("LoadFont element does not contain a urn:uuid value as expected.\n");
266           return RESULT_FORMAT;
267         }
268
269       TimedTextResourceDescriptor TmpResource;
270       memcpy(TmpResource.ResourceID, AssetID.Value(), UUIDlen);
271       TmpResource.Type = MT_OPENTYPE;
272       m_TDesc.ResourceList.push_back(TmpResource);
273       m_ResourceTypes.insert(ResourceTypeMap_t::value_type(UUID(TmpResource.ResourceID), MT_OPENTYPE));
274     }
275
276   // list of images
277   ElementList ImageList;
278   m_Root.GetChildrenWithName("Image", ImageList);
279   std::set<Kumu::UUID> visited_items;
280
281   for ( Elem_i i = ImageList.begin(); i != ImageList.end(); i++ )
282     {
283       UUID AssetID;
284       if ( ! get_UUID_from_element(*i, AssetID) )
285         {
286           DefaultLogSink(). Error("Image element does not contain a urn:uuid value as expected.\n");
287           return RESULT_FORMAT;
288         }
289
290       if ( visited_items.find(AssetID) == visited_items.end() )
291         {
292           TimedTextResourceDescriptor TmpResource;
293           memcpy(TmpResource.ResourceID, AssetID.Value(), UUIDlen);
294           TmpResource.Type = MT_PNG;
295           m_TDesc.ResourceList.push_back(TmpResource);
296           m_ResourceTypes.insert(ResourceTypeMap_t::value_type(UUID(TmpResource.ResourceID), MT_PNG));
297           visited_items.insert(AssetID);
298         }
299     }
300
301   // Calculate the timeline duration.
302   // This is a little ugly because the last element in the file is not necessarily
303   // the last instance to be displayed, e.g., element n and element n-1 may have the
304   // same start time but n-1 may have a greater duration making it the last to be seen.
305   // We must scan the list to accumulate the latest TimeOut value.
306   ElementList InstanceList;
307   ElementList::const_iterator ei;
308   ui32_t end_count = 0;
309   
310   m_Root.GetChildrenWithName("Subtitle", InstanceList);
311
312   if ( InstanceList.empty() )
313     {
314       DefaultLogSink(). Error("XML document contains no Subtitle elements.\n");
315       return RESULT_FORMAT;
316     }
317
318   // assumes edit rate is constrained above
319   ui32_t TCFrameRate = ( m_TDesc.EditRate == EditRate_23_98  ) ? 24 : m_TDesc.EditRate.Numerator;
320
321   S12MTimecode beginTC;
322   beginTC.SetFPS(TCFrameRate);
323   XMLElement* StartTime = m_Root.GetChildWithName("StartTime");
324
325   if ( StartTime != 0 )
326     beginTC.DecodeString(StartTime->GetBody());
327
328   for ( ei = InstanceList.begin(); ei != InstanceList.end(); ei++ )
329     {
330       S12MTimecode tmpTC((*ei)->GetAttrWithName("TimeOut"), TCFrameRate);
331       if ( end_count < tmpTC.GetFrames() )
332         end_count = tmpTC.GetFrames();
333     }
334
335   if ( end_count <= beginTC.GetFrames() )
336     {
337       DefaultLogSink(). Error("Timed Text file has zero-length timeline.\n");
338       return RESULT_FORMAT;
339     }
340
341   m_TDesc.ContainerDuration = end_count - beginTC.GetFrames();
342
343   return RESULT_OK;
344 }
345
346
347 //
348 Result_t
349 ASDCP::TimedText::DCSubtitleParser::h__SubtitleParser::ReadAncillaryResource(const byte_t* uuid, FrameBuffer& FrameBuf,
350                                                                              const IResourceResolver& Resolver) const
351 {
352   FrameBuf.AssetID(uuid);
353   UUID TmpID(uuid);
354   char buf[64];
355
356   ResourceTypeMap_t::const_iterator rmi = m_ResourceTypes.find(TmpID);
357
358   if ( rmi == m_ResourceTypes.end() )
359     {
360       DefaultLogSink().Error("Unknown ancillary resource id: %s\n", TmpID.EncodeHex(buf, 64));
361       return RESULT_RANGE;
362     }
363
364   Result_t result = Resolver.ResolveRID(uuid, FrameBuf);
365
366   if ( KM_SUCCESS(result) )
367     {
368       if ( (*rmi).second == MT_PNG )
369         FrameBuf.MIMEType("image/png");
370               
371       else if ( (*rmi).second == MT_OPENTYPE )
372         FrameBuf.MIMEType("application/x-font-opentype");
373
374       else
375         FrameBuf.MIMEType("application/octet-stream");
376     }
377
378   return result;
379 }
380
381 //------------------------------------------------------------------------------------------
382
383 ASDCP::TimedText::DCSubtitleParser::DCSubtitleParser()
384 {
385 }
386
387 ASDCP::TimedText::DCSubtitleParser::~DCSubtitleParser()
388 {
389 }
390
391 // Opens the stream for reading, parses enough data to provide a complete
392 // set of stream metadata for the MXFWriter below.
393 ASDCP::Result_t
394 ASDCP::TimedText::DCSubtitleParser::OpenRead(const std::string& filename) const
395 {
396   const_cast<ASDCP::TimedText::DCSubtitleParser*>(this)->m_Parser = new h__SubtitleParser;
397
398   Result_t result = m_Parser->OpenRead(filename);
399
400   if ( ASDCP_FAILURE(result) )
401     const_cast<ASDCP::TimedText::DCSubtitleParser*>(this)->m_Parser = 0;
402
403   return result;
404 }
405
406 // Parses an XML document to provide a complete set of stream metadata for the MXFWriter below.
407 Result_t
408 ASDCP::TimedText::DCSubtitleParser::OpenRead(const std::string& xml_doc, const std::string& filename) const
409 {
410   const_cast<ASDCP::TimedText::DCSubtitleParser*>(this)->m_Parser = new h__SubtitleParser;
411
412   Result_t result = m_Parser->OpenRead(xml_doc, filename);
413
414   if ( ASDCP_FAILURE(result) )
415     const_cast<ASDCP::TimedText::DCSubtitleParser*>(this)->m_Parser = 0;
416
417   return result;
418 }
419
420 //
421 ASDCP::Result_t
422 ASDCP::TimedText::DCSubtitleParser::FillTimedTextDescriptor(TimedTextDescriptor& TDesc) const
423 {
424   if ( m_Parser.empty() )
425     return RESULT_INIT;
426
427   TDesc = m_Parser->m_TDesc;
428   return RESULT_OK;
429 }
430
431 // Reads the complete Timed Text Resource into the given string.
432 ASDCP::Result_t
433 ASDCP::TimedText::DCSubtitleParser::ReadTimedTextResource(std::string& s) const
434 {
435   if ( m_Parser.empty() )
436     return RESULT_INIT;
437
438   s = m_Parser->m_XMLDoc;
439   return RESULT_OK;
440 }
441
442 //
443 ASDCP::Result_t
444 ASDCP::TimedText::DCSubtitleParser::ReadAncillaryResource(const byte_t* uuid, FrameBuffer& FrameBuf,
445                                                           const IResourceResolver* Resolver) const
446 {
447   if ( m_Parser.empty() )
448     return RESULT_INIT;
449
450   if ( Resolver == 0 )
451     Resolver = m_Parser->GetDefaultResolver();
452
453   return m_Parser->ReadAncillaryResource(uuid, FrameBuf, *Resolver);
454 }
455
456
457 //
458 // end AS_DCP_TimedTextParser.cpp
459 //