o The NamespaceURI property of AS-02 timed text files has been exposed in the API
[asdcplib.git] / src / ST2052_TextParser.cpp
1 /*
2 Copyright (c) 2013-2016, 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    ST2052_TimedText.cpp
28     \version $Id$       
29     \brief   AS-DCP library, PCM essence reader and writer implementation
30 */
31
32 #include "AS_02_internal.h"
33 #include "KM_xml.h"
34 #include <openssl/sha.h>
35
36 using namespace Kumu;
37 using namespace ASDCP;
38
39 using Kumu::DefaultLogSink;
40
41 const char* c_tt_namespace_name = "http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt";
42
43
44 //------------------------------------------------------------------------------------------
45
46 //
47 int const NS_ID_LENGTH = 16;
48
49 //
50 static byte_t s_png_id_prefix[NS_ID_LENGTH] = {
51   // RFC 4122 type 5
52   // 2067-2 5.4.5 / RFC4122 Appendix C
53   0x6b, 0xa7, 0xb8, 0x11, 0x9d, 0xad, 0x11, 0xd1,
54   0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8
55 };
56
57 //
58 static byte_t s_font_id_prefix[NS_ID_LENGTH] = {
59   // RFC 4122 type 5
60   // 2067-2 5.4.6
61   0xb6, 0xcc, 0x57, 0xa0, 0x87, 0xe7, 0x4e, 0x75,
62   0xb1, 0xc3, 0x33, 0x59, 0xf3, 0xae, 0x88, 0x17
63 };
64
65 //
66 static Kumu::UUID
67 create_4122_type5_id(const std::string& subject_name, const byte_t* ns_id)
68 {
69   SHA_CTX ctx;
70   SHA1_Init(&ctx);
71   SHA1_Update(&ctx, ns_id, NS_ID_LENGTH);
72   SHA1_Update(&ctx, (byte_t*)subject_name.c_str(), subject_name.size());
73
74   const ui32_t sha_len = 20;
75   byte_t bin_buf[sha_len];
76   SHA1_Final(bin_buf, &ctx);
77
78   // Derive the asset ID from the digest. Make it a type-5 UUID
79   byte_t buf[UUID_Length];
80   memcpy(buf, bin_buf, UUID_Length);
81   buf[6] &= 0x0f; // clear bits 4-7
82   buf[6] |= 0x50; // set UUID version 'digest'
83   buf[8] &= 0x3f; // clear bits 6&7
84   buf[8] |= 0x80; // set bit 7
85   return Kumu::UUID(buf);
86 }
87
88 //
89 static Kumu::UUID
90 create_png_name_id(const std::string& image_name)
91 {
92   return create_4122_type5_id(image_name, s_png_id_prefix);
93 }
94
95 //
96 static Kumu::UUID
97 create_font_name_id(const std::string& font_name)
98 {
99   return create_4122_type5_id(font_name, s_font_id_prefix);
100 }
101
102 //
103 static Kumu::Mutex sg_default_font_family_list_lock;
104 static std::set<std::string> sg_default_font_family_list;
105
106 static void
107 setup_default_font_family_list()
108 {
109   sg_default_font_family_list.insert("default");
110   sg_default_font_family_list.insert("monospace");
111   sg_default_font_family_list.insert("sansSerif");
112   sg_default_font_family_list.insert("serif");
113   sg_default_font_family_list.insert("monospaceSansSerif");
114   sg_default_font_family_list.insert("monospaceSerif");
115   sg_default_font_family_list.insert("proportionalSansSerif");
116   sg_default_font_family_list.insert("proportionalSerif");
117 }
118
119
120 //------------------------------------------------------------------------------------------
121
122
123 AS_02::TimedText::Type5UUIDFilenameResolver::Type5UUIDFilenameResolver() {}
124 AS_02::TimedText::Type5UUIDFilenameResolver::~Type5UUIDFilenameResolver() {}
125
126 const byte_t PNGMagic[8] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };
127 const byte_t OpenTypeMagic[5] = { 0x4f, 0x54, 0x54, 0x4f, 0x00 };
128 const byte_t TrueTypeMagic[5] = { 0x00, 0x01, 0x00, 0x00, 0x00 };
129
130 //
131 Result_t
132 AS_02::TimedText::Type5UUIDFilenameResolver::OpenRead(const std::string& dirname)
133 {
134   DirScannerEx dir_reader;
135   DirectoryEntryType_t ft;
136   std::string next_item;
137   std::string abs_dirname = PathMakeCanonical(dirname);
138   byte_t read_buffer[16];
139
140   if ( abs_dirname.empty() )
141     {
142       abs_dirname = ".";
143     }
144
145   Result_t result = dir_reader.Open(abs_dirname);
146
147   if ( KM_SUCCESS(result) )
148     {
149       while ( KM_SUCCESS(dir_reader.GetNext(next_item, ft)) )
150         {
151           if ( next_item[0] == '.' ) continue; // no hidden files
152           std::string tmp_path = PathJoin(abs_dirname, next_item);
153
154           if ( ft == DET_FILE )
155             {
156               FileReader reader;
157               Result_t read_result = reader.OpenRead(tmp_path);
158
159               if ( KM_SUCCESS(read_result) )
160                 {
161                   read_result = reader.Read(read_buffer, 16);
162                 }
163
164               if ( KM_SUCCESS(read_result) )
165                 {
166                   // is it PNG?
167                   if ( memcmp(read_buffer, PNGMagic, sizeof(PNGMagic)) == 0 )
168                     {
169                       UUID asset_id = create_png_name_id(next_item);
170                       m_ResourceMap.insert(ResourceMap::value_type(asset_id, next_item));
171                     }
172                   // is it a font?
173                   else if ( memcmp(read_buffer, OpenTypeMagic, sizeof(OpenTypeMagic)) == 0
174                             || memcmp(read_buffer, TrueTypeMagic, sizeof(TrueTypeMagic)) == 0 )
175                     {
176                       std::string font_root_name = PathSetExtension(next_item, "");
177                       UUID asset_id = create_font_name_id(font_root_name);
178                       m_ResourceMap.insert(ResourceMap::value_type(asset_id, next_item));
179                     }
180                 }
181             }
182         }
183     }
184
185   return result;
186 }
187
188 //
189 Result_t
190 AS_02::TimedText::Type5UUIDFilenameResolver::ResolveRID(const byte_t* uuid, ASDCP::TimedText::FrameBuffer& FrameBuf) const
191 {
192   Kumu::UUID tmp_id(uuid);
193   char buf[64];
194
195   ResourceMap::const_iterator i = m_ResourceMap.find(tmp_id);
196
197   if ( i == m_ResourceMap.end() )
198     {
199       DefaultLogSink().Debug("Missing timed-text resource \"%s\"\n", tmp_id.EncodeHex(buf, 64));
200       return RESULT_NOT_FOUND;
201     }
202
203   FileReader Reader;
204
205   DefaultLogSink().Debug("Retrieving resource %s from file %s\n", tmp_id.EncodeHex(buf, 64), i->second.c_str());
206
207   Result_t result = Reader.OpenRead(i->second.c_str());
208
209   if ( KM_SUCCESS(result) )
210     {
211       ui32_t read_count, read_size = Reader.Size();
212       result = FrameBuf.Capacity(read_size);
213       
214       if ( KM_SUCCESS(result) )
215         result = Reader.Read(FrameBuf.Data(), read_size, &read_count);
216       
217       if ( KM_SUCCESS(result) )
218         FrameBuf.Size(read_count);
219     }
220
221   return result;
222 }
223
224 //------------------------------------------------------------------------------------------
225
226 typedef std::map<Kumu::UUID, ASDCP::TimedText::MIMEType_t> ResourceTypeMap_t;
227
228 class AS_02::TimedText::ST2052_TextParser::h__TextParser
229 {
230   XMLElement  m_Root;
231   ResourceTypeMap_t m_ResourceTypes;
232   Result_t OpenRead(const std::string& profile_name);
233
234   ASDCP_NO_COPY_CONSTRUCT(h__TextParser);
235
236 public:
237   std::string m_Filename;
238   std::string m_XMLDoc;
239   TimedTextDescriptor  m_TDesc;
240   ASDCP::mem_ptr<ASDCP::TimedText::IResourceResolver> m_DefaultResolver;
241
242   h__TextParser() : m_Root("**ParserRoot**")
243   {
244     memset(&m_TDesc.AssetID, 0, UUIDlen);
245   }
246
247   ~h__TextParser() {}
248
249   ASDCP::TimedText::IResourceResolver* GetDefaultResolver()
250   {
251     if ( m_DefaultResolver.empty() )
252       {
253         AS_02::TimedText::Type5UUIDFilenameResolver *resolver = new AS_02::TimedText::Type5UUIDFilenameResolver;
254         resolver->OpenRead(PathDirname(m_Filename));
255         m_DefaultResolver = resolver;
256       }
257     
258     return m_DefaultResolver;
259   }
260
261   Result_t OpenRead(const std::string& filename, const std::string& profile_name);
262   Result_t OpenRead(const std::string& xml_doc, const std::string& filename, const std::string& profile_name);
263   Result_t ReadAncillaryResource(const byte_t *uuid, ASDCP::TimedText::FrameBuffer& FrameBuf,
264                                  const ASDCP::TimedText::IResourceResolver& Resolver) const;
265 };
266
267 //
268 Result_t
269 AS_02::TimedText::ST2052_TextParser::h__TextParser::OpenRead(const std::string& filename, const std::string& profile_name)
270 {
271   Result_t result = ReadFileIntoString(filename, m_XMLDoc);
272
273   if ( KM_SUCCESS(result) )
274     {
275       m_Filename = filename;
276       result = OpenRead(profile_name);
277     }
278
279   return result;
280 }
281
282 //
283 Result_t
284 AS_02::TimedText::ST2052_TextParser::h__TextParser::OpenRead(const std::string& xml_doc, const std::string& filename,
285                                                              const std::string& profile_name)
286 {
287   m_XMLDoc = xml_doc;
288   m_Filename = filename;
289   return OpenRead(profile_name);
290 }
291
292 //
293 template <class VisitorType>
294 bool
295 apply_visitor(const XMLElement& element, VisitorType& visitor)
296 {
297   const ElementList& l = element.GetChildren();
298   ElementList::const_iterator i;
299
300   for ( i = l.begin(); i != l.end(); ++i )
301     {
302       if ( ! visitor.Element(**i) )
303         {
304           return false;
305         }
306
307       if ( ! apply_visitor(**i, visitor) )
308         {
309           return false;
310         }
311     }
312
313   return true;
314 }
315
316 //
317 class AttributeVisitor
318 {
319   std::string attr_name;
320
321 public:
322   AttributeVisitor(const std::string& n) : attr_name(n) {}
323   std::set<std::string> value_list;
324
325   bool Element(const XMLElement& e)
326   {
327     const AttributeList& l = e.GetAttributes();
328     AttributeList::const_iterator i;
329  
330     for ( i = l.begin(); i != l.end(); ++i )
331       {
332         if ( i->name == attr_name )
333           {
334             value_list.insert(i->value);
335           }
336       }
337
338     return true;
339   }
340 };
341
342 //
343 Result_t
344 AS_02::TimedText::ST2052_TextParser::h__TextParser::OpenRead(const std::string& profile_name)
345 {
346   setup_default_font_family_list();
347
348   if ( ! m_Root.ParseString(m_XMLDoc.c_str()) )
349     {
350       DefaultLogSink(). Error("ST 2052-1 document is not well-formed.\n");
351       return RESULT_FORMAT;
352     }
353
354   m_TDesc.EncodingName = "UTF-8"; // the XML parser demands UTF-8
355   m_TDesc.ResourceList.clear();
356   m_TDesc.ContainerDuration = 0;
357   m_TDesc.NamespaceName = profile_name;
358
359   AttributeVisitor png_visitor("backgroundImage");
360   apply_visitor(m_Root, png_visitor);
361   std::set<std::string>::const_iterator i;
362
363   for ( i = png_visitor.value_list.begin(); i != png_visitor.value_list.end(); ++i )
364     {
365       UUID asset_id = create_png_name_id(*i);
366       TimedTextResourceDescriptor png_resource;
367       memcpy(png_resource.ResourceID, asset_id.Value(), UUIDlen);
368       png_resource.Type = ASDCP::TimedText::MT_PNG;
369       m_TDesc.ResourceList.push_back(png_resource);
370       m_ResourceTypes.insert(ResourceTypeMap_t::value_type(UUID(png_resource.ResourceID),
371                                                            ASDCP::TimedText::MT_PNG));
372     }
373
374   AttributeVisitor font_visitor("fontFamily");
375   apply_visitor(m_Root, font_visitor);
376   char buf[64];
377
378   for ( i = font_visitor.value_list.begin(); i != font_visitor.value_list.end(); ++i )
379     {
380       UUID font_id = create_font_name_id(*i);
381
382       if ( PathIsFile(font_id.EncodeHex(buf, 64))
383            || PathIsFile(*i+".ttf")
384            || PathIsFile(*i+".otf") )
385         {
386           TimedTextResourceDescriptor font_resource;
387           memcpy(font_resource.ResourceID, font_id.Value(), UUIDlen);
388           font_resource.Type = ASDCP::TimedText::MT_OPENTYPE;
389           m_TDesc.ResourceList.push_back(font_resource);
390           m_ResourceTypes.insert(ResourceTypeMap_t::value_type(UUID(font_resource.ResourceID),
391                                                                ASDCP::TimedText::MT_OPENTYPE));
392         }
393       else
394         {
395           AutoMutex l(sg_default_font_family_list_lock);
396           if ( sg_default_font_family_list.find(*i) == sg_default_font_family_list.end() )
397             {
398               DefaultLogSink(). Error("Unable to locate external font resource \"%s\".\n", i->c_str());
399               return RESULT_FORMAT;
400             }
401         }
402     }
403
404   return RESULT_OK;
405 }
406
407 //
408 Result_t
409 AS_02::TimedText::ST2052_TextParser::h__TextParser::ReadAncillaryResource(const byte_t* uuid, ASDCP::TimedText::FrameBuffer& FrameBuf,
410                                                                           const ASDCP::TimedText::IResourceResolver& Resolver) const
411 {
412   FrameBuf.AssetID(uuid);
413   UUID TmpID(uuid);
414   char buf[64];
415
416   ResourceTypeMap_t::const_iterator rmi = m_ResourceTypes.find(TmpID);
417
418   if ( rmi == m_ResourceTypes.end() )
419     {
420       DefaultLogSink().Error("Unknown ancillary resource id: %s\n", TmpID.EncodeHex(buf, 64));
421       return RESULT_RANGE;
422     }
423
424   Result_t result = Resolver.ResolveRID(uuid, FrameBuf);
425
426   if ( KM_SUCCESS(result) )
427     {
428       if ( (*rmi).second == ASDCP::TimedText::MT_PNG )
429         {
430           FrameBuf.MIMEType("image/png");
431         }    
432       else if ( (*rmi).second == ASDCP::TimedText::MT_OPENTYPE )
433         {
434           FrameBuf.MIMEType("application/x-font-opentype");
435         }
436       else
437         {
438           FrameBuf.MIMEType("application/octet-stream");
439         }
440     }
441
442   return result;
443 }
444
445
446
447 //------------------------------------------------------------------------------------------
448
449 AS_02::TimedText::ST2052_TextParser::ST2052_TextParser()
450 {
451 }
452
453 AS_02::TimedText::ST2052_TextParser::~ST2052_TextParser()
454 {
455 }
456
457 // Opens the stream for reading, parses enough data to provide a complete
458 // set of stream metadata for the MXFWriter below.
459 ASDCP::Result_t
460 AS_02::TimedText::ST2052_TextParser::OpenRead(const std::string& filename, const std::string& profile_name) const
461 {
462   const_cast<AS_02::TimedText::ST2052_TextParser*>(this)->m_Parser = new h__TextParser;
463
464   Result_t result = m_Parser->OpenRead(filename, profile_name);
465
466   if ( ASDCP_FAILURE(result) )
467     const_cast<AS_02::TimedText::ST2052_TextParser*>(this)->m_Parser = 0;
468
469   return result;
470 }
471
472 // Parses an XML document to provide a complete set of stream metadata for the MXFWriter below.
473 Result_t
474 AS_02::TimedText::ST2052_TextParser::OpenRead(const std::string& xml_doc, const std::string& filename,
475                                               const std::string& profile_name) const
476 {
477   const_cast<AS_02::TimedText::ST2052_TextParser*>(this)->m_Parser = new h__TextParser;
478
479   Result_t result = m_Parser->OpenRead(xml_doc, filename, profile_name);
480
481   if ( ASDCP_FAILURE(result) )
482     const_cast<AS_02::TimedText::ST2052_TextParser*>(this)->m_Parser = 0;
483
484   return result;
485 }
486
487 //
488 ASDCP::Result_t
489 AS_02::TimedText::ST2052_TextParser::FillTimedTextDescriptor(TimedTextDescriptor& TDesc) const
490 {
491   if ( m_Parser.empty() )
492     return RESULT_INIT;
493
494   TDesc = m_Parser->m_TDesc;
495   return RESULT_OK;
496 }
497
498 // Reads the complete Timed Text Resource into the given string.
499 ASDCP::Result_t
500 AS_02::TimedText::ST2052_TextParser::ReadTimedTextResource(std::string& s) const
501 {
502   if ( m_Parser.empty() )
503     return RESULT_INIT;
504
505   s = m_Parser->m_XMLDoc;
506   return RESULT_OK;
507 }
508
509 //
510 ASDCP::Result_t
511 AS_02::TimedText::ST2052_TextParser::ReadAncillaryResource(const Kumu::UUID& uuid, ASDCP::TimedText::FrameBuffer& FrameBuf,
512                                                            const ASDCP::TimedText::IResourceResolver* Resolver) const
513 {
514   if ( m_Parser.empty() )
515     return RESULT_INIT;
516
517   if ( Resolver == 0 )
518     Resolver = m_Parser->GetDefaultResolver();
519
520   return m_Parser->ReadAncillaryResource(uuid.Value(), FrameBuf, *Resolver);
521 }
522
523
524 //
525 // end ST2052_TextParser.cpp
526 //