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