Tag Parser  6.5.1
C++ library for reading and writing MP4 (iTunes), ID3, Vorbis, Opus, FLAC and Matroska tags
mp4tagfield.cpp
Go to the documentation of this file.
1 #include "./mp4container.h"
2 #include "./mp4tagfield.h"
3 #include "./mp4atom.h"
4 #include "./mp4ids.h"
5 
6 #include "../exceptions.h"
7 
8 #include <c++utilities/conversion/stringbuilder.h>
9 #include <c++utilities/io/binaryreader.h>
10 #include <c++utilities/io/binarywriter.h>
11 
12 #include <algorithm>
13 #include <memory>
14 #include <limits>
15 
16 using namespace std;
17 using namespace IoUtilities;
18 using namespace ConversionUtilities;
19 
20 namespace Media {
21 
30 Mp4TagField::Mp4TagField() :
31  m_parsedRawDataType(RawDataType::Reserved),
32  m_countryIndicator(0),
33  m_langIndicator(0)
34 {}
35 
39 Mp4TagField::Mp4TagField(identifierType id, const TagValue &value) :
40  TagField<Mp4TagField>(id, value),
41  m_parsedRawDataType(RawDataType::Reserved),
42  m_countryIndicator(0),
43  m_langIndicator(0)
44 {}
45 
55 Mp4TagField::Mp4TagField(const string &mean, const string &name, const TagValue &value) :
56  Mp4TagField(Mp4TagAtomIds::Extended, value)
57 {
58  m_name = name;
59  m_mean = mean;
60 }
61 
72 void Mp4TagField::reparse(Mp4Atom &ilstChild)
73 {
74  // prepare reparsing
75  using namespace Mp4AtomIds;
76  using namespace Mp4TagAtomIds;
78  string context("parsing MP4 tag field");
79  clear(); // clear old values
80  ilstChild.parse(); // ensure child has been parsed
81  setId(ilstChild.id());
82  context = "parsing MP4 tag field " + ilstChild.idToString();
83  iostream &stream = ilstChild.stream();
84  BinaryReader &reader = ilstChild.container().reader();
85  int dataAtomFound = 0, meanAtomFound = 0, nameAtomFound = 0;
86  for(Mp4Atom *dataAtom = ilstChild.firstChild(); dataAtom; dataAtom = dataAtom->nextSibling()) {
87  try {
88  dataAtom->parse();
89  if(dataAtom->id() == Mp4AtomIds::Data) {
90  if(dataAtom->dataSize() < 8) {
91  addNotification(NotificationType::Warning, "Truncated child atom \"data\" in tag atom (ilst child) found. (will be ignored)", context);
92  continue;
93  }
94  if(++dataAtomFound > 1) {
95  if(dataAtomFound == 2) {
96  addNotification(NotificationType::Warning, "Multiple \"data\" child atom in tag atom (ilst child) found. (will be ignored)", context);
97  }
98  continue;
99  }
100  stream.seekg(dataAtom->dataOffset());
101  if(reader.readByte() != 0) {
102  addNotification(NotificationType::Warning, "The version indicator byte is not zero, the tag atom might be unsupported and hence not be parsed correctly.", context);
103  }
104  setTypeInfo(m_parsedRawDataType = reader.readUInt24BE());
105  try { // try to show warning if parsed raw data type differs from expected raw data type for this atom id
106  const vector<uint32> expectedRawDataTypes = this->expectedRawDataTypes();
107  if(find(expectedRawDataTypes.cbegin(), expectedRawDataTypes.cend(), m_parsedRawDataType) == expectedRawDataTypes.cend()) {
108  addNotification(NotificationType::Warning, "Unexpected data type indicator found.", context);
109  }
110  } catch(const Failure &) {
111  // tag id is unknown, it is not possible to validate parsed data type
112  }
113  m_countryIndicator = reader.readUInt16BE();
114  m_langIndicator = reader.readUInt16BE();
115  switch(m_parsedRawDataType) {
117  stream.seekg(dataAtom->dataOffset() + 8);
118  value().assignText(reader.readString(dataAtom->dataSize() - 8), (m_parsedRawDataType == RawDataType::Utf16) ? TagTextEncoding::Utf16BigEndian : TagTextEncoding::Utf8);
119  break;
121  switch(m_parsedRawDataType) {
122  case RawDataType::Gif:
123  value().setMimeType("image/gif");
124  break;
125  case RawDataType::Jpeg:
126  value().setMimeType("image/jpeg");
127  break;
128  case RawDataType::Png:
129  value().setMimeType("image/png");
130  break;
131  case RawDataType::Bmp:
132  value().setMimeType("image/bmp");
133  break;
134  default:
135  ;
136  }
137  const streamsize coverSize = dataAtom->dataSize() - 8;
138  unique_ptr<char []> coverData = make_unique<char []>(coverSize);
139  stream.read(coverData.get(), coverSize);
140  value().assignData(move(coverData), coverSize, TagDataType::Picture);
141  break;
142  } case RawDataType::BeSignedInt: {
143  int number = 0;
144  if(dataAtom->dataSize() > (8 + 4)) {
145  addNotification(NotificationType::Warning, "Data atom stores integer of invalid size. Trying to read data anyways.", context);
146  }
147  if(dataAtom->dataSize() >= (8 + 4)) {
148  number = reader.readInt32BE();
149  } else if(dataAtom->dataSize() == (8 + 2)) {
150  number = reader.readInt16BE();
151  } else if(dataAtom->dataSize() == (8 + 1)) {
152  number = reader.readChar();
153  }
154  switch(ilstChild.id()) {
155  case PreDefinedGenre: // consider number as standard genre index
157  break;
158  default:
159  value().assignInteger(number);
160  }
161  break;
163  int number = 0;
164  if(dataAtom->dataSize() > (8 + 4)) {
165  addNotification(NotificationType::Warning, "Data atom stores integer of invalid size. Trying to read data anyways.", context);
166  }
167  if(dataAtom->dataSize() >= (8 + 4)) {
168  number = static_cast<int>(reader.readUInt32BE());
169  } else if(dataAtom->dataSize() == (8 + 2)) {
170  number = static_cast<int>(reader.readUInt16BE());
171  } else if(dataAtom->dataSize() == (8 + 1)) {
172  number = static_cast<int>(reader.readByte());
173  }
174  switch(ilstChild.id()) {
175  case PreDefinedGenre: // consider number as standard genre index
176  value().assignStandardGenreIndex(number - 1);
177  break;
178  default:
179  value().assignInteger(number);
180  }
181  break;
182  } default:
183  switch(ilstChild.id()) {
184  // track number, disk number and genre have no specific data type id
185  case TrackPosition:
186  case DiskPosition: {
187  if(dataAtom->dataSize() < (8 + 6)) {
188  addNotification(NotificationType::Warning, "Track/disk position is truncated. Trying to read data anyways.", context);
189  }
190  uint16 pos = 0, total = 0;
191  if(dataAtom->dataSize() >= (8 + 4)) {
192  stream.seekg(2, ios_base::cur);
193  pos = reader.readUInt16BE();
194  }
195  if(dataAtom->dataSize() >= (8 + 6)) {
196  total = reader.readUInt16BE();
197  }
198  value().assignPosition(PositionInSet(pos, total));
199  break;
200  }
201  case PreDefinedGenre:
202  if(dataAtom->dataSize() < (8 + 2)) {
203  addNotification(NotificationType::Warning, "Genre index is truncated.", context);
204  } else {
205  value().assignStandardGenreIndex(reader.readUInt16BE() - 1);
206  }
207  break;
208  default: // no supported data type, read raw data
209  streamsize dataSize = dataAtom->dataSize() - 8;
210  unique_ptr<char []> data = make_unique<char[]>(dataSize);
211  stream.read(data.get(), dataSize);
212  if(ilstChild.id() == Mp4TagAtomIds::Cover) {
213  value().assignData(move(data), dataSize, TagDataType::Picture);
214  } else {
215  value().assignData(move(data), dataSize, TagDataType::Undefined);
216  }
217  }
218  }
219  } else if(dataAtom->id() == Mp4AtomIds::Mean) {
220  if(dataAtom->dataSize() < 8) {
221  addNotification(NotificationType::Warning, "Truncated child atom \"mean\" in tag atom (ilst child) found. (will be ignored)", context);
222  continue;
223  }
224  if(++meanAtomFound > 1) {
225  if(meanAtomFound == 2) {
226  addNotification(NotificationType::Warning, "Tag atom contains more than one mean atom. The addiational mean atoms will be ignored.", context);
227  }
228  continue;
229  }
230  stream.seekg(dataAtom->dataOffset() + 4);
231  m_mean = reader.readString(dataAtom->dataSize() - 4);
232  } else if(dataAtom->id() == Mp4AtomIds::Name) {
233  if(dataAtom->dataSize() < 4) {
234  addNotification(NotificationType::Warning, "Truncated child atom \"name\" in tag atom (ilst child) found. (will be ignored)", context);
235  continue;
236  }
237  if(++nameAtomFound > 1) {
238  if(nameAtomFound == 2) {
239  addNotification(NotificationType::Warning, "Tag atom contains more than one name atom. The addiational name atoms will be ignored.", context);
240  }
241  continue;
242  }
243  stream.seekg(dataAtom->dataOffset() + 4);
244  m_name = reader.readString(dataAtom->dataSize() - 4);
245  } else {
246  addNotification(NotificationType::Warning, "Unkown child atom \"" % dataAtom->idToString() + "\" in tag atom (ilst child) found. (will be ignored)", context);
247  }
248  } catch(const Failure &) {
249  addNotification(NotificationType::Warning, "Unable to parse all childs atom in tag atom (ilst child) found. (will be ignored)", context);
250  }
251  }
252  if(value().isEmpty()) {
253  addNotification(NotificationType::Warning, "The field value is empty.", context);
254  }
255 }
256 
268 {
269  return Mp4TagFieldMaker(*this);
270 }
271 
279 void Mp4TagField::make(ostream &stream)
280 {
281  prepareMaking().make(stream);
282 }
283 
287 std::vector<uint32> Mp4TagField::expectedRawDataTypes() const
288 {
289  using namespace Mp4TagAtomIds;
290  std::vector<uint32> res;
291  switch(id()) {
292  case Album: case Artist: case Comment:
293  case Year: case Title: case Genre:
294  case Composer: case Encoder: case Grouping:
295  case Description: case Lyrics: case RecordLabel:
296  case Performers: case Lyricist:
297  res.push_back(RawDataType::Utf8);
298  res.push_back(RawDataType::Utf16);
299  break;
300  case PreDefinedGenre: case TrackPosition: case DiskPosition:
301  res.push_back(RawDataType::Reserved);
302  break;
303  case Bpm: case Rating:
304  res.push_back(RawDataType::BeSignedInt);
305  res.push_back(RawDataType::BeUnsignedInt);
306  break;
307  case Cover:
308  res.push_back(RawDataType::Gif);
309  res.push_back(RawDataType::Jpeg);
310  res.push_back(RawDataType::Png);
311  res.push_back(RawDataType::Bmp);
312  break;
313  case Extended:
315  throw Failure();
316  }
317  // assumption that extended "iTunes" tags always use Unicode correct?
318  res.push_back(RawDataType::Utf8);
319  res.push_back(RawDataType::Utf16);
320  break;
321  default:
322  throw Failure();
323  }
324  return res;
325 }
326 
335 {
336  using namespace Mp4TagAtomIds;
337  if(isTypeInfoAssigned()) {
338  // obtain raw data type from tag field if present
339  return typeInfo();
340  }
341 
342  // there is no raw data type assigned (tag field was not
343  // present in original file but rather was added manually)
344  // try to derive appropriate raw data type from atom id
345  switch(id()) {
346  case Album: case Artist: case Comment:
347  case Year: case Title: case Genre:
348  case Composer: case Encoder: case Grouping:
349  case Description: case Lyrics: case RecordLabel:
350  case Performers: case Lyricist:
351  switch(value().dataEncoding()) {
354  default: ;
355  }
356  break;
357  case TrackPosition: case DiskPosition:
358  return RawDataType::Reserved;
359  case PreDefinedGenre: case Bpm: case Rating:
361  case Cover: {
362  const string &mimeType = value().mimeType();
363  if(mimeType == "image/jpg" || mimeType == "image/jpeg") { // "well-known" type
364  return RawDataType::Jpeg;
365  } else if(mimeType == "image/png") {
366  return RawDataType::Png;
367  } else if(mimeType == "image/bmp") {
368  return RawDataType::Bmp;
369  }
370  }
371  break;
372  case Extended:
374  throw Failure();
375  }
376  switch(value().dataEncoding()) {
379  default: ;
380  }
381  break;
382  default:
383  ;
384  }
385  throw Failure();
386 }
387 
392 {
393  m_name.clear();
394  m_mean.clear();
395  m_parsedRawDataType = RawDataType::Reserved;
396  m_countryIndicator = 0;
397  m_langIndicator = 0;
398 }
399 
411 Mp4TagFieldMaker::Mp4TagFieldMaker(Mp4TagField &field) :
412  m_field(field),
413  m_convertedData(stringstream::in | stringstream::out | stringstream::binary),
414  m_writer(&m_convertedData),
415  m_rawDataType(0)
416 {
417  m_field.invalidateStatus();
418  if(!m_field.id()) {
419  m_field.addNotification(NotificationType::Warning, "Invalid tag atom id.", "making MP4 tag field");
420  throw InvalidDataException();
421  }
422  const string context("making MP4 tag field " + Mp4TagField::fieldIdToString(m_field.id()));
423  if(m_field.value().isEmpty() && (!m_field.mean().empty() || !m_field.name().empty())) {
424  m_field.addNotification(NotificationType::Critical, "No tag value assigned.", context);
425  throw InvalidDataException();
426  }
427 
428  try {
429  // try to use appropriate raw data type
430  m_rawDataType = m_field.appropriateRawDataType();
431  } catch(const Failure &) {
432  // unable to obtain appropriate raw data type
433  if(m_field.id() == Mp4TagAtomIds::Cover) {
434  // assume JPEG image
435  m_rawDataType = RawDataType::Utf8;
436  m_field.addNotification(NotificationType::Warning, "It was not possible to find an appropriate raw data type id. JPEG image will be assumed.", context);
437  } else {
438  // assume UTF-8 text
439  m_rawDataType = RawDataType::Utf8;
440  m_field.addNotification(NotificationType::Warning, "It was not possible to find an appropriate raw data type id. UTF-8 will be assumed.", context);
441  }
442  }
443 
444  try {
445  if(!m_field.value().isEmpty()) { // there might be only mean and name info, but no data
446  m_convertedData.exceptions(std::stringstream::failbit | std::stringstream::badbit);
447  switch(m_rawDataType) {
448  case RawDataType::Utf8:
449  case RawDataType::Utf16:
450  m_writer.writeString(m_field.value().toString());
451  break;
453  int number = m_field.value().toInteger();
454  if(number <= numeric_limits<int16>::max()
455  && number >= numeric_limits<int16>::min()) {
456  m_writer.writeInt16BE(static_cast<int16>(number));
457  } else {
458  m_writer.writeInt32BE(number);
459  }
460  break;
462  int number = m_field.value().toInteger();
463  if(number <= numeric_limits<uint16>::max()
464  && number >= numeric_limits<uint16>::min()) {
465  m_writer.writeUInt16BE(static_cast<uint16>(number));
466  } else if(number > 0) {
467  m_writer.writeUInt32BE(number);
468  } else {
469  throw ConversionException("Negative integer can not be assigned to the field with the id \"" % interpretIntegerAsString<uint32>(m_field.id()) + "\".");
470  }
471  break;
473  break; // leave converted data empty to write original data later
474  default:
475  switch(m_field.id()) {
476  // track number and disk number are exceptions
477  // raw data type 0 is used, information is stored as pair of unsigned integers
479  PositionInSet pos = m_field.value().toPositionInSet();
480  m_writer.writeInt32BE(pos.position());
481  if(pos.total() <= numeric_limits<int16>::max()) {
482  m_writer.writeInt16BE(static_cast<int16>(pos.total()));
483  } else {
484  throw ConversionException("Integer can not be assigned to the field with the id \"" % interpretIntegerAsString<uint32>(m_field.id()) + "\" because it is to big.");
485  }
486  m_writer.writeUInt16BE(0);
487  break;
488  }
490  m_writer.writeUInt16BE(m_field.value().toStandardGenreIndex());
491  break;
492  default:
493  ; // leave converted data empty to write original data later
494  }
495  }
496  }
497  } catch (ConversionException &ex) {
498  // it was not possible to perform required conversions
499  if(char_traits<char>::length(ex.what())) {
500  m_field.addNotification(NotificationType::Critical, ex.what(), context);
501  } else {
502  m_field.addNotification(NotificationType::Critical, "The assigned tag value can not be converted to be written appropriately.", context);
503  }
504  throw InvalidDataException();
505  }
506 
507  // calculate data size
508  m_dataSize = m_field.value().isEmpty()
509  ? 0 : (m_convertedData.tellp() ? static_cast<size_t>(m_convertedData.tellp()) : m_field.value().dataSize());
510  m_totalSize = 8 // calculate entire size
511  + (m_field.name().empty() ? 0 : (12 + m_field.name().length()))
512  + (m_field.mean().empty() ? 0 : (12 + m_field.mean().length()))
513  + (m_dataSize ? (16 + m_dataSize) : 0);
514 }
515 
523 void Mp4TagFieldMaker::make(ostream &stream)
524 {
525  m_writer.setStream(&stream);
526  // size of entire tag atom
527  m_writer.writeUInt32BE(m_totalSize);
528  // id of tag atom
529  m_writer.writeUInt32BE(m_field.id());
530  if(!m_field.mean().empty()) {
531  // write "mean"
532  m_writer.writeUInt32BE(12 + m_field.mean().size());
533  m_writer.writeUInt32BE(Mp4AtomIds::Mean);
534  m_writer.writeUInt32BE(0);
535  m_writer.writeString(m_field.mean());
536  }
537  if(!m_field.name().empty()) {
538  // write "name"
539  m_writer.writeUInt32BE(12 + m_field.name().length());
540  m_writer.writeUInt32BE(Mp4AtomIds::Name);
541  m_writer.writeUInt32BE(0);
542  m_writer.writeString(m_field.name());
543  }
544  if(!m_field.value().isEmpty()) { // write data
545  m_writer.writeUInt32BE(16 + m_dataSize); // size of data atom
546  m_writer.writeUInt32BE(Mp4AtomIds::Data); // id of data atom
547  m_writer.writeByte(0); // version
548  m_writer.writeUInt24BE(m_rawDataType);
549  m_writer.writeUInt16BE(m_field.countryIndicator());
550  m_writer.writeUInt16BE(m_field.languageIndicator());
551  if(m_convertedData.tellp()) {
552  // write converted data
553  stream << m_convertedData.rdbuf();
554  } else {
555  // no conversion was needed, write data directly from tag value
556  stream.write(m_field.value().dataPointer(), m_field.value().dataSize());
557  }
558  }
559 }
560 
561 }
The TagValue class wraps values of different types.
Definition: tagvalue.h:64
void invalidateStatus()
Invalidates the current status.
std::iostream & stream()
Returns the related stream.
void make(std::ostream &stream)
Saves the field to the specified stream.
void setTypeInfo(const typeInfoType &typeInfo)
Sets the type info of the current TagField.
bool isEmpty() const
Returns an indication whether an value is assigned.
Definition: tagvalue.h:344
void clear()
Clears id, value, type info and sets default flag to false.
void cleared()
Ensures the field is cleared.
void assignText(const char *text, std::size_t textSize, TagTextEncoding textEncoding=TagTextEncoding::Latin1, TagTextEncoding convertTo=TagTextEncoding::Unspecified)
Assigns a copy of the given text.
Definition: tagvalue.cpp:569
char * dataPointer() const
Returns a pointer to the raw data assigned to the current instance.
Definition: tagvalue.h:376
void make(std::ostream &stream)
Saves the field (specified when constructing the object) to the specified stream. ...
void parse()
Parses the header information of the element which is read from the related stream at the start offse...
STL namespace.
void assignStandardGenreIndex(int index)
Assigns the given standard genre index to be assigned.
Definition: tagvalue.cpp:634
void addNotification(const Notification &notification)
This method is meant to be called by the derived class to add a notification.
bool isTypeInfoAssigned() const
Returns an indication whether a type info is assigned.
const identifierType & id() const
Returns the id of the current TagField.
The Mp4TagFieldMaker class helps making tag fields.
Definition: mp4tagfield.h:78
static std::string fieldIdToString(identifierType id)
Returns the string representation for the specified id.
Definition: mp4tagfield.h:246
const std::string & mean() const
Returns the "mean" for "extended" fields.
Definition: mp4tagfield.h:180
Contains utility classes helping to read and write streams.
uint16 languageIndicator() const
Returns the language indicator.
Definition: mp4tagfield.h:212
The exception that is thrown when the data to be parsed or to be made seems invalid and therefore can...
Definition: exceptions.h:27
TagValue & value()
Returns the value of the current TagField.
const typeInfoType & typeInfo() const
Returns the type info of the current TagField.
implementationType * firstChild()
Returns the first child of the element.
const identifierType & id() const
Returns the element ID.
The class inherits from std::exception and serves as base class for exceptions thrown by the elements...
Definition: exceptions.h:11
The TagField class is used by FieldMapBasedTag to store the fields.
void assignPosition(PositionInSet value)
Assigns the given PositionInSet value.
Definition: tagvalue.h:279
void assignData(const char *data, size_t length, TagDataType type=TagDataType::Binary, TagTextEncoding encoding=TagTextEncoding::Latin1)
Assigns a copy of the given data.
Definition: tagvalue.cpp:648
uint16 countryIndicator() const
Returns the country indicator.
Definition: mp4tagfield.h:204
uint32 appropriateRawDataType() const
Returns an appropriate raw data type.
void assignInteger(int value)
Assigns the given integer value.
Definition: tagvalue.cpp:620
The Mp4Atom class helps to parse MP4 files.
Definition: mp4atom.h:57
const std::string & name() const
Returns the "name" for "extended" fields.
Definition: mp4tagfield.h:164
std::vector< uint32 > expectedRawDataTypes() const
Returns the expected raw data types for the ID of the field.
std::string idToString() const
Converts the specified atom ID to a printable string.
Definition: mp4atom.h:87
Contains all classes and functions of the TagInfo library.
Definition: exceptions.h:9
The Mp4TagField class is used by Mp4Tag to store the fields.
Definition: mp4tagfield.h:114
Mp4TagFieldMaker prepareMaking()
Prepares making.
void setMimeType(const std::string &value)
Sets the MIME type.
Definition: tagvalue.h:422
const std::string & mimeType() const
Returns the MIME type.
Definition: tagvalue.h:411
Mp4TagField()
Constructs a new Mp4TagField.
Definition: mp4tagfield.cpp:30
size_t dataSize() const
Returns the size of the assigned value in bytes.
Definition: tagvalue.h:365
void setId(const identifierType &id)
Sets the id of the current Tag Field.
containerType & container()
Returns the related container.
void reparse(Mp4Atom &ilstChild)
Parses field information from the specified Mp4Atom.
Definition: mp4tagfield.cpp:72