Tag Parser  8.2.0
C++ library for reading and writing MP4 (iTunes), ID3, Vorbis, Opus, FLAC and Matroska tags
tagvalue.cpp
Go to the documentation of this file.
1 #include "./tagvalue.h"
2 #include "./tag.h"
3 
4 #include "./id3/id3genres.h"
5 
6 #include <c++utilities/conversion/binaryconversion.h>
7 #include <c++utilities/conversion/conversionexception.h>
8 #include <c++utilities/conversion/stringbuilder.h>
9 #include <c++utilities/conversion/stringconversion.h>
10 
11 #include <algorithm>
12 #include <cstring>
13 #include <utility>
14 
15 using namespace std;
16 using namespace ConversionUtilities;
17 using namespace ChronoUtilities;
18 
19 namespace TagParser {
20 
24 const char *tagDataTypeString(TagDataType dataType)
25 {
26  switch (dataType) {
27  case TagDataType::Text:
28  return "text";
29  case TagDataType::Integer:
30  return "integer";
31  case TagDataType::PositionInSet:
32  return "position in set";
33  case TagDataType::StandardGenreIndex:
34  return "genre index";
35  case TagDataType::TimeSpan:
36  return "time span";
38  return "date time";
39  case TagDataType::Picture:
40  return "picture";
41  case TagDataType::Binary:
42  return "binary";
43  default:
44  return "undefined";
45  }
46 }
47 
59 TagValue::TagValue(const TagValue &other)
60  : m_size(other.m_size)
61  , m_desc(other.m_desc)
62  , m_mimeType(other.m_mimeType)
63  , m_language(other.m_language)
64  , m_type(other.m_type)
65  , m_encoding(other.m_encoding)
66  , m_descEncoding(other.m_descEncoding)
67  , m_labeledAsReadonly(other.m_labeledAsReadonly)
68 {
69  if (!other.isEmpty()) {
70  m_ptr = make_unique<char[]>(m_size);
71  std::copy(other.m_ptr.get(), other.m_ptr.get() + other.m_size, m_ptr.get());
72  }
73 }
74 
79 {
80  if (this == &other) {
81  return *this;
82  }
83  m_size = other.m_size;
84  m_type = other.m_type;
85  m_desc = other.m_desc;
86  m_mimeType = other.m_mimeType;
87  m_language = other.m_language;
88  m_labeledAsReadonly = other.m_labeledAsReadonly;
89  m_encoding = other.m_encoding;
90  m_descEncoding = other.m_descEncoding;
91  if (other.isEmpty()) {
92  m_ptr.reset();
93  } else {
94  m_ptr = make_unique<char[]>(m_size);
95  std::copy(other.m_ptr.get(), other.m_ptr.get() + other.m_size, m_ptr.get());
96  }
97  return *this;
98 }
99 
108 bool TagValue::operator==(const TagValue &other) const
109 {
110  // check whether meta-data is equal
111  if (m_desc != other.m_desc || (!m_desc.empty() && m_descEncoding != other.m_descEncoding) || m_mimeType != other.m_mimeType
112  || m_language != other.m_language || m_labeledAsReadonly != other.m_labeledAsReadonly) {
113  return false;
114  }
115 
116  // check for equality if both types are identical
117  if (m_type == other.m_type) {
118  switch (m_type) {
119  case TagDataType::Text:
120  if (m_size != other.m_size && m_encoding != other.m_encoding) {
121  // don't consider differently encoded text values equal
122  return false;
123  }
124  return strncmp(m_ptr.get(), other.m_ptr.get(), m_size) == 0;
126  return toPositionInSet() == other.toPositionInSet();
128  return toInteger() == other.toInteger();
130  return toStandardGenreIndex() == other.toStandardGenreIndex();
132  return toTimeSpan() == other.toTimeSpan();
134  return toDateTime() == other.toDateTime();
136  case TagDataType::Binary:
138  if (m_size != other.m_size) {
139  return false;
140  }
141  return strncmp(m_ptr.get(), other.m_ptr.get(), m_size) == 0;
142  }
143  return false;
144  }
145 
146  // check for equality if types are different by comparing the string representation
147  try {
148  return toString() == other.toString(m_encoding);
149  } catch (const ConversionException &) {
150  return false;
151  }
152 }
153 
162 {
163  m_desc.clear();
164  m_mimeType.clear();
165  m_language.clear();
166  m_labeledAsReadonly = false;
167  m_encoding = TagTextEncoding::Latin1;
168  m_descEncoding = TagTextEncoding::Latin1;
169  m_type = TagDataType::Undefined;
170 }
171 
177 int32 TagValue::toInteger() const
178 {
179  if (isEmpty()) {
180  return 0;
181  }
182  switch (m_type) {
183  case TagDataType::Text:
184  switch (m_encoding) {
188  return ConversionUtilities::bufferToNumber<int32>(m_ptr.get(), m_size);
191  u16string u16str(reinterpret_cast<char16_t *>(m_ptr.get()), m_size / 2);
192  ensureHostByteOrder(u16str, m_encoding);
193  return ConversionUtilities::stringToNumber<int32>(u16str);
194  }
198  if (m_size == sizeof(int32)) {
199  return *reinterpret_cast<int32 *>(m_ptr.get());
200  }
201  throw ConversionException("Can not convert assigned data to integer because the data size is not appropriate.");
202  default:
203  throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to integer."));
204  }
205 }
206 
213 {
214  if (isEmpty()) {
216  }
217  int index = 0;
218  switch (m_type) {
219  case TagDataType::Text: {
220  try {
221  index = toInteger();
222  } catch (const ConversionException &) {
224  if (m_encoding == TagTextEncoding::Latin1) {
225  // no need to convert Latin-1 to UTF-8 (makes no difference in case of genre strings)
226  encoding = TagTextEncoding::Unspecified;
227  }
228  index = Id3Genres::indexFromString(toString(encoding));
229  }
230  break;
231  }
234  if (m_size != sizeof(int32)) {
235  throw ConversionException("The assigned index/integer is of unappropriate size.");
236  }
237  index = static_cast<int>(*reinterpret_cast<int32 *>(m_ptr.get()));
238  break;
239  default:
240  throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to genre index."));
241  }
242  if (!Id3Genres::isEmptyGenre(index) && !Id3Genres::isIndexSupported(index)) {
243  throw ConversionException("The assigned number is not a valid standard genre index.");
244  }
245  return index;
246 }
247 
254 {
255  if (isEmpty()) {
256  return PositionInSet();
257  }
258  switch (m_type) {
259  case TagDataType::Text:
260  switch (m_encoding) {
264  return PositionInSet(string(m_ptr.get(), m_size));
267  u16string u16str(reinterpret_cast<char16_t *>(m_ptr.get()), m_size / 2);
268  ensureHostByteOrder(u16str, m_encoding);
269  return PositionInSet(u16str);
270  }
273  switch (m_size) {
274  case sizeof(int32):
275  return PositionInSet(*(reinterpret_cast<int32 *>(m_ptr.get())));
276  case 2 * sizeof(int32):
277  return PositionInSet(*(reinterpret_cast<int32 *>(m_ptr.get())), *(reinterpret_cast<int32 *>(m_ptr.get() + sizeof(int32))));
278  default:
279  throw ConversionException("The size of the assigned data is not appropriate.");
280  }
281  default:
282  throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to position in set."));
283  }
284 }
285 
292 {
293  if (isEmpty()) {
294  return TimeSpan();
295  }
296  switch (m_type) {
297  case TagDataType::Text:
298  return TimeSpan::fromString(toString(m_encoding == TagTextEncoding::Utf8 ? TagTextEncoding::Utf8 : TagTextEncoding::Latin1));
301  switch (m_size) {
302  case sizeof(int32):
303  return TimeSpan(*(reinterpret_cast<int32 *>(m_ptr.get())));
304  case sizeof(int64):
305  return TimeSpan(*(reinterpret_cast<int64 *>(m_ptr.get())));
306  default:
307  throw ConversionException("The size of the assigned integer is not appropriate for conversion to time span.");
308  }
309  default:
310  throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to time span."));
311  }
312 }
313 
319 DateTime TagValue::toDateTime() const
320 {
321  if (isEmpty()) {
322  return DateTime();
323  }
324  switch (m_type) {
325  case TagDataType::Text:
326  return DateTime::fromString(toString(m_encoding == TagTextEncoding::Utf8 ? TagTextEncoding::Utf8 : TagTextEncoding::Latin1));
329  if (m_size == sizeof(int32)) {
330  return DateTime(*(reinterpret_cast<uint32 *>(m_ptr.get())));
331  } else if (m_size == sizeof(int64)) {
332  return DateTime(*(reinterpret_cast<uint64 *>(m_ptr.get())));
333  } else {
334  throw ConversionException("The size of the assigned integer is not appropriate for conversion to date time.");
335  }
336  default:
337  throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to date time."));
338  }
339 }
340 
344 pair<const char *, float> encodingParameter(TagTextEncoding tagTextEncoding)
345 {
346  switch (tagTextEncoding) {
348  return make_pair("ISO-8859-1", 1.0f);
350  return make_pair("UTF-8", 1.0f);
352  return make_pair("UTF-16LE", 2.0f);
354  return make_pair("UTF-16BE", 2.0f);
355  default:
356  return make_pair(nullptr, 0.0f);
357  }
358 }
359 
370 {
371  if (m_encoding == encoding) {
372  return;
373  }
374  if (type() == TagDataType::Text) {
375  StringData encodedData;
376  switch (encoding) {
378  // use pre-defined methods when encoding to UTF-8
379  switch (dataEncoding()) {
381  encodedData = convertLatin1ToUtf8(m_ptr.get(), m_size);
382  break;
384  encodedData = convertUtf16LEToUtf8(m_ptr.get(), m_size);
385  break;
387  encodedData = convertUtf16BEToUtf8(m_ptr.get(), m_size);
388  break;
389  default:;
390  }
391  break;
392  default: {
393  // otherwise, determine input and output parameter to use general covertString method
394  const auto inputParameter = encodingParameter(dataEncoding());
395  const auto outputParameter = encodingParameter(encoding);
396  encodedData
397  = convertString(inputParameter.first, outputParameter.first, m_ptr.get(), m_size, outputParameter.second / inputParameter.second);
398  }
399  }
400  // can't just move the encoded data because it needs to be deleted with free
401  m_ptr = make_unique<char[]>(m_size = encodedData.second);
402  copy(encodedData.first.get(), encodedData.first.get() + encodedData.second, m_ptr.get());
403  }
404  m_encoding = encoding;
405 }
406 
412 {
413  if (type() == TagDataType::Text && !tag->canEncodingBeUsed(dataEncoding())) {
415  }
416 }
417 
427 void TagValue::toString(string &result, TagTextEncoding encoding) const
428 {
429  if (isEmpty()) {
430  result.clear();
431  return;
432  }
433 
434  switch (m_type) {
435  case TagDataType::Text:
436  if (encoding == TagTextEncoding::Unspecified || dataEncoding() == TagTextEncoding::Unspecified || encoding == dataEncoding()) {
437  result.assign(m_ptr.get(), m_size);
438  } else {
439  StringData encodedData;
440  switch (encoding) {
442  // use pre-defined methods when encoding to UTF-8
443  switch (dataEncoding()) {
445  encodedData = convertLatin1ToUtf8(m_ptr.get(), m_size);
446  break;
448  encodedData = convertUtf16LEToUtf8(m_ptr.get(), m_size);
449  break;
451  encodedData = convertUtf16BEToUtf8(m_ptr.get(), m_size);
452  break;
453  default:;
454  }
455  break;
456  default: {
457  // otherwise, determine input and output parameter to use general covertString method
458  const auto inputParameter = encodingParameter(dataEncoding());
459  const auto outputParameter = encodingParameter(encoding);
460  encodedData
461  = convertString(inputParameter.first, outputParameter.first, m_ptr.get(), m_size, outputParameter.second / inputParameter.second);
462  }
463  }
464  result.assign(encodedData.first.get(), encodedData.second);
465  }
466  return;
468  result = ConversionUtilities::numberToString(toInteger());
469  break;
471  result = toPositionInSet().toString();
472  break;
474  const auto genreIndex = toInteger();
475  if (Id3Genres::isEmptyGenre(genreIndex)) {
476  result.clear();
477  } else if (const char *genreName = Id3Genres::stringFromIndex(genreIndex)) {
478  result.assign(genreName);
479  } else {
480  throw ConversionException("No string representation for the assigned standard genre index available.");
481  }
482  break;
483  }
485  result = toTimeSpan().toString();
486  break;
488  result = toDateTime().toString();
489  break;
490  default:
491  throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to string."));
492  }
494  auto encodedData = encoding == TagTextEncoding::Utf16LittleEndian ? convertUtf8ToUtf16LE(result.data(), result.size())
495  : convertUtf8ToUtf16BE(result.data(), result.size());
496  result.assign(encodedData.first.get(), encodedData.second);
497  }
498 }
499 
507 void TagValue::toWString(std::u16string &result, TagTextEncoding encoding) const
508 {
509  if (isEmpty()) {
510  result.clear();
511  return;
512  }
513 
514  string regularStrRes;
515  switch (m_type) {
516  case TagDataType::Text:
517  if (encoding == TagTextEncoding::Unspecified || encoding == dataEncoding()) {
518  result.assign(reinterpret_cast<const char16_t *>(m_ptr.get()), m_size / sizeof(char16_t));
519  } else {
520  StringData encodedData;
521  switch (encoding) {
523  // use pre-defined methods when encoding to UTF-8
524  switch (dataEncoding()) {
526  encodedData = convertLatin1ToUtf8(m_ptr.get(), m_size);
527  break;
529  encodedData = convertUtf16LEToUtf8(m_ptr.get(), m_size);
530  break;
532  encodedData = convertUtf16BEToUtf8(m_ptr.get(), m_size);
533  break;
534  default:;
535  }
536  break;
537  default: {
538  // otherwise, determine input and output parameter to use general covertString method
539  const auto inputParameter = encodingParameter(dataEncoding());
540  const auto outputParameter = encodingParameter(encoding);
541  encodedData
542  = convertString(inputParameter.first, outputParameter.first, m_ptr.get(), m_size, outputParameter.second / inputParameter.second);
543  }
544  }
545  result.assign(reinterpret_cast<const char16_t *>(encodedData.first.get()), encodedData.second / sizeof(char16_t));
546  }
547  return;
549  regularStrRes = ConversionUtilities::numberToString(toInteger());
550  break;
552  regularStrRes = toPositionInSet().toString();
553  break;
555  const auto genreIndex = toInteger();
556  if (Id3Genres::isEmptyGenre(genreIndex)) {
557  regularStrRes.clear();
558  } else if (const char *genreName = Id3Genres::stringFromIndex(genreIndex)) {
559  regularStrRes.assign(genreName);
560  } else {
561  throw ConversionException("No string representation for the assigned standard genre index available.");
562  }
563  break;
564  }
566  regularStrRes = toTimeSpan().toString();
567  break;
568  default:
569  throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to string."));
570  }
572  auto encodedData = encoding == TagTextEncoding::Utf16LittleEndian ? convertUtf8ToUtf16LE(regularStrRes.data(), result.size())
573  : convertUtf8ToUtf16BE(regularStrRes.data(), result.size());
574  result.assign(reinterpret_cast<const char16_t *>(encodedData.first.get()), encodedData.second / sizeof(const char16_t));
575  }
576 }
577 
588 void TagValue::assignText(const char *text, std::size_t textSize, TagTextEncoding textEncoding, TagTextEncoding convertTo)
589 {
590  m_type = TagDataType::Text;
591  m_encoding = convertTo == TagTextEncoding::Unspecified ? textEncoding : convertTo;
592 
593  stripBom(text, textSize, textEncoding);
594  if (!textSize) {
595  m_size = 0;
596  m_ptr.reset();
597  return;
598  }
599 
600  if (convertTo == TagTextEncoding::Unspecified || textEncoding == convertTo) {
601  m_ptr = make_unique<char[]>(m_size = textSize);
602  copy(text, text + textSize, m_ptr.get());
603  return;
604  }
605 
606  StringData encodedData;
607  switch (textEncoding) {
609  // use pre-defined methods when encoding to UTF-8
610  switch (convertTo) {
612  encodedData = convertUtf8ToLatin1(text, textSize);
613  break;
615  encodedData = convertUtf8ToUtf16LE(text, textSize);
616  break;
618  encodedData = convertUtf8ToUtf16BE(text, textSize);
619  break;
620  default:;
621  }
622  break;
623  default: {
624  // otherwise, determine input and output parameter to use general covertString method
625  const auto inputParameter = encodingParameter(textEncoding);
626  const auto outputParameter = encodingParameter(convertTo);
627  encodedData = convertString(inputParameter.first, outputParameter.first, text, textSize, outputParameter.second / inputParameter.second);
628  }
629  }
630  // can't just move the encoded data because it needs to be deleted with free
631  m_ptr = make_unique<char[]>(m_size = encodedData.second);
632  copy(encodedData.first.get(), encodedData.first.get() + encodedData.second, m_ptr.get());
633 }
634 
639 void TagValue::assignInteger(int value)
640 {
641  m_size = sizeof(value);
642  m_ptr = make_unique<char[]>(m_size);
643  std::copy(reinterpret_cast<const char *>(&value), reinterpret_cast<const char *>(&value) + m_size, m_ptr.get());
644  m_type = TagDataType::Integer;
645  m_encoding = TagTextEncoding::Latin1;
646 }
647 
656 void TagValue::assignData(const char *data, size_t length, TagDataType type, TagTextEncoding encoding)
657 {
658  if (type == TagDataType::Text) {
659  stripBom(data, length, encoding);
660  }
661  if (length > m_size) {
662  m_ptr = make_unique<char[]>(length);
663  }
664  if (length) {
665  std::copy(data, data + length, m_ptr.get());
666  } else {
667  m_ptr.reset();
668  }
669  m_size = length;
670  m_type = type;
671  m_encoding = encoding;
672 }
673 
686 void TagValue::assignData(unique_ptr<char[]> &&data, size_t length, TagDataType type, TagTextEncoding encoding)
687 {
688  m_size = length;
689  m_type = type;
690  m_encoding = encoding;
691  m_ptr = move(data);
692 }
693 
697 void TagValue::stripBom(const char *&text, size_t &length, TagTextEncoding encoding)
698 {
699  switch (encoding) {
701  if ((length >= 3) && (ConversionUtilities::BE::toUInt24(text) == 0x00EFBBBF)) {
702  text += 3;
703  length -= 3;
704  }
705  break;
707  if ((length >= 2) && (ConversionUtilities::LE::toUInt16(text) == 0xFEFF)) {
708  text += 2;
709  length -= 2;
710  }
711  break;
713  if ((length >= 2) && (ConversionUtilities::BE::toUInt16(text) == 0xFEFF)) {
714  text += 2;
715  length -= 2;
716  }
717  break;
718  default:;
719  }
720 }
721 
726 void TagValue::ensureHostByteOrder(u16string &u16str, TagTextEncoding currentEncoding)
727 {
728  if (currentEncoding !=
729 #if defined(CONVERSION_UTILITIES_BYTE_ORDER_LITTLE_ENDIAN)
731 #elif defined(CONVERSION_UTILITIES_BYTE_ORDER_BIG_ENDIAN)
733 #else
734 #error "Host byte order not supported"
735 #endif
736  ) {
737  for (auto &c : u16str) {
738  c = swapOrder(static_cast<uint16>(c));
739  }
740  }
741 }
742 
747 {
748  static TagValue emptyTagValue;
749  return emptyTagValue;
750 }
751 
752 } // namespace TagParser
TagTextEncoding dataEncoding() const
Returns the data encoding.
Definition: tagvalue.h:538
StringType toString() const
Returns the string representation of the current PositionInSet.
static void stripBom(const char *&text, std::size_t &length, TagTextEncoding encoding)
Strips the byte order mask from the specified text.
Definition: tagvalue.cpp:697
The Tag class is used to store, read and write tag information.
Definition: tag.h:98
pair< const char *, float > encodingParameter(TagTextEncoding tagTextEncoding)
Returns the encoding parameter (name of the character set and bytes per character) for the specified ...
Definition: tagvalue.cpp:344
virtual TagTextEncoding proposedTextEncoding() const
Returns the proposed text encoding.
Definition: tag.h:149
std::u16string toWString(TagTextEncoding encoding=TagTextEncoding::Unspecified) const
Converts the value of the current TagValue object to its equivalent std::wstring representation.
Definition: tagvalue.h:378
The PositionInSet class describes the position of an element in a set which consists of a certain num...
Definition: positioninset.h:21
TagDataType
Specifies the data type.
Definition: tagvalue.h:53
static const char * stringFromIndex(int index)
Returns the genre name for the specified numerical denotation as C-style string.
Definition: id3genres.h:28
int toStandardGenreIndex() const
Converts the value of the current TagValue object to its equivalent standard genre index.
Definition: tagvalue.cpp:212
static int indexFromString(const std::string &genre)
Returns the numerical denotation of the specified genre or -1 if genre is unknown.
Definition: id3genres.cpp:43
bool isEmpty() const
Returns an indication whether an value is assigned.
Definition: tagvalue.h:389
static constexpr int emptyGenreIndex()
Returns the preferred genre index to indicate that no genre is set at all.
Definition: id3genres.h:46
void clearMetadata()
Wipes assigned meta data.
Definition: tagvalue.cpp:161
ChronoUtilities::TimeSpan toTimeSpan() const
Converts the value of the current TagValue object to its equivalent TimeSpan representation.
Definition: tagvalue.cpp:291
static const TagValue & empty()
Returns an empty TagValue.
Definition: tagvalue.cpp:746
PositionInSet toPositionInSet() const
Converts the value of the current TagValue object to its equivalent PositionInSet representation.
Definition: tagvalue.cpp:253
bool operator==(const TagValue &other) const
Returns whether both instances are equal.
Definition: tagvalue.cpp:108
const char * tagDataTypeString(TagDataType dataType)
Returns the string representation of the specified dataType.
Definition: tagvalue.cpp:24
void convertDataEncoding(TagTextEncoding encoding)
Converts the currently assigned text value to the specified encoding.
Definition: tagvalue.cpp:369
TagDataType type() const
Returns the type of the assigned value.
Definition: tagvalue.h:353
ChronoUtilities::DateTime toDateTime() const
Converts the value of the current TagValue object to its equivalent DateTime representation.
Definition: tagvalue.cpp:319
int32 toInteger() const
Converts the value of the current TagValue object to its equivalent integer representation.
Definition: tagvalue.cpp:177
void assignInteger(int value)
Assigns the given integer value.
Definition: tagvalue.cpp:639
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:588
static constexpr bool isEmptyGenre(int index)
Returns whether the genre index indicates the genre field is not set at all.
Definition: id3genres.h:54
TagValue & operator=(const TagValue &other)
Assigns the value of another TagValue to the current instance.
Definition: tagvalue.cpp:78
void assignData(const char *data, std::size_t length, TagDataType type=TagDataType::Binary, TagTextEncoding encoding=TagTextEncoding::Latin1)
static constexpr bool isIndexSupported(int index)
Returns an indication whether the specified numerical denotation is supported by this class.
Definition: id3genres.h:63
std::string toString(TagTextEncoding encoding=TagTextEncoding::Unspecified) const
Converts the value of the current TagValue object to its equivalent std::string representation.
Definition: tagvalue.h:365
void convertDataEncodingForTag(const Tag *tag)
Ensures the encoding of the currently assigned text value is supported by the specified tag.
Definition: tagvalue.cpp:411
The TagValue class wraps values of different types.
Definition: tagvalue.h:65
TagTextEncoding
Specifies the text encoding.
Definition: tagvalue.h:24
Contains all classes and functions of the TagInfo library.
Definition: aaccodebook.h:9
virtual bool canEncodingBeUsed(TagTextEncoding encoding) const
Returns an indication whether the specified encoding can be used to provide string values for the tag...
Definition: tag.h:154
static void ensureHostByteOrder(std::u16string &u16str, TagTextEncoding currentEncoding)
Ensures the byte-order of the specified UTF-16 string matches the byte-order of the machine.
Definition: tagvalue.cpp:726