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