updated readme
This commit is contained in:
parent
5e550eebb0
commit
8b91b41801
|
@ -1,4 +1,4 @@
|
|||
# tagparser
|
||||
# Tag Parser
|
||||
C++ library for reading and writing MP4 (iTunes), ID3, Vorbis and Matroska tags.
|
||||
|
||||
## Supported formats
|
||||
|
@ -18,3 +18,7 @@ For examples check out the CLI interface of my Tag Editor (which is also on Git)
|
|||
## Build instructions
|
||||
The tagparser library depends on c++utilities and is built in the same way.
|
||||
It also depends on zlib.
|
||||
|
||||
## TODO
|
||||
- Use padding to prevent rewriting the entire file to save tags.
|
||||
- Support more tag formats (EXIF, PDF metadata, ...).
|
||||
|
|
|
@ -39,14 +39,14 @@ public:
|
|||
virtual void removeAllFields();
|
||||
const std::multimap<typename FieldType::identifierType, FieldType, Compare> &fields() const;
|
||||
std::multimap<typename FieldType::identifierType, FieldType, Compare> &fields();
|
||||
virtual int fieldCount() const;
|
||||
virtual unsigned int fieldCount() const;
|
||||
virtual typename FieldType::identifierType fieldId(KnownField value) const = 0;
|
||||
virtual KnownField knownField(const typename FieldType::identifierType &id) const = 0;
|
||||
virtual bool supportsField(KnownField field) const;
|
||||
using Tag::proposedDataType;
|
||||
virtual TagDataType proposedDataType(const typename FieldType::identifierType &id) const;
|
||||
virtual int insertFields(const FieldMapBasedTag<FieldType, Compare> &from, bool overwrite);
|
||||
virtual int insertValues(const Tag &from, bool overwrite);
|
||||
virtual unsigned int insertValues(const Tag &from, bool overwrite);
|
||||
typedef FieldType fieldType;
|
||||
|
||||
private:
|
||||
|
@ -185,9 +185,15 @@ inline std::multimap<typename FieldType::identifierType, FieldType, Compare> &Fi
|
|||
}
|
||||
|
||||
template <class FieldType, class Compare>
|
||||
inline int FieldMapBasedTag<FieldType, Compare>::fieldCount() const
|
||||
unsigned int FieldMapBasedTag<FieldType, Compare>::fieldCount() const
|
||||
{
|
||||
return m_fields.size();
|
||||
int count = 0;
|
||||
for(const auto &field : m_fields) {
|
||||
if(!field.second.value().isEmpty()) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
template <class FieldType, class Compare>
|
||||
|
@ -244,7 +250,7 @@ int FieldMapBasedTag<FieldType, Compare>::insertFields(const FieldMapBasedTag<Fi
|
|||
}
|
||||
|
||||
template <class FieldType, class Compare>
|
||||
int FieldMapBasedTag<FieldType, Compare>::insertValues(const Tag &from, bool overwrite)
|
||||
unsigned int FieldMapBasedTag<FieldType, Compare>::insertValues(const Tag &from, bool overwrite)
|
||||
{
|
||||
if(type() == from.type()) {
|
||||
// the tags are of the same type, we can insert the fields directly
|
||||
|
|
|
@ -232,13 +232,14 @@ void Id3v1Tag::removeAllFields()
|
|||
m_genre.clearDataAndMetadata();
|
||||
}
|
||||
|
||||
int Id3v1Tag::fieldCount() const
|
||||
unsigned int Id3v1Tag::fieldCount() const
|
||||
{
|
||||
int count = 0;
|
||||
for(const TagValue &value : {m_title, m_artist, m_album,
|
||||
for(const auto &value : {m_title, m_artist, m_album,
|
||||
m_year, m_comment, m_trackPos, m_genre}) {
|
||||
if(!value.isEmpty())
|
||||
if(!value.isEmpty()) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ public:
|
|||
virtual bool setValueConsideringTypeInfo(KnownField field, const TagValue &value, const std::string &typeInfo);
|
||||
virtual bool hasField(KnownField field) const;
|
||||
virtual void removeAllFields();
|
||||
virtual int fieldCount() const;
|
||||
virtual unsigned int fieldCount() const;
|
||||
virtual bool supportsField(KnownField field) const;
|
||||
|
||||
void parse(std::istream &sourceStream, bool autoSeek);
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "../exceptions.h"
|
||||
|
||||
#include <c++utilities/conversion/stringconversion.h>
|
||||
#include <c++utilities/misc/memory.h>
|
||||
|
||||
#include <zlib.h>
|
||||
|
||||
|
@ -268,53 +269,53 @@ void Id3v2Frame::make(IoUtilities::BinaryWriter &writer, int32 version)
|
|||
addNotification(NotificationType::Warning, "The existing flag and group information is not supported by the version of ID3v2 and will be ignored/discarted.", context);
|
||||
}
|
||||
// create actual data, depending on the frame type
|
||||
vector<char> buffer;
|
||||
unique_ptr<char[]> buffer;
|
||||
uint32 decompressedSize;
|
||||
// check if the frame to be written is a text frame
|
||||
try {
|
||||
if(Id3v2FrameIds::isTextfield(frameId)) {
|
||||
if((version >= 3 && (frameId == Id3v2FrameIds::lTrackPosition || frameId == Id3v2FrameIds::lDiskPosition))
|
||||
|| (version < 3 && frameId == Id3v2FrameIds::sTrackPosition)) {
|
||||
// the track number or the disk number frame
|
||||
helper.makeString(buffer, value().toString(), TagTextEncoding::Latin1);
|
||||
helper.makeString(buffer, decompressedSize, value().toString(), TagTextEncoding::Latin1);
|
||||
} else if((version >= 3 && frameId == Id3v2FrameIds::lLength)
|
||||
|| (version < 3 && frameId == Id3v2FrameIds::sLength)) {
|
||||
// the length
|
||||
helper.makeString(buffer, ConversionUtilities::numberToString(value().toTimeSpan().totalMilliseconds()), TagTextEncoding::Latin1);
|
||||
helper.makeString(buffer, decompressedSize, ConversionUtilities::numberToString(value().toTimeSpan().totalMilliseconds()), TagTextEncoding::Latin1);
|
||||
} else if(value().type() == TagDataType::StandardGenreIndex && ((version >= 3 && frameId == Id3v2FrameIds::lGenre)
|
||||
|| (version < 3 && frameId == Id3v2FrameIds::sGenre))) {
|
||||
// genre/content type as standard genre index
|
||||
helper.makeString(buffer, ConversionUtilities::numberToString(value().toStandardGenreIndex()), TagTextEncoding::Latin1);
|
||||
helper.makeString(buffer, decompressedSize, ConversionUtilities::numberToString(value().toStandardGenreIndex()), TagTextEncoding::Latin1);
|
||||
} else {
|
||||
// any other text frame
|
||||
helper.makeString(buffer, value().toString(), value().dataEncoding()); // the same as a normal text frame
|
||||
helper.makeString(buffer, decompressedSize, value().toString(), value().dataEncoding()); // the same as a normal text frame
|
||||
}
|
||||
} else if((version >= 3 && frameId == Id3v2FrameIds::lCover)
|
||||
|| (version < 3 && frameId == Id3v2FrameIds::sCover)) {
|
||||
// picture frame
|
||||
helper.makePicture(buffer, value(), isTypeInfoAssigned() ? typeInfo() : 0);
|
||||
helper.makePicture(buffer, decompressedSize, value(), isTypeInfoAssigned() ? typeInfo() : 0);
|
||||
} else if(((version >= 3 && id() == Id3v2FrameIds::lComment)
|
||||
|| (version < 3 && id() == Id3v2FrameIds::sComment))
|
||||
|| ((version >= 3 && id() == Id3v2FrameIds::lUnsynchronizedLyrics)
|
||||
|| (version < 3 && id() == Id3v2FrameIds::sUnsynchronizedLyrics))) {
|
||||
// the comment frame or the unsynchronized lyrics frame
|
||||
helper.makeComment(buffer, value());
|
||||
helper.makeComment(buffer, decompressedSize, value());
|
||||
} else {
|
||||
// an unknown frame
|
||||
// create buffer
|
||||
buffer.resize(value().dataSize());
|
||||
buffer = make_unique<char[]>(decompressedSize = value().dataSize());
|
||||
// just write the data
|
||||
copy(value().dataPointer(), value().dataPointer() + value().dataSize(), buffer.data());
|
||||
copy(value().dataPointer(), value().dataPointer() + value().dataSize(), buffer.get());
|
||||
}
|
||||
} catch(ConversionException &) {
|
||||
addNotification(NotificationType::Critical, "Assigned value can not be converted appropriately.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
uint32 decompressedSize = buffer.size();
|
||||
unsigned long actualSize;
|
||||
if(version >= 3 && isCompressed()) {
|
||||
uLongf destLen = compressBound(buffer.size());
|
||||
vector<char> compressedBuffer;
|
||||
compressedBuffer.resize(destLen);
|
||||
switch(compress(reinterpret_cast<Bytef *>(compressedBuffer.data()), &destLen, reinterpret_cast<Bytef *>(buffer.data()), buffer.size())) {
|
||||
actualSize = compressBound(decompressedSize);
|
||||
auto compressedBuffer = make_unique<char[]>(actualSize);
|
||||
switch(compress(reinterpret_cast<Bytef *>(compressedBuffer.get()), &actualSize, reinterpret_cast<Bytef *>(buffer.get()), decompressedSize)) {
|
||||
case Z_MEM_ERROR:
|
||||
addNotification(NotificationType::Critical, "Decompressing failed. The source buffer was too small.", context);
|
||||
throw InvalidDataException();
|
||||
|
@ -325,17 +326,18 @@ void Id3v2Frame::make(IoUtilities::BinaryWriter &writer, int32 version)
|
|||
;
|
||||
}
|
||||
buffer.swap(compressedBuffer);
|
||||
buffer.resize(destLen);
|
||||
} else {
|
||||
actualSize = decompressedSize;
|
||||
}
|
||||
if(version < 3) {
|
||||
writer.writeUInt24BE(frameId);
|
||||
writer.writeUInt24BE(buffer.size());
|
||||
writer.writeUInt24BE(actualSize);
|
||||
} else {
|
||||
writer.writeUInt32BE(frameId);
|
||||
if(version >= 4) {
|
||||
writer.writeSynchsafeUInt32BE(buffer.size());
|
||||
writer.writeSynchsafeUInt32BE(actualSize);
|
||||
} else {
|
||||
writer.writeUInt32BE(buffer.size());
|
||||
writer.writeUInt32BE(actualSize);
|
||||
}
|
||||
writer.writeUInt16BE(m_flag);
|
||||
if(hasGroupInformation()) {
|
||||
|
@ -349,7 +351,7 @@ void Id3v2Frame::make(IoUtilities::BinaryWriter &writer, int32 version)
|
|||
}
|
||||
}
|
||||
}
|
||||
writer.write(buffer.data(), buffer.size());
|
||||
writer.write(buffer.get(), actualSize);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -605,9 +607,9 @@ void Id3v2FrameHelper::parseComment(const char *buffer, size_t dataSize, TagValu
|
|||
* \param value Specifies the string to make.
|
||||
* \param encoding Specifies the encoding of the string to make.
|
||||
*/
|
||||
void Id3v2FrameHelper::makeString(vector<char> &buffer, const string &value, TagTextEncoding encoding)
|
||||
void Id3v2FrameHelper::makeString(unique_ptr<char[]> &buffer, uint32 &bufferSize, const string &value, TagTextEncoding encoding)
|
||||
{
|
||||
makeEncodingAndData(buffer, encoding, value.c_str(), value.length());
|
||||
makeEncodingAndData(buffer, bufferSize, encoding, value.c_str(), value.length());
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -617,7 +619,7 @@ void Id3v2FrameHelper::makeString(vector<char> &buffer, const string &value, Tag
|
|||
* \param data Specifies the data.
|
||||
* \param dataSize Specifies the data size.
|
||||
*/
|
||||
void Id3v2FrameHelper::makeEncodingAndData(vector<char> &buffer, TagTextEncoding encoding, const char *data, size_t dataSize)
|
||||
void Id3v2FrameHelper::makeEncodingAndData(unique_ptr<char[]> &buffer, uint32 &bufferSize, TagTextEncoding encoding, const char *data, size_t dataSize)
|
||||
{
|
||||
// calculate buffer size
|
||||
if(!data) {
|
||||
|
@ -627,23 +629,25 @@ void Id3v2FrameHelper::makeEncodingAndData(vector<char> &buffer, TagTextEncoding
|
|||
case TagTextEncoding::Latin1:
|
||||
case TagTextEncoding::Utf8:
|
||||
case TagTextEncoding::Unspecified: // assumption
|
||||
buffer.resize(1 + dataSize + 1); // allocate buffer
|
||||
// allocate buffer
|
||||
buffer = make_unique<char[]>(bufferSize = 1 + dataSize + 1);
|
||||
break;
|
||||
case TagTextEncoding::Utf16LittleEndian:
|
||||
case TagTextEncoding::Utf16BigEndian:
|
||||
buffer.resize(1 + dataSize + 2); // allocate buffer
|
||||
// allocate buffer
|
||||
buffer = make_unique<char[]>(bufferSize = 1 + dataSize + 2);
|
||||
break;
|
||||
}
|
||||
buffer[0] = makeTextEncodingByte(encoding); // set text encoding byte
|
||||
if(dataSize > 0) {
|
||||
copy(data, data + dataSize, buffer.data() + 1); // write string data
|
||||
copy(data, data + dataSize, buffer.get() + 1); // write string data
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Writes the specified picture to the specified buffer.
|
||||
*/
|
||||
void Id3v2FrameHelper::makePicture(std::vector<char> &buffer, const TagValue &picture, byte typeInfo)
|
||||
void Id3v2FrameHelper::makePicture(unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo)
|
||||
{
|
||||
// calculate needed buffer size and create buffer
|
||||
TagTextEncoding descriptionEncoding = picture.descriptionEncoding();
|
||||
|
@ -656,9 +660,9 @@ void Id3v2FrameHelper::makePicture(std::vector<char> &buffer, const TagValue &pi
|
|||
if(descriptionLength == string::npos) {
|
||||
descriptionLength = picture.description().length();
|
||||
}
|
||||
buffer.resize(1 + mimeTypeLength + 1 + 1 + descriptionLength + (descriptionEncoding == TagTextEncoding::Utf16BigEndian || descriptionEncoding == TagTextEncoding::Utf16LittleEndian ? 2 : 1) + dataSize);
|
||||
// note: encoding byte + mime type length + 0 byte + picture type byte + description length + 1 or 2 null bytes (depends on encoding) + data size
|
||||
char *offset = buffer.data();
|
||||
buffer = make_unique<char[]>(bufferSize = 1 + mimeTypeLength + 1 + 1 + descriptionLength + (descriptionEncoding == TagTextEncoding::Utf16BigEndian || descriptionEncoding == TagTextEncoding::Utf16LittleEndian ? 2 : 1) + dataSize);
|
||||
// note: encoding byte + mime type length + 0 byte + picture type byte + description length + 1 or 2 null bytes (depends on encoding) + data size
|
||||
char *offset = buffer.get();
|
||||
// write encoding byte
|
||||
*offset = makeTextEncodingByte(descriptionEncoding);
|
||||
// write mime type
|
||||
|
@ -681,7 +685,7 @@ void Id3v2FrameHelper::makePicture(std::vector<char> &buffer, const TagValue &pi
|
|||
/*!
|
||||
* \brief Writes the specified comment to the specified buffer.
|
||||
*/
|
||||
void Id3v2FrameHelper::makeComment(std::vector<char> &buffer, const TagValue &comment)
|
||||
void Id3v2FrameHelper::makeComment(unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &comment)
|
||||
{
|
||||
static const string context("making comment frame");
|
||||
// check type and other values are valid
|
||||
|
@ -700,9 +704,9 @@ void Id3v2FrameHelper::makeComment(std::vector<char> &buffer, const TagValue &co
|
|||
if(descriptionLength == string::npos)
|
||||
descriptionLength = comment.description().length();
|
||||
uint32 dataSize = comment.dataSize();
|
||||
buffer.resize(1 + 3 + descriptionLength + (encoding == TagTextEncoding::Utf16BigEndian || encoding == TagTextEncoding::Utf16LittleEndian ? 2 : 1) + dataSize);
|
||||
// note: encoding byte + language + description length + 1 or 2 null bytes + data size
|
||||
char *offset = buffer.data();
|
||||
buffer = make_unique<char[]>(bufferSize = 1 + 3 + descriptionLength + (encoding == TagTextEncoding::Utf16BigEndian || encoding == TagTextEncoding::Utf16LittleEndian ? 2 : 1) + dataSize);
|
||||
// note: encoding byte + language + description length + 1 or 2 null bytes + data size
|
||||
char *offset = buffer.get();
|
||||
// write encoding
|
||||
*offset = makeTextEncodingByte(encoding);
|
||||
// write language
|
||||
|
@ -717,7 +721,7 @@ void Id3v2FrameHelper::makeComment(std::vector<char> &buffer, const TagValue &co
|
|||
*(++offset) = 0x00;
|
||||
}
|
||||
// write actual data
|
||||
string data = comment.toString();
|
||||
const auto data = comment.toString();
|
||||
data.copy(++offset, data.length());
|
||||
}
|
||||
|
||||
|
|
|
@ -32,10 +32,10 @@ public:
|
|||
void parsePicture(const char *buffer, size_t maxSize, TagValue &tagValue, byte &typeInfo);
|
||||
void parseComment(const char *buffer, size_t maxSize, TagValue &tagValue);
|
||||
void parseBom(const char *buffer, std::size_t maxSize, TagTextEncoding &encoding);
|
||||
void makeString(std::vector<char> &buffer, const std::string &value, TagTextEncoding encoding);
|
||||
void makeEncodingAndData(std::vector<char> &buffer, TagTextEncoding encoding, const char *data, size_t m_dataSize);
|
||||
void makePicture(std::vector<char> &buffer, const TagValue &picture, byte typeInfo);
|
||||
void makeComment(std::vector<char> &buffer, const TagValue &comment);
|
||||
void makeString(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, const std::string &value, TagTextEncoding encoding);
|
||||
void makeEncodingAndData(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, TagTextEncoding encoding, const char *data, size_t m_dataSize);
|
||||
void makePicture(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &picture, byte typeInfo);
|
||||
void makeComment(std::unique_ptr<char[]> &buffer, uint32 &bufferSize, const TagValue &comment);
|
||||
|
||||
private:
|
||||
std::string m_id;
|
||||
|
|
|
@ -191,6 +191,7 @@ startParsingSignature:
|
|||
break;
|
||||
} case ContainerFormat::Ogg:
|
||||
m_container = make_unique<OggContainer>(*this, m_containerOffset);
|
||||
static_cast<OggContainer *>(m_container.get())->setChecksumValidationEnabled(m_forceFullParse);
|
||||
break;
|
||||
default:
|
||||
;
|
||||
|
|
|
@ -266,9 +266,7 @@ void Mp4Tag::make(ostream &stream)
|
|||
int tagFieldsWritten = 0;
|
||||
for(auto i = fields().begin(), end = fields().end(); i != end; ++i) {
|
||||
Mp4TagField &field = i->second;
|
||||
if(field.value().isEmpty()) {
|
||||
continue;
|
||||
} else {
|
||||
if(!field.value().isEmpty()) {
|
||||
field.invalidateNotifications();
|
||||
try {
|
||||
field.make(stream);
|
||||
|
|
|
@ -51,7 +51,7 @@ void MpegAudioFrameStream::internalParseHeader()
|
|||
if(frame.isXingBytesfieldPresent()) {
|
||||
uint32 xingSize = frame.xingBytesfield();
|
||||
if(m_size && xingSize != m_size) {
|
||||
addNotification(NotificationType::Warning, "Real length MPEG of audio frames is not equal with value provided by Xing header. The Xing header value will be used.", context);
|
||||
addNotification(NotificationType::Warning, "Real length of MPEG audio frames is not equal with value provided by Xing header. The Xing header value will be used.", context);
|
||||
m_size = xingSize;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ void OggContainer::internalParseHeader()
|
|||
static const string context("parsing OGG bitstream header");
|
||||
// iterate through pages using OggIterator helper class
|
||||
try {
|
||||
uint32 pageSequenceNumber = 0;
|
||||
// ensure iterator is setup properly
|
||||
for(m_iterator.removeFilter(), m_iterator.reset(); m_iterator; m_iterator.nextPage()) {
|
||||
const OggPage &page = m_iterator.currentPage();
|
||||
|
@ -49,6 +50,14 @@ void OggContainer::internalParseHeader()
|
|||
m_streamsBySerialNo[page.streamSerialNumber()] = m_tracks.size();
|
||||
m_tracks.emplace_back(new OggStream(*this, m_iterator.currentPageIndex()));
|
||||
}
|
||||
if(pageSequenceNumber != page.sequenceNumber()) {
|
||||
if(pageSequenceNumber != 0) {
|
||||
addNotification(NotificationType::Warning, "Page is missing (page sequence number omitted).", context);
|
||||
pageSequenceNumber = page.sequenceNumber();
|
||||
}
|
||||
} else {
|
||||
++pageSequenceNumber;
|
||||
}
|
||||
}
|
||||
} catch(TruncatedDataException &) {
|
||||
// thrown when page exceeds max size
|
||||
|
@ -64,11 +73,13 @@ void OggContainer::internalParseHeader()
|
|||
void OggContainer::internalParseTags()
|
||||
{
|
||||
parseTracks(); // tracks needs to be parsed because tags are stored at stream level
|
||||
for(auto i : m_commentTable) {
|
||||
for(auto &i : m_commentTable) {
|
||||
//fileInfo().stream().seekg(get<1>(i));
|
||||
m_iterator.setPageIndex(get<0>(i));
|
||||
m_iterator.setSegmentIndex(get<1>(i));
|
||||
m_tags[get<2>(i)]->parse(m_iterator);
|
||||
m_iterator.setPageIndex(i.firstPageIndex);
|
||||
m_iterator.setSegmentIndex(i.firstSegmentIndex);
|
||||
m_tags[i.tagIndex]->parse(m_iterator);
|
||||
i.lastPageIndex = m_iterator.currentPageIndex();
|
||||
i.lastSegmentIndex = m_iterator.currentSegmentIndex();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,6 +111,7 @@ void OggContainer::internalMakeFile()
|
|||
{
|
||||
const string context("making OGG file");
|
||||
updateStatus("Prepare for rewriting OGG file ...");
|
||||
parseTags(); // tags need to be parsed before the file can be rewritten
|
||||
fileInfo().close();
|
||||
string backupPath;
|
||||
fstream backupStream;
|
||||
|
@ -110,10 +122,16 @@ void OggContainer::internalMakeFile()
|
|||
CopyHelper<65307> copy;
|
||||
auto commentTableIterator = m_commentTable.cbegin(), commentTableEnd = m_commentTable.cend();
|
||||
vector<uint64> updatedPageOffsets;
|
||||
uint32 pageSequenceNumber = 0;
|
||||
for(m_iterator.setStream(backupStream), m_iterator.removeFilter(), m_iterator.reset(); m_iterator; m_iterator.nextPage()) {
|
||||
const auto ¤tPage = m_iterator.currentPage();
|
||||
auto pageSize = currentPage.totalSize();
|
||||
if(commentTableIterator != commentTableEnd && m_iterator.currentPageIndex() == get<0>(*commentTableIterator) && !currentPage.segmentSizes().empty()) {
|
||||
// check whether the Vorbis Comment is present in this Ogg page
|
||||
// -> then the page needs to be rewritten
|
||||
if(commentTableIterator != commentTableEnd
|
||||
&& m_iterator.currentPageIndex() >= commentTableIterator->firstPageIndex
|
||||
&& m_iterator.currentPageIndex() <= commentTableIterator->lastPageIndex
|
||||
&& !currentPage.segmentSizes().empty()) {
|
||||
// page needs to be rewritten (not just copied)
|
||||
// -> write segments to a buffer first
|
||||
stringstream buffer(ios_base::in | ios_base::out | ios_base::binary);
|
||||
|
@ -121,67 +139,109 @@ void OggContainer::internalMakeFile()
|
|||
newSegmentSizes.reserve(currentPage.segmentSizes().size());
|
||||
uint64 segmentOffset = m_iterator.currentSegmentOffset();
|
||||
vector<uint32>::size_type segmentIndex = 0;
|
||||
for(auto segmentSize : currentPage.segmentSizes()) {
|
||||
if(segmentIndex == get<1>(*commentTableIterator)) {
|
||||
// make vorbis comment segment
|
||||
auto offset = buffer.tellp();
|
||||
m_tags[get<2>(*commentTableIterator)]->make(buffer);
|
||||
newSegmentSizes.push_back(buffer.tellp() - offset);
|
||||
} else {
|
||||
// copy other segments unchanged
|
||||
backupStream.seekg(segmentOffset);
|
||||
copy.copy(backupStream, buffer, segmentSize);
|
||||
newSegmentSizes.push_back(segmentSize);
|
||||
for(const auto segmentSize : currentPage.segmentSizes()) {
|
||||
if(segmentSize) {
|
||||
// check whether this segment contains the Vorbis Comment
|
||||
if((m_iterator.currentPageIndex() > commentTableIterator->firstPageIndex || segmentIndex >= commentTableIterator->firstSegmentIndex)
|
||||
&& (m_iterator.currentPageIndex() < commentTableIterator->lastPageIndex || segmentIndex <= commentTableIterator->lastSegmentIndex)) {
|
||||
// prevent making the comment twice if it spreads over multiple pages
|
||||
if(m_iterator.currentPageIndex() == commentTableIterator->firstPageIndex) {
|
||||
// make Vorbis Comment segment
|
||||
auto offset = buffer.tellp();
|
||||
m_tags[commentTableIterator->tagIndex]->make(buffer);
|
||||
newSegmentSizes.push_back(buffer.tellp() - offset);
|
||||
}
|
||||
if(m_iterator.currentPageIndex() > commentTableIterator->lastPageIndex
|
||||
|| (m_iterator.currentPageIndex() == commentTableIterator->lastPageIndex && segmentIndex > commentTableIterator->lastSegmentIndex)) {
|
||||
++commentTableIterator;
|
||||
}
|
||||
} else {
|
||||
// copy other segments unchanged
|
||||
backupStream.seekg(segmentOffset);
|
||||
copy.copy(backupStream, buffer, segmentSize);
|
||||
newSegmentSizes.push_back(segmentSize);
|
||||
}
|
||||
segmentOffset += segmentSize;
|
||||
}
|
||||
segmentOffset += segmentSize;
|
||||
++segmentIndex;
|
||||
}
|
||||
// write buffered data to actual stream
|
||||
auto newSegmentSizesIterator = newSegmentSizes.cbegin(), newSegmentSizesEnd = newSegmentSizes.cend();
|
||||
uint32 bytesLeft, currentSize;
|
||||
// write pages until all data in the buffer is written
|
||||
while(newSegmentSizesIterator != newSegmentSizesEnd) {
|
||||
// write header
|
||||
backupStream.seekg(currentPage.startOffset());
|
||||
copy.copy(backupStream, stream(), 27); // just copy from original file
|
||||
updatedPageOffsets.push_back(currentPage.startOffset()); // memorize offset to update checksum later
|
||||
int16 segmentSizesWritten = 0; // in the current page header only
|
||||
// write segment sizes as long as there are segment sizes to be written and
|
||||
// the max number of segment sizes is not exceeded
|
||||
bytesLeft = *newSegmentSizesIterator;
|
||||
currentSize = 0;
|
||||
while(bytesLeft > 0 && segmentSizesWritten < 0xFF) {
|
||||
while(bytesLeft >= 0xFF && segmentSizesWritten < 0xFF) {
|
||||
stream().put(0xFF);
|
||||
bytesLeft -= 0xFF;
|
||||
++segmentSizesWritten;
|
||||
bool continuePreviousSegment = false;
|
||||
if(newSegmentSizesIterator != newSegmentSizesEnd) {
|
||||
uint32 bytesLeft = *newSegmentSizesIterator;
|
||||
// write pages until all data in the buffer is written
|
||||
while(newSegmentSizesIterator != newSegmentSizesEnd) {
|
||||
// write header
|
||||
backupStream.seekg(currentPage.startOffset());
|
||||
updatedPageOffsets.push_back(stream().tellp()); // memorize offset to update checksum later
|
||||
copy.copy(backupStream, stream(), 27); // just copy header from original file
|
||||
// set continue flag
|
||||
stream().seekp(-22, ios_base::cur);
|
||||
stream().put(currentPage.headerTypeFlag() & (continuePreviousSegment ? 0xFF : 0xFE));
|
||||
continuePreviousSegment = true;
|
||||
// adjust page sequence number
|
||||
stream().seekp(12, ios_base::cur);
|
||||
writer().writeUInt32LE(pageSequenceNumber);
|
||||
stream().seekp(5, ios_base::cur);
|
||||
int16 segmentSizesWritten = 0; // in the current page header only
|
||||
// write segment sizes as long as there are segment sizes to be written and
|
||||
// the max number of segment sizes (255) is not exceeded
|
||||
uint32 currentSize = 0;
|
||||
while(bytesLeft > 0 && segmentSizesWritten < 0xFF) {
|
||||
while(bytesLeft >= 0xFF && segmentSizesWritten < 0xFF) {
|
||||
stream().put(0xFF);
|
||||
currentSize += 0xFF;
|
||||
bytesLeft -= 0xFF;
|
||||
++segmentSizesWritten;
|
||||
}
|
||||
if(bytesLeft > 0 && segmentSizesWritten < 0xFF) {
|
||||
// bytes left is here < 0xFF
|
||||
stream().put(bytesLeft);
|
||||
currentSize += bytesLeft;
|
||||
bytesLeft = 0;
|
||||
++segmentSizesWritten;
|
||||
}
|
||||
if(bytesLeft == 0) {
|
||||
// sizes for the segment have been written
|
||||
// -> continue with next segment
|
||||
if(++newSegmentSizesIterator != newSegmentSizesEnd) {
|
||||
bytesLeft = *newSegmentSizesIterator;
|
||||
continuePreviousSegment = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(bytesLeft > 0 && segmentSizesWritten < 0xFF) {
|
||||
stream().put(bytesLeft);
|
||||
bytesLeft = 0;
|
||||
++segmentSizesWritten;
|
||||
}
|
||||
currentSize += *newSegmentSizesIterator - bytesLeft;
|
||||
// there are no bytes left in the current segment; remove continue flag
|
||||
if(bytesLeft == 0) {
|
||||
// sizes for the current segemnt have been written -> continue with next segment
|
||||
++newSegmentSizesIterator;
|
||||
bytesLeft = newSegmentSizesIterator != newSegmentSizesEnd ? *newSegmentSizesIterator : 0;
|
||||
continuePreviousSegment = false;
|
||||
}
|
||||
}
|
||||
// page is full or all segment data has already been written -> write data
|
||||
if(bytesLeft == 0 || newSegmentSizesIterator != newSegmentSizesEnd) {
|
||||
// seek back and write updated page segment number
|
||||
// page is full or all segment data has been covered
|
||||
// -> write segment table size (segmentSizesWritten) and segment data
|
||||
// -> seek back and write updated page segment number
|
||||
stream().seekp(-1 - segmentSizesWritten, ios_base::cur);
|
||||
stream().put(segmentSizesWritten);
|
||||
stream().seekp(segmentSizesWritten, ios_base::cur);
|
||||
// write actual page data
|
||||
// -> write actual page data
|
||||
copy.copy(buffer, stream(), currentSize);
|
||||
++pageSequenceNumber;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// copy page unchanged
|
||||
backupStream.seekg(currentPage.startOffset());
|
||||
copy.copy(backupStream, stream(), pageSize);
|
||||
if(pageSequenceNumber != m_iterator.currentPageIndex()) {
|
||||
// just update page sequence number
|
||||
backupStream.seekg(currentPage.startOffset());
|
||||
updatedPageOffsets.push_back(stream().tellp()); // memorize offset to update checksum later
|
||||
copy.copy(backupStream, stream(), 27);
|
||||
stream().seekp(-9, ios_base::cur);
|
||||
writer().writeUInt32LE(pageSequenceNumber);
|
||||
stream().seekp(5, ios_base::cur);
|
||||
copy.copy(backupStream, stream(), pageSize - 27);
|
||||
} else {
|
||||
// copy page unchanged
|
||||
backupStream.seekg(currentPage.startOffset());
|
||||
copy.copy(backupStream, stream(), pageSize);
|
||||
}
|
||||
++pageSequenceNumber;
|
||||
}
|
||||
}
|
||||
// close backups stream; reopen new file as readable stream
|
||||
|
|
|
@ -16,6 +16,25 @@ namespace Media {
|
|||
|
||||
class MediaFileInfo;
|
||||
|
||||
struct LIB_EXPORT VorbisCommentInfo
|
||||
{
|
||||
VorbisCommentInfo(std::vector<OggPage>::size_type firstPageIndex, std::vector<OggPage>::size_type firstSegmentIndex, std::vector<OggPage>::size_type tagIndex);
|
||||
|
||||
std::vector<OggPage>::size_type firstPageIndex;
|
||||
std::vector<OggPage>::size_type firstSegmentIndex;
|
||||
std::vector<OggPage>::size_type lastPageIndex;
|
||||
std::vector<OggPage>::size_type lastSegmentIndex;
|
||||
std::vector<std::unique_ptr<VorbisComment> >::size_type tagIndex;
|
||||
};
|
||||
|
||||
inline VorbisCommentInfo::VorbisCommentInfo(std::vector<OggPage>::size_type firstPageIndex, std::vector<OggPage>::size_type firstSegmentIndex, std::vector<OggPage>::size_type tagIndex) :
|
||||
firstPageIndex(firstPageIndex),
|
||||
firstSegmentIndex(firstSegmentIndex),
|
||||
lastPageIndex(0),
|
||||
lastSegmentIndex(0),
|
||||
tagIndex(tagIndex)
|
||||
{}
|
||||
|
||||
class LIB_EXPORT OggContainer : public GenericContainer<MediaFileInfo, VorbisComment, OggStream, OggPage>
|
||||
{
|
||||
friend class OggStream;
|
||||
|
@ -37,7 +56,12 @@ private:
|
|||
void ariseComment(std::vector<OggPage>::size_type pageIndex, std::vector<uint32>::size_type segmentIndex);
|
||||
|
||||
std::unordered_map<uint32, std::vector<std::unique_ptr<OggStream> >::size_type> m_streamsBySerialNo;
|
||||
std::list<std::tuple<std::vector<OggPage>::size_type, std::vector<uint32>::size_type, std::vector<std::unique_ptr<VorbisComment> >::size_type> > m_commentTable;
|
||||
|
||||
/*!
|
||||
* \brief Consists of first page index, first segment index, last page index, last segment index and tag index (in this order).
|
||||
*/
|
||||
std::list<VorbisCommentInfo> m_commentTable;
|
||||
|
||||
OggIterator m_iterator;
|
||||
bool m_validateChecksums;
|
||||
};
|
||||
|
|
|
@ -77,28 +77,28 @@ uint32 OggPage::computeChecksum(istream &stream, uint64 startOffset)
|
|||
stream.seekg(startOffset);
|
||||
uint32 crc = 0x0;
|
||||
uint32 *crc32Table = BinaryReader::crc32Table();
|
||||
byte val;
|
||||
for(uint32 i = 0, len = 27, si = 0, sc = 0; i < len; ++i) {
|
||||
byte value, segmentTableSize = 0, segmentTableIndex = 0;
|
||||
for(uint32 i = 0, segmentLength = 27; i < segmentLength; ++i) {
|
||||
switch(i) {
|
||||
case 22:
|
||||
// bytes 22, 23, 24, 25 hold denoted checksum and must be set to zero
|
||||
stream.seekg(4, ios_base::cur);
|
||||
case 23: case 24: case 25:
|
||||
val = 0;
|
||||
value = 0;
|
||||
break;
|
||||
case 26:
|
||||
// byte 26 holds the number of segment sizes
|
||||
len += (sc = (val = stream.get()));
|
||||
segmentLength += (segmentTableSize = (value = stream.get()));
|
||||
break;
|
||||
default:
|
||||
val = stream.get();
|
||||
if(i > 26 && si < sc) {
|
||||
value = stream.get();
|
||||
if(i > 26 && segmentTableIndex < segmentTableSize) {
|
||||
// bytes 27 to (27 + segment size count) hold page size
|
||||
len += val;
|
||||
++si;
|
||||
segmentLength += value;
|
||||
++segmentTableIndex;
|
||||
}
|
||||
}
|
||||
crc = (crc << 8) ^ crc32Table[((crc >> 24) & 0xff) ^ val];
|
||||
crc = (crc << 8) ^ crc32Table[((crc >> 24) & 0xFF) ^ value];
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
|
4
tag.cpp
4
tag.cpp
|
@ -92,9 +92,9 @@ string Tag::toString() const
|
|||
* \param overwrite Indicates whether existing values should be overwritten.
|
||||
* \return Returns the number of values that have been inserted.
|
||||
*/
|
||||
int Tag::insertValues(const Tag &from, bool overwrite)
|
||||
unsigned int Tag::insertValues(const Tag &from, bool overwrite)
|
||||
{
|
||||
int count = 0;
|
||||
unsigned int count = 0;
|
||||
for(int i = static_cast<int>(KnownField::Invalid) + 1, last = static_cast<int>(KnownField::Description);
|
||||
i <= last; ++i) {
|
||||
KnownField field = static_cast<KnownField>(i);
|
||||
|
|
4
tag.h
4
tag.h
|
@ -115,12 +115,12 @@ public:
|
|||
virtual bool supportsTarget() const;
|
||||
const TagTarget &target() const;
|
||||
void setTarget(const TagTarget &target);
|
||||
virtual int fieldCount() const = 0;
|
||||
virtual unsigned int fieldCount() const = 0;
|
||||
virtual bool supportsField(KnownField field) const = 0;
|
||||
virtual TagDataType proposedDataType(KnownField field) const;
|
||||
virtual bool supportsDescription(KnownField field) const;
|
||||
virtual bool supportsMimeType(KnownField field) const;
|
||||
virtual int insertValues(const Tag &from, bool overwrite);
|
||||
virtual unsigned int insertValues(const Tag &from, bool overwrite);
|
||||
// Tag *parent() const;
|
||||
// bool setParent(Tag *tag);
|
||||
// Tag *nestedTag(size_t index) const;
|
||||
|
|
|
@ -111,7 +111,7 @@ void VorbisComment::parse(OggIterator &iterator)
|
|||
invalidateStatus();
|
||||
static const string context("parsing Vorbis comment");
|
||||
iterator.stream().seekg(iterator.currentSegmentOffset());
|
||||
uint64 startOffset = iterator.currentSegmentOffset();
|
||||
auto startOffset = iterator.currentSegmentOffset();
|
||||
try {
|
||||
// read signature: 0x3 + "vorbis"
|
||||
char sig[8];
|
||||
|
@ -120,8 +120,8 @@ void VorbisComment::parse(OggIterator &iterator)
|
|||
// read vendor (length prefixed string)
|
||||
{
|
||||
iterator.read(sig, 4);
|
||||
uint32 vendorSize = LE::toUInt32(sig);
|
||||
unique_ptr<char []> buff = make_unique<char []>(vendorSize);
|
||||
auto vendorSize = LE::toUInt32(sig);
|
||||
auto buff = make_unique<char []>(vendorSize);
|
||||
iterator.read(buff.get(), vendorSize);
|
||||
m_vendor = string(buff.get(), vendorSize);
|
||||
}
|
||||
|
@ -151,6 +151,7 @@ void VorbisComment::parse(OggIterator &iterator)
|
|||
throw InvalidDataException();
|
||||
}
|
||||
} catch(TruncatedDataException &) {
|
||||
m_size = static_cast<uint32>(static_cast<uint64>(iterator.currentCharacterOffset()) - startOffset);
|
||||
addNotification(NotificationType::Critical, "Vorbis comment is truncated.", context);
|
||||
throw;
|
||||
}
|
||||
|
@ -186,14 +187,16 @@ void VorbisComment::make(std::ostream &stream)
|
|||
// write fields
|
||||
for(auto i : fields()) {
|
||||
VorbisCommentField &field = i.second;
|
||||
try {
|
||||
field.make(writer);
|
||||
} catch(Failure &) {
|
||||
// nothing to do here since notifications will be added anyways
|
||||
if(!field.value().isEmpty()) {
|
||||
try {
|
||||
field.make(writer);
|
||||
} catch(Failure &) {
|
||||
// nothing to do here since notifications will be added anyways
|
||||
}
|
||||
// add making notifications
|
||||
addNotifications(context, field);
|
||||
field.invalidateNotifications();
|
||||
}
|
||||
// add making notifications
|
||||
addNotifications(context, field);
|
||||
field.invalidateNotifications();
|
||||
}
|
||||
// write framing byte
|
||||
stream.put(0x01);
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
#include <c++utilities/conversion/stringconversion.h>
|
||||
#include <c++utilities/misc/memory.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
using namespace std;
|
||||
using namespace IoUtilities;
|
||||
using namespace ConversionUtilities;
|
||||
|
@ -52,13 +54,12 @@ void VorbisCommentField::parse(OggIterator &iterator)
|
|||
static const string context("parsing Vorbis comment field");
|
||||
char buff[4];
|
||||
iterator.read(buff, 4);
|
||||
if(uint32 size = LE::toUInt32(buff)) { // read size
|
||||
if(auto size = LE::toUInt32(buff)) { // read size
|
||||
// read data
|
||||
unique_ptr<char[]> data = make_unique<char []>(size);
|
||||
auto data = make_unique<char []>(size);
|
||||
iterator.read(data.get(), size);
|
||||
uint32 idSize = 0;
|
||||
for(const char *i = data.get(), *end = data.get() + size; i != end && (*i) != '='; ++i, ++idSize)
|
||||
;
|
||||
for(const char *i = data.get(), *end = data.get() + size; i != end && *i != '='; ++i, ++idSize);
|
||||
// extract id
|
||||
setId(string(data.get(), idSize));
|
||||
if(!idSize) {
|
||||
|
@ -67,11 +68,30 @@ void VorbisCommentField::parse(OggIterator &iterator)
|
|||
throw InvalidDataException();
|
||||
} else if(id() == VorbisCommentIds::cover()) {
|
||||
// extract cover value
|
||||
vector<char> buffer = decodeBase64(string(data.get() + idSize + 1, size - idSize - 1));
|
||||
Id3v2FrameHelper helper(id(), *this);
|
||||
byte type;
|
||||
helper.parsePicture(buffer.data(), buffer.size(), value(), type);
|
||||
setTypeInfo(type);
|
||||
try {
|
||||
auto decoded = decodeBase64(data.get() + idSize + 1, size - idSize - 1);
|
||||
stringstream ss(ios_base::in | ios_base::out | ios_base::binary);
|
||||
ss.exceptions(ios_base::failbit | ios_base::badbit);
|
||||
ss.rdbuf()->pubsetbuf(reinterpret_cast<char *>(decoded.first.get()), decoded.second);
|
||||
BinaryReader reader(&ss);
|
||||
setTypeInfo(reader.readUInt32BE());
|
||||
auto size = reader.readUInt32BE();
|
||||
value().setMimeType(reader.readString(size));
|
||||
size = reader.readUInt32BE();
|
||||
value().setDescription(reader.readString(size));
|
||||
// skip width, height, color depth, number of colors used
|
||||
ss.seekg(4 * 4, ios_base::cur);
|
||||
size = reader.readUInt32BE();
|
||||
auto data = make_unique<char[]>(size);
|
||||
ss.read(data.get(), size);
|
||||
value().assignData(move(data), size, TagDataType::Picture);
|
||||
} catch (const ios_base::failure &) {
|
||||
addNotification(NotificationType::Critical, "An IO error occured when reading the METADATA_BLOCK_PICTURE struct.", context);
|
||||
throw Failure();
|
||||
} catch (const ConversionException &) {
|
||||
addNotification(NotificationType::Critical, "Base64 data from METADATA_BLOCK_PICTURE is invalid.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
} else if(id().size() + 1 < size) {
|
||||
// extract other values (as string)
|
||||
setValue(string(data.get() + idSize + 1, size - idSize - 1));
|
||||
|
@ -92,20 +112,42 @@ void VorbisCommentField::make(BinaryWriter &writer)
|
|||
if(id().empty()) {
|
||||
addNotification(NotificationType::Critical, "The field ID is empty.", context);
|
||||
}
|
||||
// try to convert value to string
|
||||
try {
|
||||
// try to convert value to string
|
||||
string valueString;
|
||||
if(id() == VorbisCommentIds::cover()) {
|
||||
// make cover
|
||||
Id3v2FrameHelper helper(id(), *this);
|
||||
vector<char> buffer;
|
||||
helper.makePicture(buffer, value(), isTypeInfoAssigned() ? typeInfo() : 0);
|
||||
valueString = encodeBase64(buffer);
|
||||
if(value().type() != TagDataType::Picture) {
|
||||
addNotification(NotificationType::Critical, "Assigned value of cover field is not picture data.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
try {
|
||||
uint32 dataSize = 32 + value().mimeType().size() + value().description().size() + value().dataSize();
|
||||
auto buffer = make_unique<char[]>(dataSize);
|
||||
stringstream ss(ios_base::in | ios_base::out | ios_base::binary);
|
||||
ss.exceptions(ios_base::failbit | ios_base::badbit);
|
||||
ss.rdbuf()->pubsetbuf(buffer.get(), dataSize);
|
||||
BinaryWriter writer(&ss);
|
||||
writer.writeUInt32BE(typeInfo());
|
||||
writer.writeUInt32BE(value().mimeType().size());
|
||||
writer.writeString(value().mimeType());
|
||||
writer.writeUInt32BE(value().description().size());
|
||||
writer.writeString(value().description());
|
||||
writer.writeUInt32BE(0); // skip width
|
||||
writer.writeUInt32BE(0); // skip height
|
||||
writer.writeUInt32BE(0); // skip color depth
|
||||
writer.writeUInt32BE(0); // skip number of colors used
|
||||
writer.writeUInt32BE(value().dataSize());
|
||||
writer.write(value().dataPointer(), value().dataSize());
|
||||
valueString = encodeBase64(reinterpret_cast<byte *>(buffer.get()), dataSize);
|
||||
} catch (const ios_base::failure &) {
|
||||
addNotification(NotificationType::Critical, "An IO error occured when writing the METADATA_BLOCK_PICTURE struct.", context);
|
||||
throw Failure();
|
||||
}
|
||||
} else {
|
||||
// make normal string value
|
||||
valueString = value().toString();
|
||||
}
|
||||
// write size
|
||||
writer.writeUInt32LE(id().size() + 1 + valueString.size());
|
||||
writer.writeString(id());
|
||||
writer.writeChar('=');
|
||||
|
|
|
@ -27,7 +27,7 @@ public:
|
|||
/*!
|
||||
* \brief The type info is stored using 32-bit unsigned integers.
|
||||
*/
|
||||
typedef byte typeInfoType;
|
||||
typedef uint32 typeInfoType;
|
||||
/*!
|
||||
* \brief The implementation type is VorbisCommentField.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue