diff options
author | Hubert Figuière <hub@figuiere.net> | 2013-05-14 18:21:26 -0400 |
---|---|---|
committer | Hubert Figuière <hub@figuiere.net> | 2013-05-14 18:21:26 -0400 |
commit | 81a4c6bcb1879cb321246590faca595e9746f8e5 (patch) | |
tree | cf92c416eb3e41708149905abd0030680aebadf5 /XMPFiles | |
parent | 42dbac60f15e038270d6e0c7285caba8256e86f1 (diff) |
Update to XMP SDK CS6
Diffstat (limited to 'XMPFiles')
146 files changed, 58661 insertions, 0 deletions
diff --git a/XMPFiles/source/FileHandlers/AIFF_Handler.cpp b/XMPFiles/source/FileHandlers/AIFF_Handler.cpp new file mode 100644 index 0000000..bd59ea7 --- /dev/null +++ b/XMPFiles/source/FileHandlers/AIFF_Handler.cpp @@ -0,0 +1,427 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FileHandlers/AIFF_Handler.hpp" +#include "XMPFiles/source/FormatSupport/AIFF/AIFFBehavior.h" +#include "XMPFiles/source/FormatSupport/AIFF/AIFFReconcile.h" +#include "XMPFiles/source/NativeMetadataSupport/MetadataSet.h" +#include "source/XIO.hpp" + +using namespace IFF_RIFF; + +// ================================================================================================= +/// \file AIFF_Handler.cpp +/// \brief File format handler for AIFF. +// ================================================================================================= + + +// ================================================================================================= +// AIFF_MetaHandlerCTor +// ==================== + +XMPFileHandler * AIFF_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new AIFF_MetaHandler ( parent ); +} + +// ================================================================================================= +// AIFF_CheckFormat +// =============== +// +// Checks if the given file is a valid AIFF or AIFC file. +// The first 12 bytes are checked. The first 4 must be "FORM" +// Bytes 8 to 12 must be "AIFF" or "AIFC" + +bool AIFF_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* file, + XMPFiles* parent ) +{ + // Reset file pointer position + file ->Rewind(); + + XMP_Uns8 chunkID[12]; + XMP_Int32 got = file->Read ( chunkID, 12 ); + + // Reset file pointer position + file ->Rewind(); + + // Need to have at least ID, size and Type of first chunk + if ( got < 12 ) + { + return false; + } + + const BigEndian& endian = BigEndian::getInstance(); + if ( endian.getUns32(chunkID) != kChunk_FORM ) + { + return false; + } + + XMP_Uns32 type = AIFF_MetaHandler::whatAIFFFormat( &chunkID[8] ); + if ( type == kType_AIFF || type == kType_AIFC ) + { + return true; + } + + return false; +} // AIFF_CheckFormat + + +// ================================================================================================= +// AIFF_MetaHandler::whatAIFFFormat +// =============== + +XMP_Uns32 AIFF_MetaHandler::whatAIFFFormat( XMP_Uns8* buffer ) +{ + XMP_Uns32 type = 0; + + const BigEndian& endian = BigEndian::getInstance(); + + if( buffer != 0 ) + { + if( endian.getUns32( buffer ) == kType_AIFF ) + { + type = kType_AIFF; + } + else if( endian.getUns32( buffer ) == kType_AIFC ) + { + type = kType_AIFC; + } + } + + return type; +} // whatAIFFFormat + + +// Static inits + +// ChunkIdentifier +// FORM:AIFF/APPL:XMP +const ChunkIdentifier AIFF_MetaHandler::kAIFFXMP[2] = { { kChunk_FORM, kType_AIFF }, { kChunk_APPL, kType_XMP } }; +// FORM:AIFC/APPL:XMP +const ChunkIdentifier AIFF_MetaHandler::kAIFCXMP[2] = { { kChunk_FORM, kType_AIFC }, { kChunk_APPL, kType_XMP } }; +// FORM:AIFF/NAME +const ChunkIdentifier AIFF_MetaHandler::kAIFFName[2] = { { kChunk_FORM, kType_AIFF }, { kChunk_NAME, kType_NONE } }; +// FORM:AIFC/NAME +const ChunkIdentifier AIFF_MetaHandler::kAIFCName[2] = { { kChunk_FORM, kType_AIFC }, { kChunk_NAME, kType_NONE } }; +// FORM:AIFF/AUTH +const ChunkIdentifier AIFF_MetaHandler::kAIFFAuth[2] = { { kChunk_FORM, kType_AIFF }, { kChunk_AUTH, kType_NONE } }; +// FORM:AIFC/AUTH +const ChunkIdentifier AIFF_MetaHandler::kAIFCAuth[2] = { { kChunk_FORM, kType_AIFC }, { kChunk_AUTH, kType_NONE } }; +// FORM:AIFF/(c) +const ChunkIdentifier AIFF_MetaHandler::kAIFFCpr[2] = { { kChunk_FORM, kType_AIFF }, { kChunk_CPR, kType_NONE } }; +// FORM:AIFC/(c) +const ChunkIdentifier AIFF_MetaHandler::kAIFCCpr[2] = { { kChunk_FORM, kType_AIFC }, { kChunk_CPR, kType_NONE } }; +// FORM:AIFF/ANNO +const ChunkIdentifier AIFF_MetaHandler::kAIFFAnno[2] = { { kChunk_FORM, kType_AIFF }, { kChunk_ANNO, kType_NONE } }; +// FORM:AIFC/ANNO +const ChunkIdentifier AIFF_MetaHandler::kAIFCAnno[2] = { { kChunk_FORM, kType_AIFC }, { kChunk_ANNO, kType_NONE } }; + +// ================================================================================================= +// AIFF_MetaHandler::AIFF_MetaHandler +// ================================ + +AIFF_MetaHandler::AIFF_MetaHandler ( XMPFiles * _parent ) + : mChunkBehavior(NULL), mChunkController(NULL), + mAiffMeta(), mXMPChunk(NULL), + mNameChunk(NULL), mAuthChunk(NULL), + mCprChunk(NULL), mAnnoChunk(NULL), mFileType(0) +{ + this->parent = _parent; + this->handlerFlags = kAIFF_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + + this->mChunkBehavior = new AIFFBehavior(); + this->mChunkController = new ChunkController( mChunkBehavior, true ); + +} // AIFF_MetaHandler::AIFF_MetaHandler + +// ================================================================================================= +// AIFF_MetaHandler::~AIFF_MetaHandler +// ================================= + +AIFF_MetaHandler::~AIFF_MetaHandler() +{ + if( mChunkController != NULL ) + { + delete mChunkController; + } + + if( mChunkBehavior != NULL ) + { + delete mChunkBehavior; + } +} // AIFF_MetaHandler::~AIFF_MetaHandler + + +// ================================================================================================= +// AIFF_MetaHandler::CacheFileData +// ============================== + +void AIFF_MetaHandler::CacheFileData() +{ + // Need to determine the file type, need the first 12 bytes of the file + + // Reset file pointer position + this->parent->ioRef ->Rewind(); + + XMP_Uns8 buffer[12]; + XMP_Int32 got = this->parent->ioRef->Read ( buffer, 12 ); + XMP_Assert( got == 12 ); + + XMP_Uns32 type = AIFF_MetaHandler::whatAIFFFormat( &buffer[8] ); + XMP_Assert( type == kType_AIFF || type == kType_AIFC ); + + // Reset file pointer position + this->parent->ioRef ->Rewind(); + + // Add the relevant chunk paths for the determined AIFF format + if( type == kType_AIFF ) + { + mAIFFXMPChunkPath.append( kAIFFXMP, SizeOfCIArray(kAIFFXMP) ); + mAIFFNameChunkPath.append( kAIFFName, SizeOfCIArray(kAIFFName) ); + mAIFFAuthChunkPath.append( kAIFFAuth, SizeOfCIArray(kAIFFAuth) ); + mAIFFCprChunkPath.append( kAIFFCpr, SizeOfCIArray(kAIFFCpr) ); + mAIFFAnnoChunkPath.append( kAIFFAnno, SizeOfCIArray(kAIFFAnno) ); + } + else // kType_AIFC + { + mAIFFXMPChunkPath.append( kAIFCXMP, SizeOfCIArray(kAIFCXMP) ); + mAIFFNameChunkPath.append( kAIFCName, SizeOfCIArray(kAIFCName) ); + mAIFFAuthChunkPath.append( kAIFCAuth, SizeOfCIArray(kAIFCAuth) ); + mAIFFCprChunkPath.append( kAIFCCpr, SizeOfCIArray(kAIFCCpr) ); + mAIFFAnnoChunkPath.append( kAIFCAnno, SizeOfCIArray(kAIFCAnno) ); + } + + mChunkController->addChunkPath( mAIFFXMPChunkPath ); + mChunkController->addChunkPath( mAIFFNameChunkPath ); + mChunkController->addChunkPath( mAIFFAuthChunkPath ); + mChunkController->addChunkPath( mAIFFCprChunkPath ); + mChunkController->addChunkPath( mAIFFAnnoChunkPath ); + + + // Parse the given file + // Throws exception if the file cannot be parsed + mChunkController->parseFile( this->parent->ioRef, &this->parent->openFlags ); + + // Check if the file contains XMP (last one if there are multiple chunks) + mXMPChunk = mChunkController->getChunk( mAIFFXMPChunkPath, true ); + + // Retrieve XMP packet info + if( mXMPChunk != NULL ) + { + // subtract the type size that is contained in the XMP data chunk + this->packetInfo.length = static_cast<XMP_Int32>(mXMPChunk->getSize() - 4); + this->packetInfo.charForm = kXMP_Char8Bit; + this->packetInfo.writeable = true; + + // Get actual the XMP packet without the 4byte type + this->xmpPacket.assign ( mXMPChunk->getString( this->packetInfo.length, 4 ) ); + + // set state + this->containsXMP = true; + } +} // AIFF_MetaHandler::CacheFileData + + +// ================================================================================================= +// AIFF_MetaHandler::ProcessXMP +// ============================ + +void AIFF_MetaHandler::ProcessXMP() +{ + // Must be done only once + if ( this->processedXMP ) + { + return; + } + // Set the status at start, in case something goes wrong in this method + this->processedXMP = true; + + // Parse the XMP + if ( ! this->xmpPacket.empty() ) { + + XMP_Assert ( this->containsXMP ); + + FillPacketInfo ( this->xmpPacket, &this->packetInfo ); + + this->xmpObj.ParseFromBuffer ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + + this->containsXMP = true; + } + + // Then import native properties + MetadataSet metaSet; + AIFFReconcile recon; + + // Fill the AIFF metadata object with values + + // Get NAME (title) legacy chunk + mNameChunk = mChunkController->getChunk( mAIFFNameChunkPath, true ); + if( mNameChunk != NULL ) + { + mAiffMeta.setValue<std::string>( AIFFMetadata::kName, mNameChunk->getString() ); + } + // Get AUTH (author) legacy chunk + mAuthChunk = mChunkController->getChunk( mAIFFAuthChunkPath, true ); + if( mAuthChunk != NULL ) + { + mAiffMeta.setValue<std::string>( AIFFMetadata::kAuthor, mAuthChunk->getString() ); + } + // Get CPR (Copyright) legacy chunk + mCprChunk = mChunkController->getChunk( mAIFFCprChunkPath, true ); + if( mCprChunk != NULL ) + { + mAiffMeta.setValue<std::string>( AIFFMetadata::kCopyright, mCprChunk->getString() ); + } + // Get ANNO (annotation) legacy chunk(s) + // Get the list of Annotation chunks and pick the last one not being empty + const std::vector<IChunkData*> &annoChunks = mChunkController->getChunks( mAIFFAnnoChunkPath ); + + mAnnoChunk = selectLastNonEmptyAnnoChunk( annoChunks ); + if( mAnnoChunk != NULL ) + { + mAiffMeta.setValue<std::string>( AIFFMetadata::kAnnotation, mAnnoChunk->getString() ); + } + + // Only interested in AIFF metadata + metaSet.append( &mAiffMeta ); + // Do the import + if( recon.importToXMP( this->xmpObj, metaSet ) ) + { + // Remember if anything has changed + this->containsXMP = true; + } + +} // AIFF_MetaHandler::ProcessXMP + + +IChunkData* AIFF_MetaHandler::selectLastNonEmptyAnnoChunk( const std::vector<IChunkData*> &annoChunks ) +{ + IChunkData* annoChunk = NULL; + for ( std::vector<IChunkData*>::const_reverse_iterator iter = annoChunks.rbegin(); iter != annoChunks.rend(); iter++ ) + { + if( ! (*iter)->getString().empty() && (*iter)->getString()[0] != '\0' ) + { + annoChunk = *iter; + break; + } + } + return annoChunk; +} // selectFirstNonEmptyAnnoChunk + + +// ================================================================================================= +// AIFF_MetaHandler::UpdateFile +// =========================== + +void AIFF_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + if ( ! this->needsUpdate ) { // If needsUpdate is set then at least the XMP changed. + return; + } + + if ( doSafeUpdate ) + { + XMP_Throw ( "AIFF_MetaHandler::UpdateFile: Safe update not supported", kXMPErr_Unavailable ); + } + + //update/create XMP chunk + if( this->containsXMP ) + { + this->xmpObj.SerializeToBuffer ( &(this->xmpPacket) ); + + if( mXMPChunk != NULL ) + { + mXMPChunk->setData( reinterpret_cast<const XMP_Uns8 *>(this->xmpPacket.c_str()), this->xmpPacket.length(), true ); + } + else // create XMP chunk + { + mXMPChunk = mChunkController->createChunk( kChunk_APPL, kType_XMP ); + mXMPChunk->setData( reinterpret_cast<const XMP_Uns8 *>(this->xmpPacket.c_str()), this->xmpPacket.length(), true ); + mChunkController->insertChunk( mXMPChunk ); + } + } + // XMP Packet is never completely removed from the file. + + // Export XMP to legacy chunks. Create/delete them if necessary + MetadataSet metaSet; + AIFFReconcile recon; + + metaSet.append( &mAiffMeta ); + + // If anything changes, update/create/delete the legacy chunks + if( recon.exportFromXMP( metaSet, this->xmpObj ) ) + { + updateLegacyChunk( &mNameChunk, kChunk_NAME, AIFFMetadata::kName ); + updateLegacyChunk( &mAuthChunk, kChunk_AUTH, AIFFMetadata::kAuthor ); + updateLegacyChunk( &mCprChunk, kChunk_CPR, AIFFMetadata::kCopyright ); + updateLegacyChunk( &mAnnoChunk, kChunk_ANNO, AIFFMetadata::kAnnotation ); + } + + //write tree back to file + mChunkController->writeFile( this->parent->ioRef ); + + this->needsUpdate = false; // Make sure this is only called once. +} // AIFF_MetaHandler::UpdateFile + + +void AIFF_MetaHandler::updateLegacyChunk( IChunkData **chunk, XMP_Uns32 chunkID, XMP_Uns32 legacyId ) +{ + // If there is a legacy value, update/create the appropriate chunk + if( mAiffMeta.valueExists( legacyId ) ) + { + std::string chunkValue; + std::string legacyValue = mAiffMeta.getValue<std::string>( legacyId ); + + // If the length is < 4 we need to fill up the value with \0 to a size of 4 + // This ensures that the overall size of text chunks is 12 bytes so that they can be + // converted to free chunks if necessary + if( legacyValue.length() < 4 ) + { + char buffer[4]; + memset( buffer, 0, 4 ); + memcpy( buffer, legacyValue.c_str(), legacyValue.length() ); + chunkValue.assign( buffer, 4 ); + } + else // take the value as is + { + chunkValue = legacyValue; + } + + if( *chunk != NULL ) + { + (*chunk)->setData( reinterpret_cast<const XMP_Uns8 *>(chunkValue.c_str()), chunkValue.length() ); + } + else + { + *chunk = mChunkController->createChunk( chunkID, kType_NONE ); + (*chunk)->setData( reinterpret_cast<const XMP_Uns8 *>(chunkValue.c_str()), chunkValue.length() ); + mChunkController->insertChunk( *chunk ); + } + } + else //delete chunk if existing + { + mChunkController->removeChunk ( *chunk ); + } +} // updateLegacyChunk + + +// ================================================================================================= +// AIFF_MetaHandler::WriteTempFile +// =============================== + +void AIFF_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + XMP_Throw ( "AIFF_MetaHandler::WriteTempFile is not Implemented!", kXMPErr_Unimplemented ); +} // AIFF_MetaHandler::WriteTempFile diff --git a/XMPFiles/source/FileHandlers/AIFF_Handler.hpp b/XMPFiles/source/FileHandlers/AIFF_Handler.hpp new file mode 100644 index 0000000..13bfa02 --- /dev/null +++ b/XMPFiles/source/FileHandlers/AIFF_Handler.hpp @@ -0,0 +1,158 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef __AIFF_Handler_hpp__ +#define __AIFF_Handler_hpp__ 1 + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/IFF/ChunkController.h" +#include "XMPFiles/source/FormatSupport/IFF/IChunkBehavior.h" +#include "XMPFiles/source/FormatSupport/IFF/IChunkData.h" +#include "source/Endian.h" +#include "XMPFiles/source/FormatSupport/IFF/ChunkPath.h" +#include "XMPFiles/source/FormatSupport/AIFF/AIFFMetadata.h" +#include "source/XIO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +using namespace IFF_RIFF; + +// ================================================================================================= +/// \file AIFF_Handler.hpp +/// \brief File format handler for AIFF. +// ================================================================================================= + +/** + * Contructor for the handler. + */ +extern XMPFileHandler * AIFF_MetaHandlerCTor ( XMPFiles * parent ); + +/** + * Checks the format of the file, see common code. + */ +extern bool AIFF_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ); + +/** AIFF does not need kXMPFiles_CanRewrite as we can always use UpdateFile to either do + * in-place update or append to the file. */ +static const XMP_OptionBits kAIFF_HandlerFlags = (kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_PrefersInPlace | + kXMPFiles_CanReconcile | + kXMPFiles_AllowsSafeUpdate + ); + +/** + * Main class for the the AIFF file handler. + */ +class AIFF_MetaHandler : public XMPFileHandler +{ +public: + AIFF_MetaHandler ( XMPFiles* parent ); + ~AIFF_MetaHandler(); + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + /** + * Checks if the first 4 bytes of the given buffer are either type AIFF or AIFC + * @param buffer a byte buffer that must contain at least 4 bytes and point to the correct byte + * @return Either kType_AIFF, kType_AIFC 0 if no type could be determined + */ + static XMP_Uns32 whatAIFFFormat( XMP_Uns8* buffer ); + +private: + /** + * Updates/creates/deletes a given legacy chunk depending on the given new legacy value + * If the Chunk exists and the value is not empty, it is updated. If the value is empty the + * Chunk is removed from the tree. If the Chunk does not exist but a value is given, it is created + * and initialized with that value + * + * @param chunk OUT pointer to the legacy chunk + * @param chunkID Id of the Chunk if it needs to be created + * @param legacyId ID of the legacy value + */ + void updateLegacyChunk( IChunkData **chunk, XMP_Uns32 chunkID, XMP_Uns32 legacyId ); + + /** + * Finds the last non-empty annotation chunk in the given list + * @param annoChunks list of annotation chunks + * @return pointer to the first non-empty chunk or NULL + */ + IChunkData* selectLastNonEmptyAnnoChunk( const std::vector<IChunkData*> &annoChunks ); + + /** private standard Ctor, not to be used */ + AIFF_MetaHandler (): mChunkController(NULL), mChunkBehavior(NULL), + mAiffMeta(), mXMPChunk(NULL), + mNameChunk(NULL), mAuthChunk(NULL), + mCprChunk(NULL), mAnnoChunk(NULL), mFileType(0) {}; + + + // ----- MEMBERS ----- // + + /** Controls the parsing and writing of the passed stream. */ + ChunkController *mChunkController; + /** Represents the rules how chunks are added, removed or rearranged */ + IChunkBehavior *mChunkBehavior; + /** container for Legacy metadata */ + AIFFMetadata mAiffMeta; + + /** pointer to the XMP chunk */ + IChunkData *mXMPChunk; + /** pointer to legacy chunks */ + IChunkData *mNameChunk; + IChunkData *mAuthChunk; + IChunkData *mCprChunk; + IChunkData *mAnnoChunk; + + /** Type of the file, either AIFF or AIFC */ + XMP_Uns32 mFileType; + + + // ----- CONSTANTS ----- // + + /** Chunk path identifier of interest in AIFF/AIFC */ + static const ChunkIdentifier kAIFFXMP[2]; + static const ChunkIdentifier kAIFCXMP[2]; + static const ChunkIdentifier kAIFFName[2]; + static const ChunkIdentifier kAIFCName[2]; + static const ChunkIdentifier kAIFFAuth[2]; + static const ChunkIdentifier kAIFCAuth[2]; + static const ChunkIdentifier kAIFFCpr[2]; + static const ChunkIdentifier kAIFCCpr[2]; + static const ChunkIdentifier kAIFFAnno[2]; + static const ChunkIdentifier kAIFCAnno[2]; + + /** Path to XMP chunk */ + ChunkPath mAIFFXMPChunkPath; + + /** Path to NAME chunk */ + ChunkPath mAIFFNameChunkPath; + + /** Path to AUTH chunk */ + ChunkPath mAIFFAuthChunkPath; + + /** Path to COPYRIGHT chunk */ + ChunkPath mAIFFCprChunkPath; + + /** Path to ANNOTATION chunk */ + ChunkPath mAIFFAnnoChunkPath; + +}; // AIFF_MetaHandler + +// ================================================================================================= + +#endif /* __AIFF_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/ASF_Handler.cpp b/XMPFiles/source/FileHandlers/ASF_Handler.cpp new file mode 100644 index 0000000..f7026bb --- /dev/null +++ b/XMPFiles/source/FileHandlers/ASF_Handler.cpp @@ -0,0 +1,339 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/ASF_Handler.hpp" + +// ================================================================================================= +/// \file ASF_Handler.hpp +/// \brief File format handler for ASF. +/// +/// This handler ... +/// +// ================================================================================================= + +// ================================================================================================= +// ASF_MetaHandlerCTor +// ==================== + +XMPFileHandler * ASF_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new ASF_MetaHandler ( parent ); + +} // ASF_MetaHandlerCTor + +// ================================================================================================= +// ASF_CheckFormat +// =============== + +bool ASF_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ) +{ + + IgnoreParam(format); IgnoreParam(fileRef); IgnoreParam(parent); + XMP_Assert ( format == kXMP_WMAVFile ); + + IOBuffer ioBuf; + + fileRef->Rewind(); + if ( ! CheckFileSpace ( fileRef, &ioBuf, guidLen ) ) return false; + + GUID guid; + memcpy ( &guid, ioBuf.ptr, guidLen ); + + if ( ! IsEqualGUID ( ASF_Header_Object, guid ) ) return false; + + return true; + +} // ASF_CheckFormat + +// ================================================================================================= +// ASF_MetaHandler::ASF_MetaHandler +// ================================== + +ASF_MetaHandler::ASF_MetaHandler ( XMPFiles * _parent ) +{ + this->parent = _parent; + this->handlerFlags = kASF_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + +} + +// ================================================================================================= +// ASF_MetaHandler::~ASF_MetaHandler +// =================================== + +ASF_MetaHandler::~ASF_MetaHandler() +{ + // Nothing extra to do. +} + +// ================================================================================================= +// ASF_MetaHandler::CacheFileData +// =============================== + +void ASF_MetaHandler::CacheFileData() +{ + + this->containsXMP = false; + + XMP_IO* fileRef ( this->parent->ioRef ); + if ( fileRef == 0 ) return; + + ASF_Support support ( &this->legacyManager ); + ASF_Support::ObjectState objectState; + long numTags = support.OpenASF ( fileRef, objectState ); + if ( numTags == 0 ) return; + + if ( objectState.xmpLen != 0 ) { + + // XMP present + + XMP_Int32 len = XMP_Int32 ( objectState.xmpLen ); + + this->xmpPacket.reserve( len ); + this->xmpPacket.assign ( len, ' ' ); + + bool found = ASF_Support::ReadBuffer ( fileRef, objectState.xmpPos, objectState.xmpLen, + const_cast<char *>(this->xmpPacket.data()) ); + if ( found ) { + this->packetInfo.offset = objectState.xmpPos; + this->packetInfo.length = len; + this->containsXMP = true; + } + + } + +} // ASF_MetaHandler::CacheFileData + +// ================================================================================================= +// ASF_MetaHandler::ProcessXMP +// ============================ +// +// Process the raw XMP and legacy metadata that was previously cached. + +void ASF_MetaHandler::ProcessXMP() +{ + + this->processedXMP = true; // Make sure we only come through here once. + + // Process the XMP packet. + + if ( this->xmpPacket.empty() ) { + + // import legacy in any case, when no XMP present + legacyManager.ImportLegacy ( &this->xmpObj ); + this->legacyManager.SetDigest ( &this->xmpObj ); + + } else { + + XMP_Assert ( this->containsXMP ); + XMP_StringPtr packetStr = this->xmpPacket.c_str(); + XMP_StringLen packetLen = (XMP_StringLen)this->xmpPacket.size(); + + this->xmpObj.ParseFromBuffer ( packetStr, packetLen ); + + if ( ! legacyManager.CheckDigest ( this->xmpObj ) ) { + legacyManager.ImportLegacy ( &this->xmpObj ); + } + + } + + // Assume we now have something in the XMP. + this->containsXMP = true; + +} // ASF_MetaHandler::ProcessXMP + +// ================================================================================================= +// ASF_MetaHandler::UpdateFile +// ============================ + +void ASF_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + + bool updated = false; + + if ( ! this->needsUpdate ) return; + + XMP_IO* fileRef ( this->parent->ioRef ); + if ( fileRef == 0 ) return; + + ASF_Support support; + ASF_Support::ObjectState objectState; + long numTags = support.OpenASF ( fileRef, objectState ); + if ( numTags == 0 ) return; + + XMP_StringLen packetLen = (XMP_StringLen)xmpPacket.size(); + + this->legacyManager.ExportLegacy ( this->xmpObj ); + if ( this->legacyManager.hasLegacyChanged() ) { + + this->legacyManager.SetDigest ( &this->xmpObj ); + + // serialize with updated digest + if ( objectState.xmpLen == 0 ) { + + // XMP does not exist, use standard padding + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, kXMP_UseCompactFormat ); + + } else { + + // re-use padding with static XMP size + try { + XMP_OptionBits compactExact = (kXMP_UseCompactFormat | kXMP_ExactPacketLength); + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, compactExact, XMP_StringLen(objectState.xmpLen) ); + } catch ( ... ) { + // re-use padding with exact packet length failed (legacy-digest needed too much space): try again using standard padding + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, kXMP_UseCompactFormat ); + } + + } + + } + + XMP_StringPtr packetStr = xmpPacket.c_str(); + packetLen = (XMP_StringLen)xmpPacket.size(); + if ( packetLen == 0 ) return; + + // value, when guessing for sufficient legacy padding (line-ending conversion etc.) + const int paddingTolerance = 50; + + bool xmpGrows = ( objectState.xmpLen && (packetLen > objectState.xmpLen) && ( ! objectState.xmpIsLastObject) ); + + bool legacyGrows = ( this->legacyManager.hasLegacyChanged() && + (this->legacyManager.getLegacyDiff() > (this->legacyManager.GetPadding() - paddingTolerance)) ); + + if ( doSafeUpdate || legacyGrows || xmpGrows ) { + + // do a safe update in any case + updated = SafeWriteFile(); + + } else { + + // possibly we can do an in-place update + + if ( objectState.xmpLen < packetLen ) { + + updated = SafeWriteFile(); + + } else { + + // current XMP chunk size is sufficient -> write (in place update) + updated = ASF_Support::WriteBuffer(fileRef, objectState.xmpPos, packetLen, packetStr ); + + // legacy update + if ( updated && this->legacyManager.hasLegacyChanged() ) { + + ASF_Support::ObjectIterator curPos = objectState.objects.begin(); + ASF_Support::ObjectIterator endPos = objectState.objects.end(); + + for ( ; curPos != endPos; ++curPos ) { + + ASF_Support::ObjectData object = *curPos; + + // find header-object + if ( IsEqualGUID ( ASF_Header_Object, object.guid ) ) { + // update header object + updated = support.UpdateHeaderObject ( fileRef, object, legacyManager ); + } + + } + + } + + } + + } + + if ( ! updated ) return; // If there's an error writing the chunk, bail. + + this->needsUpdate = false; + +} // ASF_MetaHandler::UpdateFile + +// ================================================================================================= +// ASF_MetaHandler::WriteTempFile +// ============================== + +void ASF_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + bool ok; + XMP_IO* originalRef = this->parent->ioRef; + + ASF_Support support; + ASF_Support::ObjectState objectState; + long numTags = support.OpenASF ( originalRef, objectState ); + if ( numTags == 0 ) return; + + tempRef->Truncate ( 0 ); + + ASF_Support::ObjectIterator curPos = objectState.objects.begin(); + ASF_Support::ObjectIterator endPos = objectState.objects.end(); + + for ( ; curPos != endPos; ++curPos ) { + + ASF_Support::ObjectData object = *curPos; + + // discard existing XMP object + if ( object.xmp ) continue; + + // update header-object, when legacy needs update + if ( IsEqualGUID ( ASF_Header_Object, object.guid) && this->legacyManager.hasLegacyChanged( ) ) { + // rewrite header object + ok = support.WriteHeaderObject ( originalRef, tempRef, object, this->legacyManager, false ); + if ( ! ok ) XMP_Throw ( "Failure writing ASF header object", kXMPErr_InternalFailure ); + } else { + // copy any other object + ok = ASF_Support::CopyObject ( originalRef, tempRef, object ); + if ( ! ok ) XMP_Throw ( "Failure copyinh ASF object", kXMPErr_InternalFailure ); + } + + // write XMP object immediately after the (one and only) top-level DataObject + if ( IsEqualGUID ( ASF_Data_Object, object.guid ) ) { + XMP_StringPtr packetStr = xmpPacket.c_str(); + XMP_StringLen packetLen = (XMP_StringLen)xmpPacket.size(); + ok = ASF_Support::WriteXMPObject ( tempRef, packetLen, packetStr ); + if ( ! ok ) XMP_Throw ( "Failure writing ASF XMP object", kXMPErr_InternalFailure ); + } + + } + + ok = support.UpdateFileSize ( tempRef ); + if ( ! ok ) XMP_Throw ( "Failure updating ASF file size", kXMPErr_InternalFailure ); + +} // ASF_MetaHandler::WriteTempFile + +// ================================================================================================= +// ASF_MetaHandler::SafeWriteFile +// ============================== + +bool ASF_MetaHandler::SafeWriteFile() +{ + XMP_IO* originalFile = this->parent->ioRef; + XMP_IO* tempFile = originalFile->DeriveTemp(); + if ( tempFile == 0 ) XMP_Throw ( "Failure creating ASF temp file", kXMPErr_InternalFailure ); + + this->WriteTempFile ( tempFile ); + originalFile->AbsorbTemp(); + + return true; + +} // ASF_MetaHandler::SafeWriteFile + +// ================================================================================================= diff --git a/XMPFiles/source/FileHandlers/ASF_Handler.hpp b/XMPFiles/source/FileHandlers/ASF_Handler.hpp new file mode 100644 index 0000000..2a4f63e --- /dev/null +++ b/XMPFiles/source/FileHandlers/ASF_Handler.hpp @@ -0,0 +1,64 @@ +#ifndef __ASF_Handler_hpp__ +#define __ASF_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "XMPFiles/source/FormatSupport/ASF_Support.hpp" + +// ================================================================================================= +/// \file ASF_Handler.hpp +/// \brief File format handler for ASF. +/// +/// This header ... +/// +// ================================================================================================= + +// *** Could derive from Basic_Handler - buffer file tail in a temp file. + +extern XMPFileHandler* ASF_MetaHandlerCTor ( XMPFiles* parent ); + +extern bool ASF_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ); + +static const XMP_OptionBits kASF_HandlerFlags = ( kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_PrefersInPlace | + kXMPFiles_CanReconcile | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_NeedsReadOnlyPacket ); + +class ASF_MetaHandler : public XMPFileHandler +{ +public: + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + bool SafeWriteFile (); + + ASF_MetaHandler ( XMPFiles* parent ); + virtual ~ASF_MetaHandler(); + +private: + + ASF_LegacyManager legacyManager; + +}; // ASF_MetaHandler + +// ================================================================================================= + +#endif /* __ASF_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/AVCHD_Handler.cpp b/XMPFiles/source/FileHandlers/AVCHD_Handler.cpp new file mode 100644 index 0000000..5b22a13 --- /dev/null +++ b/XMPFiles/source/FileHandlers/AVCHD_Handler.cpp @@ -0,0 +1,2280 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2008 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/AVCHD_Handler.hpp" + +#include "source/UnicodeConversions.hpp" +#include "third-party/zuid/interfaces/MD5.h" + +using namespace std; + +// AVCHD maker ID values. Panasonic has confirmed their Maker ID with us, the others come from examining +// sample data files. +#define kMakerIDPanasonic 0x103 +#define kMakerIDSony 0x108 +#define kMakerIDCanon 0x1011 + +// ================================================================================================= +/// \file AVCHD_Handler.cpp +/// \brief Folder format handler for AVCHD. +/// +/// This handler is for the AVCHD video format. +/// +/// A typical AVCHD layout looks like: +/// +/// BDMV/ +/// index.bdmv +/// MovieObject.bdmv +/// PLAYLIST/ +/// 00000.mpls +/// 00001.mpls +/// STREAM/ +/// 00000.m2ts +/// 00001.m2ts +/// CLIPINF/ +/// 00000.clpi +/// 00001.clpi +/// BACKUP/ +/// +// ================================================================================================= + +// ================================================================================================= + +// AVCHD Format. Book 1: Playback System Basic Specifications V 1.01. p. 76 + +struct AVCHD_blkProgramInfo +{ + XMP_Uns32 mLength; + XMP_Uns8 mReserved1[2]; + XMP_Uns32 mSPNProgramSequenceStart; + XMP_Uns16 mProgramMapPID; + XMP_Uns8 mNumberOfStreamsInPS; + XMP_Uns8 mReserved2; + + // Video stream. + struct + { + XMP_Uns8 mPresent; + XMP_Uns8 mVideoFormat; + XMP_Uns8 mFrameRate; + XMP_Uns8 mAspectRatio; + XMP_Uns8 mCCFlag; + } mVideoStream; + + // Audio stream. + struct + { + XMP_Uns8 mPresent; + XMP_Uns8 mAudioPresentationType; + XMP_Uns8 mSamplingFrequency; + XMP_Uns8 mAudioLanguageCode[4]; + } mAudioStream; + + // Pverlay bitmap stream. + struct + { + XMP_Uns8 mPresent; + XMP_Uns8 mOBLanguageCode[4]; + } mOverlayBitmapStream; + + // Menu bitmap stream. + struct + { + XMP_Uns8 mPresent; + XMP_Uns8 mBMLanguageCode[4]; + } mMenuBitmapStream; + +}; + +// AVCHD Format, Panasonic proprietary PRO_PlayListMark block + +struct AVCCAM_blkProPlayListMark +{ + XMP_Uns8 mPresent; + XMP_Uns8 mProTagID; + XMP_Uns8 mFillItem1; + XMP_Uns16 mLength; + XMP_Uns8 mMarkType; + + // Entry mark + struct + { + XMP_Uns8 mGlobalClipID[32]; + XMP_Uns8 mStartTimeCode[4]; + XMP_Uns8 mStreamTimecodeInfo; + XMP_Uns8 mStartBinaryGroup[4]; + XMP_Uns8 mLastUpdateTimeZone; + XMP_Uns8 mLastUpdateDate[7]; + XMP_Uns16 mFillItem; + } mEntryMark; + + // Shot Mark + struct + { + XMP_Uns8 mPresent; + XMP_Uns8 mShotMark; + XMP_Uns8 mFillItem[3]; + } mShotMark; + + // Access + struct + { + XMP_Uns8 mPresent; + XMP_Uns8 mCreatorCharacterSet; + XMP_Uns8 mCreatorLength; + XMP_Uns8 mCreator[32]; + XMP_Uns8 mLastUpdatePersonCharacterSet; + XMP_Uns8 mLastUpdatePersonLength; + XMP_Uns8 mLastUpdatePerson[32]; + } mAccess; + + // Device + struct + { + XMP_Uns8 mPresent; + XMP_Uns16 mMakerID; + XMP_Uns16 mMakerModelCode; + XMP_Uns8 mSerialNoCharacterCode; + XMP_Uns8 mSerialNoLength; + XMP_Uns8 mSerialNo[24]; + XMP_Uns16 mFillItem; + } mDevice; + + // Shoot + struct + { + XMP_Uns8 mPresent; + XMP_Uns8 mShooterCharacterSet; + XMP_Uns8 mShooterLength; + XMP_Uns8 mShooter[32]; + XMP_Uns8 mStartDateTimeZone; + XMP_Uns8 mStartDate[7]; + XMP_Uns8 mEndDateTimeZone; + XMP_Uns8 mEndDate[7]; + XMP_Uns16 mFillItem; + } mShoot; + + // Location + struct + { + XMP_Uns8 mPresent; + XMP_Uns8 mSource; + XMP_Uns32 mGPSLatitudeRef; + XMP_Uns32 mGPSLatitude1; + XMP_Uns32 mGPSLatitude2; + XMP_Uns32 mGPSLatitude3; + XMP_Uns32 mGPSLongitudeRef; + XMP_Uns32 mGPSLongitude1; + XMP_Uns32 mGPSLongitude2; + XMP_Uns32 mGPSLongitude3; + XMP_Uns32 mGPSAltitudeRef; + XMP_Uns32 mGPSAltitude; + XMP_Uns8 mPlaceNameCharacterSet; + XMP_Uns8 mPlaceNameLength; + XMP_Uns8 mPlaceName[64]; + XMP_Uns8 mFillItem; + } mLocation; +}; + +// AVCHD Format, Panasonic proprietary extension data (AVCCAM) + +struct AVCCAM_Pro_PlayListInfo +{ + XMP_Uns8 mPresent; + XMP_Uns8 mTagID; + XMP_Uns8 mTagVersion; + XMP_Uns16 mFillItem1; + XMP_Uns32 mLength; + XMP_Uns16 mNumberOfPlayListMarks; + XMP_Uns16 mFillItem2; + + // Although a playlist may contain multiple marks, we only store the one that corresponds to + // the clip/shot of interest. + AVCCAM_blkProPlayListMark mPlayListMark; +}; + +// AVCHD Format, Panasonic proprietary extension data (AVCCAM) + +struct AVCHD_blkPanasonicPrivateData +{ + XMP_Uns8 mPresent; + XMP_Uns16 mNumberOfData; + XMP_Uns16 mReserved; + + struct + { + XMP_Uns8 mPresent; + XMP_Uns8 mTagID; + XMP_Uns8 mTagVersion; + XMP_Uns16 mTagLength; + XMP_Uns8 mProfessionalMetaID[16]; + } mProMetaIDBlock; + + struct + { + XMP_Uns8 mPresent; + XMP_Uns8 mTagID; + XMP_Uns8 mTagVersion; + XMP_Uns16 mTagLength; + XMP_Uns8 mGlobalClipID[32]; + XMP_Uns8 mStartTimecode[4]; + XMP_Uns32 mStartBinaryGroup; + } mProClipIDBlock; + + AVCCAM_Pro_PlayListInfo mProPlaylistInfoBlock; +}; + +// AVCHD Format. Book 2: Recording Extension Specifications, section 4.2.4.2. plus Panasonic extensions + +struct AVCHD_blkMakersPrivateData +{ + XMP_Uns8 mPresent; + XMP_Uns32 mLength; + XMP_Uns32 mDataBlockStartAddress; + XMP_Uns8 mReserved[3]; + XMP_Uns8 mNumberOfMakerEntries; + XMP_Uns16 mMakerID; + XMP_Uns16 mMakerModelCode; + AVCHD_blkPanasonicPrivateData mPanasonicPrivateData; +}; + +// AVCHD Format. Book 2: Recording Extension Specifications, section 4.4.2.1 + +struct AVCHD_blkClipInfoExt +{ + XMP_Uns32 mLength; + XMP_Uns16 mMakerID; + XMP_Uns16 mMakerModelCode; +}; + +// AVCHD Format. Book 2: Recording Extension Specifications, section 4.4.1.2 + +struct AVCHD_blkClipExtensionData +{ + XMP_Uns8 mPresent; + XMP_Uns8 mTypeIndicator[4]; + XMP_Uns8 mReserved1[4]; + XMP_Uns32 mProgramInfoExtStartAddress; + XMP_Uns32 mMakersPrivateDataStartAddress; + + AVCHD_blkClipInfoExt mClipInfoExt; + AVCHD_blkMakersPrivateData mMakersPrivateData; +}; + +// AVCHD Format. Book 2: Recording Extension Specifications, section 4.3.3.1 -- although each playlist +// may contain a list of these, we only record the one that matches our target shot/clip. + +struct AVCHD_blkPlayListMarkExt +{ + XMP_Uns32 mLength; + XMP_Uns16 mNumberOfPlaylistMarks; + bool mPresent; + XMP_Uns16 mMakerID; + XMP_Uns16 mMakerModelCode; + XMP_Uns8 mReserved1[3]; + XMP_Uns8 mFlags; // bit 0: MarkWriteProtectFlag, bits 1-2: pulldown + XMP_Uns16 mRefToMarkThumbnailIndex; + XMP_Uns8 mBlkTimezone; + XMP_Uns8 mRecordDataAndTime[7]; + XMP_Uns8 mMarkCharacterSet; + XMP_Uns8 mMarkNameLength; + XMP_Uns8 mMarkName[24]; + XMP_Uns8 mMakersInformation[16]; + XMP_Uns8 mBlkTimecode[4]; + XMP_Uns16 mReserved2; +}; + +// AVCHD Format. Book 2: Recording Extension Specifications, section 4.3.2.1 + +struct AVCHD_blkPlaylistMeta +{ + XMP_Uns32 mLength; + XMP_Uns16 mMakerID; + XMP_Uns16 mMakerModelCode; + XMP_Uns32 mReserved1; + XMP_Uns16 mRefToMenuThumbnailIndex; + XMP_Uns8 mBlkTimezone; + XMP_Uns8 mRecordDataAndTime[7]; + XMP_Uns8 mReserved2; + XMP_Uns8 mPlaylistCharacterSet; + XMP_Uns8 mPlaylistNameLength; + XMP_Uns8 mPlaylistName[255]; +}; + +// AVCHD Format. Book 2: Recording Extension Specifications, section 4.3.1.2 + +struct AVCHD_blkPlayListExtensionData +{ + XMP_Uns8 mPresent; + char mTypeIndicator[4]; + XMP_Uns8 mReserved[4]; + XMP_Uns32 mPlayListMarkExtStartAddress; + XMP_Uns32 mMakersPrivateDataStartAddress; + + AVCHD_blkPlaylistMeta mPlaylistMeta; + AVCHD_blkPlayListMarkExt mPlaylistMarkExt; + AVCHD_blkMakersPrivateData mMakersPrivateData; +}; + +// AVCHD Format. Book 1: Playback System Basic Specifications V 1.01. p. 38 +struct AVCHD_blkExtensionData +{ + XMP_Uns32 mLength; + XMP_Uns32 mDataBlockStartAddress; + XMP_Uns8 mReserved[3]; + XMP_Uns8 mNumberOfDataEntries; + + struct AVCHD_blkExtDataEntry + { + XMP_Uns16 mExtDataType; + XMP_Uns16 mExtDataVersion; + XMP_Uns32 mExtDataStartAddress; + XMP_Uns32 mExtDataLength; + } mExtDataEntry; +}; + +// Simple container for the various AVCHD legacy metadata structures we care about for an AVCHD clip + +struct AVCHD_LegacyMetadata +{ + AVCHD_blkProgramInfo mProgramInfo; + AVCHD_blkClipExtensionData mClipExtensionData; + AVCHD_blkPlayListExtensionData mPlaylistExtensionData; +}; + +// ================================================================================================= +// MakeLeafPath +// ============ + +static bool MakeLeafPath ( std::string * path, XMP_StringPtr root, XMP_StringPtr group, + XMP_StringPtr clip, XMP_StringPtr suffix, bool checkFile = false ) +{ + size_t partialLen; + + *path = root; + *path += kDirChar; + *path += "BDMV"; + *path += kDirChar; + *path += group; + *path += kDirChar; + *path += clip; + partialLen = path->size(); + *path += suffix; + + if ( ! checkFile ) return true; + if ( Host_IO::GetFileMode ( path->c_str() ) == Host_IO::kFMode_IsFile ) return true; + + // Convert the suffix to uppercase and try again. Even on Mac/Win, in case a remote file system is sensitive. + for ( char* chPtr = ((char*)path->c_str() + partialLen); *chPtr != 0; ++chPtr ) { + if ( (0x61 <= *chPtr) && (*chPtr <= 0x7A) ) *chPtr -= 0x20; + } + if ( Host_IO::GetFileMode ( path->c_str() ) == Host_IO::kFMode_IsFile ) return true; + + if ( XMP_LitMatch ( suffix, ".clpi" ) ) { // Special case of ".cpi" for the clip file. + + path->erase ( partialLen ); + *path += ".cpi"; + if ( Host_IO::GetFileMode ( path->c_str() ) == Host_IO::kFMode_IsFile ) return true; + + path->erase ( partialLen ); + *path += ".CPI"; + if ( Host_IO::GetFileMode ( path->c_str() ) == Host_IO::kFMode_IsFile ) return true; + + } else if ( XMP_LitMatch ( suffix, ".mpls" ) ) { // Special case of ".mpl" for the playlist file. + + path->erase ( partialLen ); + *path += ".mpl"; + if ( Host_IO::GetFileMode ( path->c_str() ) == Host_IO::kFMode_IsFile ) return true; + + path->erase ( partialLen ); + *path += ".MPL"; + if ( Host_IO::GetFileMode ( path->c_str() ) == Host_IO::kFMode_IsFile ) return true; + + } + + // Still not found, revert to the original suffix. + path->erase ( partialLen ); + *path += suffix; + return false; + +} // MakeLeafPath + +// ================================================================================================= +// AVCHD_CheckFormat +// ================= +// +// This version checks for the presence of a top level BPAV directory, and the required files and +// directories immediately within it. The CLIPINF, PLAYLIST, and STREAM subfolders are required, as +// are the index.bdmv and MovieObject.bdmv files. +// +// The state of the string parameters depends on the form of the path passed by the client. If the +// client passed a logical clip path, like ".../MyMovie/00001", the parameters are: +// rootPath - ".../MyMovie" +// gpName - empty +// parentName - empty +// leafName - "00001" +// If the client passed a full file path, like ".../MyMovie/BDMV/CLIPINF/00001.clpi", they are: +// rootPath - ".../MyMovie" +// gpName - "BDMV" +// parentName - "CLIPINF" or "PALYLIST" or "STREAM" +// leafName - "00001" + +// ! The common code has shifted the gpName, parentName, and leafName strings to upper case. It has +// ! also made sure that for a logical clip path the rootPath is an existing folder, and that the +// ! file exists for a full file path. + +// ! Using explicit '/' as a separator when creating paths, it works on Windows. + +// ! Sample files show that the ".bdmv" extension can sometimes be ".bdm". Allow either. + +bool AVCHD_CheckFormat ( XMP_FileFormat format, + const std::string & rootPath, + const std::string & gpName, + const std::string & parentName, + const std::string & leafName, + XMPFiles * parent ) +{ + if ( gpName.empty() != parentName.empty() ) return false; // Must be both empty or both non-empty. + + if ( ! gpName.empty() ) { + if ( gpName != "BDMV" ) return false; + if ( (parentName != "CLIPINF") && (parentName != "PLAYLIST") && (parentName != "STREAM") ) return false; + } + + // Check the rest of the required general structure. Look for both ".bdmv" and ".bmd" extensions. + + std::string bdmvPath ( rootPath ); + bdmvPath += kDirChar; + bdmvPath += "BDMV"; + + if ( Host_IO::GetChildMode ( bdmvPath.c_str(), "CLIPINF" ) != Host_IO::kFMode_IsFolder ) return false; + if ( Host_IO::GetChildMode ( bdmvPath.c_str(), "PLAYLIST" ) != Host_IO::kFMode_IsFolder ) return false; + if ( Host_IO::GetChildMode ( bdmvPath.c_str(), "STREAM" ) != Host_IO::kFMode_IsFolder ) return false; + + if ( (Host_IO::GetChildMode ( bdmvPath.c_str(), "index.bdmv" ) != Host_IO::kFMode_IsFile) && + (Host_IO::GetChildMode ( bdmvPath.c_str(), "index.bdm" ) != Host_IO::kFMode_IsFile) && + (Host_IO::GetChildMode ( bdmvPath.c_str(), "INDEX.BDMV" ) != Host_IO::kFMode_IsFile) && // Some usage is all caps. + (Host_IO::GetChildMode ( bdmvPath.c_str(), "INDEX.BDM" ) != Host_IO::kFMode_IsFile) ) return false; + + if ( (Host_IO::GetChildMode ( bdmvPath.c_str(), "MovieObject.bdmv" ) != Host_IO::kFMode_IsFile) && + (Host_IO::GetChildMode ( bdmvPath.c_str(), "MovieObj.bdm" ) != Host_IO::kFMode_IsFile) && + (Host_IO::GetChildMode ( bdmvPath.c_str(), "MOVIEOBJECT.BDMV" ) != Host_IO::kFMode_IsFile) && // Some usage is all caps. + (Host_IO::GetChildMode ( bdmvPath.c_str(), "MOVIEOBJ.BDM" ) != Host_IO::kFMode_IsFile) ) return false; + + + // Make sure the .clpi file exists. + std::string tempPath; + bool foundClpi = MakeLeafPath ( &tempPath, rootPath.c_str(), "CLIPINF", leafName.c_str(), ".clpi", true /* checkFile */ ); + if ( ! foundClpi ) return false; + + // And now save the pseudo path for the handler object. + tempPath = rootPath; + tempPath += kDirChar; + tempPath += leafName; + size_t pathLen = tempPath.size() + 1; // Include a terminating nul. + parent->tempPtr = malloc ( pathLen ); + if ( parent->tempPtr == 0 ) XMP_Throw ( "No memory for AVCHD clip info", kXMPErr_NoMemory ); + memcpy ( parent->tempPtr, tempPath.c_str(), pathLen ); + + return true; + +} // AVCHD_CheckFormat + +// ================================================================================================= + +static void* CreatePseudoClipPath ( const std::string & clientPath ) { + + // Used to create the clip pseudo path when the CheckFormat function is skipped. + + std::string pseudoPath = clientPath; + + size_t pathLen; + void* tempPtr = 0; + + if ( Host_IO::Exists ( pseudoPath.c_str() ) ) { + + // The client passed a physical path. The logical clip name is the leaf name, with the + // extension removed. There are no extra suffixes on AVCHD files. The movie root path ends + // two levels up. + + std::string clipName, ignored; + + XIO::SplitLeafName ( &pseudoPath, &clipName ); // Extract the logical clip name. + XIO::SplitFileExtension ( &clipName, &ignored ); + + XIO::SplitLeafName ( &pseudoPath, &ignored ); // Remove the 2 intermediate folder levels. + XIO::SplitLeafName ( &pseudoPath, &ignored ); + + pseudoPath += kDirChar; + pseudoPath += clipName; + + } + + pathLen = pseudoPath.size() + 1; // Include a terminating nul. + tempPtr = malloc ( pathLen ); + if ( tempPtr == 0 ) XMP_Throw ( "No memory for AVCHD clip info", kXMPErr_NoMemory ); + memcpy ( tempPtr, pseudoPath.c_str(), pathLen ); + + return tempPtr; + +} // CreatePseudoClipPath + +// ================================================================================================= +// ReadAVCHDProgramInfo +// ==================== + +static bool ReadAVCHDProgramInfo ( XMPFiles_IO & cpiFile, AVCHD_blkProgramInfo& avchdProgramInfo ) +{ + avchdProgramInfo.mLength = XIO::ReadUns32_BE ( &cpiFile ); + cpiFile.ReadAll ( avchdProgramInfo.mReserved1, 2 ); + avchdProgramInfo.mSPNProgramSequenceStart = XIO::ReadUns32_BE ( &cpiFile ); + avchdProgramInfo.mProgramMapPID = XIO::ReadUns16_BE ( &cpiFile ); + cpiFile.ReadAll ( &avchdProgramInfo.mNumberOfStreamsInPS, 1 ); + cpiFile.ReadAll ( &avchdProgramInfo.mReserved2, 1 ); + + XMP_Uns16 streamPID = 0; + for ( int i=0; i<avchdProgramInfo.mNumberOfStreamsInPS; ++i ) { + + XMP_Uns8 length = 0; + XMP_Uns8 streamCodingType = 0; + + streamPID = XIO::ReadUns16_BE ( &cpiFile ); + cpiFile.ReadAll ( &length, 1 ); + + XMP_Int64 pos = cpiFile.Offset(); + + cpiFile.ReadAll ( &streamCodingType, 1 ); + + switch ( streamCodingType ) { + + case 0x1B : // Video stream case. + { + XMP_Uns8 videoFormatAndFrameRate; + cpiFile.ReadAll ( &videoFormatAndFrameRate, 1 ); + avchdProgramInfo.mVideoStream.mVideoFormat = videoFormatAndFrameRate >> 4; // hi 4 bits + avchdProgramInfo.mVideoStream.mFrameRate = videoFormatAndFrameRate & 0x0f; // lo 4 bits + + XMP_Uns8 aspectRatioAndReserved = 0; + cpiFile.ReadAll ( &aspectRatioAndReserved, 1 ); + avchdProgramInfo.mVideoStream.mAspectRatio = aspectRatioAndReserved >> 4; // hi 4 bits + + XMP_Uns8 ccFlag = 0; + cpiFile.ReadAll ( &ccFlag, 1 ); + avchdProgramInfo.mVideoStream.mCCFlag = ccFlag; + + avchdProgramInfo.mVideoStream.mPresent = 1; + } + break; + + case 0x80 : // Fall through. + case 0x81 : // Audio stream case. + { + XMP_Uns8 audioPresentationTypeAndFrequency = 0; + cpiFile.ReadAll ( &audioPresentationTypeAndFrequency, 1 ); + + avchdProgramInfo.mAudioStream.mAudioPresentationType = audioPresentationTypeAndFrequency >> 4; // hi 4 bits + avchdProgramInfo.mAudioStream.mSamplingFrequency = audioPresentationTypeAndFrequency & 0x0f; // lo 4 bits + + cpiFile.ReadAll ( avchdProgramInfo.mAudioStream.mAudioLanguageCode, 3 ); + avchdProgramInfo.mAudioStream.mAudioLanguageCode[3] = 0; + + avchdProgramInfo.mAudioStream.mPresent = 1; + } + break; + + case 0x90 : // Overlay bitmap stream case. + cpiFile.ReadAll ( &avchdProgramInfo.mOverlayBitmapStream.mOBLanguageCode, 3 ); + avchdProgramInfo.mOverlayBitmapStream.mOBLanguageCode[3] = 0; + avchdProgramInfo.mOverlayBitmapStream.mPresent = 1; + break; + + case 0x91 : // Menu bitmap stream. + cpiFile.ReadAll ( &avchdProgramInfo.mMenuBitmapStream.mBMLanguageCode, 3 ); + avchdProgramInfo.mMenuBitmapStream.mBMLanguageCode[3] = 0; + avchdProgramInfo.mMenuBitmapStream.mPresent = 1; + break; + + default : + break; + + } + + cpiFile.Seek ( pos + length, kXMP_SeekFromStart ); + + } + + return true; +} + +// ================================================================================================= +// ReadAVCHDExtensionData +// ====================== + +static bool ReadAVCHDExtensionData ( XMPFiles_IO & cpiFile, AVCHD_blkExtensionData& extensionDataHeader ) +{ + extensionDataHeader.mLength = XIO::ReadUns32_BE ( &cpiFile ); + + if ( extensionDataHeader.mLength == 0 ) { + // Nothing to read + return true; + } + + extensionDataHeader.mDataBlockStartAddress = XIO::ReadUns32_BE ( &cpiFile ); + cpiFile.ReadAll ( extensionDataHeader.mReserved, 3 ); + cpiFile.ReadAll ( &extensionDataHeader.mNumberOfDataEntries, 1 ); + + if ( extensionDataHeader.mNumberOfDataEntries != 1 ) { + // According to AVCHD Format. Book1. v. 1.01. p 38, "This field shall be set to 1 in this format." + return false; + } + + extensionDataHeader.mExtDataEntry.mExtDataType = XIO::ReadUns16_BE ( &cpiFile ); + extensionDataHeader.mExtDataEntry.mExtDataVersion = XIO::ReadUns16_BE ( &cpiFile ); + extensionDataHeader.mExtDataEntry.mExtDataStartAddress = XIO::ReadUns32_BE ( &cpiFile ); + extensionDataHeader.mExtDataEntry.mExtDataLength = XIO::ReadUns32_BE ( &cpiFile ); + + if ( extensionDataHeader.mExtDataEntry.mExtDataType != 0x1000 ) { + // According to AVCHD Format. Book1. v. 1.01. p 38, "If the metadata is for an AVCHD application, + // this value shall be set to 'Ox1OOO'." + return false; + } + + return true; +} + +// ================================================================================================= +// ReadAVCCAMProMetaID +// =================== +// +// Read Panasonic's proprietary PRO_MetaID block + +static bool ReadAVCCAMProMetaID ( XMPFiles_IO & cpiFile, XMP_Uns8 tagID, AVCHD_blkPanasonicPrivateData& extensionDataHeader ) +{ + extensionDataHeader.mPresent = 1; + extensionDataHeader.mProMetaIDBlock.mPresent = 1; + extensionDataHeader.mProMetaIDBlock.mTagID = tagID; + cpiFile.ReadAll ( &extensionDataHeader.mProMetaIDBlock.mTagVersion, 1); + extensionDataHeader.mProMetaIDBlock.mTagLength = XIO::ReadUns16_BE ( &cpiFile ); + cpiFile.ReadAll ( &extensionDataHeader.mProMetaIDBlock.mProfessionalMetaID, 16); + + return true; +} + +// ================================================================================================= +// ReadAVCCAMProClipInfo +// ===================== +// +// Read Panasonic's proprietary PRO_ClipInfo block. + +static bool ReadAVCCAMProClipInfo ( XMPFiles_IO & cpiFile, XMP_Uns8 tagID, AVCHD_blkPanasonicPrivateData& extensionDataHeader ) +{ + extensionDataHeader.mPresent = 1; + extensionDataHeader.mProClipIDBlock.mPresent = 1; + extensionDataHeader.mProClipIDBlock.mTagID = tagID; + cpiFile.ReadAll ( &extensionDataHeader.mProClipIDBlock.mTagVersion, 1); + extensionDataHeader.mProClipIDBlock.mTagLength = XIO::ReadUns16_BE ( &cpiFile ); + cpiFile.ReadAll ( &extensionDataHeader.mProClipIDBlock.mGlobalClipID, 32); + cpiFile.ReadAll ( &extensionDataHeader.mProClipIDBlock.mStartTimecode, 4 ); + extensionDataHeader.mProClipIDBlock.mStartBinaryGroup = XIO::ReadUns32_BE ( &cpiFile ); + + return true; +} + +// ================================================================================================= +// ReadAVCCAM_blkPRO_ShotMark +// ========================== +// +// Read Panasonic's proprietary PRO_ShotMark block. + +static bool ReadAVCCAM_blkPRO_ShotMark ( XMPFiles_IO & mplFile, AVCCAM_blkProPlayListMark& proMark ) +{ + proMark.mShotMark.mPresent = 1; + mplFile.ReadAll ( &proMark.mShotMark.mShotMark, 1); + mplFile.ReadAll ( &proMark.mShotMark.mFillItem, 3); + + return true; +} + +// ================================================================================================= +// ReadAVCCAM_blkPRO_Access +// ======================== +// +// Read Panasonic's proprietary PRO_Access block. + +static bool ReadAVCCAM_blkPRO_Access ( XMPFiles_IO & mplFile, AVCCAM_blkProPlayListMark& proMark ) +{ + proMark.mAccess.mPresent = 1; + mplFile.ReadAll ( &proMark.mAccess.mCreatorCharacterSet, 1 ); + mplFile.ReadAll ( &proMark.mAccess.mCreatorLength, 1 ); + mplFile.ReadAll ( &proMark.mAccess.mCreator, 32 ); + mplFile.ReadAll ( &proMark.mAccess.mLastUpdatePersonCharacterSet, 1 ); + mplFile.ReadAll ( &proMark.mAccess.mLastUpdatePersonLength, 1 ); + mplFile.ReadAll ( &proMark.mAccess.mLastUpdatePerson, 32 ); + + return true; +} + +// ================================================================================================= +// ReadAVCCAM_blkPRO_Device +// ======================== +// +// Read Panasonic's proprietary PRO_Device block. + +static bool ReadAVCCAM_blkPRO_Device ( XMPFiles_IO & mplFile, AVCCAM_blkProPlayListMark& proMark ) +{ + proMark.mDevice.mPresent = 1; + proMark.mDevice.mMakerID = XIO::ReadUns16_BE ( &mplFile ); + proMark.mDevice.mMakerModelCode = XIO::ReadUns16_BE ( &mplFile ); + mplFile.ReadAll ( &proMark.mDevice.mSerialNoCharacterCode, 1 ); + mplFile.ReadAll ( &proMark.mDevice.mSerialNoLength, 1 ); + mplFile.ReadAll ( &proMark.mDevice.mSerialNo, 24 ); + mplFile.ReadAll ( &proMark.mDevice.mFillItem, 2 ); + + return true; +} + +// ================================================================================================= +// ReadAVCCAM_blkPRO_Shoot +// ======================= +// +// Read Panasonic's proprietary PRO_Shoot block. + +static bool ReadAVCCAM_blkPRO_Shoot ( XMPFiles_IO & mplFile, AVCCAM_blkProPlayListMark& proMark ) +{ + proMark.mShoot.mPresent = 1; + mplFile.ReadAll ( &proMark.mShoot.mShooterCharacterSet, 1 ); + mplFile.ReadAll ( &proMark.mShoot.mShooterLength, 1 ); + mplFile.ReadAll ( &proMark.mShoot.mShooter, 32 ); + mplFile.ReadAll ( &proMark.mShoot.mStartDateTimeZone, 1 ); + mplFile.ReadAll ( &proMark.mShoot.mStartDate, 7 ); + mplFile.ReadAll ( &proMark.mShoot.mEndDateTimeZone, 1 ); + mplFile.ReadAll ( &proMark.mShoot.mEndDate, 7 ); + mplFile.ReadAll ( &proMark.mShoot.mFillItem, 2 ); + + return true; +} + +// ================================================================================================= +// ReadAVCCAM_blkPRO_Location +// ========================== +// +// Read Panasonic's proprietary PRO_Location block. + +static bool ReadAVCCAM_blkPRO_Location ( XMPFiles_IO & mplFile, AVCCAM_blkProPlayListMark& proMark ) +{ + proMark.mLocation.mPresent = 1; + mplFile.ReadAll ( &proMark.mLocation.mSource, 1 ); + proMark.mLocation.mGPSLatitudeRef = XIO::ReadUns32_BE ( &mplFile ); + proMark.mLocation.mGPSLatitude1 = XIO::ReadUns32_BE ( &mplFile ); + proMark.mLocation.mGPSLatitude2 = XIO::ReadUns32_BE ( &mplFile ); + proMark.mLocation.mGPSLatitude3 = XIO::ReadUns32_BE ( &mplFile ); + proMark.mLocation.mGPSLongitudeRef = XIO::ReadUns32_BE ( &mplFile ); + proMark.mLocation.mGPSLongitude1 = XIO::ReadUns32_BE ( &mplFile ); + proMark.mLocation.mGPSLongitude2 = XIO::ReadUns32_BE ( &mplFile ); + proMark.mLocation.mGPSLongitude3 = XIO::ReadUns32_BE ( &mplFile ); + proMark.mLocation.mGPSAltitudeRef = XIO::ReadUns32_BE ( &mplFile ); + proMark.mLocation.mGPSAltitude = XIO::ReadUns32_BE ( &mplFile ); + mplFile.ReadAll ( &proMark.mLocation.mPlaceNameCharacterSet, 1 ); + mplFile.ReadAll ( &proMark.mLocation.mPlaceNameLength, 1 ); + mplFile.ReadAll ( &proMark.mLocation.mPlaceName, 64 ); + mplFile.ReadAll ( &proMark.mLocation.mFillItem, 1 ); + + return true; +} + +// ================================================================================================= +// ReadAVCCAMProPlaylistInfo +// ========================= +// +// Read Panasonic's proprietary PRO_PlayListInfo block. + +static bool ReadAVCCAMProPlaylistInfo ( XMPFiles_IO & mplFile, + XMP_Uns8 tagID, + XMP_Uns16 playlistMarkID, + AVCHD_blkPanasonicPrivateData& extensionDataHeader ) +{ + AVCCAM_Pro_PlayListInfo& playlistBlock = extensionDataHeader.mProPlaylistInfoBlock; + + playlistBlock.mTagID = tagID; + mplFile.ReadAll ( &playlistBlock.mTagVersion, 1); + mplFile.ReadAll ( &playlistBlock.mFillItem1, 2); + playlistBlock.mLength = XIO::ReadUns32_BE ( &mplFile ); + playlistBlock.mNumberOfPlayListMarks = XIO::ReadUns16_BE ( &mplFile ); + mplFile.ReadAll ( &playlistBlock.mFillItem2, 2); + + if ( playlistBlock.mNumberOfPlayListMarks == 0 ) return true; + + extensionDataHeader.mPresent = 1; + + XMP_Uns64 blockStart = 0; + + for ( int i = 0; i < playlistBlock.mNumberOfPlayListMarks; ++i ) { + AVCCAM_blkProPlayListMark& currMark = playlistBlock.mPlayListMark; + + mplFile.ReadAll ( &currMark.mProTagID, 1); + mplFile.ReadAll ( &currMark.mFillItem1, 1); + currMark.mLength = XIO::ReadUns16_BE ( &mplFile ); + blockStart = mplFile.Offset(); + mplFile.ReadAll ( &currMark.mMarkType, 1 ); + + if ( ( currMark.mProTagID == 0x40 ) && ( currMark.mMarkType == 0x01 ) ) { + mplFile.ReadAll ( &currMark.mEntryMark.mGlobalClipID, 32); + + // skip marks for different clips + if ( i == playlistMarkID ) { + playlistBlock.mPresent = 1; + currMark.mPresent = 1; + mplFile.ReadAll ( &currMark.mEntryMark.mStartTimeCode, 4); + mplFile.ReadAll ( &currMark.mEntryMark.mStreamTimecodeInfo, 1); + mplFile.ReadAll ( &currMark.mEntryMark.mStartBinaryGroup, 4); + mplFile.ReadAll ( &currMark.mEntryMark.mLastUpdateTimeZone, 1); + mplFile.ReadAll ( &currMark.mEntryMark.mLastUpdateDate, 7); + mplFile.ReadAll ( &currMark.mEntryMark.mFillItem, 2); + + XMP_Uns64 currPos = mplFile.Offset(); + XMP_Uns8 blockTag = 0; + XMP_Uns8 blockFill; + XMP_Uns16 blockLength = 0; + + while ( currPos < ( blockStart + currMark.mLength ) ) { + mplFile.ReadAll ( &blockTag, 1); + mplFile.ReadAll ( &blockFill, 1); + blockLength = XIO::ReadUns16_BE ( &mplFile ); + currPos += 4; + + switch ( blockTag ) { + case 0x20: + if ( ! ReadAVCCAM_blkPRO_ShotMark ( mplFile, currMark ) ) return false; + break; + + + case 0x21: + if ( ! ReadAVCCAM_blkPRO_Access ( mplFile, currMark ) ) return false; + break; + + case 0x22: + if ( ! ReadAVCCAM_blkPRO_Device ( mplFile, currMark ) ) return false; + break; + + case 0x23: + if ( ! ReadAVCCAM_blkPRO_Shoot ( mplFile, currMark ) ) return false; + break; + + case 0x24: + if (! ReadAVCCAM_blkPRO_Location ( mplFile, currMark ) ) return false; + break; + + default : break; + } + + currPos += blockLength; + mplFile.Seek ( currPos, kXMP_SeekFromStart ); + } + } + } + + mplFile.Seek ( blockStart + currMark.mLength, kXMP_SeekFromStart ); + } + + return true; +} + +// ================================================================================================= +// ReadAVCCAMMakersPrivateData +// =========================== +// +// Read Panasonic's implementation of an AVCCAM "Maker's Private Data" block. Panasonic calls their +// extensions "AVCCAM." + +static bool ReadAVCCAMMakersPrivateData ( XMPFiles_IO & fileRef, + XMP_Uns16 playlistMarkID, + AVCHD_blkPanasonicPrivateData& avccamPrivateData ) +{ + const XMP_Uns64 blockStart = fileRef.Offset(); + + avccamPrivateData.mNumberOfData = XIO::ReadUns16_BE ( &fileRef ); + fileRef.ReadAll ( &avccamPrivateData.mReserved, 2 ); + + for (int i = 0; i < avccamPrivateData.mNumberOfData; ++i) { + const XMP_Uns8 tagID = XIO::ReadUns8 ( &fileRef ); + + switch ( tagID ) { + case 0xe0: ReadAVCCAMProMetaID ( fileRef, tagID, avccamPrivateData ); + break; + case 0xe2: ReadAVCCAMProClipInfo( fileRef, tagID, avccamPrivateData ); + break; + case 0xf0: ReadAVCCAMProPlaylistInfo( fileRef, tagID, playlistMarkID, avccamPrivateData ); + break; + + default: + // Ignore any blocks we don't now or care about + break; + } + } + + return true; +} + +// ================================================================================================= +// ReadAVCHDMakersPrivateData +// ========================== +// +// AVCHD Format. Book 2: Recording Extension Specifications, section 4.2.4.2. + +static bool ReadAVCHDMakersPrivateData ( XMPFiles_IO & mplFile, + XMP_Uns16 playlistMarkID, + AVCHD_blkMakersPrivateData& avchdLegacyData ) +{ + const XMP_Uns64 blockStart = mplFile.Offset(); + + avchdLegacyData.mLength = XIO::ReadUns32_BE ( &mplFile ); + + if ( avchdLegacyData.mLength == 0 ) return false; + + avchdLegacyData.mPresent = 1; + avchdLegacyData.mDataBlockStartAddress = XIO::ReadUns32_BE ( &mplFile ); + mplFile.ReadAll ( &avchdLegacyData.mReserved, 3 ); + mplFile.ReadAll ( &avchdLegacyData.mNumberOfMakerEntries, 1 ); + + if ( avchdLegacyData.mNumberOfMakerEntries == 0 ) return true; + + XMP_Uns16 makerID; + XMP_Uns16 makerModelCode; + XMP_Uns32 mpdStartAddress; + XMP_Uns32 mpdLength; + + for ( int i = 0; i < avchdLegacyData.mNumberOfMakerEntries; ++i ) { + makerID = XIO::ReadUns16_BE ( &mplFile ); + makerModelCode = XIO::ReadUns16_BE ( &mplFile ); + mpdStartAddress = XIO::ReadUns32_BE ( &mplFile ); + mpdLength = XIO::ReadUns32_BE ( &mplFile ); + + // We only have documentation for Panasonic's Maker's Private Data blocks, so we'll ignore everyone else's + if ( makerID == kMakerIDPanasonic ) { + avchdLegacyData.mMakerID = makerID; + avchdLegacyData.mMakerModelCode = makerModelCode; + mplFile.Seek ( blockStart + mpdStartAddress, kXMP_SeekFromStart ); + + if (! ReadAVCCAMMakersPrivateData ( mplFile, playlistMarkID, avchdLegacyData.mPanasonicPrivateData ) ) return false; + } + } + + return true; +} + +// ================================================================================================= +// ReadAVCHDClipExtensionData +// ========================== + +static bool ReadAVCHDClipExtensionData ( XMPFiles_IO & cpiFile, AVCHD_blkClipExtensionData& avchdExtensionData ) +{ + const XMP_Int64 extensionBlockStart = cpiFile.Offset(); + AVCHD_blkExtensionData extensionDataHeader; + + if ( ! ReadAVCHDExtensionData ( cpiFile, extensionDataHeader ) ) { + return false; + } + + if ( extensionDataHeader.mLength == 0 ) { + return true; + } + + const XMP_Int64 dataBlockStart = extensionBlockStart + extensionDataHeader.mDataBlockStartAddress; + + cpiFile.Seek ( dataBlockStart, kXMP_SeekFromStart ); + cpiFile.ReadAll ( avchdExtensionData.mTypeIndicator, 4 ); + + if ( strncmp ( reinterpret_cast<const char*>( avchdExtensionData.mTypeIndicator ), "CLEX", 4 ) != 0 ) return false; + + avchdExtensionData.mPresent = 1; + cpiFile.ReadAll ( avchdExtensionData.mReserved1, 4 ); + avchdExtensionData.mProgramInfoExtStartAddress = XIO::ReadUns32_BE ( &cpiFile ); + avchdExtensionData.mMakersPrivateDataStartAddress = XIO::ReadUns32_BE ( &cpiFile ); + + // read Clip info extension + cpiFile.Seek ( dataBlockStart + 40, kXMP_SeekFromStart ); + avchdExtensionData.mClipInfoExt.mLength = XIO::ReadUns32_BE ( &cpiFile ); + avchdExtensionData.mClipInfoExt.mMakerID = XIO::ReadUns16_BE ( &cpiFile ); + avchdExtensionData.mClipInfoExt.mMakerModelCode = XIO::ReadUns16_BE ( &cpiFile ); + + if ( avchdExtensionData.mMakersPrivateDataStartAddress == 0 ) return true; + + if ( avchdExtensionData.mClipInfoExt.mMakerID == kMakerIDPanasonic ) { + // Read Maker's Private Data block -- we only have Panasonic's definition for their AVCCAM models + // at this point, so we'll ignore the block if its from a different manufacturer. + cpiFile.Seek ( dataBlockStart + avchdExtensionData.mMakersPrivateDataStartAddress, kXMP_SeekFromStart ); + + if ( ! ReadAVCHDMakersPrivateData ( cpiFile, 0, avchdExtensionData.mMakersPrivateData ) ) { + return false; + } + } + + return true; +} + +// ================================================================================================= +// AVCHD_PlaylistContainsClip +// ========================== +// +// Returns true of the specified AVCHD playlist block references the specified clip, or false if not. + +static bool AVCHD_PlaylistContainsClip ( XMPFiles_IO & mplFile, XMP_Uns16& playItemID, const std::string& strClipName ) +{ + // Read clip header. ( AVCHD Format. Book1. v. 1.01. p 45 ) + struct AVCHD_blkPlayList + { + XMP_Uns32 mLength; + XMP_Uns16 mReserved; + XMP_Uns16 mNumberOfPlayItems; + XMP_Uns16 mNumberOfSubPaths; + }; + + AVCHD_blkPlayList blkPlayList; + blkPlayList.mLength = XIO::ReadUns32_BE ( &mplFile ); + mplFile.ReadAll ( &blkPlayList.mReserved, 2 ); + blkPlayList.mNumberOfPlayItems = XIO::ReadUns16_BE ( &mplFile ); + blkPlayList.mNumberOfSubPaths = XIO::ReadUns16_BE ( &mplFile ); + + // Search the play items. ( AVCHD Format. Book1. v. 1.01. p 47 ) + struct AVCHD_blkPlayItem + { + XMP_Uns16 mLength; + char mClipInformationFileName[5]; + // Note: remaining fields omitted because we don't care about them + }; + + AVCHD_blkPlayItem currPlayItem; + XMP_Uns64 blockStart = 0; + for ( playItemID = 0; playItemID < blkPlayList.mNumberOfPlayItems; ++playItemID ) { + currPlayItem.mLength = XIO::ReadUns16_BE ( &mplFile ); + + // mLength is measured from the end of mLength, not the start of the block ( AVCHD Format. Book1. v. 1.01. p 47 ) + blockStart = mplFile.Offset(); + mplFile.ReadAll ( currPlayItem.mClipInformationFileName, 5 ); + + if ( strncmp ( strClipName.c_str(), currPlayItem.mClipInformationFileName, 5 ) == 0 ) return true; + + mplFile.Seek ( blockStart + currPlayItem.mLength, kXMP_SeekFromStart ); + } + + return false; +} + +// ================================================================================================= +// ReadAVCHDPlaylistMetadataBlock +// ============================== + +static bool ReadAVCHDPlaylistMetadataBlock ( XMPFiles_IO & mplFile, + AVCHD_blkPlaylistMeta& avchdLegacyData ) +{ + avchdLegacyData.mLength = XIO::ReadUns32_BE ( &mplFile ); + + if ( avchdLegacyData.mLength < sizeof ( AVCHD_blkPlaylistMeta ) ) return false; + + avchdLegacyData.mMakerID = XIO::ReadUns16_BE ( &mplFile ); + avchdLegacyData.mMakerModelCode = XIO::ReadUns16_BE ( &mplFile ); + mplFile.ReadAll ( &avchdLegacyData.mReserved1, 4 ); + avchdLegacyData.mRefToMenuThumbnailIndex = XIO::ReadUns16_BE ( &mplFile ); + mplFile.ReadAll ( &avchdLegacyData.mBlkTimezone, 1 ); + mplFile.ReadAll ( &avchdLegacyData.mRecordDataAndTime, 7 ); + mplFile.ReadAll ( &avchdLegacyData.mReserved2, 1 ); + mplFile.ReadAll ( &avchdLegacyData.mPlaylistCharacterSet, 1 ); + mplFile.ReadAll ( &avchdLegacyData.mPlaylistNameLength, 1 ); + mplFile.ReadAll ( &avchdLegacyData.mPlaylistName, avchdLegacyData.mPlaylistNameLength ); + + return true; +} + +// ================================================================================================= +// ReadAVCHDPlaylistMarkExtension +// ============================== + +static bool ReadAVCHDPlaylistMarkExtension ( XMPFiles_IO & mplFile, + XMP_Uns16 playlistMarkID, + AVCHD_blkPlayListMarkExt& avchdLegacyData ) +{ + avchdLegacyData.mLength = XIO::ReadUns32_BE ( &mplFile ); + + if ( avchdLegacyData.mLength == 0 ) return false; + + avchdLegacyData.mNumberOfPlaylistMarks = XIO::ReadUns16_BE ( &mplFile ); + + if ( avchdLegacyData.mNumberOfPlaylistMarks <= playlistMarkID ) return true; + + // Number of bytes in blkMarkExtension, AVCHD Book 2, section 4.3.3.1 + const XMP_Uns64 markExtensionSize = 66; + + // Entries in the mark extension block correspond one-to-one with entries in + // blkPlaylistMark, so we'll only read the one that corresponds to the + // chosen clip. + const XMP_Uns64 markOffset = markExtensionSize * playlistMarkID; + + avchdLegacyData.mPresent = 1; + mplFile.Seek ( markOffset, kXMP_SeekFromCurrent ); + avchdLegacyData.mMakerID = XIO::ReadUns16_BE ( &mplFile ); + avchdLegacyData.mMakerModelCode = XIO::ReadUns16_BE ( &mplFile ); + mplFile.ReadAll ( &avchdLegacyData.mReserved1, 3 ); + mplFile.ReadAll ( &avchdLegacyData.mFlags, 1 ); + avchdLegacyData.mRefToMarkThumbnailIndex = XIO::ReadUns16_BE ( &mplFile ); + mplFile.ReadAll ( &avchdLegacyData.mBlkTimezone, 1 ); + mplFile.ReadAll ( &avchdLegacyData.mRecordDataAndTime, 7 ); + mplFile.ReadAll ( &avchdLegacyData.mMarkCharacterSet, 1 ); + mplFile.ReadAll ( &avchdLegacyData.mMarkNameLength, 1 ); + mplFile.ReadAll ( &avchdLegacyData.mMarkName, 24 ); + mplFile.ReadAll ( &avchdLegacyData.mMakersInformation, 16 ); + mplFile.ReadAll ( &avchdLegacyData.mBlkTimecode, 4 ); + mplFile.ReadAll ( &avchdLegacyData.mReserved2, 2 ); + + return true; +} + +// ================================================================================================= +// ReadAVCHDPlaylistMarkID +// ======================= +// +// Read the playlist mark block to find the ID of the playlist mark that matches the specified +// playlist item. + +static bool ReadAVCHDPlaylistMarkID ( XMPFiles_IO & mplFile, + XMP_Uns16 playItemID, + XMP_Uns16& markID ) +{ + XMP_Uns32 length = XIO::ReadUns32_BE ( &mplFile ); + XMP_Uns16 numberOfPlayListMarks = XIO::ReadUns16_BE ( &mplFile ); + + if ( length == 0 ) return false; + + XMP_Uns8 reserved; + XMP_Uns8 markType; + XMP_Uns16 refToPlayItemID; + + for ( int i = 0; i < numberOfPlayListMarks; ++i ) { + mplFile.ReadAll ( &reserved, 1 ); + mplFile.ReadAll ( &markType, 1 ); + refToPlayItemID = XIO::ReadUns16_BE ( &mplFile ); + + if ( ( markType == 0x01 ) && ( refToPlayItemID == playItemID ) ) { + markID = i; + return true; + } + + mplFile.Seek ( 10, kXMP_SeekFromCurrent ); + } + + return false; +} + +// ================================================================================================= +// ReadAVCHDPlaylistExtensionData +// ============================== + +static bool ReadAVCHDPlaylistExtensionData ( XMPFiles_IO & mplFile, + AVCHD_LegacyMetadata& avchdLegacyData, + XMP_Uns16 playlistMarkID ) +{ + const XMP_Int64 extensionBlockStart = mplFile.Offset(); + AVCHD_blkExtensionData extensionDataHeader; + + if ( ! ReadAVCHDExtensionData ( mplFile, extensionDataHeader ) ) { + return false; + } + + if ( extensionDataHeader.mLength == 0 ) { + return true; + } + + const XMP_Int64 dataBlockStart = extensionBlockStart + extensionDataHeader.mDataBlockStartAddress; + AVCHD_blkPlayListExtensionData& extensionData = avchdLegacyData.mPlaylistExtensionData; + const int reserved2Len = 24; + + mplFile.Seek ( dataBlockStart, kXMP_SeekFromStart ); + mplFile.ReadAll ( extensionData.mTypeIndicator, 4 ); + + if ( strncmp ( extensionData.mTypeIndicator, "PLEX", 4 ) != 0 ) return false; + + extensionData.mPresent = true; + mplFile.ReadAll ( extensionData.mReserved, 4 ); + extensionData.mPlayListMarkExtStartAddress = XIO::ReadUns32_BE ( &mplFile ); + extensionData.mMakersPrivateDataStartAddress = XIO::ReadUns32_BE ( &mplFile ); + mplFile.Seek ( reserved2Len, kXMP_SeekFromCurrent ); + + if ( ! ReadAVCHDPlaylistMetadataBlock ( mplFile, extensionData.mPlaylistMeta ) ) return false; + + mplFile.Seek ( dataBlockStart + extensionData.mPlayListMarkExtStartAddress, kXMP_SeekFromStart ); + + if ( ! ReadAVCHDPlaylistMarkExtension ( mplFile, playlistMarkID, extensionData.mPlaylistMarkExt ) ) return false; + + if ( extensionData.mMakersPrivateDataStartAddress > 0 ) { + + if ( ! avchdLegacyData.mClipExtensionData.mMakersPrivateData.mPanasonicPrivateData.mPresent ) return false; + + mplFile.Seek ( dataBlockStart + extensionData.mMakersPrivateDataStartAddress, kXMP_SeekFromStart ); + + if ( ! ReadAVCHDMakersPrivateData ( mplFile, playlistMarkID, extensionData.mMakersPrivateData ) ) return false; + + } + + return true; +} + +// ================================================================================================= +// ReadAVCHDLegacyClipFile +// ======================= +// +// Read the legacy metadata stored in an AVCHD .CPI file. + +static bool ReadAVCHDLegacyClipFile ( const std::string& strPath, AVCHD_LegacyMetadata& avchdLegacyData ) +{ + bool success = false; + + try { + + Host_IO::FileRef hostRef = Host_IO::Open ( strPath.c_str(), Host_IO::openReadOnly ); + if ( hostRef == Host_IO::noFileRef ) return false; // The open failed. + XMPFiles_IO cpiFile ( hostRef, strPath.c_str(), Host_IO::openReadOnly ); + + memset ( &avchdLegacyData, 0, sizeof(AVCHD_LegacyMetadata) ); + + // Read clip header. ( AVCHD Format. Book1. v. 1.01. p 64 ) + struct AVCHD_ClipInfoHeader + { + char mTypeIndicator[4]; + char mTypeIndicator2[4]; + XMP_Uns32 mSequenceInfoStartAddress; + XMP_Uns32 mProgramInfoStartAddress; + XMP_Uns32 mCPIStartAddress; + XMP_Uns32 mClipMarkStartAddress; + XMP_Uns32 mExtensionDataStartAddress; + XMP_Uns8 mReserved[12]; + }; + + // Read the AVCHD header. + AVCHD_ClipInfoHeader avchdHeader; + cpiFile.ReadAll ( avchdHeader.mTypeIndicator, 4 ); + cpiFile.ReadAll ( avchdHeader.mTypeIndicator2, 4 ); + + if ( strncmp ( avchdHeader.mTypeIndicator, "HDMV", 4 ) != 0 ) return false; + if ( strncmp ( avchdHeader.mTypeIndicator2, "0100", 4 ) != 0 ) return false; + + avchdHeader.mSequenceInfoStartAddress = XIO::ReadUns32_BE ( &cpiFile ); + avchdHeader.mProgramInfoStartAddress = XIO::ReadUns32_BE ( &cpiFile ); + avchdHeader.mCPIStartAddress = XIO::ReadUns32_BE ( &cpiFile ); + avchdHeader.mClipMarkStartAddress = XIO::ReadUns32_BE ( &cpiFile ); + avchdHeader.mExtensionDataStartAddress = XIO::ReadUns32_BE ( &cpiFile ); + cpiFile.ReadAll ( avchdHeader.mReserved, 12 ); + + // Seek to the program header. (AVCHD Format. Book1. v. 1.01. p 77 ) + cpiFile.Seek ( avchdHeader.mProgramInfoStartAddress, kXMP_SeekFromStart ); + + // Read the program info block + success = ReadAVCHDProgramInfo ( cpiFile, avchdLegacyData.mProgramInfo ); + + if ( success && ( avchdHeader.mExtensionDataStartAddress != 0 ) ) { + // Seek to the program header. (AVCHD Format. Book1. v. 1.01. p 77 ) + cpiFile.Seek ( avchdHeader.mExtensionDataStartAddress, kXMP_SeekFromStart ); + success = ReadAVCHDClipExtensionData ( cpiFile, avchdLegacyData.mClipExtensionData ); + } + + } catch ( ... ) { + + return false; + + } + + return success; + +} // ReadAVCHDLegacyClipFile + +// ================================================================================================= +// ReadAVCHDLegacyPlaylistFile +// =========================== +// +// Read the legacy metadata stored in an AVCHD .MPL file. + +static bool ReadAVCHDLegacyPlaylistFile ( const std::string& mplPath, + const std::string& strClipName, + AVCHD_LegacyMetadata& avchdLegacyData ) +{ + +#if 1 + bool success = false; + + try { + + Host_IO::FileRef hostRef = Host_IO::Open ( mplPath.c_str(), Host_IO::openReadOnly ); + if ( hostRef == Host_IO::noFileRef ) return false; // The open failed. + XMPFiles_IO mplFile ( hostRef, mplPath.c_str(), Host_IO::openReadOnly ); + + // Read playlist header. ( AVCHD Format. Book1. v. 1.01. p 43 ) + struct AVCHD_PlaylistFileHeader { + char mTypeIndicator[4]; + char mTypeIndicator2[4]; + XMP_Uns32 mPlaylistStartAddress; + XMP_Uns32 mPlaylistMarkStartAddress; + XMP_Uns32 mExtensionDataStartAddress; + }; + + // Read the AVCHD playlist file header. + AVCHD_PlaylistFileHeader avchdHeader; + mplFile.ReadAll ( avchdHeader.mTypeIndicator, 4 ); + mplFile.ReadAll ( avchdHeader.mTypeIndicator2, 4 ); + + if ( strncmp ( avchdHeader.mTypeIndicator, "MPLS", 4 ) != 0 ) return false; + if ( strncmp ( avchdHeader.mTypeIndicator2, "0100", 4 ) != 0 ) return false; + + avchdHeader.mPlaylistStartAddress = XIO::ReadUns32_BE ( &mplFile ); + avchdHeader.mPlaylistMarkStartAddress = XIO::ReadUns32_BE ( &mplFile ); + avchdHeader.mExtensionDataStartAddress = XIO::ReadUns32_BE ( &mplFile ); + + if ( avchdHeader.mExtensionDataStartAddress == 0 ) return false; + + // Seek to the start of the Playlist block. (AVCHD Format. Book1. v. 1.01. p 45 ) + mplFile.Seek ( avchdHeader.mPlaylistStartAddress, kXMP_SeekFromStart ); + + XMP_Uns16 playItemID = 0xFFFF; + XMP_Uns16 playlistMarkID = 0xFFFF; + + if ( AVCHD_PlaylistContainsClip ( mplFile, playItemID, strClipName ) ) { + mplFile.Seek ( avchdHeader.mPlaylistMarkStartAddress, kXMP_SeekFromStart ); + if ( ! ReadAVCHDPlaylistMarkID ( mplFile, playItemID, playlistMarkID ) ) return false; + mplFile.Seek ( avchdHeader.mExtensionDataStartAddress, kXMP_SeekFromStart ); + success = ReadAVCHDPlaylistExtensionData ( mplFile, avchdLegacyData, playlistMarkID ); + } + + } catch ( ... ) { + + success = false; + + } + + return success; + +#else + + bool success = false; + std::string mplPath; + char playlistName [10]; + const int rootPlaylistNum = atoi(strClipName.c_str()); + + // Find the corresponding .MPL file -- because of clip spanning the .MPL name may not match the .CPI name for + // a given clip -- we need to open .MPL files and look for one that contains a reference to the clip name. To speed + // up the search we'll start with the playlist with the same number/name as the clip and search backwards. Assuming + // this directory was generated by a camera, the clip numbers will increase sequentially across the playlist files, + // though one playlist file may reference more than one clip. + for ( int i = rootPlaylistNum; i >= 0; --i ) { + + sprintf ( playlistName, "%05d", i ); + + if ( MakeLeafPath ( &mplPath, strRootPath.c_str(), "PLAYLIST", playlistName, ".mpl", true /* checkFile */ ) ) { + + try { + + Host_IO::FileRef hostRef = Host_IO::Open ( mplPath.c_str(), Host_IO::openReadOnly ); + if ( hostRef == Host_IO::noFileRef ) return false; // The open failed. + XMPFiles_IO mplFile ( hostRef, mplPath.c_str(), Host_IO::openReadOnly ); + + // Read playlist header. ( AVCHD Format. Book1. v. 1.01. p 43 ) + struct AVCHD_PlaylistFileHeader + { + char mTypeIndicator[4]; + char mTypeIndicator2[4]; + XMP_Uns32 mPlaylistStartAddress; + XMP_Uns32 mPlaylistMarkStartAddress; + XMP_Uns32 mExtensionDataStartAddress; + }; + + // Read the AVCHD playlist file header. + AVCHD_PlaylistFileHeader avchdHeader; + mplFile.ReadAll ( avchdHeader.mTypeIndicator, 4 ); + mplFile.ReadAll ( avchdHeader.mTypeIndicator2, 4 ); + + if ( strncmp ( avchdHeader.mTypeIndicator, "MPLS", 4 ) != 0 ) return false; + if ( strncmp ( avchdHeader.mTypeIndicator2, "0100", 4 ) != 0 ) return false; + + avchdHeader.mPlaylistStartAddress = XIO::ReadUns32_BE ( &mplFile ); + avchdHeader.mPlaylistMarkStartAddress = XIO::ReadUns32_BE ( &mplFile ); + avchdHeader.mExtensionDataStartAddress = XIO::ReadUns32_BE ( &mplFile ); + + if ( avchdHeader.mExtensionDataStartAddress == 0 ) return false; + + // Seek to the start of the Playlist block. (AVCHD Format. Book1. v. 1.01. p 45 ) + mplFile.Seek ( avchdHeader.mPlaylistStartAddress, kXMP_SeekFromStart ); + + XMP_Uns16 playItemID = 0xFFFF; + XMP_Uns16 playlistMarkID = 0xFFFF; + + if ( AVCHD_PlaylistContainsClip ( mplFile, playItemID, strClipName ) ) { + mplFile.Seek ( avchdHeader.mPlaylistMarkStartAddress, kXMP_SeekFromStart ); + + if ( ! ReadAVCHDPlaylistMarkID ( mplFile, playItemID, playlistMarkID ) ) return false; + + mplFile.Seek ( avchdHeader.mExtensionDataStartAddress, kXMP_SeekFromStart ); + success = ReadAVCHDPlaylistExtensionData ( mplFile, avchdLegacyData, playlistMarkID ); + } + + } catch ( ... ) { + + return false; + + } + } + + } + + return success; + +#endif + +} // ReadAVCHDLegacyPlaylistFile + +// ================================================================================================= +// FindAVCHDLegacyPlaylistFile +// =========================== +// +// Find and read the legacy metadata stored in an AVCHD .MPL file. + +static bool FindAVCHDLegacyPlaylistFile ( const std::string& strRootPath, + const std::string& strClipName, + AVCHD_LegacyMetadata& avchdLegacyData ) +{ + bool success = false; + std::string mplPath; + + // Find the corresponding .MPL file -- because of clip spanning the .MPL name may not match the + // .CPI name for a given clip -- we need to open .MPL files and look for one that contains a + // reference to the clip name. To speed up the search we'll start with the playlist with the + // same number/name as the clip, and if that fails look into other playlist files in the + // directory. One playlist file may reference more than one clip. + + if ( MakeLeafPath ( &mplPath, strRootPath.c_str(), "PLAYLIST", strClipName.c_str(), ".mpl", true /* checkFile */ ) ) { + success = ReadAVCHDLegacyPlaylistFile ( mplPath, strClipName, avchdLegacyData ); + } + + if ( ! success ) { + + std::string playlistPath = strRootPath; + playlistPath += kDirChar; + playlistPath += "BDMV"; + playlistPath += kDirChar; + playlistPath += "PLAYLIST"; + playlistPath += kDirChar; + + std::string childName; + + if ( Host_IO::GetFileMode ( playlistPath.c_str() ) == Host_IO::kFMode_IsFolder ) { + + Host_IO::AutoFolder af; + af.folder = Host_IO::OpenFolder ( playlistPath.c_str() ); + if ( af.folder == Host_IO::noFolderRef ) return false; + + while ( (! success) && Host_IO::GetNextChild ( af.folder, &childName ) && + (childName.find(".mpl") || childName.find(".MPL")) ) { + mplPath = playlistPath + childName; + if ( Host_IO::GetFileMode ( mplPath.c_str() ) == Host_IO::kFMode_IsFile ) { + success = ReadAVCHDLegacyPlaylistFile ( mplPath, strClipName, avchdLegacyData ); + } + + } + + af.Close(); + + } + + } + + return success; + +} // FindAVCHDLegacyPlaylistFile + +// ================================================================================================= +// ReadAVCHDLegacyMetadata +// ======================= +// +// Read the legacy metadata stored in an AVCHD .CPI file. + +static bool ReadAVCHDLegacyMetadata ( const std::string& strPath, + const std::string& strRootPath, + const std::string& strClipName, + AVCHD_LegacyMetadata& avchdLegacyData ) +{ + bool success = ReadAVCHDLegacyClipFile ( strPath, avchdLegacyData ); + + if ( success && avchdLegacyData.mClipExtensionData.mPresent ) { + success = FindAVCHDLegacyPlaylistFile ( strRootPath, strClipName, avchdLegacyData ); + } + + return success; + +} // ReadAVCHDLegacyMetadata + +// ================================================================================================= +// AVCCAM_SetXMPStartTimecode +// ========================== + +static void AVCCAM_SetXMPStartTimecode ( SXMPMeta& xmpObj, const XMP_Uns8* avccamTimecode, XMP_Uns8 avchdFrameRate ) +{ + // Timecode in SMPTE 12M format, according to Panasonic's documentation + if ( *reinterpret_cast<const XMP_Uns32*>( avccamTimecode ) == 0xFFFFFFFF ) { + // 0xFFFFFFFF means timecode not specified + return; + } + + const XMP_Uns8 isColor = ( avccamTimecode[0] >> 7 ) & 0x01; + const XMP_Uns8 isDropFrame = ( avccamTimecode[0] >> 6 ) & 0x01; + const XMP_Uns8 frameTens = ( avccamTimecode[0] >> 4 ) & 0x03; + const XMP_Uns8 frameUnits = avccamTimecode[0] & 0x0f; + const XMP_Uns8 secondTens = ( avccamTimecode[1] >> 4 ) & 0x07; + const XMP_Uns8 secondUnits = avccamTimecode[1] & 0x0f; + const XMP_Uns8 minuteTens = ( avccamTimecode[2] >> 4 ) & 0x07; + const XMP_Uns8 minuteUnits = avccamTimecode[2] & 0x0f; + const XMP_Uns8 hourTens = ( avccamTimecode[3] >> 4 ) & 0x03; + const XMP_Uns8 hourUnits = avccamTimecode[3] & 0x0f; + char tcSeparator = ':'; + const char* dmTimeFormat = NULL; + const char* dmTimeScale = NULL; + const char* dmTimeSampleSize = NULL; + + switch ( avchdFrameRate ) { + case 1 : + // 23.976i + dmTimeFormat = "23976Timecode"; + dmTimeScale = "24000"; + dmTimeSampleSize = "1001"; + break; + + case 2 : + // 24p + dmTimeFormat = "24Timecode"; + dmTimeScale = "24"; + dmTimeSampleSize = "1"; + break; + + case 3 : + case 6 : + // 50i or 25p + dmTimeFormat = "25Timecode"; + dmTimeScale = "25"; + dmTimeSampleSize = "1"; + break; + + case 4 : + case 7 : + // 29.97p or 59.94i + if ( isDropFrame ) { + dmTimeFormat = "2997DropTimecode"; + tcSeparator = ';'; + } else { + dmTimeFormat = "2997NonDropTimecode"; + } + + dmTimeScale = "30000"; + dmTimeSampleSize = "1001"; + + break; + } + + if ( dmTimeFormat != NULL ) { + char timecodeBuff [12]; + + sprintf ( timecodeBuff, "%d%d%c%d%d%c%d%d%c%d%d", hourTens, hourUnits, tcSeparator, + minuteTens, minuteUnits, tcSeparator, secondTens, secondUnits, tcSeparator, frameTens, frameUnits); + + xmpObj.SetProperty( kXMP_NS_DM, "startTimeScale", dmTimeScale, kXMP_DeleteExisting ); + xmpObj.SetProperty( kXMP_NS_DM, "startTimeSampleSize", dmTimeSampleSize, kXMP_DeleteExisting ); + xmpObj.SetStructField ( kXMP_NS_DM, "startTimecode", kXMP_NS_DM, "timeValue", timecodeBuff, 0 ); + xmpObj.SetStructField ( kXMP_NS_DM, "startTimecode", kXMP_NS_DM, "timeFormat", dmTimeFormat, 0 ); + } +} + +// ================================================================================================= +// AVCHD_SetXMPMakeAndModel +// ======================== + +static bool AVCHD_SetXMPMakeAndModel ( SXMPMeta& xmpObj, const AVCHD_blkClipExtensionData& clipExtData ) +{ + if ( ! clipExtData.mPresent ) return false; + + XMP_StringPtr xmpValue = 0; + + // Set the Make. Use a hex string for unknown makes. + { + char hexMakeNumber [7]; + + switch ( clipExtData.mClipInfoExt.mMakerID ) { + case kMakerIDCanon : xmpValue = "Canon"; break; + case kMakerIDPanasonic : xmpValue = "Panasonic"; break; + case kMakerIDSony : xmpValue = "Sony"; break; + default : + std::sprintf ( hexMakeNumber, "0x%04x", clipExtData.mClipInfoExt.mMakerID ); + xmpValue = hexMakeNumber; + + break; + } + + xmpObj.SetProperty ( kXMP_NS_TIFF, "Make", xmpValue, kXMP_DeleteExisting ); + } + + // Set the Model number. Use a hex string for unknown model numbers so they can still be distinguished. + { + char hexModelNumber [7]; + + xmpValue = 0; + + switch ( clipExtData.mClipInfoExt.mMakerID ) { + case kMakerIDCanon : + switch ( clipExtData.mClipInfoExt.mMakerModelCode ) { + case 0x1000 : xmpValue = "HR10"; break; + case 0x2000 : xmpValue = "HG10"; break; + case 0x2001 : xmpValue = "HG21"; break; + case 0x3000 : xmpValue = "HF100"; break; + case 0x3003 : xmpValue = "HF S10"; break; + default : break; + } + break; + + case kMakerIDPanasonic : + switch ( clipExtData.mClipInfoExt.mMakerModelCode ) { + case 0x0202 : xmpValue = "HD-writer"; break; + case 0x0400 : xmpValue = "AG-HSC1U"; break; + case 0x0401 : xmpValue = "AG-HMC70"; break; + case 0x0410 : xmpValue = "AG-HMC150"; break; + case 0x0411 : xmpValue = "AG-HMC40"; break; + case 0x0412 : xmpValue = "AG-HMC80"; break; + case 0x0413 : xmpValue = "AG-3DA1"; break; + case 0x0414 : xmpValue = "AG-AF100"; break; + case 0x0450 : xmpValue = "AG-HMR10"; break; + case 0x0451 : xmpValue = "AJ-YCX250"; break; + case 0x0452 : xmpValue = "AG-MDR15"; break; + case 0x0490 : xmpValue = "AVCCAM Restorer"; break; + case 0x0491 : xmpValue = "AVCCAM Viewer"; break; + case 0x0492 : xmpValue = "AVCCAM Viewer for Mac"; break; + default : break; + } + + break; + + default : break; + } + + if ( ( xmpValue == 0 ) && ( clipExtData.mClipInfoExt.mMakerID != kMakerIDSony ) ) { + // Panasonic has said that if we don't have a string for the model number, they'd like to see the code + // anyway. We'll do the same for every manufacturer except Sony, who have said that they use + // the same model number for multiple cameras. + std::sprintf ( hexModelNumber, "0x%04x", clipExtData.mClipInfoExt.mMakerModelCode ); + xmpValue = hexModelNumber; + } + + if ( xmpValue != 0 ) xmpObj.SetProperty ( kXMP_NS_TIFF, "Model", xmpValue, kXMP_DeleteExisting ); + } + + return true; +} + +// ================================================================================================= +// AVCHD_StringFieldToXMP +// ====================== + +static std::string AVCHD_StringFieldToXMP ( XMP_Uns8 avchdLength, + XMP_Uns8 avchdCharacterSet, + const XMP_Uns8* avchdField, + XMP_Uns8 avchdFieldSize ) +{ + std::string xmpString; + + if ( avchdCharacterSet == 0x02 ) { + // UTF-16, Big Endian + UTF8Unit utf8Name [512]; + const XMP_Uns8 avchdMaxChars = ( avchdFieldSize / 2); + size_t utf16Read; + size_t utf8Written; + + // The spec doesn't say whether AVCHD length fields count bytes or characters, so we'll + // clamp to the max number of UTF-16 characters just in case. + const int stringLength = ( avchdLength > avchdMaxChars ) ? avchdMaxChars : avchdLength; + + UTF16BE_to_UTF8 ( reinterpret_cast<const UTF16Unit*> ( avchdField ), stringLength, + utf8Name, 512, &utf16Read, &utf8Written ); + xmpString.assign ( reinterpret_cast<const char*> ( utf8Name ), utf8Written ); + } else { + // AVCHD supports many character encodings, but UTF-8 (0x01) and ASCII (0x90) are the only ones I've + // seen in the wild at this point. We'll treat the other character sets as UTF-8 on the assumption that + // at least a few characters will come across, and something is better than nothing. + const int stringLength = ( avchdLength > avchdFieldSize ) ? avchdFieldSize : avchdLength; + + xmpString.assign ( reinterpret_cast<const char*> ( avchdField ), stringLength ); + } + + return xmpString; +} + +// ================================================================================================= +// AVCHD_SetXMPShotName +// ==================== + +static void AVCHD_SetXMPShotName ( SXMPMeta& xmpObj, const AVCHD_blkPlayListMarkExt& markExt, const std::string& strClipName ) +{ + if ( markExt.mPresent ) { + const std::string shotName = AVCHD_StringFieldToXMP ( markExt.mMarkNameLength, markExt.mMarkCharacterSet, markExt.mMarkName, 24 ); + + if ( ! shotName.empty() ) xmpObj.SetProperty ( kXMP_NS_DC, "shotName", shotName.c_str(), kXMP_DeleteExisting ); + } +} + +// ================================================================================================= +// BytesToHex +// ========== + +#define kHexDigits "0123456789ABCDEF" + +static std::string BytesToHex ( const XMP_Uns8* inClipIDBytes, int inNumBytes ) +{ + const int numChars = ( inNumBytes * 2 ); + std::string hexStr; + + hexStr.reserve(numChars); + + for ( int i = 0; i < inNumBytes; ++i ) { + const XMP_Uns8 byte = inClipIDBytes[i]; + hexStr.push_back ( kHexDigits [byte >> 4] ); + hexStr.push_back ( kHexDigits [byte & 0xF] ); + } + + return hexStr; +} + +// ================================================================================================= +// AVCHD_DateFieldToXMP +// ==================== +// +// AVCHD Format Book 2, section 4.2.2.2. + +static std::string AVCHD_DateFieldToXMP ( XMP_Uns8 avchdTimeZone, const XMP_Uns8* avchdDateTime ) +{ + const XMP_Uns8 daylightSavingsTime = ( avchdTimeZone >> 6 ) & 0x01; + const XMP_Uns8 timezoneSign = ( avchdTimeZone >> 5 ) & 0x01; + const XMP_Uns8 timezoneValue = ( avchdTimeZone >> 1 ) & 0x0F; + const XMP_Uns8 halfHourFlag = avchdTimeZone & 0x01; + int utcOffsetHours = 0; + unsigned int utcOffsetMinutes = 0; + + // It's not entirely clear how to interpret the daylightSavingsTime flag from the documentation -- my best + // guess is that it should only be used if trying to display local time, not the UTC-relative time that + // XMP specifies. + if ( timezoneValue != 0xF ) { + utcOffsetHours = timezoneSign ? -timezoneValue : timezoneValue; + utcOffsetMinutes = 30 * halfHourFlag; + } + + char dateBuff [26]; + + sprintf ( dateBuff, + "%01d%01d%01d%01d-%01d%01d-%01d%01dT%01d%01d:%01d%01d:%01d%01d%+02d:%02d", + (avchdDateTime[0] >> 4), (avchdDateTime[0] & 0x0F), + (avchdDateTime[1] >> 4), (avchdDateTime[1] & 0x0F), + (avchdDateTime[2] >> 4), (avchdDateTime[2] & 0x0F), + (avchdDateTime[3] >> 4), (avchdDateTime[3] & 0x0F), + (avchdDateTime[4] >> 4), (avchdDateTime[4] & 0x0F), + (avchdDateTime[5] >> 4), (avchdDateTime[5] & 0x0F), + (avchdDateTime[6] >> 4), (avchdDateTime[6] & 0x0F), + utcOffsetHours, utcOffsetMinutes ); + + return std::string(dateBuff); +} + +// ================================================================================================= +// AVCHD_MetaHandlerCTor +// ===================== + +XMPFileHandler * AVCHD_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new AVCHD_MetaHandler ( parent ); + +} // AVCHD_MetaHandlerCTor + +// ================================================================================================= +// AVCHD_MetaHandler::AVCHD_MetaHandler +// ==================================== + +AVCHD_MetaHandler::AVCHD_MetaHandler ( XMPFiles * _parent ) +{ + this->parent = _parent; // Inherited, can't set in the prefix. + this->handlerFlags = kAVCHD_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + + // Extract the root path and clip name. + + if ( this->parent->tempPtr == 0 ) { + // The CheckFormat call might have been skipped. + this->parent->tempPtr = CreatePseudoClipPath ( this->parent->filePath ); + } + + this->rootPath.assign ( (char*) this->parent->tempPtr ); + free ( this->parent->tempPtr ); + this->parent->tempPtr = 0; + + XIO::SplitLeafName ( &this->rootPath, &this->clipName ); + +} // AVCHD_MetaHandler::AVCHD_MetaHandler + +// ================================================================================================= +// AVCHD_MetaHandler::~AVCHD_MetaHandler +// ===================================== + +AVCHD_MetaHandler::~AVCHD_MetaHandler() +{ + + if ( this->parent->tempPtr != 0 ) { + free ( this->parent->tempPtr ); + this->parent->tempPtr = 0; + } + +} // AVCHD_MetaHandler::~AVCHD_MetaHandler + +// ================================================================================================= +// AVCHD_MetaHandler::MakeClipInfoPath +// =================================== + +bool AVCHD_MetaHandler::MakeClipInfoPath ( std::string * path, XMP_StringPtr suffix, bool checkFile /* = false */ ) const +{ + return MakeLeafPath ( path, this->rootPath.c_str(), "CLIPINF", this->clipName.c_str(), suffix, checkFile ); +} // AVCHD_MetaHandler::MakeClipInfoPath + +// ================================================================================================= +// AVCHD_MetaHandler::MakeClipStreamPath +// ===================================== + +bool AVCHD_MetaHandler::MakeClipStreamPath ( std::string * path, XMP_StringPtr suffix, bool checkFile /* = false */ ) const +{ + return MakeLeafPath ( path, this->rootPath.c_str(), "STREAM", this->clipName.c_str(), suffix, checkFile ); +} // AVCHD_MetaHandler::MakeClipStreamPath + +// ================================================================================================= +// AVCHD_MetaHandler::MakePlaylistPath +// ===================================== + +bool AVCHD_MetaHandler::MakePlaylistPath ( std::string * path, XMP_StringPtr suffix, bool checkFile /* = false */ ) const +{ + return MakeLeafPath ( path, this->rootPath.c_str(), "PLAYLIST", this->clipName.c_str(), suffix, checkFile ); +} // AVCHD_MetaHandler::MakePlaylistPath + +// ================================================================================================= +// AVCHD_MetaHandler::MakeLegacyDigest +// =================================== + +void AVCHD_MetaHandler::MakeLegacyDigest ( std::string * digestStr ) +{ + std::string strClipPath; + std::string strPlaylistPath; + std::vector<XMP_Uns8> legacyBuff; + + bool ok = this->MakeClipInfoPath ( &strClipPath, ".clpi", true /* checkFile */ ); + if ( ! ok ) return; + + ok = this->MakePlaylistPath ( &strPlaylistPath, ".mpls", true /* checkFile */ ); + if ( ! ok ) return; + + try { + { + Host_IO::FileRef hostRef = Host_IO::Open ( strClipPath.c_str(), Host_IO::openReadOnly ); + if ( hostRef == Host_IO::noFileRef ) return; // The open failed. + XMPFiles_IO cpiFile ( hostRef, strClipPath.c_str(), Host_IO::openReadOnly ); + + // Read at most the first 2k of data from the cpi file to use in the digest + // (every CPI file I've seen is less than 1k). + const XMP_Int64 cpiLen = cpiFile.Length(); + const XMP_Int64 buffLen = (cpiLen <= 2048) ? cpiLen : 2048; + + legacyBuff.resize ( (unsigned int) buffLen ); + cpiFile.ReadAll ( &(legacyBuff[0]), static_cast<XMP_Int32> ( buffLen ) ); + } + + { + Host_IO::FileRef hostRef = Host_IO::Open ( strPlaylistPath.c_str(), Host_IO::openReadOnly ); + if ( hostRef == Host_IO::noFileRef ) return; // The open failed. + XMPFiles_IO mplFile ( hostRef, strPlaylistPath.c_str(), Host_IO::openReadOnly ); + + // Read at most the first 2k of data from the cpi file to use in the digest + // (every playlist file I've seen is less than 1k). + const XMP_Int64 mplLen = mplFile.Length(); + const XMP_Int64 buffLen = (mplLen <= 2048) ? mplLen : 2048; + const XMP_Int64 clipBuffLen = legacyBuff.size(); + + legacyBuff.resize ( (unsigned int) (clipBuffLen + buffLen) ); + mplFile.ReadAll ( &( legacyBuff [(unsigned int)clipBuffLen] ), (XMP_Int32)buffLen ); + } + + } catch (...) { + return; + } + + MD5_CTX context; + unsigned char digestBin [16]; + + MD5Init ( &context ); + MD5Update ( &context, (XMP_Uns8*)&(legacyBuff[0]), (unsigned int) legacyBuff.size() ); + MD5Final ( digestBin, &context ); + + *digestStr = BytesToHex ( digestBin, 16 ); +} // AVCHD_MetaHandler::MakeLegacyDigest + +// ================================================================================================= +// AVCHD_MetaHandler::GetFileModDate +// ================================= + +static inline bool operator< ( const XMP_DateTime & left, const XMP_DateTime & right ) { + int compare = SXMPUtils::CompareDateTime ( left, right ); + return (compare < 0); +} + +bool AVCHD_MetaHandler::GetFileModDate ( XMP_DateTime * modDate ) +{ + + // The AVCHD locations of metadata: + // BDMV/ + // CLIPINF/ + // 00001.clpi + // PLAYLIST/ + // 00001.mpls + // STREAM/ + // 00001.xmp + + bool ok, haveDate = false; + std::string fullPath; + XMP_DateTime oneDate, junkDate; + if ( modDate == 0 ) modDate = &junkDate; + + ok = this->MakeClipInfoPath ( &fullPath, ".clpi", true /* checkFile */ ); + if ( ok ) ok = Host_IO::GetModifyDate ( fullPath.c_str(), &oneDate ); + if ( ok ) { + if ( (! haveDate) || (*modDate < oneDate) ) *modDate = oneDate; + haveDate = true; + } + + ok = this->MakePlaylistPath ( &fullPath, ".mpls", true /* checkFile */ ); + if ( ok ) ok = Host_IO::GetModifyDate ( fullPath.c_str(), &oneDate ); + if ( ok ) { + if ( (! haveDate) || (*modDate < oneDate) ) *modDate = oneDate; + haveDate = true; + } + + ok = this->MakeClipStreamPath ( &fullPath, ".xmp", true /* checkFile */ ); + if ( ok ) ok = Host_IO::GetModifyDate ( fullPath.c_str(), &oneDate ); + if ( ok ) { + if ( (! haveDate) || (*modDate < oneDate) ) *modDate = oneDate; + haveDate = true; + } + + return haveDate; + +} // AVCHD_MetaHandler::GetFileModDate + +// ================================================================================================= +// AVCHD_MetaHandler::CacheFileData +// ================================ + +void AVCHD_MetaHandler::CacheFileData() +{ + XMP_Assert ( ! this->containsXMP ); + + if ( this->parent->UsesClientIO() ) { + XMP_Throw ( "XDCAM cannot be used with client-managed I/O", kXMPErr_InternalFailure ); + } + + // See if the clip's .XMP file exists. + + std::string xmpPath; + bool found = this->MakeClipStreamPath ( &xmpPath, ".xmp", true /* checkFile */ ); + if ( ! found ) return; + + // Read the entire .XMP file. + + bool readOnly = XMP_OptionIsClear ( this->parent->openFlags, kXMPFiles_OpenForUpdate ); + + XMP_Assert ( this->parent->ioRef == 0 ); + XMPFiles_IO* xmpFile = XMPFiles_IO::New_XMPFiles_IO ( xmpPath.c_str(), readOnly ); + if ( xmpFile == 0 ) return; // The open failed. + this->parent->ioRef = xmpFile; + + XMP_Int64 xmpLen = xmpFile->Length(); + if ( xmpLen > 100*1024*1024 ) { + XMP_Throw ( "AVCHD XMP is outrageously large", kXMPErr_InternalFailure ); // Sanity check. + } + + this->xmpPacket.erase(); + this->xmpPacket.append ( (size_t ) xmpLen, ' ' ); + + XMP_Int32 ioCount = xmpFile->ReadAll ( (void*)this->xmpPacket.data(), (XMP_Int32)xmpLen ); + + this->packetInfo.offset = 0; + this->packetInfo.length = (XMP_Int32)xmpLen; + FillPacketInfo ( this->xmpPacket, &this->packetInfo ); + + this->containsXMP = true; + +} // AVCHD_MetaHandler::CacheFileData + +// ================================================================================================= +// AVCHD_MetaHandler::ProcessXMP +// ============================= + +void AVCHD_MetaHandler::ProcessXMP() +{ + if ( this->processedXMP ) return; + this->processedXMP = true; // Make sure only called once. + + if ( this->containsXMP ) { + this->xmpObj.ParseFromBuffer ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + } + + // read clip info + AVCHD_LegacyMetadata avchdLegacyData; + std::string strPath; + + bool ok = this->MakeClipInfoPath ( &strPath, ".clpi", true /* checkFile */ ); + if ( ok ) ReadAVCHDLegacyMetadata ( strPath, this->rootPath, this->clipName, avchdLegacyData ); + if ( ! ok ) return; + + const AVCHD_blkPlayListMarkExt& markExt = avchdLegacyData.mPlaylistExtensionData.mPlaylistMarkExt; + XMP_Uns8 pulldownFlag = 0; + + if ( markExt.mPresent ) { + const std::string dateString = AVCHD_DateFieldToXMP ( markExt.mBlkTimezone, markExt.mRecordDataAndTime ); + + if ( ! dateString.empty() ) this->xmpObj.SetProperty ( kXMP_NS_DM, "shotDate", dateString.c_str(), kXMP_DeleteExisting ); + AVCHD_SetXMPShotName ( this->xmpObj, markExt, this->clipName ); + AVCCAM_SetXMPStartTimecode ( this->xmpObj, markExt.mBlkTimecode, avchdLegacyData.mProgramInfo.mVideoStream.mFrameRate ); + pulldownFlag = (markExt.mFlags >> 1) & 0x03; // bits 1 and 2 + } + + // Video Stream. AVCHD Format v. 1.01 p. 78 + + const bool has2_2pulldown = (pulldownFlag == 0x01); + const bool has3_2pulldown = (pulldownFlag == 0x10); + XMP_StringPtr xmpValue = 0; + + if ( avchdLegacyData.mProgramInfo.mVideoStream.mPresent ) { + + // XMP videoFrameSize. + xmpValue = 0; + int frameIndex = -1; + bool isProgressiveHD = false; + const char* frameWidth[4] = { "720", "720", "1280", "1920" }; + const char* frameHeight[4] = { "480", "576", "720", "1080" }; + + switch ( avchdLegacyData.mProgramInfo.mVideoStream.mVideoFormat ) { + case 1 : frameIndex = 0; break; // 480i + case 2 : frameIndex = 1; break; // 576i + case 3 : frameIndex = 0; break; // 480p + case 4 : frameIndex = 3; break; // 1080i + case 5 : frameIndex = 2; isProgressiveHD = true; break; // 720p + case 6 : frameIndex = 3; isProgressiveHD = true; break; // 1080p + default: break; + } + + if ( frameIndex != -1 ) { + xmpValue = frameWidth[frameIndex]; + this->xmpObj.SetStructField ( kXMP_NS_DM, "videoFrameSize", kXMP_NS_XMP_Dimensions, "w", xmpValue, 0 ); + xmpValue = frameHeight[frameIndex]; + this->xmpObj.SetStructField ( kXMP_NS_DM, "videoFrameSize", kXMP_NS_XMP_Dimensions, "h", xmpValue, 0 ); + xmpValue = "pixels"; + this->xmpObj.SetStructField ( kXMP_NS_DM, "videoFrameSize", kXMP_NS_XMP_Dimensions, "unit", xmpValue, 0 ); + } + + // XMP videoFrameRate. The logic below seems pretty tortured, but matches "Table 4-4 pulldown" on page 31 of Book 2 of the AVCHD + // spec, if you interepret "frame_mbs_only_flag" as "isProgressiveHD", "frame-rate [Hz]" as the frame rate encoded in + // mVideoStream.mFrameRate, and "Video Scan Type" as the desired xmp output value. The algorithm produces correct results for + // all the AVCHD media I've tested. + xmpValue = 0; + if ( isProgressiveHD ) { + + switch ( avchdLegacyData.mProgramInfo.mVideoStream.mFrameRate ) { + case 1 : xmpValue = "23.98p"; break; // "23.976" + case 2 : xmpValue = "24p"; break; // "24" + case 3 : xmpValue = "25p"; break; // "25" + case 4 : xmpValue = has2_2pulldown ? "29.97p" : "59.94p"; break; // "29.97" + case 6 : xmpValue = has2_2pulldown ? "25p" : "50p"; break; // "50" + case 7 : // "59.94" + if ( has2_2pulldown ) + xmpValue = "29.97p"; + else + xmpValue = has3_2pulldown ? "23.98p" : "59.94p"; + + break; + default: break; + } + + } else { + + switch ( avchdLegacyData.mProgramInfo.mVideoStream.mFrameRate ) { + case 3 : xmpValue = has2_2pulldown ? "25p" : "50i"; break; // "25" (but 1080p25 is reported as 1080i25 with 2:2 pulldown...) + case 4 : // "29.97" + if ( has2_2pulldown ) + xmpValue = "29.97p"; + else + xmpValue = has3_2pulldown ? "23.98p" : "59.94i"; + + break; + default: break; + } + + } + + if ( xmpValue != 0 ) { + this->xmpObj.SetProperty ( kXMP_NS_DM, "videoFrameRate", xmpValue, kXMP_DeleteExisting ); + } + + this->containsXMP = true; + + } + + // Audio Stream. + if ( avchdLegacyData.mProgramInfo.mAudioStream.mPresent ) { + + xmpValue = 0; + switch ( avchdLegacyData.mProgramInfo.mAudioStream.mAudioPresentationType ) { + case 1 : xmpValue = "Mono"; break; + case 3 : xmpValue = "Stereo"; break; + default : break; + } + if ( xmpValue != 0 ) { + this->xmpObj.SetProperty ( kXMP_NS_DM, "audioChannelType", xmpValue, kXMP_DeleteExisting ); + } + + xmpValue = 0; + switch ( avchdLegacyData.mProgramInfo.mAudioStream.mSamplingFrequency ) { + case 1 : xmpValue = "48000"; break; + case 4 : xmpValue = "96000"; break; + case 5 : xmpValue = "192000"; break; + default : break; + } + if ( xmpValue != 0 ) { + this->xmpObj.SetProperty ( kXMP_NS_DM, "audioSampleRate", xmpValue, kXMP_DeleteExisting ); + } + + this->containsXMP = true; + } + + // Proprietary vendor extensions + if ( AVCHD_SetXMPMakeAndModel ( this->xmpObj, avchdLegacyData.mClipExtensionData ) ) this->containsXMP = true; + + this->xmpObj.SetProperty ( kXMP_NS_DM, "title", this->clipName.c_str(), kXMP_DeleteExisting ); + this->containsXMP = true; + + if ( avchdLegacyData.mClipExtensionData.mMakersPrivateData.mPresent && + ( avchdLegacyData.mClipExtensionData.mClipInfoExt.mMakerID == kMakerIDPanasonic ) ) { + + const AVCHD_blkPanasonicPrivateData& panasonicClipData = avchdLegacyData.mClipExtensionData.mMakersPrivateData.mPanasonicPrivateData; + + if ( panasonicClipData.mProClipIDBlock.mPresent ) { + const std::string globalClipIDString = BytesToHex ( panasonicClipData.mProClipIDBlock.mGlobalClipID, 32 ); + + this->xmpObj.SetProperty ( kXMP_NS_DC, "identifier", globalClipIDString.c_str(), kXMP_DeleteExisting ); + } + + const AVCHD_blkPanasonicPrivateData& panasonicPlaylistData = + avchdLegacyData.mPlaylistExtensionData.mMakersPrivateData.mPanasonicPrivateData; + + if ( panasonicPlaylistData.mProPlaylistInfoBlock.mPlayListMark.mPresent ) { + const AVCCAM_blkProPlayListMark& playlistMark = panasonicPlaylistData.mProPlaylistInfoBlock.mPlayListMark; + + if ( playlistMark.mShotMark.mPresent ) { + // Unlike P2, where "shotmark" is a boolean, Panasonic treats their AVCCAM shotmark as a bit field with + // 8 user-definable bits. For now we're going to treat any bit being set as xmpDM::good == true, and all + // bits being clear as xmpDM::good == false. + const bool isGood = ( playlistMark.mShotMark.mShotMark != 0 ); + + xmpObj.SetProperty_Bool ( kXMP_NS_DM, "good", isGood, kXMP_DeleteExisting ); + } + + if ( playlistMark.mAccess.mPresent && ( playlistMark.mAccess.mCreatorLength > 0 ) ) { + const std::string creatorString = AVCHD_StringFieldToXMP ( + playlistMark.mAccess.mCreatorLength, playlistMark.mAccess.mCreatorCharacterSet, playlistMark.mAccess.mCreator, 32 ) ; + + if ( ! creatorString.empty() ) { + xmpObj.DeleteProperty ( kXMP_NS_DC, "creator" ); + xmpObj.AppendArrayItem ( kXMP_NS_DC, "creator", kXMP_PropArrayIsOrdered, creatorString.c_str() ); + } + } + + if ( playlistMark.mDevice.mPresent && ( playlistMark.mDevice.mSerialNoLength > 0 ) ) { + const std::string serialNoString = AVCHD_StringFieldToXMP ( + playlistMark.mDevice.mSerialNoLength, playlistMark.mDevice.mSerialNoCharacterCode, playlistMark.mDevice.mSerialNo, 24 ) ; + + if ( ! serialNoString.empty() ) xmpObj.SetProperty ( kXMP_NS_EXIF_Aux, "SerialNumber", serialNoString.c_str(), kXMP_DeleteExisting ); + } + + if ( playlistMark.mLocation.mPresent && ( playlistMark.mLocation.mPlaceNameLength > 0 ) ) { + const std::string placeNameString = AVCHD_StringFieldToXMP ( + playlistMark.mLocation.mPlaceNameLength, playlistMark.mLocation.mPlaceNameCharacterSet, playlistMark.mLocation.mPlaceName, 64 ) ; + + if ( ! placeNameString.empty() ) xmpObj.SetProperty ( kXMP_NS_DM, "shotLocation", placeNameString.c_str(), kXMP_DeleteExisting ); + } + } + } + +} // AVCHD_MetaHandler::ProcessXMP + +// ================================================================================================= +// AVCHD_MetaHandler::UpdateFile +// ============================= +// +// Note that UpdateFile is only called from XMPFiles::CloseFile, so it is OK to close the file here. + +void AVCHD_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + if ( ! this->needsUpdate ) return; + this->needsUpdate = false; // Make sure only called once. + + XMP_Assert ( this->parent->UsesLocalIO() ); + + std::string newDigest; + this->MakeLegacyDigest ( &newDigest ); + this->xmpObj.SetStructField ( kXMP_NS_XMP, "NativeDigests", kXMP_NS_XMP, "AVCHD", newDigest.c_str(), kXMP_DeleteExisting ); + + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, this->GetSerializeOptions() ); + + std::string xmpPath; + this->MakeClipStreamPath ( &xmpPath, ".xmp" ); + + bool haveXMP = Host_IO::Exists ( xmpPath.c_str() ); + if ( ! haveXMP ) { + XMP_Assert ( this->parent->ioRef == 0 ); + Host_IO::Create ( xmpPath.c_str() ); + this->parent->ioRef = XMPFiles_IO::New_XMPFiles_IO ( xmpPath.c_str(), Host_IO::openReadWrite ); + if ( this->parent->ioRef == 0 ) XMP_Throw ( "Failure opening AVCHD XMP file", kXMPErr_ExternalFailure ); + } + + XMP_IO* xmpFile = this->parent->ioRef; + XMP_Assert ( xmpFile != 0 ); + XIO::ReplaceTextFile ( xmpFile, this->xmpPacket, (haveXMP & doSafeUpdate) ); + +} // AVCHD_MetaHandler::UpdateFile + +// ================================================================================================= +// AVCHD_MetaHandler::WriteTempFile +// ================================ + +void AVCHD_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + + // ! WriteTempFile is not supposed to be called for handlers that own the file. + XMP_Throw ( "AVCHD_MetaHandler::WriteTempFile should not be called", kXMPErr_InternalFailure ); + +} // AVCHD_MetaHandler::WriteTempFile + +// ================================================================================================= diff --git a/XMPFiles/source/FileHandlers/AVCHD_Handler.hpp b/XMPFiles/source/FileHandlers/AVCHD_Handler.hpp new file mode 100644 index 0000000..46c417e --- /dev/null +++ b/XMPFiles/source/FileHandlers/AVCHD_Handler.hpp @@ -0,0 +1,81 @@ +#ifndef __AVCHD_Handler_hpp__ +#define __AVCHD_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2008 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. + +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +#include "source/ExpatAdapter.hpp" + +// ================================================================================================= +/// \file AVCHD_Handler.hpp +/// \brief Folder format handler for AVCHD. +/// +/// This header ... +/// +// ================================================================================================= + +extern XMPFileHandler * AVCHD_MetaHandlerCTor ( XMPFiles * parent ); + +extern bool AVCHD_CheckFormat ( XMP_FileFormat format, + const std::string & rootPath, + const std::string & gpName, + const std::string & parentName, + const std::string & leafName, + XMPFiles * parent ); + +static const XMP_OptionBits kAVCHD_HandlerFlags = (kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_CanRewrite | + kXMPFiles_PrefersInPlace | + kXMPFiles_CanReconcile | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_HandlerOwnsFile | + kXMPFiles_AllowsSafeUpdate | + kXMPFiles_FolderBasedFormat); + +class AVCHD_MetaHandler : public XMPFileHandler +{ +public: + + bool GetFileModDate ( XMP_DateTime * modDate ); + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + XMP_OptionBits GetSerializeOptions() // *** These should be standard for standalone XMP files. + { return (kXMP_UseCompactFormat | kXMP_OmitPacketWrapper); }; + + AVCHD_MetaHandler ( XMPFiles * _parent ); + virtual ~AVCHD_MetaHandler(); + +private: + + AVCHD_MetaHandler() {}; // Hidden on purpose. + + bool MakeClipInfoPath ( std::string * path, XMP_StringPtr suffix, bool checkFile = false ) const; + bool MakeClipStreamPath ( std::string * path, XMP_StringPtr suffix, bool checkFile = false ) const; + bool MakePlaylistPath ( std::string * path, XMP_StringPtr suffix, bool checkFile = false ) const; + + void MakeLegacyDigest ( std::string * digestStr ); + + std::string rootPath, clipName; + +}; // AVCHD_MetaHandler + +// ================================================================================================= + +#endif /* __AVCHD_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/Basic_Handler.cpp b/XMPFiles/source/FileHandlers/Basic_Handler.cpp new file mode 100644 index 0000000..795dbd7 --- /dev/null +++ b/XMPFiles/source/FileHandlers/Basic_Handler.cpp @@ -0,0 +1,243 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/Basic_Handler.hpp" + +using namespace std; + +// ================================================================================================= +/// \file Basic_Handler.cpp +/// \brief Base class for basic handlers that only process in-place XMP. +/// +/// This header ... +/// +// ================================================================================================= + +// ================================================================================================= +// Basic_MetaHandler::~Basic_MetaHandler +// ===================================== + +Basic_MetaHandler::~Basic_MetaHandler() +{ + // ! Inherit the base cleanup. + +} // Basic_MetaHandler::~Basic_MetaHandler + +// ================================================================================================= +// Basic_MetaHandler::UpdateFile +// ============================= + +// ! This must be called from the destructor for all derived classes. It can't be called from the +// ! Basic_MetaHandler destructor, by then calls to the virtual functions would not go to the +// ! actual implementations for the derived class. + +void Basic_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + IgnoreParam ( doSafeUpdate ); + XMP_Assert ( ! doSafeUpdate ); // Not supported at this level. + if ( ! this->needsUpdate ) return; + + XMP_IO* fileRef = this->parent->ioRef; + XMP_PacketInfo & packetInfo = this->packetInfo; + std::string & xmpPacket = this->xmpPacket; + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + this->CaptureFileEnding ( fileRef ); // ! Do this first, before any location info changes. + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "Basic_MetaHandler::UpdateFile - User abort", kXMPErr_UserAbort ); + } + + this->NoteXMPRemoval ( fileRef ); + this->ShuffleTrailingContent ( fileRef ); + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "Basic_MetaHandler::UpdateFile - User abort", kXMPErr_UserAbort ); + } + + XMP_Int64 tempLength = this->xmpFileOffset - this->xmpPrefixSize + this->trailingContentSize; + fileRef->Truncate ( tempLength ); + + packetInfo.offset = tempLength + this->xmpPrefixSize; + this->NoteXMPInsertion ( fileRef ); + + fileRef->ToEOF(); + this->WriteXMPPrefix ( fileRef ); + fileRef->Write ( xmpPacket.c_str(), (XMP_StringLen)xmpPacket.size() ); + this->WriteXMPSuffix ( fileRef ); + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "Basic_MetaHandler::UpdateFile - User abort", kXMPErr_UserAbort ); + } + + this->RestoreFileEnding ( fileRef ); + + this->xmpFileOffset = packetInfo.offset; + this->xmpFileSize = packetInfo.length; + this->needsUpdate = false; + +} // Basic_MetaHandler::UpdateFile + +// ================================================================================================= +// Basic_MetaHandler::WriteTempFile +// ================================ + +// *** What about computing the new file length and pre-allocating the file? + +void Basic_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + XMP_IO* originalRef = this->parent->ioRef; + + // Capture the "back" of the original file. + + this->CaptureFileEnding ( originalRef ); + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "Basic_MetaHandler::UpdateFile - User abort", kXMPErr_UserAbort ); + } + + // Seek to the beginning of the original and temp files, truncate the temp. + + originalRef->Rewind(); + tempRef->Rewind(); + tempRef->Truncate ( 0 ); + + // Copy the front of the original file to the temp file. Note the XMP (pseudo) removal and + // insertion. This mainly updates info about the new XMP length. + + XMP_Int64 xmpSectionOffset = this->xmpFileOffset - this->xmpPrefixSize; + XMP_Int32 oldSectionLength = this->xmpPrefixSize + this->xmpFileSize + this->xmpSuffixSize; + + XIO::Copy ( originalRef, tempRef, xmpSectionOffset, abortProc, abortArg ); + this->NoteXMPRemoval ( originalRef ); + packetInfo.offset = this->xmpFileOffset; // ! The packet offset does not change. + this->NoteXMPInsertion ( tempRef ); + tempRef->ToEOF(); + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "Basic_MetaHandler::WriteFile - User abort", kXMPErr_UserAbort ); + } + + // Write the new XMP section to the temp file. + + this->WriteXMPPrefix ( tempRef ); + tempRef->Write ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + this->WriteXMPSuffix ( tempRef ); + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "Basic_MetaHandler::WriteFile - User abort", kXMPErr_UserAbort ); + } + + // Copy the trailing file content from the original and write the "back" of the file. + + XMP_Int64 remainderOffset = xmpSectionOffset + oldSectionLength; + + originalRef->Seek ( remainderOffset, kXMP_SeekFromStart ); + XIO::Copy ( originalRef, tempRef, this->trailingContentSize, abortProc, abortArg ); + this->RestoreFileEnding ( tempRef ); + + // Done. + + this->xmpFileOffset = packetInfo.offset; + this->xmpFileSize = packetInfo.length; + this->needsUpdate = false; + +} // Basic_MetaHandler::WriteTempFile + +// ================================================================================================= +// ShuffleTrailingContent +// ====================== +// +// Shuffle the trailing content portion of a file forward. This does not include the final "back" +// portion of the file, just the arbitrary length content between the XMP section and the back. +// Don't use XIO::Copy, that assumes separate files and hence separate I/O positions. + +// ! The XMP packet location and prefix/suffix sizes must still reflect the XMP section that is in +// ! the process of being removed. + +void Basic_MetaHandler::ShuffleTrailingContent ( XMP_IO* fileRef ) +{ + XMP_Int64 readOffset = this->packetInfo.offset + xmpSuffixSize; + XMP_Int64 writeOffset = this->packetInfo.offset - xmpPrefixSize; + + XMP_Int64 remainingLength = this->trailingContentSize; + + enum { kBufferSize = 64*1024 }; + char buffer [kBufferSize]; + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + while ( remainingLength > 0 ) { + + XMP_Int32 ioCount = kBufferSize; + if ( remainingLength < kBufferSize ) ioCount = (XMP_Int32)remainingLength; + + fileRef->Seek ( readOffset, kXMP_SeekFromStart ); + fileRef->ReadAll ( buffer, ioCount ); + fileRef->Seek ( writeOffset, kXMP_SeekFromStart ); + fileRef->Write ( buffer, ioCount ); + + readOffset += ioCount; + writeOffset += ioCount; + remainingLength -= ioCount; + + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "Basic_MetaHandler::ShuffleTrailingContent - User abort", kXMPErr_UserAbort ); + } + + } + +} // ShuffleTrailingContent + +// ================================================================================================= +// Dummies needed for VS.Net +// ========================= + +void Basic_MetaHandler::WriteXMPPrefix ( XMP_IO* fileRef ) +{ + XMP_Throw ( "Basic_MetaHandler::WriteXMPPrefix - Needs specific override", kXMPErr_InternalFailure ); +} + +void Basic_MetaHandler::WriteXMPSuffix ( XMP_IO* fileRef ) +{ + XMP_Throw ( "Basic_MetaHandler::WriteXMPSuffix - Needs specific override", kXMPErr_InternalFailure ); +} + +void Basic_MetaHandler::NoteXMPRemoval ( XMP_IO* fileRef ) +{ + XMP_Throw ( "Basic_MetaHandler::NoteXMPRemoval - Needs specific override", kXMPErr_InternalFailure ); +} + +void Basic_MetaHandler::NoteXMPInsertion ( XMP_IO* fileRef ) +{ + XMP_Throw ( "Basic_MetaHandler::NoteXMPInsertion - Needs specific override", kXMPErr_InternalFailure ); +} + +void Basic_MetaHandler::CaptureFileEnding ( XMP_IO* fileRef ) +{ + XMP_Throw ( "Basic_MetaHandler::CaptureFileEnding - Needs specific override", kXMPErr_InternalFailure ); +} + +void Basic_MetaHandler::RestoreFileEnding ( XMP_IO* fileRef ) +{ + XMP_Throw ( "Basic_MetaHandler::RestoreFileEnding - Needs specific override", kXMPErr_InternalFailure ); +} + +// ================================================================================================= diff --git a/XMPFiles/source/FileHandlers/Basic_Handler.hpp b/XMPFiles/source/FileHandlers/Basic_Handler.hpp new file mode 100644 index 0000000..d5fca0f --- /dev/null +++ b/XMPFiles/source/FileHandlers/Basic_Handler.hpp @@ -0,0 +1,117 @@ +#ifndef __Basic_Handler_hpp__ +#define __Basic_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +// ================================================================================================= +/// \file Basic_Handler.hpp +/// +/// \brief Base class for handlers that support a simple file model allowing insertion and expansion +/// of XMP, but probably not reconciliation with other forms of metadata. Reconciliation would have +/// to be done within the I/O model presented here. +/// +/// \note Any specific derived handler might not be able to do insertion, but all must support +/// expansion. If a handler can't do either it should be derived from Trivial_Handler. Common code +/// must check the actual canInject flag where appropriate. +/// +/// The model for a basic handler divides the file into 6 portions: +/// +/// \li The front of the file. This portion can be arbitrarily large. Files over 4GB are supported. +/// Adding or expanding the XMP must not require expanding this portion of the file. The XMP offset +/// or length might be written into reserved space in this section though. +/// +/// \li A prefix for the XMP section. The prefix and suffix for the XMP "section" are the format +/// specific portions that surround the raw XMP packet. They must be generated on the fly, even when +/// updating existing XMP with or without expansion. Their length must not depend on the XMP packet. +/// +/// \li The XMP packet, as created by SXMPMeta::SerializeToBuffer. The size must be less than 2GB. +/// +/// \li A suffix for the XMP section. +/// +/// \li Trailing file content. This portion can be arbitarily large. It must be possible to remove +/// the XMP, move this portion of the file forward, then reinsert the XMP after this portion. This +/// is actually how the XMP is expanded. There must not be any embedded file offsets in this part, +/// this content must not change if the XMP changes size. +/// +/// \li The back of the file. This portion must have modest size, and/or be generated on the fly. +/// When inserting XMP, part of this may be buffered in RAM (hence the modest size requirement), the +/// XMP section is written, then this portion is rewritten. There must not be any embedded file +/// offsets in this part, this content must not change if the XMP changes size. +/// +/// \note There is no general promise here about crash-safe I/O. An update to an existing file might +/// have invalid partial state, for example while moving the trailing content portion forward if the +/// XMP increases in size or even rewriting existing XMP in-place. Crash-safe updates are managed at +/// a higher level of XMPFiles, using a temporary file and final swap of file content. +/// +// ================================================================================================= + +static const XMP_OptionBits kBasic_HandlerFlags = (kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_CanRewrite | + kXMPFiles_PrefersInPlace | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_AllowsSafeUpdate); + +class Basic_MetaHandler : public XMPFileHandler +{ +public: + + Basic_MetaHandler() : + xmpFileOffset(0), xmpFileSize(0), xmpPrefixSize(0), xmpSuffixSize(0), trailingContentSize(0) {}; + ~Basic_MetaHandler(); + + virtual void CacheFileData() = 0; // Sets offset for insertion if no XMP yet. + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + +protected: + + // Write a cached or fixed prefix or suffix for the XMP. The file is passed because it could be + // either the original file or a safe-update temp file. + virtual void WriteXMPPrefix ( XMP_IO* fileRef ) = 0; // ! Must have override in actual handlers! + virtual void WriteXMPSuffix ( XMP_IO* fileRef ) = 0; // ! Must have override in actual handlers! + + // Note that the XMP is being removed or inserted. The file is passed because it could be either + // the original file or a safe-update temp file. + virtual void NoteXMPRemoval ( XMP_IO* fileRef ) = 0; // ! Must have override in actual handlers! + virtual void NoteXMPInsertion ( XMP_IO* fileRef ) = 0; // ! Must have override in actual handlers! + + // Capture or restore the tail portion of the file. The file is passed because it could be either + // the original file or a safe-update temp file. + virtual void CaptureFileEnding ( XMP_IO* fileRef ) = 0; // ! Must have override in actual handlers! + virtual void RestoreFileEnding ( XMP_IO* fileRef ) = 0; // ! Must have override in actual handlers! + + // Move the trailing content portion forward. Excludes "back" of the file. The file is passed + // because it could be either the original file or a safe-update temp file. + void ShuffleTrailingContent ( XMP_IO* fileRef ); // Has a common implementation. + + XMP_Uns64 xmpFileOffset; // The offset of the XMP in the file. + XMP_Uns32 xmpFileSize; // The size of the XMP in the file. + // ! The packetInfo offset and length are updated by PutXMP, before the file is updated! + + XMP_Uns32 xmpPrefixSize; // The size of the existing header for the XMP section. + XMP_Uns32 xmpSuffixSize; // The size of the existing trailer for the XMP section. + + XMP_Uns64 trailingContentSize; // The size of the existing trailing content. Excludes "back" of the file. + +}; // Basic_MetaHandler + +// ================================================================================================= + +#endif /* __Basic_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/FLV_Handler.cpp b/XMPFiles/source/FileHandlers/FLV_Handler.cpp new file mode 100644 index 0000000..8c1c745 --- /dev/null +++ b/XMPFiles/source/FileHandlers/FLV_Handler.cpp @@ -0,0 +1,735 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2007 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FileHandlers/FLV_Handler.hpp" + +#include "source/XIO.hpp" +#include "third-party/zuid/interfaces/MD5.h" + +using namespace std; + +// ================================================================================================= +/// \file FLV_Handler.cpp +/// \brief File format handler for FLV. +/// +/// FLV is a fairly simple format, with a strong orientation to streaming use. It consists of a +/// small file header then a sequence of tags that can contain audio data, video data, or +/// ActionScript data. All integers in FLV are big endian. +/// +/// For FLV version 1, the file header contains: +/// +/// UI24 signature - the characters "FLV" +/// UI8 version - 1 +/// UI8 flags - 0x01 = has video tags, 0x04 = has audio tags +/// UI32 length in bytes of file header +/// +/// For FLV version 1, each tag begins with an 11 byte header: +/// +/// UI8 tag type - 8 = audio tag, 9 = video tag, 18 = script data tag +/// UI24 content length in bytes +/// UI24 time - low order 3 bytes +/// UI8 time - high order byte +/// UI24 stream ID +/// +/// This is followed by the tag's content, then a UI32 "back pointer" which is the header size plus +/// the content size. A UI32 zero is placed between the file header and the first tag as a +/// terminator for backward scans. The time in a tag header is the start of playback for that tag. +/// The tags must be in ascending time order. For a given time it is preferred that script data tags +/// precede audio and video tags. +/// +/// For metadata purposes only the script data tags are of interest. Script data information becomes +/// accessible to ActionScript at the playback moment of the script data tag through a call to a +/// registered data handler. The content of a script data tag contains a string and an ActionScript +/// data value. The string is the name of the handler to be invoked, the data value is passed as an +/// ActionScript Object parameter to the handler. +/// +/// The XMP is placed in a script data tag with the name "onXMPData". A variety of legacy metadata +/// is contained in a script data tag with the name "onMetaData". This contains only "internal" +/// information (like duration or width/height), nothing that is user or author editiable (like +/// title or description). Some of these legacy items are imported into the XMP, none are updated +/// from the XMP. +/// +/// A script data tag's content is: +/// +/// UI8 0x02 +/// UI16 name length - includes nul terminator if present +/// UI8n object name - UTF-8, possibly with nul terminator +/// ... object value - serialized ActionScript value (SCRIPTDATAVALUE in FLV spec) +/// +/// The onXMPData and onMetaData values are both ECMA arrays. These have more in common with XMP +/// structs than arrays, the items have arbitrary string names. The serialized form is: +/// +/// UI8 0x08 +/// UI32 array length - need not be exact, an optimization hint +/// array items +/// UI16 name length - includes nul terminator if present +/// UI8n item name - UTF-8, possibly with nul terminator +/// ... object value - serialized ActionScript value (SCRIPTDATAVALUE in FLV spec) +/// UI24 0x000009 - array terminator +/// +/// The object names and array item names in sample files do not have a nul terminator. The policy +/// here is to treat them as optional when reading, and to omit them when writing. +/// +/// The onXMPData array typically has one item named "liveXML". The value of this is a short or long +/// string as necessary: +/// +/// UI8 type - 2 for a short string, 12 for a long string +/// UIx value length - UI16 for a short string, UI32 for a long string, includes nul terminator +/// UI8n value - UTF-8 with nul terminator +/// +// ================================================================================================= + +static inline XMP_Uns32 GetUns24BE ( const void * addr ) +{ + return (GetUns32BE(addr) >> 8); +} + +static inline void PutUns24BE ( XMP_Uns32 value, void * addr ) +{ + XMP_Uns8 * bytes = (XMP_Uns8*)addr; + bytes[0] = (XMP_Uns8)(value >> 16); + bytes[1] = (XMP_Uns8)(value >> 8); + bytes[2] = (XMP_Uns8)(value); +} + +// ================================================================================================= +// FLV_CheckFormat +// =============== +// +// Check for "FLV" and 1 in the first 4 bytes, that the header length is at least 9, that the file +// size is at least as big as the header, and that the leading 0 back pointer is present if the file +// is bigger than the header. + +#define kFLV1 0x464C5601UL + +bool FLV_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ) +{ + XMP_Uns8 buffer [9]; + + fileRef->Rewind(); + XMP_Uns32 ioCount = fileRef->Read ( buffer, 9 ); + if ( ioCount != 9 ) return false; + + XMP_Uns32 fileSignature = GetUns32BE ( &buffer[0] ); + if ( fileSignature != kFLV1 ) return false; + + XMP_Uns32 headerSize = GetUns32BE ( &buffer[5] ); + XMP_Uns64 fileSize = fileRef->Length(); + if ( (fileSize < (headerSize + 4)) && (fileSize != headerSize) ) return false; + + if ( fileSize >= (headerSize + 4) ) { + XMP_Uns32 bpZero; + fileRef->Seek ( headerSize, kXMP_SeekFromStart ); + ioCount = fileRef->Read ( &bpZero, 4 ); + if ( (ioCount != 4) || (bpZero != 0) ) return false; + } + + return true; + +} // FLV_CheckFormat + +// ================================================================================================= +// FLV_MetaHandlerCTor +// =================== + +XMPFileHandler * FLV_MetaHandlerCTor ( XMPFiles * parent ) +{ + + return new FLV_MetaHandler ( parent ); + +} // FLV_MetaHandlerCTor + +// ================================================================================================= +// FLV_MetaHandler::FLV_MetaHandler +// ================================ + +FLV_MetaHandler::FLV_MetaHandler ( XMPFiles * _parent ) + : flvHeaderLen(0), longXMP(false), xmpTagPos(0), omdTagPos(0), xmpTagLen(0), omdTagLen(0) +{ + + this->parent = _parent; // Inherited, can't set in the prefix. + this->handlerFlags = kFLV_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + +} // FLV_MetaHandler::FLV_MetaHandler + +// ================================================================================================= +// FLV_MetaHandler::~FLV_MetaHandler +// ================================= + +FLV_MetaHandler::~FLV_MetaHandler() +{ + + // Nothing to do yet. + +} // FLV_MetaHandler::~FLV_MetaHandler + +// ================================================================================================= +// GetTagInfo +// ========== +// +// Seek to the start of a tag and extract the type, data size, and timestamp. Leave the file +// positioned at the first byte of data. + +struct TagInfo { + XMP_Uns8 type; + XMP_Uns32 time; + XMP_Uns32 dataSize; +}; + +static void GetTagInfo ( XMP_IO* fileRef, XMP_Uns64 tagPos, TagInfo * info ) +{ + XMP_Uns8 buffer [11]; + + fileRef->Seek ( tagPos, kXMP_SeekFromStart ); + fileRef->ReadAll ( buffer, 11 ); + + info->type = buffer[0]; + info->time = GetUns24BE ( &buffer[4] ) || (buffer[7] << 24); + info->dataSize = GetUns24BE ( &buffer[1] ); + +} // GetTagInfo + +// ================================================================================================= +// GetASValueLen +// ============= +// +// Return the full length of a serialized ActionScript value, including the type byte, zero if unknown. + +static XMP_Uns32 GetASValueLen ( const XMP_Uns8 * asValue, const XMP_Uns8 * asLimit ) +{ + XMP_Uns32 valueLen = 0; + const XMP_Uns8 * itemPtr; + XMP_Uns32 arrayCount; + + switch ( asValue[0] ) { + + case 0 : // IEEE double + valueLen = 1 + 8; + break; + + case 1 : // UI8 Boolean + valueLen = 1 + 1; + break; + + case 2 : // Short string + valueLen = 1 + 2 + GetUns16BE ( &asValue[1] ); + break; + + case 3 : // ActionScript object, a name and value. + itemPtr = &asValue[1]; + itemPtr += 2 + GetUns16BE ( itemPtr ); // Move past the name portion. + itemPtr += GetASValueLen ( itemPtr, asLimit ); // And past the data portion. + valueLen = (XMP_Uns32) (itemPtr - asValue); + break; + + case 4 : // Short string (movie clip path) + valueLen = 1 + 2 + GetUns16BE ( &asValue[1] ); + break; + + case 5 : // Null + valueLen = 1; + break; + + case 6 : // Undefined + valueLen = 1; + break; + + case 7 : // UI16 reference ID + valueLen = 1 + 2; + break; + + case 8 : // ECMA array, ignore the count, look for the 0x000009 terminator. + itemPtr = &asValue[5]; + while ( itemPtr < asLimit ) { + XMP_Uns16 nameLen = GetUns16BE ( itemPtr ); + itemPtr += 2 + nameLen; // Move past the name portion. + if ( (nameLen == 0) && (*itemPtr == 9) ) { + itemPtr += 1; + break; // Done, found the 0x000009 terminator. + } + itemPtr += GetASValueLen ( itemPtr, asLimit ); // And past the data portion. + } + valueLen = (XMP_Uns32) (itemPtr - asValue); + break; + + case 10 : // Strict array, has an exact count. + arrayCount = GetUns32BE ( &asValue[1] ); + itemPtr = &asValue[5]; + for ( ; (arrayCount > 0) && (itemPtr < asLimit); --arrayCount ) { + itemPtr += 2 + GetUns16BE ( itemPtr ); // Move past the name portion. + itemPtr += GetASValueLen ( itemPtr, asLimit ); // And past the data portion. + } + valueLen = (XMP_Uns32) (itemPtr - asValue); + break; + + case 11 : // Date + valueLen = 1 + 8 + 2; + break; + + case 12: // Long string + valueLen = 1 + 4 + GetUns32BE ( &asValue[1] ); + break; + + } + + return valueLen; + +} // GetASValueLen + +// ================================================================================================= +// CheckName +// ========= +// +// Check for the name portion of a script data tag or array item, with optional nul terminator. The +// wantedLen must not count the terminator. + +static inline bool CheckName ( XMP_StringPtr inputName, XMP_Uns16 inputLen, + XMP_StringPtr wantedName, XMP_Uns16 wantedLen ) +{ + + if ( inputLen == wantedLen+1 ) { + if ( inputName[wantedLen] != 0 ) return false; // Extra byte must be terminating nul. + --inputLen; + } + + if ( (inputLen == wantedLen) && XMP_LitNMatch ( inputName, wantedName, wantedLen ) ) return true; + return false; + +} // CheckName + +// ================================================================================================= +// FLV_MetaHandler::CacheFileData +// ============================== +// +// Look for the onXMPData and onMetaData script data tags at time 0. Cache all of onMetaData, it +// shouldn't be that big and this removes a need to know what is reconciled. It can't be more than +// 16MB anyway, the size field is only 24 bits. + +void FLV_MetaHandler::CacheFileData() +{ + XMP_Assert ( ! this->containsXMP ); + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + XMP_IO* fileRef = this->parent->ioRef; + XMP_Uns64 fileSize = fileRef->Length(); + + XMP_Uns8 buffer [16]; // Enough for 1+2+"onMetaData"+nul. + XMP_Uns32 ioCount; + TagInfo info; + + fileRef->Seek ( 5, kXMP_SeekFromStart ); + fileRef->ReadAll ( buffer, 4 ); + + this->flvHeaderLen = GetUns32BE ( &buffer[0] ); + XMP_Uns32 firstTagPos = this->flvHeaderLen + 4; // Include the initial zero back pointer. + + if ( firstTagPos >= fileSize ) return; // Quit now if the file is just a header. + + for ( XMP_Uns64 tagPos = firstTagPos; tagPos < fileSize; tagPos += (11 + info.dataSize + 4) ) { + + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "FLV_MetaHandler::LookForMetadata - User abort", kXMPErr_UserAbort ); + } + + GetTagInfo ( fileRef, tagPos, &info ); // ! GetTagInfo seeks to the tag offset. + if ( info.time != 0 ) break; + if ( info.type != 18 ) continue; + + XMP_Assert ( sizeof(buffer) >= (1+2+10+1) ); // 02 000B onMetaData 00 + ioCount = fileRef->Read ( buffer, sizeof(buffer) ); + if ( (ioCount < 4) || (buffer[0] != 0x02) ) continue; + + XMP_Uns16 nameLen = GetUns16BE ( &buffer[1] ); + XMP_StringPtr namePtr = (XMP_StringPtr)(&buffer[3]); + + if ( this->onXMP.empty() && CheckName ( namePtr, nameLen, "onXMPData", 9 ) ) { + + // ! Put the raw data in onXMPData, analyze the value in ProcessXMP. + + this->xmpTagPos = tagPos; + this->xmpTagLen = 11 + info.dataSize + 4; // ! Includes the trailing back pointer. + + this->packetInfo.offset = tagPos + 11 + 1+2+nameLen; // ! Not the real offset yet, the offset of the onXMPData value. + + ioCount = info.dataSize - (1+2+nameLen); // Just the onXMPData value portion. + this->onXMP.reserve ( ioCount ); + this->onXMP.assign ( ioCount, ' ' ); + fileRef->Seek ( this->packetInfo.offset, kXMP_SeekFromStart ); + fileRef->ReadAll ( (void*)this->onXMP.data(), ioCount ); + + if ( ! this->onMetaData.empty() ) break; // Done if we've found both. + + } else if ( this->onMetaData.empty() && CheckName ( namePtr, nameLen, "onMetaData", 10 ) ) { + + this->omdTagPos = tagPos; + this->omdTagLen = 11 + info.dataSize + 4; // ! Includes the trailing back pointer. + + ioCount = info.dataSize - (1+2+nameLen); // Just the onMetaData value portion. + this->onMetaData.reserve ( ioCount ); + this->onMetaData.assign ( ioCount, ' ' ); + fileRef->Seek ( (tagPos + 11 + 1+2+nameLen), kXMP_SeekFromStart ); + fileRef->ReadAll ( (void*)this->onMetaData.data(), ioCount ); + + if ( ! this->onXMP.empty() ) break; // Done if we've found both. + + } + + } + +} // FLV_MetaHandler::CacheFileData + +// ================================================================================================= +// FLV_MetaHandler::MakeLegacyDigest +// ================================= + +#define kHexDigits "0123456789ABCDEF" + +void FLV_MetaHandler::MakeLegacyDigest ( std::string * digestStr ) +{ + MD5_CTX context; + unsigned char digestBin [16]; + + MD5Init ( &context ); + MD5Update ( &context, (XMP_Uns8*)this->onMetaData.data(), (unsigned int)this->onMetaData.size() ); + MD5Final ( digestBin, &context ); + + char buffer [40]; + for ( int in = 0, out = 0; in < 16; in += 1, out += 2 ) { + XMP_Uns8 byte = digestBin[in]; + buffer[out] = kHexDigits [ byte >> 4 ]; + buffer[out+1] = kHexDigits [ byte & 0xF ]; + } + buffer[32] = 0; + digestStr->erase(); + digestStr->append ( buffer, 32 ); + +} // FLV_MetaHandler::MakeLegacyDigest + +// ================================================================================================= +// FLV_MetaHandler::ExtractLiveXML +// =============================== +// +// Extract the XMP packet from the cached onXMPData ECMA array's "liveXMP" item. + +void FLV_MetaHandler::ExtractLiveXML() +{ + if ( this->onXMP[0] != 0x08 ) return; // Make sure onXMPData is an ECMA array. + const XMP_Uns8 * ecmaArray = (const XMP_Uns8 *) this->onXMP.c_str(); + const XMP_Uns8 * ecmaLimit = ecmaArray + this->onXMP.size(); + + if ( this->onXMP.size() >= 3 ) { // Omit the 0x000009 terminator, simplifies the loop. + if ( GetUns24BE ( ecmaLimit-3 ) == 9 ) ecmaLimit -= 3; + } + + for ( const XMP_Uns8 * itemPtr = ecmaArray + 5; itemPtr < ecmaLimit; /* internal increment */ ) { + + // Find the "liveXML" array item, make sure it is a short or long string. + + XMP_Uns16 nameLen = GetUns16BE ( itemPtr ); + const XMP_Uns8 * namePtr = itemPtr + 2; + + itemPtr += (2 + nameLen); // Move to the value portion. + XMP_Uns32 valueLen = GetASValueLen ( itemPtr, ecmaLimit ); + if ( valueLen == 0 ) return; // ! Unknown value type, can't look further. + + if ( CheckName ( (char*)namePtr, nameLen, "liveXML", 7 ) ) { + + XMP_Uns32 lenLen = 2; // Assume a short string. + if ( *itemPtr == 12 ) { + lenLen = 4; + this->longXMP = true; + } else if ( *itemPtr != 2 ) { + return; // Not a short or long string. + } + + valueLen -= (1 + lenLen); + itemPtr += (1 + lenLen); + + this->packetInfo.offset += (itemPtr - ecmaArray); + this->packetInfo.length += valueLen; + + this->xmpPacket.reserve ( valueLen ); + this->xmpPacket.assign ( (char*)itemPtr, valueLen ); + + return; + + } + + itemPtr += valueLen; // Move past the value portion. + + } + +} // FLV_MetaHandler::ExtractLiveXML + +// ================================================================================================= +// FLV_MetaHandler::ProcessXMP +// =========================== + +void FLV_MetaHandler::ProcessXMP() +{ + if ( this->processedXMP ) return; + this->processedXMP = true; // Make sure only called once. + + if ( ! this->onXMP.empty() ) { // Look for the XMP packet. + + this->ExtractLiveXML(); + if ( ! this->xmpPacket.empty() ) { + FillPacketInfo ( this->xmpPacket, &this->packetInfo ); + this->xmpObj.ParseFromBuffer ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + this->containsXMP = true; + } + + } + + // Now process the legacy, if necessary. + + if ( this->onMetaData.empty() ) return; // No legacy, we're done. + + std::string oldDigest; + bool oldDigestFound = this->xmpObj.GetStructField ( kXMP_NS_XMP, "NativeDigests", kXMP_NS_XMP, "FLV", &oldDigest, 0 ); + + if ( oldDigestFound ) { + std::string newDigest; + this->MakeLegacyDigest ( &newDigest ); + if ( oldDigest == newDigest ) return; // No legacy changes. + } + + // *** No spec yet for what legacy to reconcile. + +} // FLV_MetaHandler::ProcessXMP + +// ================================================================================================= +// FLV_MetaHandler::UpdateFile +// =========================== + +void FLV_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + if ( ! this->needsUpdate ) return; + XMP_Assert ( ! doSafeUpdate ); // This should only be called for "unsafe" updates. + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + XMP_IO* fileRef = this->parent->ioRef; + XMP_Uns64 fileSize = fileRef->Length(); + + // Make sure the XMP has a legacy digest if appropriate. + + if ( ! this->onMetaData.empty() ) { + + std::string newDigest; + this->MakeLegacyDigest ( &newDigest ); + this->xmpObj.SetStructField ( kXMP_NS_XMP, "NativeDigests", + kXMP_NS_XMP, "FLV", newDigest.c_str(), kXMP_DeleteExisting ); + + try { + XMP_StringLen xmpLen = (XMP_StringLen)this->xmpPacket.size(); + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, (kXMP_UseCompactFormat | kXMP_ExactPacketLength), xmpLen ); + } catch ( ... ) { + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, kXMP_UseCompactFormat ); + } + + } + + // Rewrite the packet in-place if it fits. Otherwise rewrite the whole file. + + if ( this->xmpPacket.size() == (size_t)this->packetInfo.length ) { + + fileRef->Seek ( this->packetInfo.offset, kXMP_SeekFromStart ); + fileRef->Write ( this->xmpPacket.data(), (XMP_Int32)this->xmpPacket.size() ); + + } else { + + XMP_IO* tempRef = fileRef->DeriveTemp(); + if ( tempRef == 0 ) XMP_Throw ( "Failure creating FLV temp file", kXMPErr_InternalFailure ); + + this->WriteTempFile ( tempRef ); + fileRef->AbsorbTemp(); + + } + + this->needsUpdate = false; + +} // FLV_MetaHandler::UpdateFile + +// ================================================================================================= +// WriteOnXMP +// ========== +// +// Write the XMP packet wrapped up in an ECMA array script data tag: +// +// 0 UI8 tag type : 18 +// 1 UI24 content length : 1+2+9+1+4+2+7+1 + <2 or 4> + XMP packet size + 1 + 3 +// 4 UI24 time low : 0 +// 7 UI8 time high : 0 +// 8 UI24 stream ID : 0 +// +// 11 UI8 0x02 +// 12 UI16 name length : 9 +// 14 str9 tag name : "onXMPData", no nul terminator +// 23 UI8 value type : 8 +// 24 UI32 array count : 1 +// 28 UI16 name length : 7 +// 30 str7 item name : "liveXML", no nul terminator +// +// 37 UI8 value type : 2 for a short string, 12 for a long string +// 38 UIn XMP packet size + 1, UI16 or UI32 as needed +// -- str XMP packet, with nul terminator +// +// -- UI24 array terminator : 0x000009 +// -- UI32 back pointer : content length + 11 + +static void WriteOnXMP ( XMP_IO* fileRef, const std::string & xmpPacket ) +{ + char buffer [64]; + bool longXMP = false; + XMP_Uns32 tagLen = 1+2+9+1+4+2+7+1 + 2 + (XMP_Uns32)xmpPacket.size() + 1 + 3; + + if ( xmpPacket.size() > 0xFFFE ) { + longXMP = true; + tagLen += 2; + } + + if ( tagLen > 16*1024*1024 ) XMP_Throw ( "FLV tags can't be larger than 16MB", kXMPErr_TBD ); + + // Fill in the script data tag header. + + buffer[0] = 18; + PutUns24BE ( tagLen, &buffer[1] ); + PutUns24BE ( 0, &buffer[4] ); + buffer[7] = 0; + PutUns24BE ( 0, &buffer[8] ); + + // Fill in the "onXMPData" name, ECMA array start, and "liveXML" name. + + buffer[11] = 2; + PutUns16BE ( 9, &buffer[12] ); + memcpy ( &buffer[14], "onXMPData", 9 ); // AUDIT: Safe, buffer has 64 chars. + buffer[23] = 8; + PutUns32BE ( 1, &buffer[24] ); + PutUns16BE ( 7, &buffer[28] ); + memcpy ( &buffer[30], "liveXML", 7 ); // AUDIT: Safe, buffer has 64 chars. + + // Fill in the XMP packet string type and length, write what we have so far. + + fileRef->ToEOF(); + if ( ! longXMP ) { + buffer[37] = 2; + PutUns16BE ( (XMP_Uns16)xmpPacket.size()+1, &buffer[38] ); + fileRef->Write ( buffer, 40 ); + } else { + buffer[37] = 12; + PutUns32BE ( (XMP_Uns32)xmpPacket.size()+1, &buffer[38] ); + fileRef->Write ( buffer, 42 ); + } + + // Write the XMP packet, nul terminator, array terminator, and back pointer. + + fileRef->Write ( xmpPacket.c_str(), (XMP_Int32)xmpPacket.size()+1 ); + PutUns24BE ( 9, &buffer[0] ); + PutUns32BE ( tagLen+11, &buffer[3] ); + fileRef->Write ( buffer, 7 ); + +} // WriteOnXMP + +// ================================================================================================= +// FLV_MetaHandler::WriteTempFile +// ============================== +// +// Use a source (old) file and the current XMP to build a destination (new) file. All of the source +// file is copied except for previous XMP. The current XMP is inserted after onMetaData, or at least +// before the first time 0 audio or video tag. + +// ! We do not currently update anything in onMetaData. + +void FLV_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + if ( ! this->needsUpdate ) return; + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + XMP_IO* originalRef = this->parent->ioRef; + + XMP_Uns64 sourceLen = originalRef->Length(); + XMP_Uns64 sourcePos = 0; + + originalRef->Rewind(); + tempRef->Rewind(); + tempRef->Truncate ( 0 ); + + // First do whatever is needed to put the new XMP after any existing onMetaData tag, or as the + // first time 0 tag. + + if ( this->omdTagPos == 0 ) { + + // There is no onMetaData tag. Copy the file header, then write the new XMP as the first tag. + // Allow the degenerate case of a file with just a header, no initial back pointer or tags. + + originalRef->Seek ( sourcePos, kXMP_SeekFromStart ); + XIO::Copy ( originalRef, tempRef, this->flvHeaderLen, abortProc, abortArg ); + + XMP_Uns32 zero = 0; // Ensure that the initial back offset really is zero. + tempRef->Write ( &zero, 4 ); + sourcePos = this->flvHeaderLen + 4; + + WriteOnXMP ( tempRef, this->xmpPacket ); + + } else { + + // There is an onMetaData tag. Copy the front of the file through the onMetaData tag, + // skipping any XMP that happens to be in the way. The XMP should not be before onMetaData, + // but let's be robust. Write the new XMP immediately after onMetaData, at the same time. + + XMP_Uns64 omdEnd = this->omdTagPos + this->omdTagLen; + + if ( (this->xmpTagPos != 0) && (this->xmpTagPos < this->omdTagPos) ) { + // The XMP tag was in front of the onMetaData tag. Copy up to it, then skip it. + originalRef->Seek ( sourcePos, kXMP_SeekFromStart ); + XIO::Copy ( originalRef, tempRef, this->xmpTagPos, abortProc, abortArg ); + sourcePos = this->xmpTagPos + this->xmpTagLen; // The tag length includes the trailing size field. + } + + // Copy through the onMetaData tag, then write the XMP. + originalRef->Seek ( sourcePos, kXMP_SeekFromStart ); + XIO::Copy ( originalRef, tempRef, (omdEnd - sourcePos), abortProc, abortArg ); + sourcePos = omdEnd; + + WriteOnXMP ( tempRef, this->xmpPacket ); + + } + + // Copy the rest of the file, skipping any XMP that is in the way. + + if ( (this->xmpTagPos != 0) && (this->xmpTagPos >= sourcePos) ) { + originalRef->Seek ( sourcePos, kXMP_SeekFromStart ); + XIO::Copy ( originalRef, tempRef, (this->xmpTagPos - sourcePos), abortProc, abortArg ); + sourcePos = this->xmpTagPos + this->xmpTagLen; + } + + originalRef->Seek ( sourcePos, kXMP_SeekFromStart ); + XIO::Copy ( originalRef, tempRef, (sourceLen - sourcePos), abortProc, abortArg ); + + this->needsUpdate = false; + +} // FLV_MetaHandler::WriteTempFile + +// ================================================================================================= diff --git a/XMPFiles/source/FileHandlers/FLV_Handler.hpp b/XMPFiles/source/FileHandlers/FLV_Handler.hpp new file mode 100644 index 0000000..7a63b5c --- /dev/null +++ b/XMPFiles/source/FileHandlers/FLV_Handler.hpp @@ -0,0 +1,80 @@ +#ifndef __FLV_Handler_hpp__ +#define __FLV_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2007 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" + +// ================================================================================================ +/// \file FLV_Handler.hpp +/// \brief File format handler for FLV. +/// +/// This header ... +/// +// ================================================================================================ + +extern XMPFileHandler * FLV_MetaHandlerCTor ( XMPFiles * parent ); + +extern bool FLV_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ); + +static const XMP_OptionBits kFLV_HandlerFlags = ( kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_CanRewrite | + kXMPFiles_PrefersInPlace | + kXMPFiles_CanReconcile | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_AllowsSafeUpdate + ); + +class FLV_MetaHandler : public XMPFileHandler +{ +public: + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + + FLV_MetaHandler ( XMPFiles * _parent ); + virtual ~FLV_MetaHandler(); + +private: + + FLV_MetaHandler() : flvHeaderLen(0), longXMP(false), + xmpTagPos(0), omdTagPos(0), xmpTagLen(0), omdTagLen(0) {}; // Hidden on purpose. + + void ExtractLiveXML(); + void MakeLegacyDigest ( std::string * digestStr ); + + XMP_Uns32 flvHeaderLen; + bool longXMP; // True if the stored XMP is a long string (4 byte length). + + XMP_Uns64 xmpTagPos, omdTagPos; // The file offset and length of onXMP and onMetaData tags. + XMP_Uns32 xmpTagLen, omdTagLen; // Zero if the tag is not present. + + std::string onXMP, onMetaData; // ! Actually contains structured binary data. + +}; // FLV_MetaHandler + +// ================================================================================================= + +#endif // __FLV_Handler_hpp__ diff --git a/XMPFiles/source/FileHandlers/InDesign_Handler.cpp b/XMPFiles/source/FileHandlers/InDesign_Handler.cpp new file mode 100644 index 0000000..5031cef --- /dev/null +++ b/XMPFiles/source/FileHandlers/InDesign_Handler.cpp @@ -0,0 +1,419 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FileHandlers/InDesign_Handler.hpp" + +#include "source/XIO.hpp" + +using namespace std; + +// ================================================================================================= +/// \file InDesign_Handler.cpp +/// \brief File format handler for InDesign files. +/// +/// This header ... +/// +/// The layout of an InDesign file in terms of the Basic_MetaHandler model is: +/// +/// \li The front of the file. This is everything up to the XMP contiguous object section. The file +/// starts with a pair of master pages, followed by the data pages, followed by contiguous object +/// sections, finished with padding to a page boundary. +/// +/// \li A prefix for the XMP section. This is the contiguous object header. The offset is +/// (this->packetInfo.offset - this->xmpPrefixSize). +/// +/// \li The XMP packet. The offset is this->packetInfo.offset. +/// +/// \li A suffix for the XMP section. This is the contiguous object header. The offset is +/// (this->packetInfo.offset + this->packetInfo.length). +/// +/// \li Trailing file content. This is the contiguous objects that follow the XMP. The offset is +/// (this->packetInfo.offset + this->packetInfo.length + this->xmpSuffixSize). +/// +/// \li The back of the file. This is the final padding to a page boundary. The offset is +/// (this->packetInfo.offset + this->packetInfo.length + this->xmpSuffixSize + this->trailingContentSize). +/// +// ================================================================================================= + +// *** Add PutXMP overrides that throw if the file does not contain XMP. + +#ifndef TraceInDesignHandler + #define TraceInDesignHandler 0 +#endif + +enum { kInDesignGUIDSize = 16 }; + +struct InDesignMasterPage { + XMP_Uns8 fGUID [kInDesignGUIDSize]; + XMP_Uns8 fMagicBytes [8]; + XMP_Uns8 fObjectStreamEndian; + XMP_Uns8 fIrrelevant1 [239]; + XMP_Uns64 fSequenceNumber; + XMP_Uns8 fIrrelevant2 [8]; + XMP_Uns32 fFilePages; + XMP_Uns8 fIrrelevant3 [3812]; +}; + +enum { + kINDD_PageSize = 4096, + kINDD_PageMask = (kINDD_PageSize - 1), + kINDD_LittleEndian = 1, + kINDD_BigEndian = 2 }; + +struct InDesignContigObjMarker { + XMP_Uns8 fGUID [kInDesignGUIDSize]; + XMP_Uns32 fObjectUID; + XMP_Uns32 fObjectClassID; + XMP_Uns32 fStreamLength; + XMP_Uns32 fChecksum; +}; + +static const XMP_Uns8 * kINDD_MasterPageGUID = + (const XMP_Uns8 *) "\x06\x06\xED\xF5\xD8\x1D\x46\xE5\xBD\x31\xEF\xE7\xFE\x74\xB7\x1D"; + +static const XMP_Uns8 * kINDDContigObjHeaderGUID = + (const XMP_Uns8 *) "\xDE\x39\x39\x79\x51\x88\x4B\x6C\x8E\x63\xEE\xF8\xAE\xE0\xDD\x38"; + +static const XMP_Uns8 * kINDDContigObjTrailerGUID = + (const XMP_Uns8 *) "\xFD\xCE\xDB\x70\xF7\x86\x4B\x4F\xA4\xD3\xC7\x28\xB3\x41\x71\x06"; + +// ================================================================================================= +// InDesign_MetaHandlerCTor +// ======================== + +XMPFileHandler * InDesign_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new InDesign_MetaHandler ( parent ); + +} // InDesign_MetaHandlerCTor + +// ================================================================================================= +// InDesign_CheckFormat +// ==================== +// +// For InDesign we check that the pair of master pages begin with the 16 byte GUID. + +bool InDesign_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ) +{ + IgnoreParam(format); IgnoreParam(filePath); IgnoreParam(parent); + XMP_Assert ( format == kXMP_InDesignFile ); + XMP_Assert ( strlen ( (const char *) kINDD_MasterPageGUID ) == kInDesignGUIDSize ); + + enum { kBufferSize = 2*kINDD_PageSize }; + XMP_Uns8 buffer [kBufferSize]; + + XMP_Int64 filePos = 0; + XMP_Uns8 * bufPtr = buffer; + XMP_Uns8 * bufLimit = bufPtr + kBufferSize; + + fileRef->Rewind(); + size_t bufLen = fileRef->Read ( buffer, kBufferSize ); + if ( bufLen != kBufferSize ) return false; + + if ( ! CheckBytes ( bufPtr, kINDD_MasterPageGUID, kInDesignGUIDSize ) ) return false; + if ( ! CheckBytes ( bufPtr+kINDD_PageSize, kINDD_MasterPageGUID, kInDesignGUIDSize ) ) return false; + + return true; + +} // InDesign_CheckFormat + +// ================================================================================================= +// InDesign_MetaHandler::InDesign_MetaHandler +// ========================================== + +InDesign_MetaHandler::InDesign_MetaHandler ( XMPFiles * _parent ) : streamBigEndian(0), xmpObjID(0), xmpClassID(0) +{ + this->parent = _parent; + this->handlerFlags = kInDesign_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + +} // InDesign_MetaHandler::InDesign_MetaHandler + +// ================================================================================================= +// InDesign_MetaHandler::~InDesign_MetaHandler +// =========================================== + +InDesign_MetaHandler::~InDesign_MetaHandler() +{ + // Nothing to do here. + +} // InDesign_MetaHandler::~InDesign_MetaHandler + +// ================================================================================================= +// InDesign_MetaHandler::CacheFileData +// =================================== +// +// Look for the XMP in an InDesign database file. This is a paged database using 4K byte pages, +// followed by redundant "contiguous object streams". Each contiguous object stream is a copy of a +// database object stored as a contiguous byte stream. The XMP that we want is one of these. +// +// The first 2 pages of the database are alternating master pages. A generation number is used to +// select the active master page. The master page contains an offset to the start of the contiguous +// object streams. Each of the contiguous object streams contains a header and trailer, allowing +// fast motion from one stream to the next. +// +// There is no unique "what am I" tagging to the contiguous object streams, so we simply pick the +// first one that looks right. At present this is a 4 byte little endian packet size followed by the +// packet. + +// ! Note that insertion of XMP is not allowed for InDesign, the XMP must be a contiguous copy of an +// ! internal database object. So we don't set the packet offset to an insertion point if not found. + +void InDesign_MetaHandler::CacheFileData() +{ + XMP_IO* fileRef = this->parent->ioRef; + XMP_PacketInfo & packetInfo = this->packetInfo; + + IOBuffer ioBuf; + size_t dbPages; + XMP_Uns8 cobjEndian; + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + XMP_Assert ( kINDD_PageSize == sizeof(InDesignMasterPage) ); + XMP_Assert ( kIOBufferSize >= (2 * kINDD_PageSize) ); + + this->containsXMP = false; + + // --------------------------------------------------------------------------------- + // Figure out which master page is active and seek to the contiguous object portion. + + { + FillBuffer ( fileRef, 0, &ioBuf ); + if ( ioBuf.len < (2 * kINDD_PageSize) ) XMP_Throw ( "GetMainPacket/ScanInDesignFile: Read failure", kXMPErr_ExternalFailure ); + + InDesignMasterPage * masters = (InDesignMasterPage *) ioBuf.ptr; + XMP_Uns64 seq0 = GetUns64LE ( (XMP_Uns8 *) &masters[0].fSequenceNumber ); + XMP_Uns64 seq1 = GetUns64LE ( (XMP_Uns8 *) &masters[1].fSequenceNumber ); + + dbPages = GetUns32LE ( (XMP_Uns8 *) &masters[0].fFilePages ); + cobjEndian = masters[0].fObjectStreamEndian; + if ( seq1 > seq0 ) { + dbPages = GetUns32LE ( (XMP_Uns8 *) &masters[1].fFilePages ); + cobjEndian = masters[1].fObjectStreamEndian; + } + } + + XMP_Assert ( ! this->streamBigEndian ); + if ( cobjEndian == kINDD_BigEndian ) this->streamBigEndian = true; + + // --------------------------------------------------------------------------------------------- + // Look for the XMP contiguous object stream. Most of the time there will be just one stream and + // it will be the XMP. So we might as well fill the whole buffer and not worry about reading too + // much and seeking back to the start of the following stream. + + XMP_Int64 cobjPos = (XMP_Int64)dbPages * kINDD_PageSize; // ! Use a 64 bit multiply! + cobjPos -= (2 * sizeof(InDesignContigObjMarker)); // ! For the first pass in the loop. + XMP_Uns32 streamLength = 0; // ! For the first pass in the loop. + + while ( true ) { + + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "InDesign_MetaHandler::LocateXMP - User abort", kXMPErr_UserAbort ); + } + + // Fetch the start of the next stream and check the contiguous object header. + // ! The writeable bit of fObjectClassID is ignored, we use the packet trailer flag. + + cobjPos += streamLength + (2 * sizeof(InDesignContigObjMarker)); + FillBuffer ( fileRef, cobjPos, &ioBuf ); // Make sure buffer starts at cobjPos for length check. + if ( ioBuf.len < (2 * sizeof(InDesignContigObjMarker)) ) break; // Too small, must be end of file. + + const InDesignContigObjMarker * cobjHeader = (const InDesignContigObjMarker *) ioBuf.ptr; + if ( ! CheckBytes ( Uns8Ptr(&cobjHeader->fGUID), kINDDContigObjHeaderGUID, kInDesignGUIDSize ) ) break; // Not a contiguous object header. + this->xmpObjID = cobjHeader->fObjectUID; // Save these now while the buffer is good. + this->xmpClassID = cobjHeader->fObjectClassID; + streamLength = GetUns32LE ( (XMP_Uns8 *) &cobjHeader->fStreamLength ); + ioBuf.ptr += sizeof ( InDesignContigObjMarker ); + + // See if this is the XMP stream. Only check for UTF-8, others get caught in fallback scanning. + + if ( ! CheckFileSpace ( fileRef, &ioBuf, 4 ) ) continue; // Too small, can't possibly be XMP. + + XMP_Uns32 innerLength = GetUns32LE ( ioBuf.ptr ); + if ( this->streamBigEndian ) innerLength = GetUns32BE ( ioBuf.ptr ); + if ( innerLength != (streamLength - 4) ) { + // Be tolerant of a mistake with the endian flag. + innerLength = Flip4 ( innerLength ); + if ( innerLength != (streamLength - 4) ) continue; // Not legit XMP. + } + ioBuf.ptr += 4; + + if ( ! CheckFileSpace ( fileRef, &ioBuf, kUTF8_PacketHeaderLen ) ) continue; // Too small, can't possibly be XMP. + + if ( ! CheckBytes ( ioBuf.ptr, kUTF8_PacketStart, strlen((char*)kUTF8_PacketStart) ) ) continue; + ioBuf.ptr += strlen((char*)kUTF8_PacketStart); + + XMP_Uns8 quote = *ioBuf.ptr; + if ( (quote != '\'') && (quote != '"') ) continue; + ioBuf.ptr += 1; + if ( *ioBuf.ptr != quote ) { + if ( ! CheckBytes ( ioBuf.ptr, Uns8Ptr("\xEF\xBB\xBF"), 3 ) ) continue; + ioBuf.ptr += 3; + } + if ( *ioBuf.ptr != quote ) continue; + ioBuf.ptr += 1; + + if ( ! CheckBytes ( ioBuf.ptr, Uns8Ptr(" id="), 4 ) ) continue; + ioBuf.ptr += 4; + quote = *ioBuf.ptr; + if ( (quote != '\'') && (quote != '"') ) continue; + ioBuf.ptr += 1; + if ( ! CheckBytes ( ioBuf.ptr, kUTF8_PacketID, strlen((char*)kUTF8_PacketID) ) ) continue; + ioBuf.ptr += strlen((char*)kUTF8_PacketID); + if ( *ioBuf.ptr != quote ) continue; + ioBuf.ptr += 1; + + // We've seen enough, it is the XMP. To fit the Basic_Handler model we need to compute the + // total size of remaining contiguous objects, the trailingContentSize. + + this->xmpPrefixSize = sizeof(InDesignContigObjMarker) + 4; + this->xmpSuffixSize = sizeof(InDesignContigObjMarker); + packetInfo.offset = cobjPos + this->xmpPrefixSize; + packetInfo.length = innerLength; + + + XMP_Int64 tcStart = cobjPos + streamLength + (2 * sizeof(InDesignContigObjMarker)); + while ( true ) { + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "InDesign_MetaHandler::LocateXMP - User abort", kXMPErr_UserAbort ); + } + cobjPos += streamLength + (2 * sizeof(InDesignContigObjMarker)); + FillBuffer ( fileRef, cobjPos, &ioBuf ); // Make sure buffer starts at cobjPos for length check. + if ( ioBuf.len < sizeof(InDesignContigObjMarker) ) break; // Too small, must be end of file. + cobjHeader = (const InDesignContigObjMarker *) ioBuf.ptr; + if ( ! CheckBytes ( Uns8Ptr(&cobjHeader->fGUID), kINDDContigObjHeaderGUID, kInDesignGUIDSize ) ) break; // Not a contiguous object header. + streamLength = GetUns32LE ( (XMP_Uns8 *) &cobjHeader->fStreamLength ); + } + this->trailingContentSize = cobjPos - tcStart; + + #if TraceInDesignHandler + XMP_Uns32 pktOffset = (XMP_Uns32)this->packetInfo.offset; + printf ( "Found XMP in InDesign file, offsets:\n" ); + printf ( " CObj head %X, XMP %X, CObj tail %X, file tail %X, padding %X\n", + (pktOffset - this->xmpPrefixSize), pktOffset, (pktOffset + this->packetInfo.length), + (pktOffset + this->packetInfo.length + this->xmpSuffixSize), + (pktOffset + this->packetInfo.length + this->xmpSuffixSize + (XMP_Uns32)this->trailingContentSize) ); + #endif + + this->containsXMP = true; + break; + + } + + if ( this->containsXMP ) { + this->xmpFileOffset = packetInfo.offset; + this->xmpFileSize = packetInfo.length; + ReadXMPPacket ( this ); + } + +} // InDesign_MetaHandler::CacheFileData + +// ================================================================================================= +// InDesign_MetaHandler::WriteXMPPrefix +// ==================================== + +void InDesign_MetaHandler::WriteXMPPrefix ( XMP_IO* fileRef ) +{ + // Write the contiguous object header and the 4 byte length of the XMP packet. + + XMP_Uns32 packetSize = (XMP_Uns32)this->xmpPacket.size(); + + InDesignContigObjMarker header; + memcpy ( header.fGUID, kINDDContigObjHeaderGUID, sizeof(header.fGUID) ); // AUDIT: Use of dest sizeof for length is safe. + header.fObjectUID = this->xmpObjID; + header.fObjectClassID = this->xmpClassID; + header.fStreamLength = MakeUns32LE ( 4 + packetSize ); + header.fChecksum = (XMP_Uns32)(-1); + fileRef->Write ( &header, sizeof(header) ); + + XMP_Uns32 pktLength = MakeUns32LE ( packetSize ); + if ( this->streamBigEndian ) pktLength = MakeUns32BE ( packetSize ); + fileRef->Write ( &pktLength, sizeof(pktLength) ); + +} // InDesign_MetaHandler::WriteXMPPrefix + +// ================================================================================================= +// InDesign_MetaHandler::WriteXMPSuffix +// ==================================== + +void InDesign_MetaHandler::WriteXMPSuffix ( XMP_IO* fileRef ) +{ + // Write the contiguous object trailer. + + XMP_Uns32 packetSize = (XMP_Uns32)this->xmpPacket.size(); + + InDesignContigObjMarker trailer; + + memcpy ( trailer.fGUID, kINDDContigObjTrailerGUID, sizeof(trailer.fGUID) ); // AUDIT: Use of dest sizeof for length is safe. + trailer.fObjectUID = this->xmpObjID; + trailer.fObjectClassID = this->xmpClassID; + trailer.fStreamLength = MakeUns32LE ( 4 + packetSize ); + trailer.fChecksum = (XMP_Uns32)(-1); + + fileRef->Write ( &trailer, sizeof(trailer) ); + +} // InDesign_MetaHandler::WriteXMPSuffix + +// ================================================================================================= +// InDesign_MetaHandler::NoteXMPRemoval +// ==================================== + +void InDesign_MetaHandler::NoteXMPRemoval ( XMP_IO* fileRef ) +{ + // Nothing to do. + +} // InDesign_MetaHandler::NoteXMPRemoval + +// ================================================================================================= +// InDesign_MetaHandler::NoteXMPInsertion +// ====================================== + +void InDesign_MetaHandler::NoteXMPInsertion ( XMP_IO* fileRef ) +{ + // Nothing to do. + +} // InDesign_MetaHandler::NoteXMPInsertion + +// ================================================================================================= +// InDesign_MetaHandler::CaptureFileEnding +// ======================================= + +void InDesign_MetaHandler::CaptureFileEnding ( XMP_IO* fileRef ) +{ + // Nothing to do. The back of an InDesign file is the final zero padding. + +} // InDesign_MetaHandler::CaptureFileEnding + +// ================================================================================================= +// InDesign_MetaHandler::RestoreFileEnding +// ======================================= + +void InDesign_MetaHandler::RestoreFileEnding ( XMP_IO* fileRef ) +{ + // Pad the file with zeros to a page boundary. + + XMP_Int64 dataLength = fileRef->Length(); + XMP_Int32 padLength = (kINDD_PageSize - ((XMP_Int32)dataLength & kINDD_PageMask)) & kINDD_PageMask; + + XMP_Uns8 buffer [kINDD_PageSize]; + memset ( buffer, 0, kINDD_PageSize ); + fileRef->Write ( buffer, padLength ); + +} // InDesign_MetaHandler::RestoreFileEnding + +// ================================================================================================= diff --git a/XMPFiles/source/FileHandlers/InDesign_Handler.hpp b/XMPFiles/source/FileHandlers/InDesign_Handler.hpp new file mode 100644 index 0000000..66d9f79 --- /dev/null +++ b/XMPFiles/source/FileHandlers/InDesign_Handler.hpp @@ -0,0 +1,68 @@ +#ifndef __InDesign_Handler_hpp__ +#define __InDesign_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" + +#include "XMPFiles/source/FileHandlers/Basic_Handler.hpp" + +// ================================================================================================= +/// \file InDesign_Handler.hpp +/// \brief File format handler for InDesign files. +/// +/// This header ... +/// +// ================================================================================================= + +extern XMPFileHandler * InDesign_MetaHandlerCTor ( XMPFiles * parent ); + +extern bool InDesign_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ); + +static const XMP_OptionBits kInDesign_HandlerFlags = kBasic_HandlerFlags & (~kXMPFiles_CanInjectXMP); // ! InDesign can't inject. + +class InDesign_MetaHandler : public Basic_MetaHandler +{ +public: + + InDesign_MetaHandler ( XMPFiles * parent ); + ~InDesign_MetaHandler(); + + void CacheFileData(); + +protected: + + void WriteXMPPrefix ( XMP_IO* fileRef ); + void WriteXMPSuffix ( XMP_IO* fileRef ); + + void NoteXMPRemoval ( XMP_IO* fileRef ); + void NoteXMPInsertion ( XMP_IO* fileRef ); + + void CaptureFileEnding ( XMP_IO* fileRef ); + void RestoreFileEnding ( XMP_IO* fileRef ); + + bool streamBigEndian; // Set from master page's fObjectStreamEndian. + XMP_Uns32 xmpObjID; // Set from contiguous object's fObjectID, still as little endian. + XMP_Uns32 xmpClassID; // Set from contiguous object's fObjectClassID, still as little endian. + +}; // InDesign_MetaHandler + +// ================================================================================================= + +#endif /* __InDesign_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/JPEG_Handler.cpp b/XMPFiles/source/FileHandlers/JPEG_Handler.cpp new file mode 100644 index 0000000..2cc4c31 --- /dev/null +++ b/XMPFiles/source/FileHandlers/JPEG_Handler.cpp @@ -0,0 +1,1037 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/JPEG_Handler.hpp" + +#include "XMPFiles/source/FormatSupport/TIFF_Support.hpp" +#include "XMPFiles/source/FormatSupport/PSIR_Support.hpp" +#include "XMPFiles/source/FormatSupport/IPTC_Support.hpp" +#include "XMPFiles/source/FormatSupport/ReconcileLegacy.hpp" +#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp" + +#include "third-party/zuid/interfaces/MD5.h" + +using namespace std; + +// ================================================================================================= +/// \file JPEG_Handler.cpp +/// \brief File format handler for JPEG. +/// +/// This handler ... +/// +// ================================================================================================= + +static const char * kExifSignatureString = "Exif\0\x00"; +static const char * kExifSignatureAltStr = "Exif\0\xFF"; +static const size_t kExifSignatureLength = 6; +static const size_t kExifMaxDataLength = 0xFFFF - 2 - kExifSignatureLength; + +static const char * kPSIRSignatureString = "Photoshop 3.0\0"; +static const size_t kPSIRSignatureLength = 14; +static const size_t kPSIRMaxDataLength = 0xFFFF - 2 - kPSIRSignatureLength; + +static const char * kMainXMPSignatureString = "http://ns.adobe.com/xap/1.0/\0"; +static const size_t kMainXMPSignatureLength = 29; + +static const char * kExtXMPSignatureString = "http://ns.adobe.com/xmp/extension/\0"; +static const size_t kExtXMPSignatureLength = 35; +static const size_t kExtXMPPrefixLength = kExtXMPSignatureLength + 32 + 4 + 4; + +typedef std::map < XMP_Uns32 /* offset */, std::string /* portion */ > ExtXMPPortions; + +struct ExtXMPContent { + XMP_Uns32 length; + ExtXMPPortions portions; + ExtXMPContent() : length(0) {}; + ExtXMPContent ( XMP_Uns32 _length ) : length(_length) {}; +}; + +typedef std::map < JPEG_MetaHandler::GUID_32 /* guid */, ExtXMPContent /* content */ > ExtendedXMPInfo; + +#ifndef Trace_UnlimitedJPEG + #define Trace_UnlimitedJPEG 0 +#endif + +// ================================================================================================= +// JPEG_MetaHandlerCTor +// ==================== + +XMPFileHandler * JPEG_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new JPEG_MetaHandler ( parent ); + +} // JPEG_MetaHandlerCTor + +// ================================================================================================= +// JPEG_CheckFormat +// ================ + +// For JPEG we just check for the initial SOI standalone marker followed by any of the other markers +// that might, well, follow it. A more aggressive check might be to read 4KB then check for legit +// marker segments within that portion. Probably won't buy much, and thrashes the dCache more. We +// tolerate only a small amount of 0xFF padding between the SOI and following marker. This formally +// violates the rules of JPEG, but in practice there won't be any padding anyway. +// +// ! The CheckXyzFormat routines don't track the filePos, that is left to ScanXyzFile. + +bool JPEG_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ) +{ + IgnoreParam(format); IgnoreParam(filePath); IgnoreParam(parent); + XMP_Assert ( format == kXMP_JPEGFile ); + + IOBuffer ioBuf; + + fileRef->Rewind ( ); + if ( ! CheckFileSpace ( fileRef, &ioBuf, 4 ) ) return false; // We need at least 4, the buffer is filled anyway. + + // First look for the SOI standalone marker. Then skip all 0xFF bytes, padding plus the high + // order byte of the next marker. Finally see if the next marker is legit. + + if ( ! CheckBytes ( ioBuf.ptr, "\xFF\xD8", 2 ) ) return false; + ioBuf.ptr += 2; // Move past the SOI. + while ( (ioBuf.ptr < ioBuf.limit) && (*ioBuf.ptr == 0xFF) ) ++ioBuf.ptr; + if ( ioBuf.ptr == ioBuf.limit ) return false; + + XMP_Uns8 id = *ioBuf.ptr; + if ( id >= 0xDD ) return true; // The most probable cases. + if ( (id < 0xC0) || ((id & 0xF8) == 0xD0) || (id == 0xD8) || (id == 0xDA) || (id == 0xDC) ) return false; + return true; + +} // JPEG_CheckFormat + +// ================================================================================================= +// JPEG_MetaHandler::JPEG_MetaHandler +// ================================== + +JPEG_MetaHandler::JPEG_MetaHandler ( XMPFiles * _parent ) + : exifMgr(0), psirMgr(0), iptcMgr(0), skipReconcile(false) +{ + this->parent = _parent; + this->handlerFlags = kJPEG_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + +} // JPEG_MetaHandler::JPEG_MetaHandler + +// ================================================================================================= +// JPEG_MetaHandler::~JPEG_MetaHandler +// =================================== + +JPEG_MetaHandler::~JPEG_MetaHandler() +{ + + if ( exifMgr != 0 ) delete ( exifMgr ); + if ( psirMgr != 0 ) delete ( psirMgr ); + if ( iptcMgr != 0 ) delete ( iptcMgr ); + +} // JPEG_MetaHandler::~JPEG_MetaHandler + +// ================================================================================================= +// JPEG_MetaHandler::CacheFileData +// =============================== +// +// Look for the Exif metadata, Photoshop image resources, and XMP in a JPEG (JFIF) file. The native +// thumbnail is inside the Exif. The general layout of a JPEG file is: +// SOI marker, 2 bytes, 0xFFD8 +// Marker segments for tables and metadata +// SOFn marker segment +// Image data +// EOI marker, 2 bytes, 0xFFD9 +// +// Each marker segment begins with a 2 byte big endian marker and a 2 byte big endian length. The +// length includes the 2 bytes of the length field but not the marker. The high order byte of a +// marker is 0xFF, the low order byte tells what kind of marker. A marker can be preceeded by any +// number of 0xFF fill bytes, however there are no alignment constraints. +// +// There are virtually no constraints on the order of the marker segments before the SOFn. A reader +// must be prepared to handle any order. +// +// The Exif metadata is in an APP1 marker segment with a 6 byte signature string of "Exif\0\0" at +// the start of the data. The rest of the data is a TIFF stream. +// +// The Photoshop image resources are in an APP13 marker segment with a 14 byte signature string of +// "Photoshop 3.0\0". The rest of the data is a sequence of image resources. +// +// The main XMP is in an APP1 marker segment with a 29 byte signature string of +// "http://ns.adobe.com/xap/1.0/\0". The rest of the data is the serialized XMP packet. This is the +// only XMP if everything fits within the 64KB limit for marker segment data. If not, there will be +// a series of XMP extension segments. +// +// Each XMP extension segment is an APP1 marker segment whose data contains: +// - A 35 byte signature string of "http://ns.adobe.com/xmp/extension/\0". +// - A 128 bit GUID stored as 32 ASCII hex digits, capital A-F, no nul termination. +// - A 32 bit unsigned integer length for the full extended XMP serialization. +// - A 32 bit unsigned integer offset for this portion of the extended XMP serialization. +// - A portion of the extended XMP serialization, up to about 65400 bytes (at most 65458). +// +// A reader must be prepared to encounter the extended XMP portions out of order. Also to encounter +// defective files that have differing extended XMP according to the GUID. The main XMP contains the +// GUID for the associated extended XMP. + +// *** This implementation simply returns when invalid JPEG is encountered. Should we throw instead? + +void JPEG_MetaHandler::CacheFileData() +{ + XMP_IO* fileRef = this->parent->ioRef; + XMP_PacketInfo & packetInfo = this->packetInfo; + + size_t segLen; + bool ok; + IOBuffer ioBuf; + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + ExtendedXMPInfo extXMP; + + XMP_Assert ( ! this->containsXMP ); + // Set containsXMP to true here only if the standard XMP packet is found. + + XMP_Assert ( kPSIRSignatureLength == (strlen(kPSIRSignatureString) + 1) ); + XMP_Assert ( kMainXMPSignatureLength == (strlen(kMainXMPSignatureString) + 1) ); + XMP_Assert ( kExtXMPSignatureLength == (strlen(kExtXMPSignatureString) + 1) ); + + // ------------------------------------------------------------------------------------------- + // Look for any of the Exif, PSIR, main XMP, or extended XMP marker segments. Quit when we hit + // an SOFn, EOI, or invalid/unexpected marker. + + fileRef->Seek ( 2, kXMP_SeekFromStart ); // Skip the SOI. The JPEG header has already been verified. + ioBuf.filePos = 2; + RefillBuffer ( fileRef, &ioBuf ); + + while ( true ) { + + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "JPEG_MetaHandler::CacheFileData - User abort", kXMPErr_UserAbort ); + } + + if ( ! CheckFileSpace ( fileRef, &ioBuf, 2 ) ) return; + + if ( *ioBuf.ptr != 0xFF ) return; // All valid markers have a high byte of 0xFF. + while ( *ioBuf.ptr == 0xFF ) { // Skip padding 0xFF bytes and the marker's high byte. + ++ioBuf.ptr; + if ( ! CheckFileSpace ( fileRef, &ioBuf, 1 ) ) return; + } + + XMP_Uns16 marker = 0xFF00 + *ioBuf.ptr; + + if ( (marker == 0xFFDA) || (marker == 0xFFD9) ) break; // Quit reading at the first SOS marker or at EOI. + + if ( (marker == 0xFF01) || // Ill-formed file if we encounter a TEM or RSTn marker. + ((0xFFD0 <= marker) && (marker <= 0xFFD7)) ) return; + + if ( marker == 0xFFED ) { + + // This is an APP13 marker, is it the Photoshop image resources? + + ++ioBuf.ptr; // Move ioBuf.ptr to the marker segment length field. + if ( ! CheckFileSpace ( fileRef, &ioBuf, 2 ) ) return; + + segLen = GetUns16BE ( ioBuf.ptr ); + if ( segLen < 2 ) return; // Invalid JPEG. + + ioBuf.ptr += 2; // Move ioBuf.ptr to the marker segment content. + segLen -= 2; // Adjust segLen to count just the content portion. + + ok = CheckFileSpace ( fileRef, &ioBuf, kPSIRSignatureLength ); + if ( ok && (segLen >= kPSIRSignatureLength) && + CheckBytes ( ioBuf.ptr, kPSIRSignatureString, kPSIRSignatureLength ) ) { + + // This is the Photoshop image resources, cache the contents. + + ioBuf.ptr += kPSIRSignatureLength; // Move ioBuf.ptr to the image resources. + segLen -= kPSIRSignatureLength; // Adjust segLen to count just the image resources. + ok = CheckFileSpace ( fileRef, &ioBuf, segLen ); // Buffer the full content portion. + if ( ! ok ) return; // Must be a truncated file. + + this->psirContents.assign ( (XMP_StringPtr)ioBuf.ptr, segLen ); + ioBuf.ptr += segLen; + + } else { + + // This is the not Photoshop image resources, skip the marker segment's content. + + if ( segLen <= size_t(ioBuf.limit - ioBuf.ptr) ) { + ioBuf.ptr += segLen; // The next marker is in this buffer. + } else { + // The next marker is beyond this buffer, move to the start of it and fill the buffer. + size_t skipCount = segLen - (ioBuf.limit - ioBuf.ptr); // The amount to move beyond this buffer. + XMP_Int64 bufferEnd = ioBuf.filePos + (XMP_Int64)ioBuf.len; // File offset at the end of this buffer. + XMP_Int64 nextPos = bufferEnd + (XMP_Int64)skipCount; + MoveToOffset ( fileRef, nextPos, &ioBuf ); + } + + } + + continue; // Move on to the next marker. + + } else if ( marker == 0xFFE1 ) { + + // This is an APP1 marker, is it the Exif, main XMP, or extended XMP? + // ! Check in that order, which happens to be increasing signature string length. + + ++ioBuf.ptr; // Move ioBuf.ptr to the marker segment length field. + if ( ! CheckFileSpace ( fileRef, &ioBuf, 2 ) ) return; + + segLen = GetUns16BE ( ioBuf.ptr ); + if ( segLen < 2 ) return; // Invalid JPEG. + + ioBuf.ptr += 2; // Move ioBuf.ptr to the marker segment content. + segLen -= 2; // Adjust segLen to count just the content portion. + + // Check for the Exif APP1 marker segment. + + ok = CheckFileSpace ( fileRef, &ioBuf, kExifSignatureLength ); + if ( ok && (segLen >= kExifSignatureLength) && + (CheckBytes ( ioBuf.ptr, kExifSignatureString, kExifSignatureLength ) || + CheckBytes ( ioBuf.ptr, kExifSignatureAltStr, kExifSignatureLength )) ) { + + // This is the Exif metadata, cache the contents. + + ioBuf.ptr += kExifSignatureLength; // Move ioBuf.ptr to the TIFF stream. + segLen -= kExifSignatureLength; // Adjust segLen to count just the TIFF stream. + ok = CheckFileSpace ( fileRef, &ioBuf, segLen ); // Buffer the full content portion. + if ( ! ok ) return; // Must be a truncated file. + + this->exifContents.assign ( (XMP_StringPtr)ioBuf.ptr, segLen ); + ioBuf.ptr += segLen; + + continue; // Move on to the next marker. + + } + + // Check for the main XMP APP1 marker segment. + + ok = CheckFileSpace ( fileRef, &ioBuf, kMainXMPSignatureLength ); + if ( ok && (segLen >= kMainXMPSignatureLength) && + CheckBytes ( ioBuf.ptr, kMainXMPSignatureString, kMainXMPSignatureLength ) ) { + + // This is the main XMP, cache the contents. + + ioBuf.ptr += kMainXMPSignatureLength; // Move ioBuf.ptr to the XMP Packet. + segLen -= kMainXMPSignatureLength; // Adjust segLen to count just the XMP Packet. + ok = CheckFileSpace ( fileRef, &ioBuf, segLen ); // Buffer the full content portion. + if ( ! ok ) return; // Must be a truncated file. + + this->packetInfo.offset = ioBuf.filePos + (ioBuf.ptr - &ioBuf.data[0]); + this->packetInfo.length = (XMP_Int32)segLen; + this->packetInfo.padSize = 0; // Assume for now, set these properly in ProcessXMP. + this->packetInfo.charForm = kXMP_CharUnknown; + this->packetInfo.writeable = true; + + this->xmpPacket.assign ( (XMP_StringPtr)ioBuf.ptr, segLen ); + ioBuf.ptr += segLen; // ! Set this->packetInfo.offset first! + + this->containsXMP = true; // Found the standard XMP packet. + continue; // Move on to the next marker. + + } + + // Check for an extension XMP APP1 marker segment. + + ok = CheckFileSpace ( fileRef, &ioBuf, kExtXMPPrefixLength ); // ! The signature, GUID, length, and offset. + if ( ok && (segLen >= kExtXMPPrefixLength) && + CheckBytes ( ioBuf.ptr, kExtXMPSignatureString, kExtXMPSignatureLength ) ) { + + // This is a portion of the extended XMP, cache the contents. This is complicated by + // the need to tolerate files where the extension portions are not in order. The + // local ExtendedXMPInfo map uses the GUID as the key and maps that to a struct that + // has the full length and a map of the known portions. This known portion map uses + // the offset of the portion as the key and maps that to a string. Only fully seen + // extended XMP streams are kept, the right one gets picked in ProcessXMP. + + segLen -= kExtXMPPrefixLength; // Adjust segLen to count just the XMP stream portion. + + ioBuf.ptr += kExtXMPSignatureLength; // Move ioBuf.ptr to the GUID. + GUID_32 guid; + XMP_Assert ( sizeof(guid.data) == 32 ); + memcpy ( &guid.data[0], ioBuf.ptr, sizeof(guid.data) ); // AUDIT: Use of sizeof(guid.data) is safe. + + ioBuf.ptr += 32; // Move ioBuf.ptr to the length and offset. + XMP_Uns32 fullLen = GetUns32BE ( ioBuf.ptr ); + XMP_Uns32 offset = GetUns32BE ( ioBuf.ptr+4 ); + + ioBuf.ptr += 8; // Move ioBuf.ptr to the XMP stream portion. + + #if Trace_UnlimitedJPEG + printf ( "New extended XMP portion: fullLen %d, offset %d, GUID %.32s\n", fullLen, offset, guid.data ); + #endif + + // Find the ExtXMPContent for this GUID, and the string for this portion's offset. + + ExtendedXMPInfo::iterator guidPos = extXMP.find ( guid ); + if ( guidPos == extXMP.end() ) { + ExtXMPContent newExtContent ( fullLen ); + guidPos = extXMP.insert ( extXMP.begin(), ExtendedXMPInfo::value_type ( guid, newExtContent ) ); + } + + ExtXMPPortions::iterator offsetPos; + ExtXMPContent & extContent = guidPos->second; + + if ( extContent.portions.empty() ) { + // When new create a full size offset 0 string, to which all in-order portions will get appended. + offsetPos = extContent.portions.insert ( extContent.portions.begin(), + ExtXMPPortions::value_type ( 0, std::string() ) ); + offsetPos->second.reserve ( extContent.length ); + } + + // Try to append this portion to a logically contiguous preceeding one. + + if ( offset == 0 ) { + offsetPos = extContent.portions.begin(); + XMP_Assert ( (offsetPos->first == 0) && (offsetPos->second.size() == 0) ); + } else { + offsetPos = extContent.portions.lower_bound ( offset ); + --offsetPos; // Back up to the portion whose offset is less than the new offset. + if ( (offsetPos->first + offsetPos->second.size()) != offset ) { + // Can't append, create a new portion. + offsetPos = extContent.portions.insert ( extContent.portions.begin(), + ExtXMPPortions::value_type ( offset, std::string() ) ); + } + } + + // Cache this portion of the extended XMP. + + std::string & extPortion = offsetPos->second; + ok = CheckFileSpace ( fileRef, &ioBuf, segLen ); // Buffer the full content portion. + if ( ! ok ) return; // Must be a truncated file. + extPortion.append ( (XMP_StringPtr)ioBuf.ptr, segLen ); + ioBuf.ptr += segLen; + + continue; // Move on to the next marker. + + } + + // If we get here this is some other uninteresting APP1 marker segment, skip it. + + if ( segLen <= size_t(ioBuf.limit - ioBuf.ptr) ) { + ioBuf.ptr += segLen; // The next marker is in this buffer. + } else { + // The next marker is beyond this buffer, move to the start of it and fill the buffer. + size_t skipCount = segLen - (ioBuf.limit - ioBuf.ptr); // The amount to move beyond this buffer. + XMP_Int64 bufferEnd = ioBuf.filePos + (XMP_Int64)ioBuf.len; // File offset at the end of this buffer. + XMP_Int64 nextPos = bufferEnd + (XMP_Int64)skipCount; + MoveToOffset ( fileRef, nextPos, &ioBuf ); + } + + } else { + + // This is a non-terminating but uninteresting marker segment. Skip it. + + ++ioBuf.ptr; // Move ioBuf.ptr to the marker segment length field. + if ( ! CheckFileSpace ( fileRef, &ioBuf, 2 ) ) return; + + segLen = GetUns16BE ( ioBuf.ptr ); // Remember that the length includes itself. + if ( segLen < 2 ) return; // Invalid JPEG. + + if ( segLen <= size_t(ioBuf.limit - ioBuf.ptr) ) { + ioBuf.ptr += segLen; // The next marker is in this buffer. + } else { + // The next marker is beyond this buffer, move to the start of it and fill the buffer. + size_t skipCount = segLen - (ioBuf.limit - ioBuf.ptr); // The amount to move beyond this buffer. + XMP_Int64 bufferEnd = ioBuf.filePos + (XMP_Int64)ioBuf.len; // File offset at the end of this buffer. + XMP_Int64 nextPos = bufferEnd + (XMP_Int64)skipCount; + MoveToOffset ( fileRef, nextPos, &ioBuf ); + } + + continue; // Move on to the next marker. + + } + + } + + if ( ! extXMP.empty() ) { + + // We have extended XMP. Find out which ones are complete, collapse them into a single + // string, and save them for ProcessXMP. + + ExtendedXMPInfo::iterator guidPos = extXMP.begin(); + ExtendedXMPInfo::iterator guidEnd = extXMP.end(); + + for ( ; guidPos != guidEnd; ++guidPos ) { + + ExtXMPContent & thisContent = guidPos->second; + ExtXMPPortions::iterator partZero = thisContent.portions.begin(); + ExtXMPPortions::iterator partEnd = thisContent.portions.end(); + ExtXMPPortions::iterator partPos = partZero; + + #if Trace_UnlimitedJPEG + printf ( "Extended XMP portions for GUID %.32s, full length %d\n", + guidPos->first.data, guidPos->second.length ); + printf ( " Offset %d, length %d, next offset %d\n", + partZero->first, partZero->second.size(), (partZero->first + partZero->second.size()) ); + #endif + + for ( ++partPos; partPos != partEnd; ++partPos ) { + #if Trace_UnlimitedJPEG + printf ( " Offset %d, length %d, next offset %d\n", + partPos->first, partPos->second.size(), (partPos->first + partPos->second.size()) ); + #endif + if ( partPos->first != partZero->second.size() ) break; // Quit if not contiguous. + partZero->second.append ( partPos->second ); + } + + if ( (partPos == partEnd) && (partZero->first == 0) && (partZero->second.size() == thisContent.length) ) { + // This is a complete extended XMP stream. + this->extendedXMP.insert ( ExtendedXMPMap::value_type ( guidPos->first, partZero->second ) ); + #if Trace_UnlimitedJPEG + printf ( "Full extended XMP for GUID %.32s, full length %d\n", + guidPos->first.data, partZero->second.size() ); + #endif + } + + } + + } + +} // JPEG_MetaHandler::CacheFileData + +// ================================================================================================= +// TrimFullExifAPP1 +// ================ +// +// Try to trim trailing padding from full Exif APP1 segment written by some Nikon cameras. Do a +// temporary read-only parse of the Exif APP1 contents, determine the highest used offset, trim the +// padding if all zero bytes. + +static const char * IFDNames[] = { "Primary", "TNail", "Exif", "GPS", "Interop", }; + +static void TrimFullExifAPP1 ( std::string * exifContents ) +{ + TIFF_MemoryReader tempMgr; + TIFF_MemoryReader::TagInfo tagInfo; + bool tagFound, isNikon; + + // ! Make a copy of the data to parse! The RO memory TIFF manager will flip bytes in-place! + tempMgr.ParseMemoryStream ( exifContents->data(), (XMP_Uns32)exifContents->size(), true /* copy data */ ); + + // Only trim the Exif APP1 from Nikon cameras. + tagFound = tempMgr.GetTag ( kTIFF_PrimaryIFD, kTIFF_Make, &tagInfo ); + isNikon = tagFound && (tagInfo.type == kTIFF_ASCIIType) && (tagInfo.count >= 5) && + (memcmp ( tagInfo.dataPtr, "NIKON", 5) == 0); + if ( ! isNikon ) return; + + // Find the start of the padding, one past the highest used offset. Look at the IFD structure, + // and the thumbnail info. Ignore the MakerNote tag, Nikon says they are self-contained. + + XMP_Uns32 padOffset = 0; + + for ( XMP_Uns8 ifd = 0; ifd < kTIFF_KnownIFDCount; ++ifd ) { + + TIFF_MemoryReader::TagInfoMap tagMap; + bool ifdFound = tempMgr.GetIFD ( ifd, &tagMap ); + if ( ! ifdFound ) continue; + + TIFF_MemoryReader::TagInfoMap::const_iterator mapPos = tagMap.begin(); + TIFF_MemoryReader::TagInfoMap::const_iterator mapEnd = tagMap.end(); + + for ( ; mapPos != mapEnd; ++mapPos ) { + const TIFF_MemoryReader::TagInfo & tagInfo = mapPos->second; + XMP_Uns32 tagEnd = tempMgr.GetValueOffset ( ifd, tagInfo.id ) + tagInfo.dataLen; + if ( tagEnd > padOffset ) padOffset = tagEnd; + } + + } + + tagFound = tempMgr.GetTag ( kTIFF_TNailIFD, kTIFF_JPEGInterchangeFormat, &tagInfo ); + if ( tagFound ) { + XMP_Uns32 tnailOffset = tempMgr.GetUns32 ( tagInfo.dataPtr ); + tagFound = tempMgr.GetTag ( kTIFF_TNailIFD, kTIFF_JPEGInterchangeFormatLength, &tagInfo ); + if ( ! tagFound ) return; // Don't trim if there is a TNail offset but no length. + tnailOffset += tempMgr.GetUns32 ( tagInfo.dataPtr ); + if ( tnailOffset > padOffset ) padOffset = tnailOffset; + } + + // Decide if it is OK to trim the Exif segment. It is OK if the padding is all zeros. It is OK + // if the last non-zero byte is no more than 64 bytes into the padding and there are at least + // an additional 64 bytes of padding after it. + + if ( padOffset >= exifContents->size() ) return; // Sanity check for an OK last used offset. + + size_t lastNonZero = exifContents->size() - 1; + while ( (lastNonZero >= padOffset) && ((*exifContents)[lastNonZero] == 0) ) --lastNonZero; + + bool ok = lastNonZero < padOffset; + if ( ! ok ) { + size_t nzSize = lastNonZero - padOffset + 1; + size_t finalSize = (exifContents->size() - 1) - lastNonZero; + if ( (nzSize < 64) && (finalSize > 64) ) { + padOffset = lastNonZero + 64; + assert ( padOffset < exifContents->size() ); + ok = true; + } + } + + if ( ok ) exifContents->erase ( padOffset ); + +} // TrimFullExifAPP1 + +// ================================================================================================= +// JPEG_MetaHandler::ProcessXMP +// ============================ +// +// Process the raw XMP and legacy metadata that was previously cached. + +void JPEG_MetaHandler::ProcessXMP() +{ + + XMP_Assert ( ! this->processedXMP ); + this->processedXMP = true; // Make sure we only come through here once. + + // Create the PSIR and IPTC handlers, even if there is no legacy. They might be needed for updates. + + XMP_Assert ( (this->psirMgr == 0) && (this->iptcMgr == 0) ); // ProcessTNail might create the exifMgr. + + bool readOnly = ((this->parent->openFlags & kXMPFiles_OpenForUpdate) == 0); + + if ( readOnly ) { + if ( this->exifMgr == 0 ) this->exifMgr = new TIFF_MemoryReader(); + this->psirMgr = new PSIR_MemoryReader(); + this->iptcMgr = new IPTC_Reader(); // ! Parse it later. + } else { + if ( this->exifContents.size() == (65534 - 2 - 6) ) TrimFullExifAPP1 ( &this->exifContents ); + if ( this->exifMgr == 0 ) this->exifMgr = new TIFF_FileWriter(); + this->psirMgr = new PSIR_FileWriter(); + this->iptcMgr = new IPTC_Writer(); // ! Parse it later. + } + + // Set up everything for the legacy import, but don't do it yet. This lets us do a forced legacy + // import if the XMP packet gets parsing errors. + + TIFF_Manager & exif = *this->exifMgr; // Give the compiler help in recognizing non-aliases. + PSIR_Manager & psir = *this->psirMgr; + IPTC_Manager & iptc = *this->iptcMgr; + + bool haveExif = (! this->exifContents.empty()); + if ( haveExif ) { + exif.ParseMemoryStream ( this->exifContents.c_str(), (XMP_Uns32)this->exifContents.size() ); + } + + bool havePSIR = (! this->psirContents.empty()); + if ( havePSIR ) { + psir.ParseMemoryResources ( this->psirContents.c_str(), (XMP_Uns32)this->psirContents.size() ); + } + + PSIR_Manager::ImgRsrcInfo iptcInfo; + bool haveIPTC = false; + if ( havePSIR ) haveIPTC = psir.GetImgRsrc ( kPSIR_IPTC, &iptcInfo );; + int iptcDigestState = kDigestMatches; + + if ( haveIPTC ) { + + bool haveDigest = false; + PSIR_Manager::ImgRsrcInfo digestInfo; + if ( havePSIR ) haveDigest = psir.GetImgRsrc ( kPSIR_IPTCDigest, &digestInfo ); + if ( digestInfo.dataLen != 16 ) haveDigest = false; + + if ( ! haveDigest ) { + iptcDigestState = kDigestMissing; + } else { + iptcDigestState = PhotoDataUtils::CheckIPTCDigest ( iptcInfo.dataPtr, iptcInfo.dataLen, digestInfo.dataPtr ); + } + + } + + XMP_OptionBits options = 0; + if ( this->containsXMP ) options |= k2XMP_FileHadXMP; + if ( haveExif ) options |= k2XMP_FileHadExif; + if ( haveIPTC ) options |= k2XMP_FileHadIPTC; + + // Process the main XMP packet. If it fails to parse, do a forced legacy import but still throw + // an exception. This tells the caller that an error happened, but gives them recovered legacy + // should they want to proceed with that. + + bool haveXMP = false; + + if ( ! this->xmpPacket.empty() ) { + XMP_Assert ( this->containsXMP ); + // Common code takes care of packetInfo.charForm, .padSize, and .writeable. + XMP_StringPtr packetStr = this->xmpPacket.c_str(); + XMP_StringLen packetLen = (XMP_StringLen)this->xmpPacket.size(); + try { + this->xmpObj.ParseFromBuffer ( packetStr, packetLen ); + haveXMP = true; + } catch ( ... ) { + XMP_ClearOption ( options, k2XMP_FileHadXMP ); + if ( haveIPTC ) iptc.ParseMemoryDataSets ( iptcInfo.dataPtr, iptcInfo.dataLen ); + if ( iptcDigestState == kDigestMatches ) iptcDigestState = kDigestMissing; + ImportPhotoData ( exif, iptc, psir, iptcDigestState, &this->xmpObj, options ); + throw; // ! Rethrow the exception, don't absorb it. + } + } + + // Process the extended XMP if it has a matching GUID. + + if ( ! this->extendedXMP.empty() ) { + + bool found; + GUID_32 g32; + std::string extGUID, extPacket; + ExtendedXMPMap::iterator guidPos = this->extendedXMP.end(); + + found = this->xmpObj.GetProperty ( kXMP_NS_XMP_Note, "HasExtendedXMP", &extGUID, 0 ); + if ( found && (extGUID.size() == sizeof(g32.data)) ) { + XMP_Assert ( sizeof(g32.data) == 32 ); + memcpy ( g32.data, extGUID.c_str(), sizeof(g32.data) ); // AUDIT: Use of sizeof(g32.data) is safe. + guidPos = this->extendedXMP.find ( g32 ); + this->xmpObj.DeleteProperty ( kXMP_NS_XMP_Note, "HasExtendedXMP" ); // ! Must only be in the file. + #if Trace_UnlimitedJPEG + printf ( "%s extended XMP for GUID %s\n", + ((guidPos != this->extendedXMP.end()) ? "Found" : "Missing"), extGUID.c_str() ); + #endif + } + + if ( guidPos != this->extendedXMP.end() ) { + try { + XMP_StringPtr extStr = guidPos->second.c_str(); + XMP_StringLen extLen = (XMP_StringLen)guidPos->second.size(); + SXMPMeta extXMP ( extStr, extLen ); + SXMPUtils::MergeFromJPEG ( &this->xmpObj, extXMP ); + } catch ( ... ) { + // Ignore failures, let the rest of the XMP and legacy be kept. + } + } + + } + + // Process the legacy metadata. + + if ( haveIPTC && (! haveXMP) && (iptcDigestState == kDigestMatches) ) iptcDigestState = kDigestMissing; + bool parseIPTC = (iptcDigestState != kDigestMatches) || (! readOnly); + if ( parseIPTC ) iptc.ParseMemoryDataSets ( iptcInfo.dataPtr, iptcInfo.dataLen ); + ImportPhotoData ( exif, iptc, psir, iptcDigestState, &this->xmpObj, options ); + + this->containsXMP = true; // Assume we had something for the XMP. + +} // JPEG_MetaHandler::ProcessXMP + +// ================================================================================================= +// JPEG_MetaHandler::UpdateFile +// ============================ + +void JPEG_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + XMP_Assert ( ! doSafeUpdate ); // This should only be called for "unsafe" updates. + + XMP_Int64 oldPacketOffset = this->packetInfo.offset; + XMP_Int32 oldPacketLength = this->packetInfo.length; + + if ( oldPacketOffset == kXMPFiles_UnknownOffset ) oldPacketOffset = 0; // ! Simplify checks. + if ( oldPacketLength == kXMPFiles_UnknownLength ) oldPacketLength = 0; + + bool fileHadXMP = ((oldPacketOffset != 0) && (oldPacketLength != 0)); + + // Update the IPTC-IIM and native TIFF/Exif metadata. ExportPhotoData also trips the tiff: and + // exif: copies from the XMP, so reserialize the now final XMP packet. + + ExportPhotoData ( kXMP_JPEGFile, &this->xmpObj, this->exifMgr, this->iptcMgr, this->psirMgr ); + + try { + XMP_OptionBits options = kXMP_UseCompactFormat; + if ( fileHadXMP ) options |= kXMP_ExactPacketLength; + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, options, oldPacketLength ); + } catch ( ... ) { + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, kXMP_UseCompactFormat ); + } + + // Decide whether to do an in-place update. This can only happen if all of the following are true: + // - There is a standard packet in the file. + // - There is no extended XMP in the file. + // - The are no changes to the legacy Exif or PSIR portions. (The IPTC is in the PSIR.) + // - The new XMP can fit in the old space, without extensions. + + bool doInPlace = (fileHadXMP && (this->xmpPacket.size() <= (size_t)oldPacketLength)); + + if ( ! this->extendedXMP.empty() ) doInPlace = false; + + if ( (this->exifMgr != 0) && (this->exifMgr->IsLegacyChanged()) ) doInPlace = false; + if ( (this->psirMgr != 0) && (this->psirMgr->IsLegacyChanged()) ) doInPlace = false; + + if ( doInPlace ) { + + #if GatherPerformanceData + sAPIPerf->back().extraInfo += ", JPEG in-place update"; + #endif + + if ( this->xmpPacket.size() < (size_t)this->packetInfo.length ) { + // They ought to match, cheap to be sure. + size_t extraSpace = (size_t)this->packetInfo.length - this->xmpPacket.size(); + this->xmpPacket.append ( extraSpace, ' ' ); + } + + XMP_IO* liveFile = this->parent->ioRef; + std::string & newPacket = this->xmpPacket; + + XMP_Assert ( newPacket.size() == (size_t)oldPacketLength ); // ! Done by common PutXMP logic. + + liveFile->Seek ( oldPacketOffset, kXMP_SeekFromStart ); + liveFile->Write ( newPacket.c_str(), (XMP_Int32)newPacket.size() ); + + } else { + + #if GatherPerformanceData + sAPIPerf->back().extraInfo += ", JPEG copy update"; + #endif + + XMP_IO* origRef = this->parent->ioRef; + XMP_IO* tempRef = origRef->DeriveTemp(); + + try { + XMP_Assert ( ! this->skipReconcile ); + this->skipReconcile = true; + this->WriteTempFile ( tempRef ); + this->skipReconcile = false; + } catch ( ... ) { + this->skipReconcile = false; + origRef->DeleteTemp(); + throw; + } + + origRef->AbsorbTemp(); + + } + + this->needsUpdate = false; + +} // JPEG_MetaHandler::UpdateFile + +// ================================================================================================= +// JPEG_MetaHandler::WriteTempFile +// =============================== +// +// The metadata parts of a JPEG file are APP1 marker segments for Exif and XMP, and an APP13 marker +// segment for Photoshop image resources which contain the IPTC. Corresponding marker segments in +// the source file are ignored, other parts of the source file are copied. Any initial APP0 marker +// segments are copied first. Then the new Exif, XMP, and PSIR marker segments are written. Then the +// rest of the file is copied, skipping the old Exif, XMP, and PSIR. The checking for old metadata +// stops at the first SOFn marker. + +// *** What about Mac resources? + +void JPEG_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + XMP_IO* origRef = this->parent->ioRef; + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + XMP_Uns16 marker; + size_t segLen; // ! Must be a size to hold at least 64k+2. + IOBuffer ioBuf; + XMP_Uns32 first4; + + XMP_Assert ( kIOBufferSize >= (2 + 64*1024) ); // Enough for a marker plus maximum contents. + + if ( origRef->Length() == 0 ) return; // Tolerate empty files. + origRef->Rewind(); + tempRef->Truncate ( 0 ); + + if ( ! skipReconcile ) { + // Update the IPTC-IIM and native TIFF/Exif metadata, and reserialize the now final XMP packet. + ExportPhotoData ( kXMP_JPEGFile, &this->xmpObj, this->exifMgr, this->iptcMgr, this->psirMgr ); + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, kXMP_UseCompactFormat ); + } + + RefillBuffer ( origRef, &ioBuf ); + if ( ! CheckFileSpace ( origRef, &ioBuf, 4 ) ) { + XMP_Throw ( "JPEG must have at least SOI and EOI markers", kXMPErr_BadJPEG ); + } + + marker = GetUns16BE ( ioBuf.ptr ); + if ( marker != 0xFFD8 ) XMP_Throw ( "Missing SOI marker", kXMPErr_BadJPEG ); + tempRef->Write ( ioBuf.ptr, 2 ); + ioBuf.ptr += 2; + + // Copy the leading APP0 marker segments. + + while ( true ) { + + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "JPEG_MetaHandler::WriteFile - User abort", kXMPErr_UserAbort ); + } + + if ( ! CheckFileSpace ( origRef, &ioBuf, 2 ) ) XMP_Throw ( "Unexpected end to JPEG", kXMPErr_BadJPEG ); + marker = GetUns16BE ( ioBuf.ptr ); + if ( marker == 0xFFFF ) { + tempRef->Write ( ioBuf.ptr, 1 ); // Copy the 0xFF pad byte. + ++ioBuf.ptr; + continue; + } + + if ( marker != 0xFFE0 ) break; + + if ( ! CheckFileSpace ( origRef, &ioBuf, 4 ) ) XMP_Throw ( "Unexpected end to JPEG", kXMPErr_BadJPEG ); + segLen = GetUns16BE ( ioBuf.ptr+2 ); + segLen += 2; // ! Don't do above in case machine does 16 bit "+". + + if ( ! CheckFileSpace ( origRef, &ioBuf, segLen ) ) XMP_Throw ( "Unexpected end to JPEG", kXMPErr_BadJPEG ); + tempRef->Write ( ioBuf.ptr, (XMP_Int32)segLen ); + ioBuf.ptr += segLen; + + } + + // Write the new Exif APP1 marker segment. + + if ( this->exifMgr != 0 ) { + + void* exifPtr; + XMP_Uns32 exifLen = this->exifMgr->UpdateMemoryStream ( &exifPtr ); + if ( exifLen > kExifMaxDataLength ) exifLen = this->exifMgr->UpdateMemoryStream ( &exifPtr, true /* compact */ ); + if ( exifLen > kExifMaxDataLength ) { + // XMP_Throw ( "Overflow of Exif APP1 data", kXMPErr_BadJPEG ); ** Used to throw, now rewrite original Exif. + exifPtr = (void*)this->exifContents.c_str(); + exifLen = this->exifContents.size(); + } + + if ( exifLen > 0 ) { + first4 = MakeUns32BE ( 0xFFE10000 + 2 + kExifSignatureLength + exifLen ); + tempRef->Write ( &first4, 4 ); + tempRef->Write ( kExifSignatureString, kExifSignatureLength ); + tempRef->Write ( exifPtr, exifLen ); + } + + } + + // Write the new XMP APP1 marker segment, with possible extension marker segments. + + std::string mainXMP, extXMP, extDigest; + SXMPUtils::PackageForJPEG ( this->xmpObj, &mainXMP, &extXMP, &extDigest ); + XMP_Assert ( (extXMP.size() == 0) || (extDigest.size() == 32) ); + + first4 = MakeUns32BE ( 0xFFE10000 + 2 + kMainXMPSignatureLength + (XMP_Uns32)mainXMP.size() ); + tempRef->Write ( &first4, 4 ); + tempRef->Write ( kMainXMPSignatureString, kMainXMPSignatureLength ); + tempRef->Write ( mainXMP.c_str(), (XMP_Int32)mainXMP.size() ); + + size_t extPos = 0; + size_t extLen = extXMP.size(); + + while ( extLen > 0 ) { + + size_t partLen = extLen; + if ( partLen > 65000 ) partLen = 65000; + + first4 = MakeUns32BE ( 0xFFE10000 + 2 + kExtXMPPrefixLength + (XMP_Uns32)partLen ); + tempRef->Write ( &first4, 4 ); + + tempRef->Write ( kExtXMPSignatureString, kExtXMPSignatureLength ); + tempRef->Write ( extDigest.c_str(), (XMP_Int32)extDigest.size() ); + + first4 = MakeUns32BE ( (XMP_Int32)extXMP.size() ); + tempRef->Write ( &first4, 4 ); + first4 = MakeUns32BE ( (XMP_Int32)extPos ); + tempRef->Write ( &first4, 4 ); + + tempRef->Write ( &extXMP[extPos], (XMP_Int32)partLen ); + + extPos += partLen; + extLen -= partLen; + + } + + // Write the new PSIR APP13 marker segment. + + if ( this->psirMgr != 0 ) { + + void* psirPtr; + XMP_Uns32 psirLen = this->psirMgr->UpdateMemoryResources ( &psirPtr ); + if ( psirLen > kPSIRMaxDataLength ) XMP_Throw ( "Overflow of PSIR APP13 data", kXMPErr_BadJPEG ); + + if ( psirLen > 0 ) { + first4 = MakeUns32BE ( 0xFFED0000 + 2 + kPSIRSignatureLength + psirLen ); + tempRef->Write ( &first4, 4 ); + tempRef->Write ( kPSIRSignatureString, kPSIRSignatureLength ); + tempRef->Write ( psirPtr, psirLen ); + } + + } + + // Copy remaining marker segments, skipping old metadata, to the first SOS marker or to EOI. + + while ( true ) { + + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "JPEG_MetaHandler::WriteFile - User abort", kXMPErr_UserAbort ); + } + + if ( ! CheckFileSpace ( origRef, &ioBuf, 2 ) ) XMP_Throw ( "Unexpected end to JPEG", kXMPErr_BadJPEG ); + marker = GetUns16BE ( ioBuf.ptr ); + if ( marker == 0xFFFF ) { + tempRef->Write ( ioBuf.ptr, 1 ); // Copy the 0xFF pad byte. + ++ioBuf.ptr; + continue; + } + + if ( (marker == 0xFFDA) || (marker == 0xFFD9) ) break; // Quit at the first SOS marker or at EOI. + + if ( (marker == 0xFF01) || // Ill-formed file if we encounter a TEM or RSTn marker. + ((0xFFD0 <= marker) && (marker <= 0xFFD7)) ) { + XMP_Throw ( "Unexpected TEM or RSTn marker", kXMPErr_BadJPEG ); + } + + if ( ! CheckFileSpace ( origRef, &ioBuf, 4 ) ) XMP_Throw ( "Unexpected end to JPEG", kXMPErr_BadJPEG ); + segLen = GetUns16BE ( ioBuf.ptr+2 ); + + if ( ! CheckFileSpace ( origRef, &ioBuf, 2+segLen ) ) XMP_Throw ( "Unexpected end to JPEG", kXMPErr_BadJPEG ); + + bool copySegment = true; + XMP_Uns8* signaturePtr = ioBuf.ptr + 4; + + if ( marker == 0xFFED ) { + if ( (segLen >= kPSIRSignatureLength) && + CheckBytes ( signaturePtr, kPSIRSignatureString, kPSIRSignatureLength ) ) { + copySegment = false; + } + } else if ( marker == 0xFFE1 ) { + if ( (segLen >= kExifSignatureLength) && + (CheckBytes ( signaturePtr, kExifSignatureString, kExifSignatureLength ) || + CheckBytes ( signaturePtr, kExifSignatureAltStr, kExifSignatureLength )) ) { + copySegment = false; + } else if ( (segLen >= kMainXMPSignatureLength) && + CheckBytes ( signaturePtr, kMainXMPSignatureString, kMainXMPSignatureLength ) ) { + copySegment = false; + } else if ( (segLen >= kExtXMPPrefixLength) && + CheckBytes ( signaturePtr, kExtXMPSignatureString, kExtXMPSignatureLength ) ) { + copySegment = false; + } + } + + if ( copySegment ) tempRef->Write ( ioBuf.ptr, (XMP_Int32)(2+segLen) ); + + ioBuf.ptr += 2+segLen; + + } + + // Copy the remainder of the source file. + + size_t bufTail = ioBuf.len - (ioBuf.ptr - &ioBuf.data[0]); + tempRef->Write ( ioBuf.ptr, (XMP_Int32)bufTail ); + ioBuf.ptr += bufTail; + + while ( true ) { + RefillBuffer ( origRef, &ioBuf ); + if ( ioBuf.len == 0 ) break; + tempRef->Write ( ioBuf.ptr, (XMP_Int32)ioBuf.len ); + ioBuf.ptr += ioBuf.len; + } + + this->needsUpdate = false; + +} // JPEG_MetaHandler::WriteTempFile diff --git a/XMPFiles/source/FileHandlers/JPEG_Handler.hpp b/XMPFiles/source/FileHandlers/JPEG_Handler.hpp new file mode 100644 index 0000000..568ee1b --- /dev/null +++ b/XMPFiles/source/FileHandlers/JPEG_Handler.hpp @@ -0,0 +1,98 @@ +#ifndef __JPEG_Handler_hpp__ +#define __JPEG_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! Must be the first #include! + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/FormatSupport/TIFF_Support.hpp" +#include "XMPFiles/source/FormatSupport/PSIR_Support.hpp" +#include "XMPFiles/source/FormatSupport/IPTC_Support.hpp" + +// ================================================================================================= +/// \file JPEG_Handler.hpp +/// \brief File format handler for JPEG. +/// +/// This header ... +/// +// ================================================================================================= + +// *** Could derive from Basic_Handler - buffer file tail in a temp file. + +extern XMPFileHandler * JPEG_MetaHandlerCTor ( XMPFiles * parent ); + +extern bool JPEG_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO * fileRef, + XMPFiles * parent ); + +static const XMP_OptionBits kJPEG_HandlerFlags = (kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_CanRewrite | + kXMPFiles_PrefersInPlace | + kXMPFiles_CanReconcile | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_AllowsSafeUpdate); + +class JPEG_MetaHandler : public XMPFileHandler +{ +public: + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + struct GUID_32 { // A hack to get an assignment operator for an array. + char data [32]; + void operator= ( const GUID_32 & in ) + { + memcpy ( this->data, in.data, sizeof(this->data) ); // AUDIT: Use of sizeof(this->data) is safe. + }; + bool operator< ( const GUID_32 & right ) const + { + return (memcmp ( this->data, right.data, sizeof(this->data) ) < 0); + }; + bool operator== ( const GUID_32 & right ) const + { + return (memcmp ( this->data, right.data, sizeof(this->data) ) == 0); + }; + }; + + JPEG_MetaHandler ( XMPFiles * parent ); + virtual ~JPEG_MetaHandler(); + +private: + + JPEG_MetaHandler() : exifMgr(0), psirMgr(0), iptcMgr(0), skipReconcile(false) {}; // Hidden on purpose. + + std::string exifContents; + std::string psirContents; + + TIFF_Manager * exifMgr; // The Exif manager will be created by ProcessTNail or ProcessXMP. + PSIR_Manager * psirMgr; // Need to use pointers so we can properly select between read-only and + IPTC_Manager * iptcMgr; // read-write modes of usage. + + bool skipReconcile; // ! Used between UpdateFile and WriteFile. + + typedef std::map < GUID_32, std::string > ExtendedXMPMap; + + ExtendedXMPMap extendedXMP; // ! Only contains those with complete data. + +}; // JPEG_MetaHandler + +// ================================================================================================= + +#endif /* __JPEG_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/MP3_Handler.cpp b/XMPFiles/source/FileHandlers/MP3_Handler.cpp new file mode 100644 index 0000000..47f3aae --- /dev/null +++ b/XMPFiles/source/FileHandlers/MP3_Handler.cpp @@ -0,0 +1,748 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2008 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/MP3_Handler.hpp" + +// ================================================================================================= +/// \file MP3_Handler.cpp +/// \brief MP3 handler class. +// ================================================================================================= + +// ================================================================================================= +// Helper structs and private routines +// ==================== +struct ReconProps { + const char* mainID; // The stored v2.3 and v2.4 ID, also used as the main logical ID. + const char* v22ID; // The stored v2.2 ID. + const char* ns; + const char* prop; +}; + +const static XMP_Uns32 XMP_V23_ID = 0x50524956; // PRIV +const static XMP_Uns32 XMP_V22_ID = 0x50525600; // PRV + +const static ReconProps reconProps[] = { + { "TPE1", "TP1", kXMP_NS_DM, "artist" }, + { "TALB", "TAL", kXMP_NS_DM, "album" }, + { "TRCK", "TRK", kXMP_NS_DM, "trackNumber" }, + // exceptions that need attention: + { "TCON", "TCO", kXMP_NS_DM, "genre" }, // genres might be numeric + { "TIT2", "TT2", kXMP_NS_DC, "title" }, // ["x-default"] language alternatives + { "COMM", "COM", kXMP_NS_DM, "logComment" }, // two distinct strings, language alternative + + { "TYER", "TYE", kXMP_NS_XMP, "CreateDate" }, // Year (YYYY) Deprecated in 2.4 + { "TDAT", "TDA", kXMP_NS_XMP, "CreateDate" }, // Date (DDMM) Deprecated in 2.4 + { "TIME", "TIM", kXMP_NS_XMP, "CreateDate" }, // Time (HHMM) Deprecated in 2.4 + { "TDRC", "", kXMP_NS_XMP, "CreateDate" }, // assembled date/time v2.4 + + // new reconciliations introduced in Version 5 + { "TCMP", "TCP", kXMP_NS_DM, "partOfCompilation" }, // presence/absence of TCMP frame dedides + { "USLT", "ULT", kXMP_NS_DM, "lyrics" }, + { "TCOM", "TCM", kXMP_NS_DM, "composer" }, + { "TPOS", "TPA", kXMP_NS_DM, "discNumber" }, // * a text field! might contain "/<total>" + { "TCOP", "TCR", kXMP_NS_DC, "rights" }, // ["x-default"] language alternatives + { "TPE4", "TP4", kXMP_NS_DM, "engineer" }, + { "WCOP", "WCP", kXMP_NS_XMP_Rights, "WebStatement" }, + + { 0, 0, 0, 0 } // must be last as a sentinel +}; + +// ================================================================================================= +// MP3_MetaHandlerCTor +// ==================== + +XMPFileHandler * MP3_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new MP3_MetaHandler ( parent ); +} + +// ================================================================================================= +// MP3_CheckFormat +// =============== + +// For MP3 we check parts .... See the MP3 spec for offset info. + +bool MP3_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* file, + XMPFiles * parent ) +{ + IgnoreParam(filePath); IgnoreParam(parent); //supress warnings + XMP_Assert ( format == kXMP_MP3File ); //standard assert + + if ( file->Length() < 10 ) return false; + file ->Rewind(); + + XMP_Uns8 header[3]; + file->ReadAll ( header, 3 ); + if ( ! CheckBytes( &header[0], "ID3", 3 ) ) return (parent->format == kXMP_MP3File); + + XMP_Uns8 major = XIO::ReadUns8( file ); + XMP_Uns8 minor = XIO::ReadUns8( file ); + + if ( (major < 2) || (major > 4) || (minor == 0xFF) ) return false; + + XMP_Uns8 flags = XIO::ReadUns8 ( file ); + + //TODO + if ( flags & 0x10 ) XMP_Throw ( "no support for MP3 with footer", kXMPErr_Unimplemented ); + if ( flags & 0x80 ) return false; //no support for unsynchronized MP3 (as before, also see [1219125]) + if ( flags & 0x0F ) XMP_Throw ( "illegal header lower bits", kXMPErr_Unimplemented ); + + XMP_Uns32 size = XIO::ReadUns32_BE ( file ); + if ( (size & 0x80808080) != 0 ) return false; //if any bit survives -> not a valid synchsafe 32 bit integer + + return true; + +} // MP3_CheckFormat + + +// ================================================================================================= +// MP3_MetaHandler::MP3_MetaHandler +// ================================ + +MP3_MetaHandler::MP3_MetaHandler ( XMPFiles * _parent ) +{ + this->parent = _parent; + this->handlerFlags = kMP3_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; +} + +// ================================================================================================= +// MP3_MetaHandler::~MP3_MetaHandler +// ================================= + +MP3_MetaHandler::~MP3_MetaHandler() +{ + // free frames + ID3v2Frame* curFrame; + while ( !this->framesVector.empty() ) { + curFrame = this->framesVector.back(); + delete curFrame; + framesVector.pop_back(); + } +} + +// ================================================================================================= +// MP3_MetaHandler::CacheFileData +// ============================== + +void MP3_MetaHandler::CacheFileData() +{ + + //*** abort procedures + this->containsXMP = false; //assume no XMP for now + + XMP_IO* file = this->parent->ioRef; + XMP_PacketInfo &packetInfo = this->packetInfo; + + file->Rewind(); + + this->hasID3Tag = this->id3Header.read( file ); + this->majorVersion = this->id3Header.fields[ID3Header::o_vMajor]; + this->minorVersion = this->id3Header.fields[ID3Header::o_vMinor]; + this->hasExtHeader = (0 != ( 0x40 & this->id3Header.fields[ID3Header::o_flags])); //'naturally' false if no ID3Tag + this->hasFooter = ( 0 != ( 0x10 & this->id3Header.fields[ID3Header::o_flags])); //'naturally' false if no ID3Tag + + // stored size is w/o initial header (thus adding 10) + // + but extended header (if existing) + // + padding + frames after unsynchronisation (?) + // (if no ID3 tag existing, constructed default correctly sets size to 10.) + this->oldTagSize = ID3Header::kID3_TagHeaderSize + synchToInt32(GetUns32BE( &id3Header.fields[ID3Header::o_size] )); + + if ( ! hasExtHeader ) { + + this->extHeaderSize = 0; // := there is no such header. + + } else { + + this->extHeaderSize = synchToInt32( XIO::ReadInt32_BE( file)); + XMP_Uns8 extHeaderNumFlagBytes = XIO::ReadUns8( file ); + + // v2.3 doesn't include the size, while v2.4 does + if ( this->majorVersion < 4 ) this->extHeaderSize += 4; + XMP_Validate( this->extHeaderSize >= 6, "extHeader size too small", kXMPErr_BadFileFormat ); + + file->Seek ( this->extHeaderSize - 6, kXMP_SeekFromCurrent ); + + } + + this->framesVector.clear(); //mac precaution + ID3v2Frame* curFrame = 0; // reusable + + //////////////////////////////////////////////////// + // read frames + + XMP_Uns32 xmpID = XMP_V23_ID; + if ( this->majorVersion == 2 ) xmpID = XMP_V22_ID; + + while ( file->Offset() < this->oldTagSize ) { + + curFrame = new ID3v2Frame(); + + try { + XMP_Int64 frameSize = curFrame->read ( file, this->majorVersion ); + if ( frameSize == 0 ) { + delete curFrame; // ..since not becoming part of vector for latter delete. + break; // not a throw. There's nothing wrong with padding. + } + this->containsXMP = true; + } catch ( ... ) { + delete curFrame; + throw; + } + + // these are both pointer assignments, no (copy) construction + // (MemLeak Note: for all things pushed, memory cleanup is taken care of in destructor.) + this->framesVector.push_back ( curFrame ); + + //remember XMP-Frame, if it occurs: + if ( (curFrame->id ==xmpID) && + (curFrame->contentSize > 8) && CheckBytes ( &curFrame->content[0], "XMP\0", 4 ) ) { + + // be sure that this is the first packet (all else would be illegal format) + XMP_Validate ( this->framesMap[xmpID] == 0, "two XMP packets in one file", kXMPErr_BadFileFormat ); + //add this to map, needed on reconciliation + this->framesMap[xmpID] = curFrame; + + this->packetInfo.length = curFrame->contentSize - 4; // content minus "XMP\0" + this->packetInfo.offset = ( file->Offset() - this->packetInfo.length ); + + this->xmpPacket.erase(); //safety + this->xmpPacket.assign( &curFrame->content[4], curFrame->contentSize - 4 ); + this->containsXMP = true; // do this last, after all possible failure + + } + + // No space for another frame? => assume into ID3v2.4 padding. + XMP_Int64 newPos = file->Offset(); + XMP_Int64 spaceLeft = this->oldTagSize - newPos; // Depends on first check below! + if ( (newPos > this->oldTagSize) || (spaceLeft < ID3Header::kID3_TagHeaderSize) ) break; + + } + + //////////////////////////////////////////////////// + // padding + + this->oldPadding = this->oldTagSize - file->Offset(); + this->oldFramesSize = this->oldTagSize - ID3Header::kID3_TagHeaderSize - this->oldPadding; + + XMP_Validate ( (this->oldPadding >= 0), "illegal oldTagSize or padding value", kXMPErr_BadFileFormat ); + + for ( XMP_Int64 i = this->oldPadding; i > 0; ) { + if ( i >= 8 ) { + if ( XIO::ReadInt64_BE(file) != 0 ) XMP_Throw ( "padding not nulled out", kXMPErr_BadFileFormat ); + i -= 8; + continue; + } + if ( XIO::ReadUns8(file) != 0) XMP_Throw ( "padding(2) not nulled out", kXMPErr_BadFileFormat ); + i--; + } + + //// read ID3v1 tag + if ( ! this->containsXMP ) this->containsXMP = id3v1Tag.read ( file, &this->xmpObj ); + +} // MP3_MetaHandler::CacheFileData + + +// ================================================================================================= +// MP3_MetaHandler::ProcessXMP +// =========================== +// +// Process the raw XMP and legacy metadata that was previously cached. + +void MP3_MetaHandler::ProcessXMP() +{ + + // Process the XMP packet. + if ( ! this->xmpPacket.empty() ) { + XMP_Assert ( this->containsXMP ); + XMP_StringPtr packetStr = this->xmpPacket.c_str(); + XMP_StringLen packetLen = (XMP_StringLen) this->xmpPacket.size(); + this->xmpObj.ParseFromBuffer ( packetStr, packetLen ); + this->processedXMP = true; + } + + /////////////////////////////////////////////////////////////////// + // assumptions on presence-absence "flag tags" + // ( unless no xmp whatsoever present ) + if ( ! this->xmpPacket.empty() ) this->xmpObj.SetProperty ( kXMP_NS_DM, "partOfCompilation", "false" ); + + //////////////////////////////////////////////////////////////////// + // import of legacy properties + ID3v2Frame* curFrame; + XMP_Bool hasTDRC = false; + XMP_DateTime newDateTime; + + if ( this->hasID3Tag ) { // otherwise pretty pointless... + + for ( int r = 0; reconProps[r].mainID != 0; ++r ) { + + //get the frame ID to look for + XMP_Uns32 logicalID = GetUns32BE ( reconProps[r].mainID ); + XMP_Uns32 storedID = logicalID; + if ( this->majorVersion == 2 ) storedID = GetUns32BE ( reconProps[r].v22ID ); + + // deal with each such frame in the frameVector + // (since there might be several, some of them not applicable, i.e. COMM) + + vector<ID3_Support::ID3v2Frame*>::iterator it; + for ( it = this->framesVector.begin(); it != this->framesVector.end(); ++it ) { + + curFrame = *it; + if ( storedID != curFrame->id ) continue; + + // go deal with it! + // get the property + std::string utf8string; + bool result = curFrame->getFrameValue ( this->majorVersion, logicalID, &utf8string ); + if ( ! result ) continue; //ignore but preserve this frame (i.e. not applicable COMM frame) + + ////////////////////////////////////////////////////////////////////////////////// + // if we come as far as here, it's proven that there's a relevant XMP property + + this->containsXMP = true; + + ID3_Support::ID3v2Frame* t = this->framesMap [ storedID ]; + if ( t != 0 ) t->active = false; + + // add this to map (needed on reconciliation) + // note: above code reaches, that COMM/USLT frames + // only then reach this map, if they are 'eng'(lish) + // multiple occurences indeed leads to last one survives + // ( in this map, all survive in the file ) + this->framesMap [ storedID ] = curFrame; + + // now write away as needed; + // merely based on existence, relevant even if empty: + if ( logicalID == 0x54434D50) { // TCMP if exists: part of compilation + + this->xmpObj.SetProperty ( kXMP_NS_DM, "partOfCompilation", "true" ); + + } else if ( ! utf8string.empty() ) { + + switch ( logicalID ) { + + case 0x54495432: // TIT2 -> title["x-default"] + case 0x54434F50: // TCOP -> rights["x-default"] + this->xmpObj.SetLocalizedText ( reconProps[r].ns, reconProps[r].prop,"", "x-default", utf8string ); + break; + + case 0x54434F4E: // TCON -> genre ( might be numeric string. prior to 2.3 a one-byte numeric value? ) + { + XMP_Int32 pos = 0; // going through input string + if ( utf8string[pos] == '(' ) { // number in brackets? + pos++; + XMP_Uns8 iGenre = (XMP_Uns8) atoi( &utf8string.c_str()[1] ); + if ( iGenre < 127 ) { + utf8string.assign( Genres[iGenre] ); + } else { + utf8string.assign( Genres[12] ); // "Other" + } + } else { + // Text, let's "try" to find it anyway (for best upper/lower casing) + int i; + const char* genreCString = utf8string.c_str(); + for ( i = 0; i < 127; ++i ) { + if ( (strlen ( genreCString ) == strlen(Genres[i])) && //fixing buggy stricmp behaviour on PPC + (stricmp ( genreCString, Genres[i] ) == 0 )) { + utf8string.assign ( Genres[i] ); // found, let's use the one in the list + break; + } + } + // otherwise (if for-loop runs through): leave as is + } + // write out property (derived or direct, but certainly non-numeric) + this->xmpObj.SetProperty ( reconProps[r].ns, reconProps[r].prop, utf8string ); + } + break; + + case 0x54594552: // TYER -> xmp:CreateDate.year + { + try { // Don't let wrong dates in id3 stop import. + if ( ! hasTDRC ) { + newDateTime.year = SXMPUtils::ConvertToInt ( utf8string ); + newDateTime.hasDate = true; + } + } catch ( ... ) { + // Do nothing, let other imports proceed. + } + break; + } + + case 0x54444154: //TDAT -> xmp:CreateDate.month and day + { + try { // Don't let wrong dates in id3 stop import. + // only if no TDRC has been found before + //&& must have the format DDMM + if ( (! hasTDRC) && (utf8string.length() == 4) ) { + newDateTime.day = SXMPUtils::ConvertToInt (utf8string.substr(0,2)); + newDateTime.month = SXMPUtils::ConvertToInt ( utf8string.substr(2,2)); + newDateTime.hasDate = true; + } + } catch ( ... ) { + // Do nothing, let other imports proceed. + } + break; + } + + case 0x54494D45: //TIME -> xmp:CreateDate.hours and minutes + { + try { // Don't let wrong dates in id3 stop import. + // only if no TDRC has been found before + // && must have the format HHMM + if ( (! hasTDRC) && (utf8string.length() == 4) ) { + newDateTime.hour = SXMPUtils::ConvertToInt (utf8string.substr(0,2)); + newDateTime.minute = SXMPUtils::ConvertToInt ( utf8string.substr(2,2)); + newDateTime.hasTime = true; + } + } catch ( ... ) { + // Do nothing, let other imports proceed. + } + break; + } + + case 0x54445243: // TDRC -> xmp:CreateDate //id3 v2.4 + { + try { // Don't let wrong dates in id3 stop import. + hasTDRC = true; + // This always wins over TYER, TDAT and TIME + SXMPUtils::ConvertToDate ( utf8string, &newDateTime ); + } catch ( ... ) { + // Do nothing, let other imports proceed. + } + break; + } + + default: + // NB: COMM/USLT need no special fork regarding language alternatives/multiple occurence. + // relevant code forks are in ID3_Support::getFrameValue() + this->xmpObj.SetProperty ( reconProps[r].ns, reconProps[r].prop, utf8string ); + break; + + }//switch + + } + + } //for iterator + + }//for reconProps + + // import DateTime + XMP_DateTime oldDateTime; + xmpObj.GetProperty_Date ( kXMP_NS_XMP, "CreateDate", &oldDateTime, 0 ); + + // NOTE: no further validation nessesary the function "SetProperty_Date" will care about validating date and time + // any exception will be caught and block import + try { + bool haveNewDateTime = (newDateTime.year != 0) && + ( (newDateTime.year != oldDateTime.year) || + ( (newDateTime.month != 0 ) && ( (newDateTime.day != oldDateTime.day) || (newDateTime.month != oldDateTime.month) ) ) || + ( newDateTime.hasTime && ( (newDateTime.hour != oldDateTime.minute) || (newDateTime.hour != oldDateTime.minute) ) ) ); + if ( haveNewDateTime ) { + this->xmpObj.SetProperty_Date ( kXMP_NS_XMP, "CreateDate", newDateTime ); + } + } catch ( ... ) { + // Dont import invalid dates from ID3 + } + + } + + // very important to avoid multiple runs! (in which case I'd need to clean certain + // fields (i.e. regarding ->active setting) + this->processedXMP = true; + +} // MP3_MetaHandler::ProcessXMP + + +// ================================================================================================= +// MP3_MetaHandler::UpdateFile +// =========================== +void MP3_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + if ( doSafeUpdate ) XMP_Throw ( "MP3_MetaHandler::UpdateFile: Safe update not supported", kXMPErr_Unavailable ); + + XMP_IO* file = this->parent->ioRef; + + // leave 2.3 resp. 2.4 header, since we want to let alone + // and don't know enough about the encoding of unrelated frames... + XMP_Assert( this->containsXMP ); + + tagIsDirty = false; + mustShift = false; + + // write out native properties: + // * update existing ones + // * create new frames as needed + // * delete frames if property is gone! + // see what there is to do for us: + + // RECON LOOP START + for (int r = 0; reconProps[r].mainID != 0; r++ ) { + + std::string value; + bool needDescriptor = false; + bool need16LE = true; + bool needEncodingByte = true; + + XMP_Uns32 logicalID = GetUns32BE ( reconProps[r].mainID ); + XMP_Uns32 storedID = logicalID; + if ( this->majorVersion == 2 ) storedID = GetUns32BE ( reconProps[r].v22ID ); + + ID3v2Frame* frame = framesMap[ storedID ]; // the actual frame (if already existing) + + // get XMP property + // * honour specific exceptions + // * leave value empty() if it doesn't exist ==> frame must be delete/not created + switch ( logicalID ) { + + case 0x54434D50: // TCMP if exists: part of compilation + need16LE = false; + if ( xmpObj.GetProperty( kXMP_NS_DM, "partOfCompilation", &value, 0 ) && ( 0 == stricmp( value.c_str(), "true" ) )) { + value = "1"; // set a TCMP frame of value 1 + } else { + value.erase(); // delete/prevent creation of frame + } + break; + + case 0x54495432: // TIT2 -> title["x-default"] + case 0x54434F50: // TCOP -> rights["x-default"] + if (! xmpObj.GetLocalizedText( reconProps[r].ns, reconProps[r].prop, "", "x-default", 0, &value, 0 )) value.erase(); // if not, erase string. + break; + + case 0x54434F4E: // TCON -> genre + { + if (! xmpObj.GetProperty( reconProps[r].ns, reconProps[r].prop, &value, 0 )) { // nothing found? -> Erase string. (Leads to Unset below) + value.erase(); + break; + } + // genre: we need to get the number back, if possible + XMP_Int16 iFound = -1; // flag as "not found" + for ( int i=0; i < 127; ++i ) { + if ( (value.size() == strlen(Genres[i])) && (stricmp( value.c_str(), Genres[i] ) == 0) ) { + iFound = i; // Found + break; + } + } + if ( iFound == -1 ) break; // stick with the literal value (also for v2.3, since this is common practice!) + + need16LE = false; // no unicode need for (##) + char strGenre[64]; + snprintf ( strGenre, sizeof(strGenre), "(%d)", iFound ); // AUDIT: Using sizeof(strGenre) is safe. + value.assign(strGenre); + } + break; + + case 0x434F4D4D: // COMM + case 0x55534C54: // USLT, both need descriptor. + needDescriptor = true; + if (! xmpObj.GetProperty( reconProps[r].ns, reconProps[r].prop, &value, 0 )) value.erase(); + break; + + case 0x54594552: //TYER + case 0x54444154: //TDAT + case 0x54494D45: //TIME + { + if ( majorVersion <= 3 ) { // TYER, TIME and TDAT depricated since v. 2.4 -> else use TDRC + + XMP_DateTime dateTime; + if (! xmpObj.GetProperty_Date( reconProps[r].ns, reconProps[r].prop, &dateTime, 0 )) { // nothing found? -> Erase string. (Leads to Unset below) + value.erase(); + break; + } + + // TYER + if ( logicalID == 0x54594552 ) { + XMP_Validate( dateTime.year <= 9999 && dateTime.year > 0, "Year is out of range", kXMPErr_BadParam); + // get only Year! + SXMPUtils::ConvertFromInt( dateTime.year, "", &value ); + break; + } else if ( logicalID == 0x54444154 && dateTime.hasDate ) { + std::string day, month; + SXMPUtils::ConvertFromInt( dateTime.day, "", &day ); + SXMPUtils::ConvertFromInt( dateTime.month, "", &month ); + if ( dateTime.day < 10 ) + value = "0"; + value += day; + if ( dateTime.month < 10 ) + value += "0"; + value += month; + break; + } else if ( logicalID == 0x54494D45 && dateTime.hasTime ) { + std::string hour, minute; + SXMPUtils::ConvertFromInt( dateTime.hour, "", &hour ); + SXMPUtils::ConvertFromInt( dateTime.minute, "", &minute ); + if ( dateTime.hour < 10 ) + value = "0"; + value += hour; + if ( dateTime.minute < 10 ) + value += "0"; + value += minute; + break; + } else { + value.erase(); + break; + } + } else { + value.erase(); + break; + } + } + break; + + case 0x54445243: //TDRC (only v2.4) + { + // only export for id3 > v2.4 + if ( majorVersion > 3 ) { + if (! xmpObj.GetProperty( reconProps[r].ns, reconProps[r].prop, &value, 0 )) value.erase(); + } + break; + } + break; + + case 0x57434F50: //WCOP + needEncodingByte = false; + need16LE = false; + if (! xmpObj.GetProperty( reconProps[r].ns, reconProps[r].prop, &value, 0 )) value.erase(); // if not, erase string + break; + + case 0x5452434B: // TRCK + case 0x54504F53: // TPOS + need16LE = false; + // no break, go on: + + default: + if (! xmpObj.GetProperty( reconProps[r].ns, reconProps[r].prop, &value, 0 )) value.erase(); // if not, erase string + break; + + } + + // [XMP exist] x [frame exist] => four cases: + // 1/4) nothing before, nothing now + if ( value.empty() && (frame==0)) continue; // nothing to do + + // all else means there will be rewrite work to do: + tagIsDirty = true; + + // 2/4) value before, now gone: + if ( value.empty() && (frame!=0)) { + frame->active = false; //mark for non-use + continue; + } + + // 3/4) no old value, create new value + if ( frame != 0 ) { + frame->setFrameValue( value, needDescriptor, need16LE, false, needEncodingByte ); + } else { + ID3v2Frame* newFrame=new ID3v2Frame( storedID ); + newFrame->setFrameValue( value, needDescriptor, need16LE, false, needEncodingByte ); //always write as utf16-le incl. BOM + framesVector.push_back( newFrame ); + framesMap[ storedID ] = newFrame; + continue; + } + + } // RECON LOOP END + + ///////////////////////////////////////////////////////////////////////////////// + // (Re)Build XMP frame: + + XMP_Uns32 xmpID = XMP_V23_ID; + if ( this->majorVersion == 2 ) xmpID = XMP_V22_ID; + + ID3v2Frame* frame = framesMap[ xmpID ]; + if ( frame != 0 ) { + frame->setFrameValue( this->xmpPacket, false, false, true ); + } else { + ID3v2Frame* newFrame=new ID3v2Frame( xmpID ); + newFrame->setFrameValue ( this->xmpPacket, false, false, true ); + framesVector.push_back ( newFrame ); + framesMap[ xmpID ] = newFrame; + } + + //////////////////////////////////////////////////////////////////////////////// + // Decision making + + XMP_Int32 frameHeaderSize = ID3v2Frame::kV23_FrameHeaderSize; + if ( this->majorVersion == 2 ) frameHeaderSize = ID3v2Frame::kV22_FrameHeaderSize; + + newFramesSize = 0; + for ( XMP_Uns32 i = 0; i < framesVector.size(); i++ ) { + if ( framesVector[i]->active ) newFramesSize += (frameHeaderSize + framesVector[i]->contentSize); + } + + mustShift = (newFramesSize > (oldTagSize - ID3Header::kID3_TagHeaderSize)) || + //optimization: If more than 8K can be saved by rewriting the MP3, go do it: + ((newFramesSize + 8*1024) < oldTagSize ); + + if ( ! mustShift ) { // fill what we got + newTagSize = oldTagSize; + } else { // if need to shift anyway, get some nice 2K padding + newTagSize = newFramesSize + 2048 + ID3Header::kID3_TagHeaderSize; + } + newPadding = newTagSize - ID3Header::kID3_TagHeaderSize - newFramesSize; + + // shifting needed? -> shift + if ( mustShift ) { + XMP_Int64 filesize = file ->Length(); + if ( this->hasID3Tag ) { + XIO::Move ( file, oldTagSize, file, newTagSize, filesize - oldTagSize ); //fix [2338569] + } else { + XIO::Move ( file, 0, file, newTagSize, filesize ); // move entire file up. + } + } + + // correct size stuff, write out header + file ->Rewind(); + id3Header.write ( file, newTagSize ); + + // write out tags + for ( XMP_Uns32 i = 0; i < framesVector.size(); i++ ) { + if ( framesVector[i]->active ) framesVector[i]->write ( file, majorVersion ); + } + + // write out padding: + for ( XMP_Int64 i = newPadding; i > 0; ) { + const XMP_Uns64 zero = 0; + if ( i >= 8 ) { + file->Write ( &zero, 8 ); + i -= 8; + continue; + } + file->Write ( &zero, 1 ); + i--; + } + + // check end of file for ID3v1 tag + XMP_Int64 possibleTruncationPoint = file->Seek ( -128, kXMP_SeekFromEnd ); + bool alreadyHasID3v1 = (XIO::ReadInt32_BE( file ) & 0xFFFFFF00) == 0x54414700; // "TAG" + if ( ! alreadyHasID3v1 ) file->Seek ( 128, kXMP_SeekFromEnd ); // Seek will extend the file. + id3v1Tag.write( file, &this->xmpObj ); + + this->needsUpdate = false; //do last for safety reasons + +} // MP3_MetaHandler::UpdateFile + +// ================================================================================================= +// MP3_MetaHandler::WriteTempFile +// ============================== + +void MP3_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + IgnoreParam(tempRef); + XMP_Throw ( "MP3_MetaHandler::WriteTempFile: Not supported", kXMPErr_Unimplemented ); +} // MP3_MetaHandler::WriteTempFile diff --git a/XMPFiles/source/FileHandlers/MP3_Handler.hpp b/XMPFiles/source/FileHandlers/MP3_Handler.hpp new file mode 100644 index 0000000..261ca3e --- /dev/null +++ b/XMPFiles/source/FileHandlers/MP3_Handler.hpp @@ -0,0 +1,93 @@ +#ifndef __MP3_Handler_hpp__ +#define __MP3_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2008 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "XMPFiles/source/FormatSupport/ID3_Support.hpp" + +using namespace std; +using namespace ID3_Support; + +extern XMPFileHandler * MP3_MetaHandlerCTor ( XMPFiles * parent ); + +extern bool MP3_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ); + +static const XMP_OptionBits kMP3_HandlerFlags = (kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_PrefersInPlace | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket| + kXMPFiles_CanReconcile); + +class MP3_MetaHandler : public XMPFileHandler +{ +public: + MP3_MetaHandler ( XMPFiles * parent ); + ~MP3_MetaHandler(); + + void CacheFileData(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + void ProcessXMP(); + +private: + //////////////////////////////////////////////////////////////////////////////////// + // instance vars + XMP_Int64 oldTagSize; // the entire tag, including padding, including 10B header + XMP_Int64 oldPadding; // number of padding bytes + XMP_Int64 oldFramesSize; // actual space needed by frames := oldTagSize - 10 - oldPadding + + XMP_Int64 newTagSize; + XMP_Int64 newPadding; + XMP_Int64 newFramesSize; + + // decision making: + bool tagIsDirty; // true, if any legacy properties changed. + bool mustShift; // entire tag to rewrite? (or possibly just XMP?) + + + XMP_Uns8 majorVersion, minorVersion; // Version Number post ID3v2, i.e. 3 0 ==> ID3v2.3.0 + bool hasID3Tag; //incoming file has an ID3 tag? + bool hasFooter; + bool legacyChanged; // tag rewrite certainly needed? + + ID3Header id3Header; + + XMP_Int64 extHeaderSize; + bool hasExtHeader; + + // the frames + // * all to be kept till write-out + // * parsed/understood only if needed + // * vector used to free memory in handler destructor + std::vector<ID3_Support::ID3v2Frame*> framesVector; + + // ID3v1 - treated as a single object + ID3v1Tag id3v1Tag; + + // * also kept in a map for better import<->export access + // * only keeps legacy 'relevant frames' (i.e. no abused COMM frames) + // * only keeps last relevant frame + // * earlier 'relevant frames' will be deleted. This map also helps in this + // * key shall be the FrameID, always interpreted as BE + std::map<XMP_Uns32,ID3_Support::ID3v2Frame*> framesMap; + +}; // MP3_MetaHandler + +// ================================================================================================= + +#endif /* __MP3_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/MPEG2_Handler.cpp b/XMPFiles/source/FileHandlers/MPEG2_Handler.cpp new file mode 100644 index 0000000..cd8512e --- /dev/null +++ b/XMPFiles/source/FileHandlers/MPEG2_Handler.cpp @@ -0,0 +1,220 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2005 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#if XMP_WinBuild + #pragma warning ( disable : 4996 ) // '...' was declared deprecated +#endif + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/MPEG2_Handler.hpp" + +using namespace std; + +// ================================================================================================= +/// \file MPEG2_Handler.cpp +/// \brief File format handler for MPEG2. +/// +/// BLECH! YUCK! GAG! MPEG-2 is done using a sidecar and recognition only by file extension! BARF!!!!! +/// +// ================================================================================================= + +// ================================================================================================= +// FindFileExtension +// ================= + +static inline XMP_StringPtr FindFileExtension ( XMP_StringPtr filePath ) +{ + + XMP_StringPtr pathEnd = filePath + strlen(filePath); + XMP_StringPtr extPtr; + + for ( extPtr = pathEnd-1; extPtr > filePath; --extPtr ) { + if ( (*extPtr == '.') || (*extPtr == '/') ) break; + #if XMP_WinBuild + if ( (*extPtr == '\\') || (*extPtr == ':') ) break; + #endif + } + + if ( (extPtr < filePath) || (*extPtr != '.') ) return pathEnd; + return extPtr; + +} // FindFileExtension + +// ================================================================================================= +// MPEG2_MetaHandlerCTor +// ===================== + +XMPFileHandler * MPEG2_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new MPEG2_MetaHandler ( parent ); + +} // MPEG2_MetaHandlerCTor + +// ================================================================================================= +// MPEG2_CheckFormat +// ================= + +// The MPEG-2 handler uses just the file extension, not the file content. Worse yet, it also uses a +// sidecar file for the XMP. This works better if the handler owns the file, we open the sidecar +// instead of the actual MPEG-2 file. + +bool MPEG2_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ) +{ + IgnoreParam(format); IgnoreParam(filePath); IgnoreParam(fileRef); + + XMP_Assert ( (format == kXMP_MPEGFile) || (format == kXMP_MPEG2File) ); + XMP_Assert ( fileRef == 0 ); + + return ( (parent->format == kXMP_MPEGFile) || (parent->format == kXMP_MPEG2File) ); // ! Just use the first call's format hint. + +} // MPEG2_CheckFormat + +// ================================================================================================= +// MPEG2_MetaHandler::MPEG2_MetaHandler +// ==================================== + +MPEG2_MetaHandler::MPEG2_MetaHandler ( XMPFiles * _parent ) +{ + this->parent = _parent; + this->handlerFlags = kMPEG2_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + +} // MPEG2_MetaHandler::MPEG2_MetaHandler + +// ================================================================================================= +// MPEG2_MetaHandler::~MPEG2_MetaHandler +// ===================================== + +MPEG2_MetaHandler::~MPEG2_MetaHandler() +{ + // Nothing to do. + +} // MPEG2_MetaHandler::~MPEG2_MetaHandler + +// ================================================================================================= +// MPEG2_MetaHandler::GetFileModDate +// ================================= + +bool MPEG2_MetaHandler::GetFileModDate ( XMP_DateTime * modDate ) +{ + + XMP_StringPtr filePath = this->parent->filePath.c_str(); + XMP_StringPtr extPtr = FindFileExtension ( filePath ); + this->sidecarPath.assign ( filePath, (extPtr - filePath) ); + this->sidecarPath += ".xmp"; + + if ( ! Host_IO::Exists ( this->sidecarPath.c_str() ) ) return false; + return Host_IO::GetModifyDate ( this->sidecarPath.c_str(), modDate ); + +} // MPEG2_MetaHandler::GetFileModDate + +// ================================================================================================= +// MPEG2_MetaHandler::CacheFileData +// ================================ + +void MPEG2_MetaHandler::CacheFileData() +{ + bool readOnly = (! (this->parent->openFlags & kXMPFiles_OpenForUpdate)); + + if ( this->parent->UsesClientIO() ) { + XMP_Throw ( "MPEG2 cannot be used with client-managed I/O", kXMPErr_InternalFailure ); + } + + this->containsXMP = false; + this->processedXMP = true; // Whatever we do here is all that we do for XMPFiles::OpenFile. + + // Try to open the sidecar XMP file. Tolerate an open failure, there might not be any XMP. + // Note that MPEG2_CheckFormat can't save the sidecar path because the handler doesn't exist then. + + XMP_StringPtr filePath = this->parent->filePath.c_str(); + XMP_StringPtr extPtr = FindFileExtension ( filePath ); + this->sidecarPath.assign ( filePath, (extPtr - filePath) ); + this->sidecarPath += ".xmp"; + + if ( ! Host_IO::Exists ( this->sidecarPath.c_str() ) ) return; // OK to not have XMP. + + XMPFiles_IO * localFile = XMPFiles_IO::New_XMPFiles_IO ( this->sidecarPath.c_str(), readOnly ); + if ( localFile == 0 ) XMP_Throw ( "Failure opening MPEG-2 XMP file", kXMPErr_ExternalFailure ); + this->parent->ioRef = localFile; + + // Extract the sidecar's contents and parse. + + this->packetInfo.offset = 0; // We take the whole sidecar file. + this->packetInfo.length = (XMP_Int32) localFile->Length(); + + if ( this->packetInfo.length > 0 ) { + + this->xmpPacket.assign ( this->packetInfo.length, ' ' ); + localFile->ReadAll ( (void*)this->xmpPacket.c_str(), this->packetInfo.length ); + + this->xmpObj.ParseFromBuffer ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + this->containsXMP = true; + + } + + if ( readOnly ) { + localFile->Close(); + delete localFile; + this->parent->ioRef = 0; + } + +} // MPEG2_MetaHandler::CacheFileData + +// ================================================================================================= +// MPEG2_MetaHandler::UpdateFile +// ============================= + +void MPEG2_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + if ( ! this->needsUpdate ) return; + + XMP_Assert ( this->parent->UsesLocalIO() ); + + if ( this->parent->ioRef == 0 ) { + XMP_Assert ( ! Host_IO::Exists ( this->sidecarPath.c_str() ) ); + Host_IO::Create ( this->sidecarPath.c_str() ); + this->parent->ioRef = XMPFiles_IO::New_XMPFiles_IO ( this->sidecarPath.c_str(), Host_IO::openReadWrite ); + if ( this->parent->ioRef == 0 ) XMP_Throw ( "Failure opening MPEG-2 XMP file", kXMPErr_ExternalFailure ); + } + + XMP_IO* fileRef = this->parent->ioRef; + XMP_Assert ( fileRef != 0 ); + XIO::ReplaceTextFile ( fileRef, this->xmpPacket, doSafeUpdate ); + + XMPFiles_IO* localFile = (XMPFiles_IO*)fileRef; + localFile->Close(); + delete localFile; + this->parent->ioRef = 0; + + this->needsUpdate = false; + +} // MPEG2_MetaHandler::UpdateFile + +// ================================================================================================= +// MPEG2_MetaHandler::WriteTempFile +// ================================ + +void MPEG2_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + IgnoreParam(tempRef); + + XMP_Throw ( "MPEG2_MetaHandler::WriteTempFile: Should never be called", kXMPErr_Unavailable ); + +} // MPEG2_MetaHandler::WriteTempFile diff --git a/XMPFiles/source/FileHandlers/MPEG2_Handler.hpp b/XMPFiles/source/FileHandlers/MPEG2_Handler.hpp new file mode 100644 index 0000000..be6d38b --- /dev/null +++ b/XMPFiles/source/FileHandlers/MPEG2_Handler.hpp @@ -0,0 +1,65 @@ +#ifndef __MPEG2_Handler_hpp__ +#define __MPEG2_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2005 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +// ================================================================================================= +/// \file MPEG2_Handler.hpp +/// \brief File format handler for MPEG2. +/// +/// This header ... +/// +// ================================================================================================= + +extern XMPFileHandler * MPEG2_MetaHandlerCTor ( XMPFiles * parent ); + +extern bool MPEG2_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO * fileRef, + XMPFiles * parent); + +static const XMP_OptionBits kMPEG2_HandlerFlags = ( kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_CanRewrite | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_HandlerOwnsFile | + kXMPFiles_AllowsSafeUpdate | + kXMPFiles_UsesSidecarXMP ); + +class MPEG2_MetaHandler : public XMPFileHandler +{ +public: + + std::string sidecarPath; + + MPEG2_MetaHandler ( XMPFiles * parent ); + ~MPEG2_MetaHandler(); + + bool GetFileModDate ( XMP_DateTime * modDate ); + + void CacheFileData(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO * tempRef ); + + +}; // MPEG2_MetaHandler + +// ================================================================================================= + +#endif /* __MPEG2_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/MPEG4_Handler.cpp b/XMPFiles/source/FileHandlers/MPEG4_Handler.cpp new file mode 100644 index 0000000..fad238f --- /dev/null +++ b/XMPFiles/source/FileHandlers/MPEG4_Handler.cpp @@ -0,0 +1,2581 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/MPEG4_Handler.hpp" + +#include "XMPFiles/source/FormatSupport/ISOBaseMedia_Support.hpp" +#include "XMPFiles/source/FormatSupport/MOOV_Support.hpp" + +#include "source/UnicodeConversions.hpp" +#include "third-party/zuid/interfaces/MD5.h" + +#if XMP_WinBuild + #pragma warning ( disable : 4996 ) // '...' was declared deprecated +#endif + +using namespace std; + +// ================================================================================================= +/// \file MPEG4_Handler.cpp +/// \brief File format handler for MPEG-4, a flavor of the ISO Base Media File Format. +/// +/// This handler ... +/// +// ================================================================================================= + +// The basic content of a timecode sample description table entry. Does not include trailing boxes. + +#pragma pack ( push, 1 ) + +struct stsdBasicEntry { + XMP_Uns32 entrySize; + XMP_Uns32 format; + XMP_Uns8 reserved_1 [6]; + XMP_Uns16 dataRefIndex; + XMP_Uns32 reserved_2; + XMP_Uns32 flags; + XMP_Uns32 timeScale; + XMP_Uns32 frameDuration; + XMP_Uns8 frameCount; + XMP_Uns8 reserved_3; +}; + +#pragma pack ( pop ) + +// ================================================================================================= + +// ! The buffer and constants are both already big endian. +#define Get4CharCode(buffPtr) (*((XMP_Uns32*)(buffPtr))) + +// ================================================================================================= + +static inline bool IsClassicQuickTimeBox ( XMP_Uns32 boxType ) +{ + if ( (boxType == ISOMedia::k_moov) || (boxType == ISOMedia::k_mdat) || (boxType == ISOMedia::k_pnot) || + (boxType == ISOMedia::k_free) || (boxType == ISOMedia::k_skip) || (boxType == ISOMedia::k_wide) ) return true; + return false; +} // IsClassicQuickTimeBox + +// ================================================================================================= + +// Pairs of 3 letter ISO 639-2 codes mapped to 2 letter ISO 639-1 codes from: +// http://www.loc.gov/standards/iso639-2/php/code_list.php +// Would have to write an "==" operator to use std::map, must compare values not pointers. +// ! Not fully sorted, do not use a binary search. + +static XMP_StringPtr kKnownLangs[] = + { "aar", "aa", "abk", "ab", "afr", "af", "aka", "ak", "alb", "sq", "sqi", "sq", "amh", "am", + "ara", "ar", "arg", "an", "arm", "hy", "hye", "hy", "asm", "as", "ava", "av", "ave", "ae", + "aym", "ay", "aze", "az", "bak", "ba", "bam", "bm", "baq", "eu", "eus", "eu", "bel", "be", + "ben", "bn", "bih", "bh", "bis", "bi", "bod", "bo", "tib", "bo", "bos", "bs", "bre", "br", + "bul", "bg", "bur", "my", "mya", "my", "cat", "ca", "ces", "cs", "cze", "cs", "cha", "ch", + "che", "ce", "chi", "zh", "zho", "zh", "chu", "cu", "chv", "cv", "cor", "kw", "cos", "co", + "cre", "cr", "cym", "cy", "wel", "cy", "cze", "cs", "ces", "cs", "dan", "da", "deu", "de", + "ger", "de", "div", "dv", "dut", "nl", "nld", "nl", "dzo", "dz", "ell", "el", "gre", "el", + "eng", "en", "epo", "eo", "est", "et", "eus", "eu", "baq", "eu", "ewe", "ee", "fao", "fo", + "fas", "fa", "per", "fa", "fij", "fj", "fin", "fi", "fra", "fr", "fre", "fr", "fre", "fr", + "fra", "fr", "fry", "fy", "ful", "ff", "geo", "ka", "kat", "ka", "ger", "de", "deu", "de", + "gla", "gd", "gle", "ga", "glg", "gl", "glv", "gv", "gre", "el", "ell", "el", "grn", "gn", + "guj", "gu", "hat", "ht", "hau", "ha", "heb", "he", "her", "hz", "hin", "hi", "hmo", "ho", + "hrv", "hr", "scr", "hr", "hun", "hu", "hye", "hy", "arm", "hy", "ibo", "ig", "ice", "is", + "isl", "is", "ido", "io", "iii", "ii", "iku", "iu", "ile", "ie", "ina", "ia", "ind", "id", + "ipk", "ik", "isl", "is", "ice", "is", "ita", "it", "jav", "jv", "jpn", "ja", "kal", "kl", + "kan", "kn", "kas", "ks", "kat", "ka", "geo", "ka", "kau", "kr", "kaz", "kk", "khm", "km", + "kik", "ki", "kin", "rw", "kir", "ky", "kom", "kv", "kon", "kg", "kor", "ko", "kua", "kj", + "kur", "ku", "lao", "lo", "lat", "la", "lav", "lv", "lim", "li", "lin", "ln", "lit", "lt", + "ltz", "lb", "lub", "lu", "lug", "lg", "mac", "mk", "mkd", "mk", "mah", "mh", "mal", "ml", + "mao", "mi", "mri", "mi", "mar", "mr", "may", "ms", "msa", "ms", "mkd", "mk", "mac", "mk", + "mlg", "mg", "mlt", "mt", "mol", "mo", "mon", "mn", "mri", "mi", "mao", "mi", "msa", "ms", + "may", "ms", "mya", "my", "bur", "my", "nau", "na", "nav", "nv", "nbl", "nr", "nde", "nd", + "ndo", "ng", "nep", "ne", "nld", "nl", "dut", "nl", "nno", "nn", "nob", "nb", "nor", "no", + "nya", "ny", "oci", "oc", "oji", "oj", "ori", "or", "orm", "om", "oss", "os", "pan", "pa", + "per", "fa", "fas", "fa", "pli", "pi", "pol", "pl", "por", "pt", "pus", "ps", "que", "qu", + "roh", "rm", "ron", "ro", "rum", "ro", "rum", "ro", "ron", "ro", "run", "rn", "rus", "ru", + "sag", "sg", "san", "sa", "scc", "sr", "srp", "sr", "scr", "hr", "hrv", "hr", "sin", "si", + "slk", "sk", "slo", "sk", "slo", "sk", "slk", "sk", "slv", "sl", "sme", "se", "smo", "sm", + "sna", "sn", "snd", "sd", "som", "so", "sot", "st", "spa", "es", "sqi", "sq", "alb", "sq", + "srd", "sc", "srp", "sr", "scc", "sr", "ssw", "ss", "sun", "su", "swa", "sw", "swe", "sv", + "tah", "ty", "tam", "ta", "tat", "tt", "tel", "te", "tgk", "tg", "tgl", "tl", "tha", "th", + "tib", "bo", "bod", "bo", "tir", "ti", "ton", "to", "tsn", "tn", "tso", "ts", "tuk", "tk", + "tur", "tr", "twi", "tw", "uig", "ug", "ukr", "uk", "urd", "ur", "uzb", "uz", "ven", "ve", + "vie", "vi", "vol", "vo", "wel", "cy", "cym", "cy", "wln", "wa", "wol", "wo", "xho", "xh", + "yid", "yi", "yor", "yo", "zha", "za", "zho", "zh", "chi", "zh", "zul", "zu", + 0, 0 }; + +static inline XMP_StringPtr Lookup2LetterLang ( XMP_StringPtr lang3 ) +{ + for ( size_t i = 0; kKnownLangs[i] != 0; i += 2 ) { + if ( XMP_LitMatch ( lang3, kKnownLangs[i] ) ) return kKnownLangs[i+1]; + } + return ""; +} + +static inline XMP_StringPtr Lookup3LetterLang ( XMP_StringPtr lang2 ) +{ + for ( size_t i = 0; kKnownLangs[i] != 0; i += 2 ) { + if ( XMP_LitMatch ( lang2, kKnownLangs[i+1] ) ) return kKnownLangs[i]; + } + return ""; +} + +// ================================================================================================= +// MPEG4_CheckFormat +// ================= +// +// There are 3 variations of recognized file: +// - Normal MPEG-4 - has an 'ftyp' box containing a known compatible brand but not 'qt '. +// - Modern QuickTime - has an 'ftyp' box containing 'qt ' as a compatible brand. +// - Classic QuickTime - has no 'ftyp' box, should have recognized top level boxes. +// +// An MPEG-4 or modern QuickTime file is an instance of an ISO Base Media file, ISO 14496-12 and -14. +// A classic QuickTime file has the same physical box structure, but somewhat different box types. +// The ISO files must begin with an 'ftyp' box containing 'mp41', 'mp42', 'f4v ', or 'qt ' in the +// compatible brands. +// +// The general box structure is: +// +// 0 4 uns32 box size, 0 means "to EoF", 1 means 64-bit size follows +// 4 4 uns32 box type +// 8 8 uns64 box size, present only if uns32 size is 1 +// - * box content +// +// The 'ftyp' box content is: +// +// - 4 uns32 major brand +// - 4 uns32 minor version +// - * uns32 sequence of compatible brands, to the end of the box + +// ! With the addition of QuickTime support there is some change in behavior in OpenFile when the +// ! kXMPFiles_OpenStrictly option is used wth a specific file format. The kXMP_MPEG4File and +// ! kXMP_MOVFile formats are treated uniformly, you can't force "real MOV" or "real MPEG-4". You +// ! can check afterwards using GetFileInfo to see what the file happens to be. + +bool MPEG4_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles* parent ) +{ + XMP_Uns8 buffer [4*1024]; + XMP_Uns32 ioCount, brandCount, brandOffset; + XMP_Uns64 fileSize, nextOffset; + ISOMedia::BoxInfo currBox; + + #define IsTolerableBoxChar(ch) ( ((0x20 <= (ch)) && ((ch) <= 0x7E)) || ((ch) == 0xA9) ) + + XMP_AbortProc abortProc = parent->abortProc; + void * abortArg = parent->abortArg; + const bool checkAbort = (abortProc != 0); + + bool openStrictly = XMP_OptionIsSet ( parent->openFlags, kXMPFiles_OpenStrictly); + + // Get the first box's info, see if it is 'ftyp' or not. + + XMP_Assert ( (parent->tempPtr == 0) && (parent->tempUI32 == 0) ); + + fileSize = fileRef->Length(); + if ( fileSize < 8 ) return false; + + nextOffset = ISOMedia::GetBoxInfo ( fileRef, 0, fileSize, &currBox ); + if ( currBox.headerSize < 8 ) return false; // Can't be an ISO or QuickTime file. + + if ( currBox.boxType == ISOMedia::k_ftyp ) { + + // Have an 'ftyp' box, look through the compatible brands. If 'qt ' is present then this is + // a modern QuickTime file, regardless of what else is found. Otherwise this is plain ISO if + // any of the other recognized brands are found. + + if ( currBox.contentSize < 12 ) return false; // No compatible brands at all. + if ( currBox.contentSize > 1024*1024 ) return false; // Sanity check and make sure count fits in 32 bits. + brandCount = ((XMP_Uns32)currBox.contentSize - 8) >> 2; + + fileRef->Seek ( 8, kXMP_SeekFromCurrent ); // Skip the major and minor brands. + ioCount = brandOffset = 0; + + bool haveCompatibleBrand = false; + + for ( ; brandCount > 0; --brandCount, brandOffset += 4 ) { + + if ( brandOffset >= ioCount ) { + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "MPEG4_CheckFormat - User abort", kXMPErr_UserAbort ); + } + ioCount = 4 * brandCount; + if ( ioCount > sizeof(buffer) ) ioCount = sizeof(buffer); + ioCount = fileRef->ReadAll ( buffer, ioCount ); + brandOffset = 0; + } + + XMP_Uns32 brand = GetUns32BE ( &buffer[brandOffset] ); + if ( brand == ISOMedia::k_qt ) { // Don't need to look further. + if ( openStrictly && (format != kXMP_MOVFile) ) return false; + parent->format = kXMP_MOVFile; + parent->tempUI32 = MOOV_Manager::kFileIsModernQT; + return true; + } else if ( (brand == ISOMedia::k_mp41) || (brand == ISOMedia::k_mp42) || (brand == ISOMedia::k_f4v) ) { + haveCompatibleBrand = true; // Need to keep looking in case 'qt ' follows. + } + + } + + if ( ! haveCompatibleBrand ) return false; + if ( openStrictly && (format != kXMP_MPEG4File) ) return false; + parent->format = kXMP_MPEG4File; + parent->tempUI32 = MOOV_Manager::kFileIsNormalISO; + return true; + + } else { + + // No 'ftyp', look for classic QuickTime: 'moov', 'mdat', 'pnot', 'free', 'skip', and 'wide'. + // As an expedient, quit when a 'moov' box is found. Tolerate other boxes, they are in the + // wild for ill-formed files, e.g. seen when 'moov'/'udta' children get left at top level. + + while ( currBox.boxType != ISOMedia::k_moov ) { + + if ( ! IsClassicQuickTimeBox ( currBox.boxType ) ) { + // Make sure the box type is 4 ASCII characters or 0xA9 (MacRoman copyright). + XMP_Uns8 b1 = (XMP_Uns8) (currBox.boxType >> 24); + XMP_Uns8 b2 = (XMP_Uns8) ((currBox.boxType >> 16) & 0xFF); + XMP_Uns8 b3 = (XMP_Uns8) ((currBox.boxType >> 8) & 0xFF); + XMP_Uns8 b4 = (XMP_Uns8) (currBox.boxType & 0xFF); + bool ok = IsTolerableBoxChar(b1) && IsTolerableBoxChar(b2) && + IsTolerableBoxChar(b3) && IsTolerableBoxChar(b4); + if ( ! ok ) return false; + } + if ( nextOffset >= fileSize ) return false; + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "MPEG4_CheckFormat - User abort", kXMPErr_UserAbort ); + } + nextOffset = ISOMedia::GetBoxInfo ( fileRef, nextOffset, fileSize, &currBox ); + + } + + if ( openStrictly && (format != kXMP_MOVFile) ) return false; + parent->format = kXMP_MOVFile; + parent->tempUI32 = MOOV_Manager::kFileIsTraditionalQT; + return true; + + } + + return false; + +} // MPEG4_CheckFormat + +// ================================================================================================= +// MPEG4_MetaHandlerCTor +// ===================== + +XMPFileHandler * MPEG4_MetaHandlerCTor ( XMPFiles * parent ) +{ + + return new MPEG4_MetaHandler ( parent ); + +} // MPEG4_MetaHandlerCTor + +// ================================================================================================= +// MPEG4_MetaHandler::MPEG4_MetaHandler +// ==================================== + +MPEG4_MetaHandler::MPEG4_MetaHandler ( XMPFiles * _parent ) + : fileMode(0), havePreferredXMP(false), xmpBoxPos(0), moovBoxPos(0), xmpBoxSize(0), moovBoxSize(0) +{ + + this->parent = _parent; // Inherited, can't set in the prefix. + this->handlerFlags = kMPEG4_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + this->fileMode = (XMP_Uns8)_parent->tempUI32; + _parent->tempUI32 = 0; + +} // MPEG4_MetaHandler::MPEG4_MetaHandler + +// ================================================================================================= +// MPEG4_MetaHandler::~MPEG4_MetaHandler +// ===================================== + +MPEG4_MetaHandler::~MPEG4_MetaHandler() +{ + + // Nothing to do. + +} // MPEG4_MetaHandler::~MPEG4_MetaHandler + +// ================================================================================================= +// SecondsToXMPDate +// ================ + +// *** ASF has similar code with different origin, should make a shared utility. + +static void SecondsToXMPDate ( XMP_Uns64 isoSeconds, XMP_DateTime * xmpDate ) +{ + memset ( xmpDate, 0, sizeof(XMP_DateTime) ); // AUDIT: Using sizeof(XMP_DateTime) is safe. + + XMP_Int32 days = (XMP_Int32) (isoSeconds / 86400); + isoSeconds -= ((XMP_Uns64)days * 86400); + + XMP_Int32 hour = (XMP_Int32) (isoSeconds / 3600); + isoSeconds -= ((XMP_Uns64)hour * 3600); + + XMP_Int32 minute = (XMP_Int32) (isoSeconds / 60); + isoSeconds -= ((XMP_Uns64)minute * 60); + + XMP_Int32 second = (XMP_Int32)isoSeconds; + + xmpDate->year = 1904; // Start with the ISO origin. + xmpDate->month = 1; + xmpDate->day = 1; + + xmpDate->day += days; // Add in the delta. + xmpDate->hour = hour; + xmpDate->minute = minute; + xmpDate->second = second; + + xmpDate->hasTimeZone = true; // ! Needed for ConvertToUTCTime to do anything. + SXMPUtils::ConvertToUTCTime ( xmpDate ); // Normalize the date/time. + +} // SecondsToXMPDate + +// ================================================================================================= +// XMPDateToSeconds +// ================ + +// *** ASF has similar code with different origin, should make a shared utility. + +static bool IsLeapYear ( XMP_Int32 year ) +{ + if ( year < 0 ) year = -year + 1; // Fold the negative years, assuming there is a year 0. + if ( (year % 4) != 0 ) return false; // Not a multiple of 4. + if ( (year % 100) != 0 ) return true; // A multiple of 4 but not a multiple of 100. + if ( (year % 400) == 0 ) return true; // A multiple of 400. + return false; // A multiple of 100 but not a multiple of 400. +} + +// ------------------------------------------------------------------------------------------------- + +static XMP_Int32 DaysInMonth ( XMP_Int32 year, XMP_Int32 month ) +{ + static XMP_Int32 daysInMonth[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + // Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec + XMP_Int32 days = daysInMonth[month]; + if ( (month == 2) && IsLeapYear(year) ) days += 1; + return days; +} + +// ------------------------------------------------------------------------------------------------- + +static void XMPDateToSeconds ( const XMP_DateTime & _xmpDate, XMP_Uns64 * isoSeconds ) +{ + XMP_DateTime xmpDate = _xmpDate; + SXMPUtils::ConvertToUTCTime ( &xmpDate ); + + XMP_Uns64 tempSeconds = (XMP_Uns64)xmpDate.second; + tempSeconds += (XMP_Uns64)xmpDate.minute * 60; + tempSeconds += (XMP_Uns64)xmpDate.hour * 3600; + + XMP_Int32 days = (xmpDate.day - 1); + + --xmpDate.month; + while ( xmpDate.month >= 1 ) { + days += DaysInMonth ( xmpDate.year, xmpDate.month ); + --xmpDate.month; + } + + --xmpDate.year; + while ( xmpDate.year >= 1904 ) { + days += (IsLeapYear ( xmpDate.year) ? 366 : 365 ); + --xmpDate.year; + } + + tempSeconds += (XMP_Uns64)days * 86400; + *isoSeconds = tempSeconds; + +} // XMPDateToSeconds + +// ================================================================================================= +// ImportMVHDItems +// =============== + +static bool ImportMVHDItems ( MOOV_Manager::BoxInfo mvhdInfo, SXMPMeta * xmp ) +{ + XMP_Assert ( mvhdInfo.boxType == ISOMedia::k_mvhd ); + if ( mvhdInfo.contentSize < 4 ) return false; // Just enough to check the version/flags at first. + + XMP_Uns8 mvhdVersion = *mvhdInfo.content; + if ( mvhdVersion > 1 ) return false; + + XMP_Uns64 creationTime, modificationTime, duration; + XMP_Uns32 timescale; + + if ( mvhdVersion == 0 ) { + + if ( mvhdInfo.contentSize < sizeof ( MOOV_Manager::Content_mvhd_0 ) ) return false; + MOOV_Manager::Content_mvhd_0 * mvhdRaw_0 = (MOOV_Manager::Content_mvhd_0*) mvhdInfo.content; + + creationTime = (XMP_Uns64) GetUns32BE ( &mvhdRaw_0->creationTime ); + modificationTime = (XMP_Uns64) GetUns32BE ( &mvhdRaw_0->modificationTime ); + timescale = GetUns32BE ( &mvhdRaw_0->timescale ); + duration = (XMP_Uns64) GetUns32BE ( &mvhdRaw_0->duration ); + + } else { + + XMP_Assert ( mvhdVersion == 1 ); + if ( mvhdInfo.contentSize < sizeof ( MOOV_Manager::Content_mvhd_1 ) ) return false; + MOOV_Manager::Content_mvhd_1 * mvhdRaw_1 = (MOOV_Manager::Content_mvhd_1*) mvhdInfo.content; + + creationTime = GetUns64BE ( &mvhdRaw_1->creationTime ); + modificationTime = GetUns64BE ( &mvhdRaw_1->modificationTime ); + timescale = GetUns32BE ( &mvhdRaw_1->timescale ); + duration = GetUns64BE ( &mvhdRaw_1->duration ); + + } + + bool haveImports = false; + XMP_DateTime xmpDate; + + if ( (creationTime >> 32) < 0xFF ) { // Sanity check for bogus date info. + SecondsToXMPDate ( creationTime, &xmpDate ); + xmp->SetProperty_Date ( kXMP_NS_XMP, "CreateDate", xmpDate ); + haveImports = true; + } + + if ( (modificationTime >> 32) < 0xFF ) { // Sanity check for bogus date info. + SecondsToXMPDate ( modificationTime, &xmpDate ); + xmp->SetProperty_Date ( kXMP_NS_XMP, "ModifyDate", xmpDate ); + haveImports = true; + } + + if ( timescale != 0 ) { // Avoid 1/0 for the scale field. + char buffer [32]; // A 64-bit number is at most 20 digits. + xmp->DeleteProperty ( kXMP_NS_DM, "duration" ); // Delete the whole struct. + snprintf ( buffer, sizeof(buffer), "%llu", duration ); // AUDIT: The buffer is big enough. + xmp->SetStructField ( kXMP_NS_DM, "duration", kXMP_NS_DM, "value", &buffer[0] ); + snprintf ( buffer, sizeof(buffer), "1/%u", timescale ); // AUDIT: The buffer is big enough. + xmp->SetStructField ( kXMP_NS_DM, "duration", kXMP_NS_DM, "scale", &buffer[0] ); + haveImports = true; + } + + return haveImports; + +} // ImportMVHDItems + +// ================================================================================================= +// ExportMVHDItems +// =============== + +static void ExportMVHDItems ( const SXMPMeta & xmp, MOOV_Manager * moovMgr ) +{ + XMP_DateTime xmpDate; + bool createFound, modifyFound; + XMP_Uns64 createSeconds = 0, modifySeconds = 0; + + MOOV_Manager::BoxInfo mvhdInfo; + MOOV_Manager::BoxRef mvhdRef = moovMgr->GetBox ( "moov/mvhd", &mvhdInfo ); + if ( (mvhdRef == 0) || (mvhdInfo.contentSize < 4) ) return; + + XMP_Uns8 version = *mvhdInfo.content; + if ( version > 1 ) return; + + createFound = xmp.GetProperty_Date ( kXMP_NS_XMP, "CreateDate", &xmpDate, 0 ); + if ( createFound ) XMPDateToSeconds ( xmpDate, &createSeconds ); + + modifyFound = xmp.GetProperty_Date ( kXMP_NS_XMP, "ModifyDate", &xmpDate, 0 ); + if ( modifyFound ) XMPDateToSeconds ( xmpDate, &modifySeconds ); + + if ( version == 1 ) { + + // Modify the v1 box in-place. + + if ( mvhdInfo.contentSize < sizeof ( MOOV_Manager::Content_mvhd_1 ) ) return; + + XMP_Uns64 oldCreate = GetUns64BE ( mvhdInfo.content + 4 ); + XMP_Uns64 oldModify = GetUns64BE ( mvhdInfo.content + 12 ); + + if ( createFound ) { + if ( createSeconds != oldCreate ) PutUns64BE ( createSeconds, ((XMP_Uns8*)mvhdInfo.content + 4) ); + moovMgr->NoteChange(); + } + if ( modifyFound ) { + if ( modifySeconds != oldModify ) PutUns64BE ( modifySeconds, ((XMP_Uns8*)mvhdInfo.content + 12) ); + moovMgr->NoteChange(); + } + + } else if ( ((createSeconds >> 32) == 0) && ((modifySeconds >> 32) == 0) ) { + + // Modify the v0 box in-place. + + if ( mvhdInfo.contentSize < sizeof ( MOOV_Manager::Content_mvhd_0 ) ) return; + + XMP_Uns32 oldCreate = GetUns32BE ( mvhdInfo.content + 4 ); + XMP_Uns32 oldModify = GetUns32BE ( mvhdInfo.content + 8 ); + + if ( createFound ) { + if ( (XMP_Uns32)createSeconds != oldCreate ) PutUns32BE ( (XMP_Uns32)createSeconds, ((XMP_Uns8*)mvhdInfo.content + 4) ); + moovMgr->NoteChange(); + } + if ( modifyFound ) { + if ( (XMP_Uns32)modifySeconds != oldModify ) PutUns32BE ( (XMP_Uns32)modifySeconds, ((XMP_Uns8*)mvhdInfo.content + 8) ); + moovMgr->NoteChange(); + } + + } else { + + // Replace the v0 box with a v1 box. + + XMP_Assert ( createFound | modifyFound ); // One of them has high bits set. + if ( mvhdInfo.contentSize != sizeof ( MOOV_Manager::Content_mvhd_0 ) ) return; + + MOOV_Manager::Content_mvhd_0 * mvhdV0 = (MOOV_Manager::Content_mvhd_0*) mvhdInfo.content; + MOOV_Manager::Content_mvhd_1 mvhdV1; + + // Copy the unchanged fields directly. + + mvhdV1.timescale = mvhdV0->timescale; + mvhdV1.rate = mvhdV0->rate; + mvhdV1.volume = mvhdV0->volume; + mvhdV1.pad_1 = mvhdV0->pad_1; + mvhdV1.pad_2 = mvhdV0->pad_2; + mvhdV1.pad_3 = mvhdV0->pad_3; + for ( int i = 0; i < 9; ++i ) mvhdV1.matrix[i] = mvhdV0->matrix[i]; + for ( int i = 0; i < 6; ++i ) mvhdV1.preDef[i] = mvhdV0->preDef[i]; + mvhdV1.nextTrackID = mvhdV0->nextTrackID; + + // Set the fields that have changes. + + mvhdV1.vFlags = (1 << 24) | (mvhdV0->vFlags & 0xFFFFFF); + mvhdV1.duration = MakeUns64BE ( (XMP_Uns64) GetUns32BE ( &mvhdV0->duration ) ); + + XMP_Uns64 temp64; + + temp64 = (XMP_Uns64) GetUns32BE ( &mvhdV0->creationTime ); + if ( createFound ) temp64 = createSeconds; + mvhdV1.creationTime = MakeUns64BE ( temp64 ); + + temp64 = (XMP_Uns64) GetUns32BE ( &mvhdV0->modificationTime ); + if ( modifyFound ) temp64 = modifySeconds; + mvhdV1.modificationTime = MakeUns64BE ( temp64 ); + + moovMgr->SetBox ( mvhdRef, &mvhdV1, sizeof ( MOOV_Manager::Content_mvhd_1 ) ); + + } + +} // ExportMVHDItems + +// ================================================================================================= +// ImportISOCopyrights +// =================== +// +// The cached 'moov'/'udta'/'cprt' boxes are full boxes. The "real" content is a UInt16 packed 3 +// character language code and a UTF-8 or UTF-16 string. + +static bool ImportISOCopyrights ( const std::vector<MOOV_Manager::BoxInfo> & cprtBoxes, SXMPMeta * xmp ) +{ + bool haveImports = false; + + std::string tempStr; + char lang3 [4]; // The unpacked ISO-639-2/T language code with final null. + lang3[3] = 0; + + for ( size_t i = 0, limit = cprtBoxes.size(); i < limit; ++i ) { + + const MOOV_Manager::BoxInfo & currBox = cprtBoxes[i]; + if ( currBox.contentSize < 4+2+1 ) continue; // Want enough for a non-empty value. + if ( *currBox.content != 0 ) continue; // Only proceed for version 0, ignore the flags. + + XMP_Uns16 packedLang = GetUns16BE ( currBox.content + 4 ); + lang3[0] = (packedLang >> 10) + 0x60; + lang3[1] = ((packedLang >> 5) & 0x1F) + 0x60; + lang3[2] = (packedLang & 0x1F) + 0x60; + XMP_StringPtr xmpLang = Lookup2LetterLang ( lang3 ); + if ( *xmpLang == 0 ) continue; + + XMP_StringPtr textPtr = (XMP_StringPtr) (currBox.content + 6); + XMP_StringLen textLen = currBox.contentSize - 6; + + if ( (textLen >= 2) && (GetUns16BE(textPtr) == 0xFEFF) ) { + FromUTF16 ( (UTF16Unit*)textPtr, textLen/2, &tempStr, true /* big endian */ ); + textPtr = tempStr.c_str(); + } + + xmp->SetLocalizedText ( kXMP_NS_DC, "rights", xmpLang, xmpLang, textPtr ); + haveImports = true; + + } + + return haveImports; + +} // ImportISOCopyrights + +// ================================================================================================= +// ExportISOCopyrights +// =================== + +static void ExportISOCopyrights ( const SXMPMeta & xmp, MOOV_Manager * moovMgr ) +{ + bool haveMappings = false; // True if any ISO-XMP language mappings are found. + + // Go through the ISO 'cprt' items and look for a corresponding XMP item. Ignore the ISO item if + // there is no language mapping to XMP. Update the ISO item if the mappable XMP exists, delete + // the ISO item if the mappable XMP does not exist. Since the import side would have made sure + // the mappable XMP items existed, if they don't now they must have been deleted. + + MOOV_Manager::BoxInfo udtaInfo; + MOOV_Manager::BoxRef udtaRef = moovMgr->GetBox ( "moov/udta", &udtaInfo ); + if ( udtaRef == 0 ) return; + + std::string xmpPath, xmpValue, xmpLang, tempStr; + char lang3 [4]; // An unpacked ISO-639-2/T language code. + lang3[3] = 0; + + for ( XMP_Uns32 ordinal = udtaInfo.childCount; ordinal > 0; --ordinal ) { // ! Go backwards because of deletions. + + MOOV_Manager::BoxInfo cprtInfo; + MOOV_Manager::BoxRef cprtRef = moovMgr->GetNthChild ( udtaRef, ordinal-1, &cprtInfo ); + if ( (cprtRef == 0) ) break; // Sanity check, should not happen. + if ( (cprtInfo.boxType != ISOMedia::k_cprt) || (cprtInfo.contentSize < 6) ) continue; + if ( *cprtInfo.content != 0 ) continue; // Only accept version 0, ignore the flags. + + XMP_Uns16 packedLang = GetUns16BE ( cprtInfo.content + 4 ); + lang3[0] = (packedLang >> 10) + 0x60; + lang3[1] = ((packedLang >> 5) & 0x1F) + 0x60; + lang3[2] = (packedLang & 0x1F) + 0x60; + + XMP_StringPtr lang2 = Lookup2LetterLang ( lang3 ); + if ( *lang2 == 0 ) continue; // No language mapping to XMP. + haveMappings = true; + + bool xmpFound = xmp.GetLocalizedText ( kXMP_NS_DC, "rights", lang2, lang2, &xmpLang, &xmpValue, 0 ); + if ( xmpFound ) { + if ( (xmpLang.size() < 2) || + ( (xmpLang.size() == 2) && (xmpLang != lang2) ) || + ( (xmpLang.size() > 2) && ( (xmpLang[2] != '-') || (! XMP_LitNMatch ( xmpLang.c_str(), lang2, 2)) ) ) ) { + xmpFound = false; // The language does not match, the corresponding XMP does not exist. + } + } + + if ( ! xmpFound ) { + + // No XMP, delete the ISO item. + moovMgr->DeleteNthChild ( udtaRef, ordinal-1 ); + + } else { + + // Update the ISO item if necessary. + XMP_StringPtr isoStr = (XMP_StringPtr)cprtInfo.content + 6; + size_t rawLen = cprtInfo.contentSize - 6; + if ( (rawLen >= 8) && (GetUns16BE(isoStr) == 0xFEFF) ) { + FromUTF16 ( (UTF16Unit*)(isoStr+2), (rawLen-2)/2, &tempStr, true /* big endian */ ); + isoStr = tempStr.c_str(); + } + if ( xmpValue != isoStr ) { + std::string newContent = "vfffll"; + newContent += xmpValue; + memcpy ( (char*)newContent.c_str(), cprtInfo.content, 6 ); // Keep old version, flags, and language. + moovMgr->SetBox ( cprtRef, newContent.c_str(), (XMP_Uns32)(newContent.size() + 1) ); + } + + } + + } + + // Go through the XMP items and look for a corresponding ISO item. Skip if found (did it above), + // otherwise add a new ISO item. + + bool haveXDefault = false; + XMP_Index xmpCount = xmp.CountArrayItems ( kXMP_NS_DC, "rights" ); + + for ( XMP_Index xmpIndex = 1; xmpIndex <= xmpCount; ++xmpIndex ) { // ! The first XMP array index is 1. + + SXMPUtils::ComposeArrayItemPath ( kXMP_NS_DC, "rights", xmpIndex, &xmpPath ); + xmp.GetArrayItem ( kXMP_NS_DC, "rights", xmpIndex, &xmpValue, 0 ); + bool hasLang = xmp.GetQualifier ( kXMP_NS_DC, xmpPath.c_str(), kXMP_NS_XML, "lang", &xmpLang, 0 ); + if ( ! hasLang ) continue; // Sanity check. + if ( xmpLang == "x-default" ) { + haveXDefault = true; // See later special case. + continue; + } + + XMP_StringPtr isoLang = ""; + size_t rootLen = xmpLang.find ( '-' ); + if ( rootLen == std::string::npos ) rootLen = xmpLang.size(); + if ( rootLen == 2 ) { + if( xmpLang.size() > 2 ) xmpLang[2] = 0; + isoLang = Lookup3LetterLang ( xmpLang.c_str() ); + if ( *isoLang == 0 ) continue; + } else if ( rootLen == 3 ) { + if( xmpLang.size() > 3 ) xmpLang[3] = 0; + isoLang = xmpLang.c_str(); + } else { + continue; + } + haveMappings = true; + + bool isoFound = false; + XMP_Uns16 packedLang = ((isoLang[0] - 0x60) << 10) | ((isoLang[1] - 0x60) << 5) | (isoLang[2] - 0x60); + + for ( XMP_Uns32 isoIndex = 0; (isoIndex < udtaInfo.childCount) && (! isoFound); ++isoIndex ) { + + MOOV_Manager::BoxInfo cprtInfo; + MOOV_Manager::BoxRef cprtRef = moovMgr->GetNthChild ( udtaRef, isoIndex, &cprtInfo ); + if ( (cprtRef == 0) ) break; // Sanity check, should not happen. + if ( (cprtInfo.boxType != ISOMedia::k_cprt) || (cprtInfo.contentSize < 6) ) continue; + if ( *cprtInfo.content != 0 ) continue; // Only accept version 0, ignore the flags. + if ( packedLang != GetUns16BE ( cprtInfo.content + 4 ) ) continue; // Look for matching language. + + isoFound = true; // Found the language entry, whether or not we update it. + + } + + if ( ! isoFound ) { + + std::string newContent = "vfffll"; + newContent += xmpValue; + *((XMP_Uns32*)newContent.c_str()) = 0; // Set the version and flags to zero. + PutUns16BE ( packedLang, (char*)newContent.c_str() + 4 ); + moovMgr->AddChildBox ( udtaRef, ISOMedia::k_cprt, newContent.c_str(), (XMP_Uns32)(newContent.size() + 1) ); + + } + + } + + // If there were no mappings in the loops, export the XMP "x-default" value to the first ISO item. + + if ( ! haveMappings ) { + + MOOV_Manager::BoxInfo cprtInfo; + MOOV_Manager::BoxRef cprtRef = moovMgr->GetTypeChild ( udtaRef, ISOMedia::k_cprt, &cprtInfo ); + + if ( (cprtRef != 0) && (cprtInfo.contentSize >= 6) && (*cprtInfo.content == 0) ) { + + bool xmpFound = xmp.GetLocalizedText ( kXMP_NS_DC, "rights", "", "x-default", &xmpLang, &xmpValue, 0 ); + + if ( xmpFound && (xmpLang == "x-default") ) { + + // Update the ISO item if necessary. + XMP_StringPtr isoStr = (XMP_StringPtr)cprtInfo.content + 6; + size_t rawLen = cprtInfo.contentSize - 6; + if ( (rawLen >= 8) && (GetUns16BE(isoStr) == 0xFEFF) ) { + FromUTF16 ( (UTF16Unit*)(isoStr+2), (rawLen-2)/2, &tempStr, true /* big endian */ ); + isoStr = tempStr.c_str(); + } + if ( xmpValue != isoStr ) { + std::string newContent = "vfffll"; + newContent += xmpValue; + memcpy ( (char*)newContent.c_str(), cprtInfo.content, 6 ); // Keep old version, flags, and language. + moovMgr->SetBox ( cprtRef, newContent.c_str(), (XMP_Uns32)(newContent.size() + 1) ); + } + + } + + } + + } + +} // ExportISOCopyrights + +// ================================================================================================= +// ExportQuickTimeItems +// ==================== + +static void ExportQuickTimeItems ( const SXMPMeta & xmp, TradQT_Manager * qtMgr, MOOV_Manager * moovMgr ) +{ + + // The QuickTime 'udta' timecode items are done here for simplicity. + + #define createWithZeroLang true + + qtMgr->ExportSimpleXMP ( kQTilst_Reel, xmp, kXMP_NS_DM, "tapeName" ); + qtMgr->ExportSimpleXMP ( kQTilst_Timecode, xmp, kXMP_NS_DM, "startTimecode/xmpDM:timeValue", createWithZeroLang ); + qtMgr->ExportSimpleXMP ( kQTilst_TimeScale, xmp, kXMP_NS_DM, "startTimeScale", createWithZeroLang ); + qtMgr->ExportSimpleXMP ( kQTilst_TimeSize, xmp, kXMP_NS_DM, "startTimeSampleSize", createWithZeroLang ); + + qtMgr->UpdateChangedBoxes ( moovMgr ); + +} // ExportQuickTimeItems + +// ================================================================================================= +// SelectTimeFormat +// ================ + +static const char * SelectTimeFormat ( const MPEG4_MetaHandler::TimecodeTrackInfo & tmcdInfo ) +{ + const char * timeFormat = 0; + + float fltFPS = (float)tmcdInfo.timeScale / (float)tmcdInfo.frameDuration; + int intFPS = (int) (fltFPS + 0.5); + + switch ( intFPS ) { + + case 30: + if ( fltFPS >= 29.985 ) { + timeFormat = "30Timecode"; + } else if ( tmcdInfo.isDropFrame ) { + timeFormat = "2997DropTimecode"; + } else { + timeFormat = "2997NonDropTimecode"; + } + break; + + case 24: + if ( fltFPS >= 23.988 ) { + timeFormat = "24Timecode"; + } else { + timeFormat = "23976Timecode"; + } + break; + + case 25: + timeFormat = "25Timecode"; + break; + + case 50: + timeFormat = "50Timecode"; + break; + + case 60: + if ( fltFPS >= 59.97 ) { + timeFormat = "60Timecode"; + } else if ( tmcdInfo.isDropFrame ) { + timeFormat = "5994DropTimecode"; + } else { + timeFormat = "5994NonDropTimecode"; + } + break; + + } + + return timeFormat; + +} // SelectTimeFormat + +// ================================================================================================= +// SelectTimeFormat +// ================ + +static const char * SelectTimeFormat ( const SXMPMeta & xmp ) +{ + bool ok; + MPEG4_MetaHandler::TimecodeTrackInfo tmcdInfo; + + XMP_Int64 timeScale; + ok = xmp.GetProperty_Int64 ( kXMP_NS_DM, "startTimeScale", &timeScale, 0 ); + if ( ! ok ) return 0; + tmcdInfo.timeScale = (XMP_Uns32)timeScale; + + XMP_Int64 frameDuration; + ok = xmp.GetProperty_Int64 ( kXMP_NS_DM, "startTimeSampleSize", &frameDuration, 0 ); + if ( ! ok ) return 0; + tmcdInfo.frameDuration = (XMP_Uns32)frameDuration; + + std::string timecode; + ok = xmp.GetProperty ( kXMP_NS_DM, "startTimecode/xmpDM:timeValue", &timecode, 0 ); + if ( ! ok ) return 0; + if ( (timecode.size() == 11) && (timecode[8] == ';') ) tmcdInfo.isDropFrame = true; + + return SelectTimeFormat ( tmcdInfo ); + +} // SelectTimeFormat + +// ================================================================================================= +// ComposeTimecode +// =============== + +static const char * kDecDigits = "0123456789"; +#define TwoDigits(val,str) (str)[0] = kDecDigits[(val)/10]; (str)[1] = kDecDigits[(val)%10] + +static bool ComposeTimecode ( const MPEG4_MetaHandler::TimecodeTrackInfo & tmcdInfo, std::string * strValue ) +{ + float fltFPS = (float)tmcdInfo.timeScale / (float)tmcdInfo.frameDuration; + int intFPS = (int) (fltFPS + 0.5); + if ( (intFPS != 30) && (intFPS != 24) && (intFPS != 25) && (intFPS != 50) && (intFPS != 60) ) return false; + + XMP_Uns32 framesPerDay = intFPS * 24*60*60; + XMP_Uns32 dropLimit = 2; // Used in the drop-frame correction. + + if ( tmcdInfo.isDropFrame ) { + if ( intFPS == 30 ) { + framesPerDay = 2589408; // = 29.97 * 24*60*60 + } else if ( intFPS == 60 ) { + framesPerDay = 5178816; // = 59.94 * 24*60*60 + dropLimit = 4; + } else { + strValue->erase(); + return false; // Dropframe can only apply to 29.97 and 59.94. + } + } + + XMP_Uns32 framesPerHour = framesPerDay / 24; + XMP_Uns32 framesPerTenMinutes = framesPerHour / 6; + XMP_Uns32 framesPerMinute = framesPerTenMinutes / 10; + + XMP_Uns32 frameCount = tmcdInfo.timecodeSample; + while (frameCount >= framesPerDay ) frameCount -= framesPerDay; // Normalize to be within 24 hours. + + XMP_Uns32 hours, minHigh, minLow, seconds; + + hours = frameCount / framesPerHour; + frameCount -= (hours * framesPerHour); + + minHigh = frameCount / framesPerTenMinutes; + frameCount -= (minHigh * framesPerTenMinutes); + + minLow = frameCount / framesPerMinute; + frameCount -= (minLow * framesPerMinute); + + // Do some drop-frame corrections at this point: If this is drop-frame and the units of minutes + // is non-zero, and the seconds are zero, and the frames are zero or one, the time is illegal. + // Perform correction by subtracting 1 from the units of minutes and adding 1798 to the frames.Ê + // For example, 1:00:00 becomes 59:28, and 1:00:01 becomes 59:29. A special case can occur for + // when the frameCount just before the minHigh calculation is less than framesPerTenMinutes but + // more than 10*framesPerMinute. This happens because of roundoff, and will result in a minHigh + // of 0 and a minLow of 10.ÊThe drop frame correction mustÊalso be performed for this case. + + if ( tmcdInfo.isDropFrame ) { + if ( (minLow == 10) || ((minLow != 0) && (frameCount < dropLimit)) ) { + minLow -= 1; + frameCount += framesPerMinute; + } + } + + seconds = frameCount / intFPS; + frameCount -= (seconds * intFPS); + + if ( tmcdInfo.isDropFrame ) { + *strValue = "hh;mm;ss;ff"; + } else { + *strValue = "hh:mm:ss:ff"; + } + + char * str = (char*)strValue->c_str(); + TwoDigits ( hours, str ); + str[3] = kDecDigits[minHigh]; str[4] = kDecDigits[minLow]; + TwoDigits ( seconds, str+6 ); + TwoDigits ( frameCount, str+9 ); + + return true; + +} // ComposeTimecode + +// ================================================================================================= +// DecomposeTimecode +// ================= + +static bool DecomposeTimecode ( const char * strValue, MPEG4_MetaHandler::TimecodeTrackInfo * tmcdInfo ) +{ + float fltFPS = (float)tmcdInfo->timeScale / (float)tmcdInfo->frameDuration; + int intFPS = (int) (fltFPS + 0.5); + if ( (intFPS != 30) && (intFPS != 24) && (intFPS != 25) && (intFPS != 50) && (intFPS != 60) ) return false; + + XMP_Uns32 framesPerDay = intFPS * 24*60*60; + + int items, hours, minutes, seconds, frames; + + if ( ! tmcdInfo->isDropFrame ) { + items = sscanf ( strValue, "%d:%d:%d:%d", &hours, &minutes, &seconds, &frames ); + } else { + items = sscanf ( strValue, "%d;%d;%d;%d", &hours, &minutes, &seconds, &frames ); + if ( intFPS == 30 ) { + framesPerDay = 2589408; // = 29.97 * 24*60*60 + } else if ( intFPS == 60 ) { + framesPerDay = 5178816; // = 59.94 * 24*60*60 + } else { + return false; // Dropframe can only apply to 29.97 and 59.94. + } + } + + if ( items != 4 ) return false; + int minHigh = minutes / 10; + int minLow = minutes % 10; + + XMP_Uns32 framesPerHour = framesPerDay / 24; + XMP_Uns32 framesPerTenMinutes = framesPerHour / 6; + XMP_Uns32 framesPerMinute = framesPerTenMinutes / 10; + + tmcdInfo->timecodeSample = (hours * framesPerHour) + (minHigh * framesPerTenMinutes) + + (minLow * framesPerMinute) + (seconds * intFPS) + frames; + + return true; + +} // DecomposeTimecode + +// ================================================================================================= +// FindTimecode_trak +// ================= +// +// Look for a well-formed timecode track, return the trak box ref. + +static MOOV_Manager::BoxRef FindTimecode_trak ( const MOOV_Manager & moovMgr ) +{ + + // Find a 'trak' box with a handler type of 'tmcd'. + + MOOV_Manager::BoxInfo moovInfo; + MOOV_Manager::BoxRef moovRef = moovMgr.GetBox ( "moov", &moovInfo ); + XMP_Assert ( moovRef != 0 ); + + MOOV_Manager::BoxInfo trakInfo; + MOOV_Manager::BoxRef trakRef; + + size_t i = 0; + for ( ; i < moovInfo.childCount; ++i ) { + + trakRef = moovMgr.GetNthChild ( moovRef, i, &trakInfo ); + if ( trakRef == 0 ) return 0; // Sanity check, should not happen. + if ( trakInfo.boxType != ISOMedia::k_trak ) continue; + + MOOV_Manager::BoxRef innerRef; + MOOV_Manager::BoxInfo innerInfo; + + innerRef = moovMgr.GetTypeChild ( trakRef, ISOMedia::k_mdia, &innerInfo ); + if ( innerRef == 0 ) continue; + + innerRef = moovMgr.GetTypeChild ( innerRef, ISOMedia::k_hdlr, &innerInfo ); + if ( (innerRef == 0) || (innerInfo.contentSize < sizeof ( MOOV_Manager::Content_hdlr )) ) continue; + + const MOOV_Manager::Content_hdlr * hdlr = (MOOV_Manager::Content_hdlr*) innerInfo.content; + if ( hdlr->versionFlags != 0 ) continue; + if ( GetUns32BE ( &hdlr->handlerType ) == ISOMedia::k_tmcd ) break; + + } + + if ( i == moovInfo.childCount ) return 0; + return trakRef; + +} // FindTimecode_trak + +// ================================================================================================= +// FindTimecode_stbl +// ================= +// +// Look for the mdia/minf/stbl box within a well-formed timecode track, return the stbl box ref. + +static MOOV_Manager::BoxRef FindTimecode_stbl ( const MOOV_Manager & moovMgr ) +{ + + MOOV_Manager::BoxRef trakRef = FindTimecode_trak ( moovMgr ); + if ( trakRef == 0 ) return 0; + + MOOV_Manager::BoxInfo tempInfo; + MOOV_Manager::BoxRef tempRef, stblRef; + + tempRef = moovMgr.GetTypeChild ( trakRef, ISOMedia::k_mdia, &tempInfo ); + if ( tempRef == 0 ) return 0; + + tempRef = moovMgr.GetTypeChild ( tempRef, ISOMedia::k_minf, &tempInfo ); + if ( tempRef == 0 ) return 0; + + stblRef = moovMgr.GetTypeChild ( tempRef, ISOMedia::k_stbl, &tempInfo ); + return stblRef; + +} // FindTimecode_stbl + +// ================================================================================================= +// FindTimecode_elst +// ================= +// +// Look for the edts/elst box within a well-formed timecode track, return the elst box ref. + +static MOOV_Manager::BoxRef FindTimecode_elst ( const MOOV_Manager & moovMgr ) +{ + + MOOV_Manager::BoxRef trakRef = FindTimecode_trak ( moovMgr ); + if ( trakRef == 0 ) return 0; + + MOOV_Manager::BoxInfo tempInfo; + MOOV_Manager::BoxRef tempRef, elstRef; + + tempRef = moovMgr.GetTypeChild ( trakRef, ISOMedia::k_edts, &tempInfo ); + if ( tempRef == 0 ) return 0; + + elstRef = moovMgr.GetTypeChild ( tempRef, ISOMedia::k_elst, &tempInfo ); + return elstRef; + +} // FindTimecode_elst + +// ================================================================================================= +// ImportTimecodeItems +// =================== + +static bool ImportTimecodeItems ( const MPEG4_MetaHandler::TimecodeTrackInfo & tmcdInfo, + const TradQT_Manager & qtInfo, SXMPMeta * xmp ) +{ + std::string xmpValue; + bool haveItem; + bool haveImports = false; + + // The QT user data item '©REL' goes into xmpDM:tapeName, and the 'name' box at the end of the + // timecode sample description goes into xmpDM:altTapeName. + haveImports |= qtInfo.ImportSimpleXMP ( kQTilst_Reel, xmp, kXMP_NS_DM, "tapeName" ); + if ( ! tmcdInfo.macName.empty() ) { + haveItem = ConvertFromMacLang ( tmcdInfo.macName, tmcdInfo.macLang, &xmpValue ); + if ( haveItem ) { + xmp->SetProperty ( kXMP_NS_DM, "altTapeName", xmpValue.c_str() ); + haveImports = true; + } + } + + // The QT user data item '©TSC' goes into xmpDM:startTimeScale. If that isn't present, then + // the timecode sample description's timeScale is used. + haveItem = qtInfo.ImportSimpleXMP ( kQTilst_TimeScale, xmp, kXMP_NS_DM, "startTimeScale" ); + if ( tmcdInfo.stsdBoxFound & (! haveItem) ) { + xmp->SetProperty_Int64 ( kXMP_NS_DM, "startTimeScale", tmcdInfo.timeScale ); + haveItem = true; + } + haveImports |= haveItem; + + // The QT user data item '©TSZ' goes into xmpDM:startTimeSampleSize. If that isn't present, then + // the timecode sample description's frameDuration is used. + haveItem = qtInfo.ImportSimpleXMP ( kQTilst_TimeSize, xmp, kXMP_NS_DM, "startTimeSampleSize" ); + if ( tmcdInfo.stsdBoxFound & (! haveItem) ) { + xmp->SetProperty_Int64 ( kXMP_NS_DM, "startTimeSampleSize", tmcdInfo.frameDuration ); + haveItem = true; + } + haveImports |= haveItem; + + const char * timeFormat; + + // The Timecode struct type is used for xmpDM:startTimecode and xmpDM:altTimecode. For both, only + // the xmpDM:timeValue and xmpDM:timeFormat fields are set. + + // The QT user data item '©TIM' goes into xmpDM:startTimecode/xmpDM:timeValue. This is an already + // formatted timecode string. The XMP values of xmpDM:startTimeScale, xmpDM:startTimeSampleSize, + // and xmpDM:startTimecode/xmpDM:timeValue are used to select the timeFormat value. + haveImports |= qtInfo.ImportSimpleXMP ( kQTilst_Timecode, xmp, kXMP_NS_DM, "startTimecode/xmpDM:timeValue" ); + timeFormat = SelectTimeFormat ( *xmp ); + if ( timeFormat != 0 ) { + xmp->SetProperty ( kXMP_NS_DM, "startTimecode/xmpDM:timeFormat", timeFormat ); + haveImports = true; + } + + if ( tmcdInfo.stsdBoxFound ) { + + haveItem = ComposeTimecode ( tmcdInfo, &xmpValue ); + if ( haveItem ) { + xmp->SetProperty ( kXMP_NS_DM, "altTimecode/xmpDM:timeValue", xmpValue.c_str() ); + haveImports = true; + } + + timeFormat = SelectTimeFormat ( tmcdInfo ); + if ( timeFormat != 0 ) { + xmp->SetProperty ( kXMP_NS_DM, "altTimecode/xmpDM:timeFormat", timeFormat ); + haveImports = true; + } + + } + + return haveImports; + +} // ImportTimecodeItems + +// ================================================================================================= +// ExportTimecodeItems +// =================== + +static void ExportTimecodeItems ( const SXMPMeta & xmp, MPEG4_MetaHandler::TimecodeTrackInfo * tmcdInfo, + TradQT_Manager * qtMgr, MOOV_Manager * moovMgr ) +{ + // Export the items that go into the timecode track: + // - the timescale and frame duration in the first 'stsd' table entry + // - the 'name' box appended to the first 'stsd' table entry + // - the first timecode sample + // ! The QuickTime 'udta' timecode items are handled in ExportQuickTimeItems. + + if ( ! tmcdInfo->stsdBoxFound ) return; // Don't make changes unless there is a well-formed timecode track. + + MOOV_Manager::BoxRef stblRef = FindTimecode_stbl ( *moovMgr ); + if ( stblRef == 0 ) return; + + MOOV_Manager::BoxInfo stsdInfo; + MOOV_Manager::BoxRef stsdRef; + + stsdRef = moovMgr->GetTypeChild ( stblRef, ISOMedia::k_stsd, &stsdInfo ); + if ( stsdRef == 0 ) return; + if ( stsdInfo.contentSize < (8 + sizeof ( MOOV_Manager::Content_stsd_entry )) ) return; + if ( GetUns32BE ( stsdInfo.content + 4 ) == 0 ) return; // Make sure the entry count is non-zero. + + MOOV_Manager::Content_stsd_entry * stsdRawEntry = (MOOV_Manager::Content_stsd_entry*) (stsdInfo.content + 8); + + XMP_Uns32 stsdEntrySize = GetUns32BE ( &stsdRawEntry->entrySize ); + if ( stsdEntrySize > (stsdInfo.contentSize - 4) ) stsdEntrySize = stsdInfo.contentSize - 4; + if ( stsdEntrySize < sizeof ( MOOV_Manager::Content_stsd_entry ) ) return; + + bool ok, haveScale = false, haveDuration = false; + std::string xmpValue; + XMP_Int64 int64; // Used to allow UInt32 values, GetProperty_Int is SInt32. + + // The tmcdInfo timeScale field is set from xmpDM:startTimeScale. + ok = xmp.GetProperty_Int64 ( kXMP_NS_DM, "startTimeScale", &int64, 0 ); + if ( ok && (int64 <= 0xFFFFFFFF) ) { + haveScale = true; + if ( tmcdInfo->timeScale != 0 ) { // Entry must not be created if not existing before + tmcdInfo->timeScale = (XMP_Uns32)int64; + PutUns32BE ( tmcdInfo->timeScale, (void*)&stsdRawEntry->timeScale ); + moovMgr->NoteChange(); + } + } + + // The tmcdInfo frameDuration field is set from xmpDM:startTimeSampleSize. + ok = xmp.GetProperty_Int64 ( kXMP_NS_DM, "startTimeSampleSize", &int64, 0 ); + if ( ok && (int64 <= 0xFFFFFFFF) ) { + haveDuration = true; + if ( tmcdInfo->frameDuration != 0 ) { // Entry must not be created if not existing before + tmcdInfo->frameDuration = (XMP_Uns32)int64; + PutUns32BE ( tmcdInfo->frameDuration, (void*)&stsdRawEntry->frameDuration ); + moovMgr->NoteChange(); + } + } + + // The tmcdInfo frameCount field is a simple ratio of the timeScale and frameDuration. + if ( (haveScale & haveDuration) && (tmcdInfo->frameDuration != 0) ) { + float floatScale = (float) tmcdInfo->timeScale; + float floatDuration = (float) tmcdInfo->frameDuration; + XMP_Uns8 newCount = (XMP_Uns8) ( (floatScale / floatDuration) + 0.5 ); + if ( newCount != stsdRawEntry->frameCount ) { + stsdRawEntry->frameCount = newCount; + moovMgr->NoteChange(); + } + } + + // The tmcdInfo isDropFrame flag is set from xmpDM:altTimecode/xmpDM:timeValue. The timeScale + // and frameDuration must be updated first, they are used by DecomposeTimecode. Compute the new + // UInt32 timecode sample, but it gets written to the file later by UpdateFile. + + ok = xmp.GetProperty ( kXMP_NS_DM, "altTimecode/xmpDM:timeValue", &xmpValue, 0 ); + if ( ok && (xmpValue.size() == 11) ) { + + bool oldDropFrame = tmcdInfo->isDropFrame; + tmcdInfo->isDropFrame = false; + if ( xmpValue[8] == ';' ) tmcdInfo->isDropFrame = true; + if ( oldDropFrame != tmcdInfo->isDropFrame ) { + XMP_Uns32 flags = GetUns32BE ( &stsdRawEntry->flags ); + flags = (flags & 0xFFFFFFFE) | (XMP_Uns32)tmcdInfo->isDropFrame; + PutUns32BE ( flags, (void*)&stsdRawEntry->flags ); + moovMgr->NoteChange(); + } + + XMP_Uns32 oldSample = tmcdInfo->timecodeSample; + ok = DecomposeTimecode ( xmpValue.c_str(), tmcdInfo ); + if ( ok && (oldSample != tmcdInfo->timecodeSample) ) moovMgr->NoteChange(); + + } + + // The 'name' box attached to the first 'stsd' table entry is set from xmpDM:altTapeName. + + bool replaceNameBox = false; + + ok = xmp.GetProperty ( kXMP_NS_DM, "altTapeName", &xmpValue, 0 ); + if ( (! ok) || xmpValue.empty() ) { + if ( tmcdInfo->nameOffset != 0 ) replaceNameBox = true; // No XMP, get rid of existing name. + } else { + std::string macValue; + ok = ConvertToMacLang ( xmpValue, tmcdInfo->macLang, &macValue ); + if ( ok && (macValue != tmcdInfo->macName) ) { + tmcdInfo->macName = macValue; + replaceNameBox = true; // Write changed name. + } + } + + if ( replaceNameBox ) { + + // To replace the 'name' box we have to create an entire new 'stsd' box, and attach the + // new name to the first 'stsd' table entry. The 'name' box content is a UInt16 text length, + // UInt16 language code, and Mac encoded text with no nul termination. + + if ( tmcdInfo->macName.size() > 0xFFFF ) tmcdInfo->macName.erase ( 0xFFFF ); + + ISOMedia::BoxInfo oldNameInfo; + XMP_Assert ( (oldNameInfo.headerSize == 0) && (oldNameInfo.contentSize == 0) ); + if ( tmcdInfo->nameOffset != 0 ) { + const XMP_Uns8 * oldNamePtr = stsdInfo.content + tmcdInfo->nameOffset; + const XMP_Uns8 * oldNameLimit = stsdInfo.content + stsdInfo.contentSize; + (void) ISOMedia::GetBoxInfo ( oldNamePtr, oldNameLimit, &oldNameInfo ); + } + + XMP_Uns32 oldNameBoxSize = (XMP_Uns32)oldNameInfo.headerSize + (XMP_Uns32)oldNameInfo.contentSize; + XMP_Uns32 newNameBoxSize = 0; + if ( ! tmcdInfo->macName.empty() ) newNameBoxSize = 4+4 + 2+2 + (XMP_Uns32)tmcdInfo->macName.size(); + + XMP_Uns32 stsdNewContentSize = stsdInfo.contentSize - oldNameBoxSize + newNameBoxSize; + RawDataBlock stsdNewContent; + stsdNewContent.assign ( stsdNewContentSize, 0 ); // Get the space allocated, direct fill below. + + XMP_Uns32 stsdPrefixSize = tmcdInfo->nameOffset; + if ( tmcdInfo->nameOffset == 0 ) stsdPrefixSize = 4+4 + sizeof ( MOOV_Manager::Content_stsd_entry ); + + XMP_Uns32 oldSuffixOffset = stsdPrefixSize + oldNameBoxSize; + XMP_Uns32 newSuffixOffset = stsdPrefixSize + newNameBoxSize; + XMP_Uns32 stsdSuffixSize = stsdInfo.contentSize - oldSuffixOffset; + + memcpy ( &stsdNewContent[0], stsdInfo.content, stsdPrefixSize ); + if ( stsdSuffixSize != 0 ) memcpy ( &stsdNewContent[newSuffixOffset], (stsdInfo.content + oldSuffixOffset), stsdSuffixSize ); + + XMP_Uns32 newEntrySize = stsdEntrySize - oldNameBoxSize + newNameBoxSize; + MOOV_Manager::Content_stsd_entry * stsdNewEntry = (MOOV_Manager::Content_stsd_entry*) (&stsdNewContent[0] + 8); + PutUns32BE ( newEntrySize, &stsdNewEntry->entrySize ); + + if ( newNameBoxSize != 0 ) { + PutUns32BE ( newNameBoxSize, &stsdNewContent[stsdPrefixSize] ); + PutUns32BE ( ISOMedia::k_name, &stsdNewContent[stsdPrefixSize+4] ); + PutUns16BE ( (XMP_Uns16)tmcdInfo->macName.size(), &stsdNewContent[stsdPrefixSize+8] ); + PutUns16BE ( tmcdInfo->macLang, &stsdNewContent[stsdPrefixSize+10] ); + memcpy ( &stsdNewContent[stsdPrefixSize+12], tmcdInfo->macName.c_str(), tmcdInfo->macName.size() ); + } + + moovMgr->SetBox ( stsdRef, &stsdNewContent[0], stsdNewContentSize ); + + } + +} // ExportTimecodeItems + +// ================================================================================================= +// ImportCr8rItems +// =============== + +#pragma pack ( push, 1 ) + +struct PrmLBoxContent { + XMP_Uns32 magic; + XMP_Uns32 size; + XMP_Uns16 verAPI; + XMP_Uns16 verCode; + XMP_Uns32 exportType; + XMP_Uns16 MacVRefNum; + XMP_Uns32 MacParID; + char filePath[260]; +}; + +enum { kExportTypeMovie = 0, kExportTypeStill = 1, kExportTypeAudio = 2, kExportTypeCustom = 3 }; + +struct Cr8rBoxContent { + XMP_Uns32 magic; + XMP_Uns32 size; + XMP_Uns16 majorVer; + XMP_Uns16 minorVer; + XMP_Uns32 creatorCode; + XMP_Uns32 appleEvent; + char fileExt[16]; + char appOptions[16]; + char appName[32]; +}; + +#pragma pack ( pop ) + +// ------------------------------------------------------------------------------------------------- + +static bool ImportCr8rItems ( const MOOV_Manager & moovMgr, SXMPMeta * xmp ) +{ + bool haveXMP = false; + std::string fieldPath; + + MOOV_Manager::BoxInfo infoPrmL, infoCr8r; + MOOV_Manager::BoxRef refPrmL = moovMgr.GetBox ( "moov/udta/PrmL", &infoPrmL ); + MOOV_Manager::BoxRef refCr8r = moovMgr.GetBox ( "moov/udta/Cr8r", &infoCr8r ); + + bool havePrmL = ( (refPrmL != 0) && (infoPrmL.contentSize == sizeof ( PrmLBoxContent )) ); + bool haveCr8r = ( (refCr8r != 0) && (infoCr8r.contentSize == sizeof ( Cr8rBoxContent )) ); + + if ( havePrmL ) { + + PrmLBoxContent rawPrmL; + XMP_Assert ( sizeof ( rawPrmL ) == 282 ); + XMP_Assert ( sizeof ( rawPrmL.filePath ) == 260 ); + memcpy ( &rawPrmL, infoPrmL.content, sizeof ( rawPrmL ) ); + if ( rawPrmL.magic != 0xBEEFCAFE ) { + Flip4 ( &rawPrmL.exportType ); // The only numeric field that we care about. + } + + rawPrmL.filePath[259] = 0; // Ensure a terminating nul. + if ( rawPrmL.filePath[0] != 0 ) { + if ( rawPrmL.filePath[0] == '/' ) { + haveXMP = true; + SXMPUtils::ComposeStructFieldPath ( kXMP_NS_CreatorAtom, "macAtom", + kXMP_NS_CreatorAtom, "posixProjectPath", &fieldPath ); + if ( ! xmp->DoesPropertyExist ( kXMP_NS_CreatorAtom, fieldPath.c_str() ) ) { + xmp->SetProperty ( kXMP_NS_CreatorAtom, fieldPath.c_str(), rawPrmL.filePath ); + } + } else if ( XMP_LitNMatch ( rawPrmL.filePath, "\\\\?\\", 4 ) ) { + haveXMP = true; + SXMPUtils::ComposeStructFieldPath ( kXMP_NS_CreatorAtom, "windowsAtom", + kXMP_NS_CreatorAtom, "uncProjectPath", &fieldPath ); + if ( ! xmp->DoesPropertyExist ( kXMP_NS_CreatorAtom, fieldPath.c_str() ) ) { + xmp->SetProperty ( kXMP_NS_CreatorAtom, fieldPath.c_str(), rawPrmL.filePath ); + } + } + } + + const char * exportStr = 0; + switch ( rawPrmL.exportType ) { + case kExportTypeMovie : exportStr = "movie"; break; + case kExportTypeStill : exportStr = "still"; break; + case kExportTypeAudio : exportStr = "audio"; break; + case kExportTypeCustom : exportStr = "custom"; break; + } + if ( exportStr != 0 ) { + haveXMP = true; + SXMPUtils::ComposeStructFieldPath ( kXMP_NS_DM, "projectRef", kXMP_NS_DM, "type", &fieldPath ); + if ( ! xmp->DoesPropertyExist ( kXMP_NS_DM, fieldPath.c_str() ) ) { + xmp->SetProperty ( kXMP_NS_DM, fieldPath.c_str(), exportStr ); + } + } + + } + + if ( haveCr8r ) { + + Cr8rBoxContent rawCr8r; + XMP_Assert ( sizeof ( rawCr8r ) == 84 ); + XMP_Assert ( sizeof ( rawCr8r.fileExt ) == 16 ); + XMP_Assert ( sizeof ( rawCr8r.appOptions ) == 16 ); + XMP_Assert ( sizeof ( rawCr8r.appName ) == 32 ); + memcpy ( &rawCr8r, infoCr8r.content, sizeof ( rawCr8r ) ); + if ( rawCr8r.magic != 0xBEEFCAFE ) { + Flip4 ( &rawCr8r.creatorCode ); // The only numeric fields that we care about. + Flip4 ( &rawCr8r.appleEvent ); + } + + std::string fieldPath; + + SXMPUtils::ComposeStructFieldPath ( kXMP_NS_CreatorAtom, "macAtom", kXMP_NS_CreatorAtom, "applicationCode", &fieldPath ); + if ( (rawCr8r.creatorCode != 0) && (! xmp->DoesPropertyExist ( kXMP_NS_CreatorAtom, fieldPath.c_str() )) ) { + haveXMP = true; + xmp->SetProperty_Int64 ( kXMP_NS_CreatorAtom, fieldPath.c_str(), (XMP_Int64)rawCr8r.creatorCode ); // ! Unsigned trickery. + } + + SXMPUtils::ComposeStructFieldPath ( kXMP_NS_CreatorAtom, "macAtom", kXMP_NS_CreatorAtom, "invocationAppleEvent", &fieldPath ); + if ( (rawCr8r.appleEvent != 0) && (! xmp->DoesPropertyExist ( kXMP_NS_CreatorAtom, fieldPath.c_str() )) ) { + haveXMP = true; + xmp->SetProperty_Int64 ( kXMP_NS_CreatorAtom, fieldPath.c_str(), (XMP_Int64)rawCr8r.appleEvent ); // ! Unsigned trickery. + } + + rawCr8r.fileExt[15] = 0; // Ensure a terminating nul. + SXMPUtils::ComposeStructFieldPath ( kXMP_NS_CreatorAtom, "windowsAtom", kXMP_NS_CreatorAtom, "extension", &fieldPath ); + if ( (rawCr8r.fileExt[0] != 0) && (! xmp->DoesPropertyExist ( kXMP_NS_CreatorAtom, fieldPath.c_str() )) ) { + haveXMP = true; + xmp->SetProperty ( kXMP_NS_CreatorAtom, fieldPath.c_str(), rawCr8r.fileExt ); + } + + rawCr8r.appOptions[15] = 0; // Ensure a terminating nul. + SXMPUtils::ComposeStructFieldPath ( kXMP_NS_CreatorAtom, "windowsAtom", kXMP_NS_CreatorAtom, "invocationFlags", &fieldPath ); + if ( (rawCr8r.appOptions[0] != 0) && (! xmp->DoesPropertyExist ( kXMP_NS_CreatorAtom, fieldPath.c_str() )) ) { + haveXMP = true; + xmp->SetProperty ( kXMP_NS_CreatorAtom, fieldPath.c_str(), rawCr8r.appOptions ); + } + + rawCr8r.appName[31] = 0; // Ensure a terminating nul. + if ( (rawCr8r.appName[0] != 0) && (! xmp->DoesPropertyExist ( kXMP_NS_XMP, "CreatorTool" )) ) { + haveXMP = true; + xmp->SetProperty ( kXMP_NS_XMP, "CreatorTool", rawCr8r.appName ); + } + + } + + return haveXMP; + +} // ImportCr8rItems + +// ================================================================================================= +// ExportCr8rItems +// =============== + +static inline void SetBufferedString ( char * dest, const std::string source, size_t limit ) +{ + memset ( dest, 0, limit ); + size_t count = source.size(); + if ( count >= limit ) count = limit - 1; // Ensure a terminating nul. + memcpy ( dest, source.c_str(), count ); +} + +// ------------------------------------------------------------------------------------------------- + +static void ExportCr8rItems ( const SXMPMeta & xmp, MOOV_Manager * moovMgr ) +{ + bool haveNewCr8r = false; + std::string creatorCode, appleEvent, fileExt, appOptions, appName; + + haveNewCr8r |= xmp.GetStructField ( kXMP_NS_CreatorAtom, "macAtom", kXMP_NS_CreatorAtom, "applicationCode", &creatorCode, 0 ); + haveNewCr8r |= xmp.GetStructField ( kXMP_NS_CreatorAtom, "macAtom", kXMP_NS_CreatorAtom, "invocationAppleEvent", &appleEvent, 0 ); + haveNewCr8r |= xmp.GetStructField ( kXMP_NS_CreatorAtom, "windowsAtom", kXMP_NS_CreatorAtom, "extension", &fileExt, 0 ); + haveNewCr8r |= xmp.GetStructField ( kXMP_NS_CreatorAtom, "windowsAtom", kXMP_NS_CreatorAtom, "invocationFlags", &appOptions, 0 ); + haveNewCr8r |= xmp.GetProperty ( kXMP_NS_XMP, "CreatorTool", &appName, 0 ); + + MOOV_Manager::BoxInfo infoCr8r; + MOOV_Manager::BoxRef refCr8r = moovMgr->GetBox ( "moov/udta/Cr8r", &infoCr8r ); + bool haveOldCr8r = ( (refCr8r != 0) && (infoCr8r.contentSize == sizeof ( Cr8rBoxContent )) ); + + if ( ! haveNewCr8r ) { + if ( haveOldCr8r ) { + MOOV_Manager::BoxRef udtaRef = moovMgr->GetBox ( "moov/udta", 0 ); + moovMgr->DeleteTypeChild ( udtaRef, 0x43723872 /* 'Cr8r' */ ); + } + return; + } + + Cr8rBoxContent newCr8r; + const Cr8rBoxContent * oldCr8r = (Cr8rBoxContent*) infoCr8r.content; + + if ( ! haveOldCr8r ) { + + memset ( &newCr8r, 0, sizeof(newCr8r) ); + newCr8r.magic = MakeUns32BE ( 0xBEEFCAFE ); + newCr8r.size = MakeUns32BE ( sizeof ( newCr8r ) ); + newCr8r.majorVer = MakeUns16BE ( 1 ); + + } else { + + memcpy ( &newCr8r, oldCr8r, sizeof(newCr8r) ); + if ( GetUns32BE ( &newCr8r.magic ) != 0xBEEFCAFE ) { // Make sure we write BE numbers. + Flip4 ( &newCr8r.magic ); + Flip4 ( &newCr8r.size ); + Flip2 ( &newCr8r.majorVer ); + Flip2 ( &newCr8r.minorVer ); + Flip4 ( &newCr8r.creatorCode ); + Flip4 ( &newCr8r.appleEvent ); + } + + } + + if ( ! creatorCode.empty() ) { + newCr8r.creatorCode = MakeUns32BE ( (XMP_Uns32) strtoul ( creatorCode.c_str(), 0, 0 ) ); + } + + if ( ! appleEvent.empty() ) { + newCr8r.appleEvent = MakeUns32BE ( (XMP_Uns32) strtoul ( appleEvent.c_str(), 0, 0 ) ); + } + + if ( ! fileExt.empty() ) SetBufferedString ( newCr8r.fileExt, fileExt, sizeof ( newCr8r.fileExt ) ); + if ( ! appOptions.empty() ) SetBufferedString ( newCr8r.appOptions, appOptions, sizeof ( newCr8r.appOptions ) ); + if ( ! appName.empty() ) SetBufferedString ( newCr8r.appName, appName, sizeof ( newCr8r.appName ) ); + + moovMgr->SetBox ( "moov/udta/Cr8r", &newCr8r, sizeof(newCr8r) ); + +} // ExportCr8rItems + +// ================================================================================================= +// GetAtomInfo +// =========== + +struct AtomInfo { + XMP_Int64 atomSize; + XMP_Uns32 atomType; + bool hasLargeSize; +}; + +enum { // ! Do not rearrange, code depends on this order. + kBadQT_NoError = 0, // No errors. + kBadQT_SmallInner = 1, // An extra 1..7 bytes at the end of an inner span. + kBadQT_LargeInner = 2, // More serious inner garbage, found as invalid atom length. + kBadQT_SmallOuter = 3, // An extra 1..7 bytes at the end of the file. + kBadQT_LargeOuter = 4 // More serious EOF garbage, found as invalid atom length. +}; +typedef XMP_Uns8 QTErrorMode; + +static QTErrorMode GetAtomInfo ( XMP_IO* qtFile, XMP_Int64 spanSize, int nesting, AtomInfo * info ) +{ + QTErrorMode status = kBadQT_NoError; + XMP_Uns8 buffer [8]; + + info->hasLargeSize = false; + + qtFile->ReadAll ( buffer, 8 ); // Will throw if 8 bytes aren't available. + info->atomSize = GetUns32BE ( &buffer[0] ); // ! Yes, the initial size is big endian UInt32. + info->atomType = GetUns32BE ( &buffer[4] ); + + if ( info->atomSize == 0 ) { // Does the atom extend to EOF? + + if ( nesting != 0 ) return kBadQT_LargeInner; + info->atomSize = spanSize; // This outer atom goes to EOF. + + } else if ( info->atomSize == 1 ) { // Does the atom have a 64-bit size? + + if ( spanSize < 16 ) { // Is there room in the span for the 16 byte header? + status = kBadQT_LargeInner; + if ( nesting == 0 ) status += 2; // Convert to "outer". + return status; + } + + qtFile->ReadAll ( buffer, 8 ); + info->atomSize = (XMP_Int64) GetUns64BE ( &buffer[0] ); + info->hasLargeSize = true; + + } + + XMP_Assert ( status == kBadQT_NoError ); + return status; + +} // GetAtomInfo + +// ================================================================================================= +// CheckAtomList +// ============= +// +// Check that a sequence of atoms fills a given span. The I/O position must be at the start of the +// span, it is left just past the span on success. Recursive checks are done for top level 'moov' +// atoms, and second level 'udta' atoms ('udta' inside 'moov'). +// +// Checking continues for "small inner" errors. They will be reported if no other kinds of errors +// are found, otherwise the other error is reported. Checking is immediately aborted for any "large" +// error. The rationale is that QuickTime can apparently handle small inner errors. They might be +// arise from updates that shorten an atom by less than 8 bytes. Larger shrinkage should introduce a +// 'free' atom. + +static QTErrorMode CheckAtomList ( XMP_IO* qtFile, XMP_Int64 spanSize, int nesting ) +{ + QTErrorMode status = kBadQT_NoError; + AtomInfo info; + + const static XMP_Uns32 moovAtomType = 0x6D6F6F76; // ! Don't use MakeUns32BE, already big endian. + const static XMP_Uns32 udtaAtomType = 0x75647461; + + for ( ; spanSize >= 8; spanSize -= info.atomSize ) { + + QTErrorMode atomStatus = GetAtomInfo ( qtFile, spanSize, nesting, &info ); + if ( atomStatus != kBadQT_NoError ) return atomStatus; + + XMP_Int64 headerSize = 8; + if ( info.hasLargeSize ) headerSize = 16; + + if ( (info.atomSize < headerSize) || (info.atomSize > spanSize) ) { + status = kBadQT_LargeInner; + if ( nesting == 0 ) status += 2; // Convert to "outer". + return status; + } + + bool doChildren = false; + if ( (nesting == 0) && (info.atomType == moovAtomType) ) doChildren = true; + if ( (nesting == 1) && (info.atomType == udtaAtomType) ) doChildren = true; + + XMP_Int64 dataSize = info.atomSize - headerSize; + + if ( ! doChildren ) { + qtFile->Seek ( dataSize, kXMP_SeekFromCurrent ); + } else { + QTErrorMode innerStatus = CheckAtomList ( qtFile, dataSize, nesting+1 ); + if ( innerStatus > kBadQT_SmallInner ) return innerStatus; // Quit for serious errors. + if ( status == kBadQT_NoError ) status = innerStatus; // Remember small inner errors. + } + + } + + XMP_Assert ( status <= kBadQT_SmallInner ); // Else already returned. + // ! Make sure inner kBadQT_SmallInner is propagated if this span is OK. + + if ( spanSize != 0 ) { + qtFile->Seek ( spanSize, kXMP_SeekFromCurrent ); // ! Skip the trailing garbage of this span. + status = kBadQT_SmallInner; + if ( spanSize >= 8 ) status = kBadQT_LargeInner; + if ( nesting == 0 ) status += 2; // Convert to "outer". + } + + return status; + +} // CheckAtomList + +// ================================================================================================= +// AttemptFileRepair +// ================= + +static void AttemptFileRepair ( XMP_IO* qtFile, XMP_Int64 fileSpace, QTErrorMode status ) +{ + + switch ( status ) { + case kBadQT_NoError : return; // Sanity check. + case kBadQT_SmallInner : return; // Fixed in normal update code for the 'udta' box. + case kBadQT_LargeInner : XMP_Throw ( "Can't repair QuickTime file", kXMPErr_BadFileFormat ); + case kBadQT_SmallOuter : break; // Truncate file below. + case kBadQT_LargeOuter : break; // Truncate file below. + default : XMP_Throw ( "Invalid QuickTime error mode", kXMPErr_InternalFailure ); + } + + AtomInfo info; + XMP_Int64 headerSize; + + // Process the top level atoms until an error is found. + + qtFile->Rewind(); + + for ( ; fileSpace >= 8; fileSpace -= info.atomSize ) { + + QTErrorMode atomStatus = GetAtomInfo ( qtFile, fileSpace, 0, &info ); + + headerSize = 8; // ! Set this before checking atomStatus, used after the loop. + if ( info.hasLargeSize ) headerSize = 16; + + if ( atomStatus != kBadQT_NoError ) break; + if ( (info.atomSize < headerSize) || (info.atomSize > fileSpace) ) break; + + XMP_Int64 dataSize = info.atomSize - headerSize; + qtFile->Seek ( dataSize, kXMP_SeekFromCurrent ); + + } + + // Truncate the file. If fileSpace >= 8 then the loop exited early due to a bad atom, seek back + // to the atom's start. Otherwise, the loop exited because no more atoms are possible, no seek. + + if ( fileSpace >= 8 ) qtFile->Seek ( -headerSize, kXMP_SeekFromCurrent ); + XMP_Int64 currPos = qtFile->Offset(); + qtFile->Truncate ( currPos ); + +} // AttemptFileRepair + +// ================================================================================================= +// CheckQTFileStructure +// ==================== + +static void CheckQTFileStructure ( XMPFileHandler * thiz, bool doRepair ) +{ + XMPFiles * parent = thiz->parent; + XMP_IO* fileRef = parent->ioRef; + XMP_Int64 fileSize = fileRef->Length(); + + // Check the basic file structure and try to repair if asked. + + fileRef->Rewind(); + QTErrorMode status = CheckAtomList ( fileRef, fileSize, 0 ); + + if ( status != kBadQT_NoError ) { + if ( doRepair || (status == kBadQT_SmallInner) || (status == kBadQT_SmallOuter) ) { + AttemptFileRepair ( fileRef, fileSize, status ); // Will throw if the attempt fails. + } else if ( status != kBadQT_SmallInner ) { + XMP_Throw ( "Ill-formed QuickTime file", kXMPErr_BadFileFormat ); + } else { + return; // ! Ignore these, QT seems to be able to handle them. + // *** Might want to throw for check-only, ignore when repairing. + } + } + +} // CheckQTFileStructure; + +// ================================================================================================= +// CheckFinalBox +// ============= +// +// Before appending anything new, check if the final top level box has a "to EoF" length. If so, fix +// it to have an explicit length. + +static void CheckFinalBox ( XMP_IO* fileRef, XMP_AbortProc abortProc, void * abortArg ) +{ + const bool checkAbort = (abortProc != 0); + + XMP_Uns64 fileSize = fileRef->Length(); + + // Find the last 2 boxes in the file. Need the previous to last in case it is an Apple 'wide' box. + + XMP_Uns64 prevPos, lastPos, nextPos; + ISOMedia::BoxInfo prevBox, lastBox; + XMP_Uns8 buffer [16]; // Enough to create an extended header. + + memset ( &prevBox, 0, sizeof(prevBox) ); // AUDIT: Using sizeof(prevBox) is safe. + memset ( &lastBox, 0, sizeof(lastBox) ); // AUDIT: Using sizeof(lastBox) is safe. + prevPos = lastPos = nextPos = 0; + while ( nextPos != fileSize ) { + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "MPEG4_MetaHandler::CheckFinalBox - User abort", kXMPErr_UserAbort ); + } + prevBox = lastBox; + prevPos = lastPos; + lastPos = nextPos; + nextPos = ISOMedia::GetBoxInfo ( fileRef, lastPos, fileSize, &lastBox, true /* throw errors */ ); + } + + // See if the last box is valid and has a "to EoF" size. + + if ( lastBox.headerSize < 8 ) XMP_Throw ( "MPEG-4 final box is invalid", kXMPErr_EnforceFailure ); + fileRef->Seek ( lastPos, kXMP_SeekFromStart ); + fileRef->Read ( buffer, 4 ); + XMP_Uns64 lastSize = GetUns32BE ( &buffer[0] ); // ! Yes, the file has a 32-bit value. + if ( lastSize != 0 ) return; + + // Have a final "to EoF" box, try to write the explicit size. + + lastSize = lastBox.headerSize + lastBox.contentSize; + if ( lastSize <= 0xFFFFFFFFUL ) { + + // Fill in the 32-bit exact size. + PutUns32BE ( (XMP_Uns32)lastSize, &buffer[0] ); + fileRef->Seek ( lastPos, kXMP_SeekFromStart ); + fileRef->Write ( buffer, 4 ); + + } else { + + // Try to convert to using an extended header. + + if ( (prevBox.boxType != ISOMedia::k_wide) || (prevBox.headerSize != 8) || (prevBox.contentSize != 0) ) { + XMP_Throw ( "Can't expand final box header", kXMPErr_EnforceFailure ); + } + XMP_Assert ( prevPos == (lastPos - 8) ); + + PutUns32BE ( 1, &buffer[0] ); + PutUns32BE ( lastBox.boxType, &buffer[4] ); + PutUns64BE ( lastSize, &buffer[8] ); + fileRef->Seek ( prevPos, kXMP_SeekFromStart ); + fileRef->Write ( buffer, 16 ); + + } + +} // CheckFinalBox + +// ================================================================================================= +// WriteBoxHeader +// ============== + +static void WriteBoxHeader ( XMP_IO* fileRef, XMP_Uns32 boxType, XMP_Uns64 boxSize ) +{ + XMP_Uns32 u32; + XMP_Uns64 u64; + XMP_Enforce ( boxSize >= 8 ); // The size must be the full size, not just the content. + + if ( boxSize <= 0xFFFFFFFF ) { + + u32 = MakeUns32BE ( (XMP_Uns32)boxSize ); + fileRef->Write ( &u32, 4 ); + u32 = MakeUns32BE ( boxType ); + fileRef->Write ( &u32, 4 ); + + } else { + + u32 = MakeUns32BE ( 1 ); + fileRef->Write ( &u32, 4 ); + u32 = MakeUns32BE ( boxType ); + fileRef->Write ( &u32, 4 ); + u64 = MakeUns64BE ( boxSize ); + fileRef->Write ( &u64, 8 ); + + } + +} // WriteBoxHeader + +// ================================================================================================= +// WipeBoxFree +// =========== +// +// Change the box's type to 'free' (or create a 'free' box) and zero the content. + +static XMP_Uns8 kZeroes [64*1024]; // C semantics guarantee zero initialization. + +static void WipeBoxFree ( XMP_IO* fileRef, XMP_Uns64 boxOffset, XMP_Uns32 boxSize ) +{ + if ( boxSize == 0 ) return; + XMP_Enforce ( boxSize >= 8 ); + + fileRef->Seek ( boxOffset, kXMP_SeekFromStart ); + XMP_Uns32 u32; + u32 = MakeUns32BE ( boxSize ); // ! The actual size should not change, but might have had a long header. + fileRef->Write ( &u32, 4 ); + u32 = MakeUns32BE ( ISOMedia::k_free ); + fileRef->Write ( &u32, 4 ); + + XMP_Uns32 ioCount = sizeof ( kZeroes ); + for ( boxSize -= 8; boxSize > 0; boxSize -= ioCount ) { + if ( ioCount > boxSize ) ioCount = boxSize; + fileRef->Write ( &kZeroes[0], ioCount ); + } + +} // WipeBoxFree + +// ================================================================================================= +// CreateFreeSpaceList +// =================== + +struct SpaceInfo { + XMP_Uns64 offset, size; + SpaceInfo() : offset(0), size(0) {}; + SpaceInfo ( XMP_Uns64 _offset, XMP_Uns64 _size ) : offset(_offset), size(_size) {}; +}; + +typedef std::vector<SpaceInfo> FreeSpaceList; + +static void CreateFreeSpaceList ( XMP_IO* fileRef, XMP_Uns64 fileSize, + XMP_Uns64 oldOffset, XMP_Uns32 oldSize, FreeSpaceList * spaceList ) +{ + XMP_Uns64 boxPos, boxNext, adjacentFree; + ISOMedia::BoxInfo currBox; + + fileRef->Rewind(); + spaceList->clear(); + + for ( boxPos = 0; boxPos < fileSize; boxPos = boxNext ) { + + boxNext = ISOMedia::GetBoxInfo ( fileRef, boxPos, fileSize, &currBox, true /* throw errors */ ); + XMP_Uns64 currSize = currBox.headerSize + currBox.contentSize; + + if ( (currBox.boxType == ISOMedia::k_free) || + (currBox.boxType == ISOMedia::k_skip) || + ((boxPos == oldOffset) && (currSize == oldSize)) ) { + + if ( spaceList->empty() || (boxPos != adjacentFree) ) { + spaceList->push_back ( SpaceInfo ( boxPos, currSize ) ); + adjacentFree = boxPos + currSize; + } else { + SpaceInfo * lastSpace = &spaceList->back(); + lastSpace->size += currSize; + } + + } + + } + +} // CreateFreeSpaceList + +// ================================================================================================= +// MPEG4_MetaHandler::CacheFileData +// ================================ +// +// There are 3 file variants: normal ISO Base Media, modern QuickTime, and classic QuickTime. The +// XMP is placed differently between the ISO and two QuickTime forms, and there is different but not +// colliding native metadata. The entire 'moov' subtree is cached, along with the top level 'uuid' +// box of XMP if present. + +void MPEG4_MetaHandler::CacheFileData() +{ + XMP_Assert ( ! this->containsXMP ); + + XMPFiles * parent = this->parent; + XMP_OptionBits openFlags = parent->openFlags; + + XMP_IO* fileRef = parent->ioRef; + + XMP_AbortProc abortProc = parent->abortProc; + void * abortArg = parent->abortArg; + const bool checkAbort = (abortProc != 0); + + // First do some special case repair to QuickTime files, based on bad files in the wild. + + const bool isUpdate = XMP_OptionIsSet ( openFlags, kXMPFiles_OpenForUpdate ); + const bool doRepair = XMP_OptionIsSet ( openFlags, kXMPFiles_OpenRepairFile ); + + if ( isUpdate && (parent->format == kXMP_MOVFile) ) { + CheckQTFileStructure ( this, doRepair ); // Will throw for failure. + } + + // Cache the top level 'moov' and 'uuid'/XMP boxes. + + XMP_Uns64 fileSize = fileRef->Length(); + + XMP_Uns64 boxPos, boxNext; + ISOMedia::BoxInfo currBox; + + bool xmpOnly = XMP_OptionIsSet ( openFlags, kXMPFiles_OpenOnlyXMP ); + bool haveISOFile = (this->fileMode == MOOV_Manager::kFileIsNormalISO); + + bool uuidFound = (! haveISOFile); // Ignore the XMP 'uuid' box for QuickTime files. + bool moovIgnored = (xmpOnly & haveISOFile); // Ignore the 'moov' box for XMP-only ISO files. + bool moovFound = moovIgnored; + + for ( boxPos = 0; boxPos < fileSize; boxPos = boxNext ) { + + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "MPEG4_MetaHandler::CacheFileData - User abort", kXMPErr_UserAbort ); + } + + + boxNext = ISOMedia::GetBoxInfo ( fileRef, boxPos, fileSize, &currBox ); + + if ( (! moovFound) && (currBox.boxType == ISOMedia::k_moov) ) { + + XMP_Uns64 fullMoovSize = currBox.headerSize + currBox.contentSize; + if ( fullMoovSize > moovBoxSizeLimit ) { // From here on we know 32-bit offsets are safe. + XMP_Throw ( "Oversize 'moov' box", kXMPErr_EnforceFailure ); + } + + this->moovMgr.fullSubtree.assign ( (XMP_Uns32)fullMoovSize, 0 ); + fileRef->Seek ( boxPos, kXMP_SeekFromStart ); + fileRef->Read ( &this->moovMgr.fullSubtree[0], (XMP_Uns32)fullMoovSize ); + + this->moovBoxPos = boxPos; + this->moovBoxSize = (XMP_Uns32)fullMoovSize; + moovFound = true; + if ( uuidFound ) break; // Exit the loop when both are found. + + } else if ( (! uuidFound) && (currBox.boxType == ISOMedia::k_uuid) ) { + + if ( currBox.contentSize < 16 ) continue; + + XMP_Uns8 uuid [16]; + fileRef->ReadAll ( uuid, 16 ); + if ( memcmp ( uuid, ISOMedia::k_xmpUUID, 16 ) != 0 ) continue; // Check for the XMP GUID. + + XMP_Uns64 fullUuidSize = currBox.headerSize + currBox.contentSize; + if ( fullUuidSize > moovBoxSizeLimit ) { // From here on we know 32-bit offsets are safe. + XMP_Throw ( "Oversize XMP 'uuid' box", kXMPErr_EnforceFailure ); + } + + this->packetInfo.offset = boxPos + currBox.headerSize + 16; // The 16 is for the UUID. + this->packetInfo.length = (XMP_Int32) (currBox.contentSize - 16); + + this->xmpPacket.assign ( this->packetInfo.length, ' ' ); + fileRef->ReadAll ( (void*)this->xmpPacket.data(), this->packetInfo.length ); + + this->xmpBoxPos = boxPos; + this->xmpBoxSize = (XMP_Uns32)fullUuidSize; + uuidFound = true; + if ( moovFound ) break; // Exit the loop when both are found. + + } + + } + + if ( (! moovFound) && (! moovIgnored) ) XMP_Throw ( "No 'moov' box", kXMPErr_BadFileFormat ); + +} // MPEG4_MetaHandler::CacheFileData + +// ================================================================================================= +// MPEG4_MetaHandler::ProcessXMP +// ============================= + +void MPEG4_MetaHandler::ProcessXMP() +{ + if ( this->processedXMP ) return; + this->processedXMP = true; // Make sure only called once. + + XMPFiles * parent = this->parent; + XMP_OptionBits openFlags = parent->openFlags; + + bool xmpOnly = XMP_OptionIsSet ( openFlags, kXMPFiles_OpenOnlyXMP ); + bool haveISOFile = (this->fileMode == MOOV_Manager::kFileIsNormalISO); + + // Process the cached XMP (from the 'uuid' box) if that is all we want and this is an ISO file. + + if ( xmpOnly & haveISOFile ) { + + this->containsXMP = this->havePreferredXMP = (this->packetInfo.length != 0); + + if ( this->containsXMP ) { + FillPacketInfo ( this->xmpPacket, &this->packetInfo ); + this->xmpObj.ParseFromBuffer ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + this->xmpObj.DeleteProperty ( kXMP_NS_XMP, "NativeDigests" ); // No longer used. + } + + return; + + } + + // Parse the cached 'moov' subtree, parse the preferred XMP. + + if ( this->moovMgr.fullSubtree.empty() ) XMP_Throw ( "No 'moov' box", kXMPErr_BadFileFormat ); + this->moovMgr.ParseMemoryTree ( this->fileMode ); + + if ( (this->xmpBoxPos == 0) || (! haveISOFile) ) { + + // Look for the QuickTime moov/uuid/XMP_ box. + + MOOV_Manager::BoxInfo xmpInfo; + MOOV_Manager::BoxRef xmpRef = this->moovMgr.GetBox ( "moov/udta/XMP_", &xmpInfo ); + + if ( (xmpRef != 0) && (xmpInfo.contentSize != 0) ) { + + this->xmpBoxPos = this->moovBoxPos + this->moovMgr.GetParsedOffset ( xmpRef ); + this->packetInfo.offset = this->xmpBoxPos + this->moovMgr.GetHeaderSize ( xmpRef ); + this->packetInfo.length = xmpInfo.contentSize; + + this->xmpPacket.assign ( (char*)xmpInfo.content, this->packetInfo.length ); + this->havePreferredXMP = (! haveISOFile); + + } + + } + + if ( this->xmpBoxPos != 0 ) { + this->containsXMP = true; + FillPacketInfo ( this->xmpPacket, &this->packetInfo ); + this->xmpObj.ParseFromBuffer ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + this->xmpObj.DeleteProperty ( kXMP_NS_XMP, "NativeDigests" ); // No longer used. + } + + // Import the non-XMP items. Do the imports in reverse priority order, last import wins! + + MOOV_Manager::BoxInfo mvhdInfo; + MOOV_Manager::BoxRef mvhdRef = this->moovMgr.GetBox ( "moov/mvhd", &mvhdInfo ); + bool mvhdFound = ((mvhdRef != 0) && (mvhdInfo.contentSize != 0)); + + MOOV_Manager::BoxInfo udtaInfo; + MOOV_Manager::BoxRef udtaRef = this->moovMgr.GetBox ( "moov/udta", &udtaInfo ); + std::vector<MOOV_Manager::BoxInfo> cprtBoxes; + + if ( udtaRef != 0 ) { + for ( XMP_Uns32 i = 0; i < udtaInfo.childCount; ++i ) { + MOOV_Manager::BoxInfo currInfo; + MOOV_Manager::BoxRef currRef = this->moovMgr.GetNthChild ( udtaRef, i, &currInfo ); + if ( currRef == 0 ) break; // Sanity check, should not happen. + if ( currInfo.boxType != ISOMedia::k_cprt ) continue; + cprtBoxes.push_back ( currInfo ); + } + } + bool cprtFound = (! cprtBoxes.empty()); + + bool tradQTFound = this->tradQTMgr.ParseCachedBoxes ( this->moovMgr ); + bool tmcdFound = this->ParseTimecodeTrack(); + + if ( this->fileMode == MOOV_Manager::kFileIsNormalISO ) { + + if ( mvhdFound ) this->containsXMP |= ImportMVHDItems ( mvhdInfo, &this->xmpObj ); + if ( cprtFound ) this->containsXMP |= ImportISOCopyrights ( cprtBoxes, &this->xmpObj ); + } else { // This is a QuickTime file, either traditional or modern. + + if ( mvhdFound ) this->containsXMP |= ImportMVHDItems ( mvhdInfo, &this->xmpObj ); + if ( cprtFound ) this->containsXMP |= ImportISOCopyrights ( cprtBoxes, &this->xmpObj ); + if ( tmcdFound | tradQTFound ) { + // Some of the timecode items are in the .../udta/©... set but handled by ImportTimecodeItems. + this->containsXMP |= ImportTimecodeItems ( this->tmcdInfo, this->tradQTMgr, &this->xmpObj ); + } + + this->containsXMP |= ImportCr8rItems ( this->moovMgr, &this->xmpObj ); + + } + +} // MPEG4_MetaHandler::ProcessXMP + +// ================================================================================================= +// MPEG4_MetaHandler::ParseTimecodeTrack +// ===================================== + +bool MPEG4_MetaHandler::ParseTimecodeTrack() +{ + MOOV_Manager::BoxRef stblRef = FindTimecode_stbl ( this->moovMgr ); + if ( stblRef == 0 ) return false; + + // Find the .../stbl/stsd box and process the first table entry. + + MOOV_Manager::BoxInfo stsdInfo; + MOOV_Manager::BoxRef stsdRef; + + stsdRef = this->moovMgr.GetTypeChild ( stblRef, ISOMedia::k_stsd, &stsdInfo ); + if ( stsdRef == 0 ) return false; + if ( stsdInfo.contentSize < (8 + sizeof ( MOOV_Manager::Content_stsd_entry )) ) return false; + if ( GetUns32BE ( stsdInfo.content + 4 ) == 0 ) return false; // Make sure the entry count is non-zero. + + const MOOV_Manager::Content_stsd_entry * stsdRawEntry = (MOOV_Manager::Content_stsd_entry*) (stsdInfo.content + 8); + + XMP_Uns32 stsdEntrySize = GetUns32BE ( &stsdRawEntry->entrySize ); + if ( stsdEntrySize > (stsdInfo.contentSize - 4) ) stsdEntrySize = stsdInfo.contentSize - 4; + if ( stsdEntrySize < sizeof ( MOOV_Manager::Content_stsd_entry ) ) return false; + + XMP_Uns32 stsdEntryFormat = GetUns32BE ( &stsdRawEntry->format ); + if ( stsdEntryFormat != ISOMedia::k_tmcd ) return false; + + this->tmcdInfo.timeScale = GetUns32BE ( &stsdRawEntry->timeScale ); + this->tmcdInfo.frameDuration = GetUns32BE ( &stsdRawEntry->frameDuration ); + + double floatCount = (double)this->tmcdInfo.timeScale / (double)this->tmcdInfo.frameDuration; + XMP_Uns8 expectedCount = (XMP_Uns8) (floatCount + 0.5); + if ( expectedCount != stsdRawEntry->frameCount ) { + double countRatio = (double)stsdRawEntry->frameCount / (double)expectedCount; + this->tmcdInfo.timeScale = (XMP_Uns32) (((double)this->tmcdInfo.timeScale * countRatio) + 0.5); + } + + XMP_Uns32 flags = GetUns32BE ( &stsdRawEntry->flags ); + this->tmcdInfo.isDropFrame = flags & 0x1; + + // Look for a trailing 'name' box on the first stsd table entry. + + XMP_Uns32 stsdTrailerSize = stsdEntrySize - sizeof ( MOOV_Manager::Content_stsd_entry ); + if ( stsdTrailerSize > 8 ) { // Room for a non-empty 'name' box? + + const XMP_Uns8 * trailerStart = stsdInfo.content + 8 + sizeof ( MOOV_Manager::Content_stsd_entry ); + const XMP_Uns8 * trailerLimit = trailerStart + stsdTrailerSize; + const XMP_Uns8 * trailerPos; + const XMP_Uns8 * trailerNext; + ISOMedia::BoxInfo trailerInfo; + + for ( trailerPos = trailerStart; trailerPos < trailerLimit; trailerPos = trailerNext ) { + + trailerNext = ISOMedia::GetBoxInfo ( trailerPos, trailerLimit, &trailerInfo ); + + if ( trailerInfo.boxType == ISOMedia::k_name ) { + + this->tmcdInfo.nameOffset = (XMP_Uns32) (trailerPos - stsdInfo.content); + + if ( trailerInfo.contentSize > 4 ) { + + XMP_Uns16 textLen = GetUns16BE ( trailerPos + trailerInfo.headerSize ); + this->tmcdInfo.macLang = GetUns16BE ( trailerPos + trailerInfo.headerSize + 2 ); + + if ( trailerInfo.contentSize >= (XMP_Uns64)(textLen + 4) ) { + const char * textPtr = (char*) (trailerPos + trailerInfo.headerSize + 4); + this->tmcdInfo.macName.assign ( textPtr, textLen ); + } + + } + + break; // Done after finding the first 'name' box. + + } + + } + + } + + // Find the timecode sample. + + XMP_Uns64 sampleOffset = 0; + MOOV_Manager::BoxInfo tempInfo; + MOOV_Manager::BoxRef tempRef; + + tempRef = this->moovMgr.GetTypeChild ( stblRef, ISOMedia::k_stsc, &tempInfo ); + if ( tempRef == 0 ) return false; + if ( tempInfo.contentSize < (8 + sizeof ( MOOV_Manager::Content_stsc_entry )) ) return false; + if ( GetUns32BE ( tempInfo.content + 4 ) == 0 ) return false; // Make sure the entry count is non-zero. + + XMP_Uns32 firstChunkNumber = GetUns32BE ( tempInfo.content + 8 ); // Want first field of first entry. + + tempRef = this->moovMgr.GetTypeChild ( stblRef, ISOMedia::k_stco, &tempInfo ); + + if ( tempRef != 0 ) { + + if ( tempInfo.contentSize < (8 + 4) ) return false; + XMP_Uns32 stcoCount = GetUns32BE ( tempInfo.content + 4 ); + if ( stcoCount < firstChunkNumber ) return false; + XMP_Uns32 * stcoPtr = (XMP_Uns32*) (tempInfo.content + 8); + sampleOffset = GetUns32BE ( &stcoPtr[firstChunkNumber-1] ); // ! Chunk number is 1-based. + + } else { + + tempRef = this->moovMgr.GetTypeChild ( stblRef, ISOMedia::k_co64, &tempInfo ); + if ( (tempRef == 0) || (tempInfo.contentSize < (8 + 8)) ) return false; + XMP_Uns32 co64Count = GetUns32BE ( tempInfo.content + 4 ); + if ( co64Count < firstChunkNumber ) return false; + XMP_Uns64 * co64Ptr = (XMP_Uns64*) (tempInfo.content + 8); + sampleOffset = GetUns64BE ( &co64Ptr[firstChunkNumber-1] ); // ! Chunk number is 1-based. + + } + + if ( sampleOffset != 0 ) { // Read the timecode sample. + + XMPFiles_IO* localFile = 0; + + if ( this->parent->ioRef == 0 ) { // Local read-only files get closed in CacheFileData. + XMP_Assert ( this->parent->UsesLocalIO() ); + localFile = XMPFiles_IO::New_XMPFiles_IO ( this->parent->filePath.c_str(), Host_IO::openReadOnly ); + XMP_Enforce ( localFile != 0 ); + this->parent->ioRef = localFile; + } + + this->parent->ioRef->Seek ( sampleOffset, kXMP_SeekFromStart ); + this->parent->ioRef->ReadAll ( &this->tmcdInfo.timecodeSample, 4 ); + this->tmcdInfo.timecodeSample = MakeUns32BE ( this->tmcdInfo.timecodeSample ); + if ( localFile != 0 ) { + localFile->Close(); + delete localFile; + this->parent->ioRef = 0; + } + + } + + // If this is a QT file, look for an edit list offset to add to the timecode sample. Look in the + // timecode track for an edts/elst box. The content is a UInt8 version, UInt8[3] flags, a UInt32 + // entry count, and a sequence of UInt32 triples (trackDuration, mediaTime, mediaRate). Take + // mediaTime from the first entry, divide it by tmcdInfo.frameDuration, add that to + // tmcdInfo.timecodeSample. + + bool isQT = (this->fileMode == MOOV_Manager::kFileIsModernQT) || + (this->fileMode == MOOV_Manager::kFileIsTraditionalQT); + + MOOV_Manager::BoxRef elstRef = 0; + if ( isQT ) elstRef = FindTimecode_elst ( this->moovMgr ); + if ( elstRef != 0 ) { + + MOOV_Manager::BoxInfo elstInfo; + this->moovMgr.GetBoxInfo ( elstRef, &elstInfo ); + + if ( elstInfo.contentSize >= (4+4+12) ) { + XMP_Uns32 elstCount = GetUns32BE ( elstInfo.content + 4 ); + if ( elstCount >= 1 ) { + XMP_Uns32 mediaTime = GetUns32BE ( elstInfo.content + (4+4+4) ); + this->tmcdInfo.timecodeSample += (mediaTime / this->tmcdInfo.frameDuration); + } + } + + } + + // Finally update this->tmcdInfo to remember (for update) that there is an OK timecode track. + + this->tmcdInfo.stsdBoxFound = true; + this->tmcdInfo.sampleOffset = sampleOffset; + return true; + +} // MPEG4_MetaHandler::ParseTimecodeTrack + +// ================================================================================================= +// MPEG4_MetaHandler::UpdateTopLevelBox +// ==================================== + +void MPEG4_MetaHandler::UpdateTopLevelBox ( XMP_Uns64 oldOffset, XMP_Uns32 oldSize, + const XMP_Uns8 * newBox, XMP_Uns32 newSize ) +{ + if ( (oldSize == 0) && (newSize == 0) ) return; // Sanity check, should not happen. + + XMP_IO* fileRef = this->parent->ioRef; + XMP_Uns64 oldFileSize = fileRef->Length(); + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + + if ( newSize == oldSize ) { + + // Trivial case, update the existing box in-place. + fileRef->Seek ( oldOffset, kXMP_SeekFromStart ); + fileRef->Write ( newBox, oldSize ); + + } else if ( (oldOffset + oldSize) == oldFileSize ) { + + // The old box was at the end, write the new and truncate the file if necessary. + fileRef->Seek ( oldOffset, kXMP_SeekFromStart ); + fileRef->Write ( newBox, newSize ); + fileRef->Truncate ( (oldOffset + newSize) ); // Does nothing if new size is bigger. + + } else if ( (newSize < oldSize) && ((oldSize - newSize) >= 8) ) { + + // The new size is smaller and there is enough room to create a free box. + fileRef->Seek ( oldOffset, kXMP_SeekFromStart ); + fileRef->Write ( newBox, newSize ); + WipeBoxFree ( fileRef, (oldOffset + newSize), (oldSize - newSize) ); + + } else { + + // Look for a trailing free box with enough space. If not found, consider any free space. + // If still not found, append the new box and make the old one free. + + ISOMedia::BoxInfo nextBoxInfo; + (void) ISOMedia::GetBoxInfo ( fileRef, (oldOffset + oldSize), oldFileSize, &nextBoxInfo, true /* throw errors */ ); + + XMP_Uns64 totalRoom = oldSize + nextBoxInfo.headerSize + nextBoxInfo.contentSize; + + bool nextIsFree = (nextBoxInfo.boxType == ISOMedia::k_free) || (nextBoxInfo.boxType == ISOMedia::k_skip); + bool haveEnoughRoom = (newSize == totalRoom) || + ( (newSize < totalRoom) && ((totalRoom - newSize) >= 8) ); + + if ( nextIsFree & haveEnoughRoom ) { + + fileRef->Seek ( oldOffset, kXMP_SeekFromStart ); + fileRef->Write ( newBox, newSize ); + + if ( newSize < totalRoom ) { + // Don't wipe, at most 7 old bytes left, it will be covered by the free header. + WriteBoxHeader ( fileRef, ISOMedia::k_free, (totalRoom - newSize) ); + } + + } else { + + // Create a list of all top level free space, including the old space as free. Use the + // earliest space that fits. If none, append. + + FreeSpaceList spaceList; + CreateFreeSpaceList ( fileRef, oldFileSize, oldOffset, oldSize, &spaceList ); + + size_t freeSlot, limit; + for ( freeSlot = 0, limit = spaceList.size(); freeSlot < limit; ++freeSlot ) { + XMP_Uns64 freeSize = spaceList[freeSlot].size; + if ( (newSize == freeSize) || ( (newSize < freeSize) && ((freeSize - newSize) >= 8) ) ) break; + } + + if ( freeSlot == spaceList.size() ) { + + // No available free space, append the new box. + CheckFinalBox ( fileRef, abortProc, abortArg ); + fileRef->ToEOF(); + fileRef->Write ( newBox, newSize ); + WipeBoxFree ( fileRef, oldOffset, oldSize ); + + } else { + + // Use the available free space. Wipe non-overlapping parts of the old box. The old + // box is either included in the new space, or is fully disjoint. + + SpaceInfo & newSpace = spaceList[freeSlot]; + + bool oldIsDisjoint = ((oldOffset + oldSize) <= newSpace.offset) || // Old is in front. + ((newSpace.offset + newSpace.size) <= oldOffset); // Old is behind. + + XMP_Assert ( (newSize == newSpace.size) || + ( (newSize < newSpace.size) && ((newSpace.size - newSize) >= 8) ) ); + + XMP_Assert ( oldIsDisjoint || + ( (newSpace.offset <= oldOffset) && + ((oldOffset + oldSize) <= (newSpace.offset + newSpace.size)) ) /* old is included */ ); + + XMP_Uns64 newFreeOffset = newSpace.offset + newSize; + XMP_Uns64 newFreeSize = newSpace.size - newSize; + + fileRef->Seek ( newSpace.offset, kXMP_SeekFromStart ); + fileRef->Write ( newBox, newSize ); + + if ( newFreeSize > 0 ) WriteBoxHeader ( fileRef, ISOMedia::k_free, newFreeSize ); + + if ( oldIsDisjoint ) { + + WipeBoxFree ( fileRef, oldOffset, oldSize ); + + } else { + + // Clear the exposed portion of the old box. + + XMP_Uns64 zeroStart = newFreeOffset + 8; + if ( newFreeSize > 0xFFFFFFFF ) zeroStart += 8; + if ( oldOffset > zeroStart ) zeroStart = oldOffset; + XMP_Uns64 zeroEnd = newFreeOffset + newFreeSize; + if ( (oldOffset + oldSize) < zeroEnd ) zeroEnd = oldOffset + oldSize; + + if ( zeroStart < zeroEnd ) { // The new box might cover the old. + XMP_Assert ( (zeroEnd - zeroStart) <= (XMP_Uns64)oldSize ); + XMP_Uns32 zeroSize = (XMP_Uns32) (zeroEnd - zeroStart); + fileRef->Seek ( zeroStart, kXMP_SeekFromStart ); + for ( XMP_Uns32 ioCount = sizeof ( kZeroes ); zeroSize > 0; zeroSize -= ioCount ) { + if ( ioCount > zeroSize ) ioCount = zeroSize; + fileRef->Write ( &kZeroes[0], ioCount ); + } + } + + } + + } + + } + + } + +} // MPEG4_MetaHandler::UpdateTopLevelBox + +// ================================================================================================= +// MPEG4_MetaHandler::UpdateFile +// ============================= +// +// Revamp notes: +// The 'moov' subtree and possibly the XMP 'uuid' box get updated. Compose the new copy of each and +// see if it fits in existing space, incorporating adjacent 'free' boxes if necessary. If that won't +// work, look for a sufficient 'free' box anywhere in the file. As a last resort, append the new copy. +// Assume no location sensitive data within 'moov', i.e. no offsets into it. This lets it be moved +// and its children freely rearranged. + +void MPEG4_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + if ( ! this->needsUpdate ) { // If needsUpdate is set then at least the XMP changed. + return; + } + + this->needsUpdate = false; // Make sure only called once. + XMP_Assert ( ! doSafeUpdate ); // This should only be called for "unsafe" updates. + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + XMP_IO* fileRef = this->parent->ioRef; + XMP_Uns64 fileSize = fileRef->Length(); + + bool haveISOFile = (this->fileMode == MOOV_Manager::kFileIsNormalISO); + + // Update the 'moov' subtree with exports from the XMP, but not the XMP itself (for QT files). + + ExportMVHDItems ( this->xmpObj, &this->moovMgr ); + ExportISOCopyrights ( this->xmpObj, &this->moovMgr ); + ExportQuickTimeItems ( this->xmpObj, &this->tradQTMgr, &this->moovMgr ); + ExportTimecodeItems ( this->xmpObj, &this->tmcdInfo, &this->tradQTMgr, &this->moovMgr ); + + if ( ! haveISOFile ) ExportCr8rItems ( this->xmpObj, &this->moovMgr ); + + // Try to update the XMP in-place if that is all that changed, or if it is in a preferred 'uuid' box. + // The XMP has already been serialized by common code to the appropriate length. Otherwise, update + // the 'moov'/'udta'/'XMP_' box in the MOOV_Manager, or the 'uuid' XMP box in the file. + + bool useUuidXMP = (this->fileMode == MOOV_Manager::kFileIsNormalISO); + bool inPlaceXMP = (this->xmpPacket.size() == (size_t)this->packetInfo.length) && + ( (useUuidXMP & this->havePreferredXMP) || (! this->moovMgr.IsChanged()) ); + + + if ( inPlaceXMP ) { + + // Update the existing XMP in-place. + fileRef->Seek ( this->packetInfo.offset, kXMP_SeekFromStart ); + fileRef->Write ( this->xmpPacket.c_str(), (XMP_Int32)this->xmpPacket.size() ); + + } else if ( useUuidXMP ) { + + // Don't leave an old 'moov'/'udta'/'XMP_' box around. + MOOV_Manager::BoxRef udtaRef = this->moovMgr.GetBox ( "moov/udta", 0 ); + if ( udtaRef != 0 ) this->moovMgr.DeleteTypeChild ( udtaRef, ISOMedia::k_XMP_ ); + + } else { + // Don't leave an old uuid XMP around (if we know about it). + if ( (! havePreferredXMP) && (this->xmpBoxSize != 0) ) { + WipeBoxFree ( fileRef, this->xmpBoxPos, this->xmpBoxSize ); + } + + // The udta form of XMP has just the XMP packet. + this->moovMgr.SetBox ( "moov/udta/XMP_", this->xmpPacket.c_str(), (XMP_Uns32)this->xmpPacket.size() ); + + } + + // Update the 'moov' subtree if necessary, and finally update the timecode sample. + + if ( this->moovMgr.IsChanged() ) { + this->moovMgr.UpdateMemoryTree(); + this->UpdateTopLevelBox ( moovBoxPos, moovBoxSize, &this->moovMgr.fullSubtree[0], (XMP_Uns32)this->moovMgr.fullSubtree.size() ); + } + + if ( this->tmcdInfo.sampleOffset != 0 ) { + fileRef->Seek ( this->tmcdInfo.sampleOffset, kXMP_SeekFromStart ); + XMP_Uns32 sample = MakeUns32BE ( this->tmcdInfo.timecodeSample ); + fileRef->Write ( &sample, 4 ); + } + + // Update the 'uuid' XMP box if necessary. + + if ( useUuidXMP & (! inPlaceXMP) ) { + + // The uuid form of XMP has the 16-byte UUID in front of the XMP packet. Form the complete + // box (including size/type header) for UpdateTopLevelBox. + RawDataBlock uuidBox; + XMP_Uns32 uuidSize = 4+4 + 16 + (XMP_Uns32)this->xmpPacket.size(); + uuidBox.assign ( uuidSize, 0 ); + PutUns32BE ( uuidSize, &uuidBox[0] ); + PutUns32BE ( ISOMedia::k_uuid, &uuidBox[4] ); + memcpy ( &uuidBox[8], ISOMedia::k_xmpUUID, 16 ); + memcpy ( &uuidBox[24], this->xmpPacket.c_str(), this->xmpPacket.size() ); + this->UpdateTopLevelBox ( this->xmpBoxPos, this->xmpBoxSize, &uuidBox[0], uuidSize ); + + } + +} // MPEG4_MetaHandler::UpdateFile + +// ================================================================================================= +// MPEG4_MetaHandler::WriteTempFile +// ================================ +// +// Since the XMP and legacy is probably a miniscule part of the entire file, and since we can't +// change the offset of most of the boxes, just copy the entire original file to the temp file, then +// do an in-place update to the temp file. + +void MPEG4_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + XMP_Assert ( this->needsUpdate ); + + XMP_IO* originalRef = this->parent->ioRef; + + originalRef->Rewind(); + tempRef->Rewind(); + XIO::Copy ( originalRef, tempRef, originalRef->Length(), + this->parent->abortProc, this->parent->abortArg ); + + try { + this->parent->ioRef = tempRef; // ! Fool UpdateFile into using the temp file. + this->UpdateFile ( false ); + this->parent->ioRef = originalRef; + } catch ( ... ) { + this->parent->ioRef = originalRef; + throw; + } + +} // MPEG4_MetaHandler::WriteTempFile + +// ================================================================================================= diff --git a/XMPFiles/source/FileHandlers/MPEG4_Handler.hpp b/XMPFiles/source/FileHandlers/MPEG4_Handler.hpp new file mode 100644 index 0000000..6474f0d --- /dev/null +++ b/XMPFiles/source/FileHandlers/MPEG4_Handler.hpp @@ -0,0 +1,95 @@ +#ifndef __MPEG4_Handler_hpp__ +#define __MPEG4_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +#include "XMPFiles/source/FormatSupport/MOOV_Support.hpp" +#include "XMPFiles/source/FormatSupport/QuickTime_Support.hpp" + +// ================================================================================================ +/// \file MPEG4_Handler.hpp +/// \brief File format handler for MPEG-4. +/// +/// This header ... +/// +// ================================================================================================ + +extern XMPFileHandler * MPEG4_MetaHandlerCTor ( XMPFiles * parent ); + +extern bool MPEG4_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ); + +static const XMP_OptionBits kMPEG4_HandlerFlags = ( kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_CanRewrite | + kXMPFiles_PrefersInPlace | + kXMPFiles_CanReconcile | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_AllowsSafeUpdate + ); + +class MPEG4_MetaHandler : public XMPFileHandler +{ +public: + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + + MPEG4_MetaHandler ( XMPFiles * _parent ); + virtual ~MPEG4_MetaHandler(); + + struct TimecodeTrackInfo { // Info about a QuickTime timecode track. + bool stsdBoxFound, isDropFrame; + XMP_Uns32 timeScale; + XMP_Uns32 frameDuration; + XMP_Uns32 timecodeSample; + XMP_Uns64 sampleOffset; // Absolute file offset of the timecode sample, 0 if none. + XMP_Uns32 nameOffset; // The offset of the 'name' box relative to the 'stsd' box content. + XMP_Uns16 macLang; // The Mac language code of the trailing 'name' box. + std::string macName; // The text part of the trailing 'name' box, in macLang encoding. + TimecodeTrackInfo() + : stsdBoxFound(false), isDropFrame(false), timeScale(0), frameDuration(0), + timecodeSample(0), sampleOffset(0), nameOffset(0), macLang(0) {}; + }; + +private: + + MPEG4_MetaHandler() : fileMode(0), havePreferredXMP(false), + xmpBoxPos(0), moovBoxPos(0), xmpBoxSize(0), moovBoxSize(0) {}; // Hidden on purpose. + + bool ParseTimecodeTrack(); + + void UpdateTopLevelBox ( XMP_Uns64 oldOffset, XMP_Uns32 oldSize, const XMP_Uns8 * newBox, XMP_Uns32 newSize ); + + XMP_Uns8 fileMode; + bool havePreferredXMP; + XMP_Uns64 xmpBoxPos; // The file offset of the XMP box (the size field, not the content). + XMP_Uns64 moovBoxPos; // The file offset of the 'moov' box (the size field, not the content). + XMP_Uns32 xmpBoxSize, moovBoxSize; // The full size of the boxes, not just the content. + + MOOV_Manager moovMgr; + TradQT_Manager tradQTMgr; + + TimecodeTrackInfo tmcdInfo; + +}; // MPEG4_MetaHandler + +// ================================================================================================= + +#endif // __MPEG4_Handler_hpp__ diff --git a/XMPFiles/source/FileHandlers/P2_Handler.cpp b/XMPFiles/source/FileHandlers/P2_Handler.cpp new file mode 100644 index 0000000..295a7a1 --- /dev/null +++ b/XMPFiles/source/FileHandlers/P2_Handler.cpp @@ -0,0 +1,1401 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2007 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/P2_Handler.hpp" + +#include "third-party/zuid/interfaces/MD5.h" + +#include <cmath> + +using namespace std; + +// ================================================================================================= +/// \file P2_Handler.cpp +/// \brief Folder format handler for P2. +/// +/// This handler is for the P2 video format. This is a pseudo-package, visible files but with a very +/// well-defined layout and naming rules. A typical P2 example looks like: +/// +/// .../MyMovie +/// CONTENTS/ +/// CLIP/ +/// 0001AB.XML +/// 0001AB.XMP +/// 0002CD.XML +/// 0002CD.XMP +/// VIDEO/ +/// 0001AB.MXF +/// 0002CD.MXF +/// AUDIO/ +/// 0001AB00.MXF +/// 0001AB01.MXF +/// 0002CD00.MXF +/// 0002CD01.MXF +/// ICON/ +/// 0001AB.BMP +/// 0002CD.BMP +/// VOICE/ +/// 0001AB.WAV +/// 0002CD.WAV +/// PROXY/ +/// 0001AB.MP4 +/// 0002CD.MP4 +/// +/// From the user's point of view, .../MyMovie contains P2 stuff, in this case 2 clips whose raw +/// names are 0001AB and 0002CD. There may be mapping information for nicer clip names to the raw +/// names, but that can be ignored for now. Each clip is stored as a collection of files, each file +/// holding some specific aspect of the clip's data. +/// +/// The P2 handler operates on clips. The path from the client of XMPFiles can be either a logical +/// clip path, like ".../MyMovie/0001AB", or a full path to one of the files. In the latter case the +/// handler must figure out the intended clip, it must not blindly use the named file. +/// +/// Once the P2 structure and intended clip are identified, the handler only deals with the .XMP and +/// .XML files in the CLIP folder. The .XMP file, if present, contains the XMP for the clip. The .XML +/// file must be present to define the existance of the clip. It contains a variety of information +/// about the clip, including some legacy metadata. +/// +// ================================================================================================= + +static const char * kContentFolderNames[] = { "CLIP", "VIDEO", "AUDIO", "ICON", "VOICE", "PROXY", 0 }; +static int kNumRequiredContentFolders = 6; // All 6 of the above. + +static inline bool CheckContentFolderName ( const std::string & folderName ) +{ + for ( int i = 0; kContentFolderNames[i] != 0; ++i ) { + if ( folderName == kContentFolderNames[i] ) return true; + } + return false; +} + +// ================================================================================================= +// InternalMakeClipFilePath +// ======================== +// +// P2_CheckFormat can't use the member function. + +static void InternalMakeClipFilePath ( std::string * path, + const std::string & rootPath, + const std::string & clipName, + XMP_StringPtr suffix ) +{ + + *path = rootPath; + *path += kDirChar; + *path += "CONTENTS"; + *path += kDirChar; + *path += "CLIP"; + *path += kDirChar; + *path += clipName; + *path += suffix; + +} // InternalMakeClipFilePath + +// ================================================================================================= +// P2_CheckFormat +// ============== +// +// This version does fairly simple checks. The top level folder (.../MyMovie) must have a child +// folder called CONTENTS. This must have a subfolder called CLIP. It may also have subfolders +// called VIDEO, AUDIO, ICON, VOICE, and PROXY. Any mixture of these additional folders is allowed, +// but no other children are allowed in CONTENTS. The CLIP folder must contain a .XML file for the +// desired clip. The name checks are case insensitive. +// +// The state of the string parameters depends on the form of the path passed by the client. If the +// client passed a logical clip path, like ".../MyMovie/0001AB", the parameters are: +// rootPath - ".../MyMovie" +// gpName - empty +// parentName - empty +// leafName - "0001AB" +// If the client passed a full file path, like ".../MyMovie/CONTENTS/VOICE/0001AB.WAV", they are: +// rootPath - ".../MyMovie" +// gpName - "CONTENTS" +// parentName - "VOICE" +// leafName - "0001AB" +// +// For most of the content files the base file name is the raw clip name. Files in the AUDIO and +// VOICE folders have an extra 2 digits appended to the raw clip name. These must be trimmed. + +// ! The common code has shifted the gpName, parentName, and leafName strings to upper case. It has +// ! also made sure that for a logical clip path the rootPath is an existing folder, and that the +// ! file exists for a full file path. + +bool P2_CheckFormat ( XMP_FileFormat format, + const std::string & rootPath, + const std::string & gpName, + const std::string & parentName, + const std::string & leafName, + XMPFiles * parent ) +{ + Host_IO::AutoFolder aFolder; + std::string tempPath, childName; + + std::string clipName = leafName; + + // Do some basic checks on the gpName and parentName. + + if ( gpName.empty() != parentName.empty() ) return false; // Must be both empty or both non-empty. + + if ( ! gpName.empty() ) { + + if ( gpName != "CONTENTS" ) return false; + if ( ! CheckContentFolderName ( parentName ) ) return false; + + if ( (parentName == "AUDIO") | (parentName == "VOICE") ) { + if ( clipName.size() < 3 ) return false; + clipName.erase ( clipName.size() - 2 ); + } + + } + + tempPath = rootPath; + tempPath += kDirChar; + tempPath += "CONTENTS"; + if ( Host_IO::GetFileMode ( tempPath.c_str() ) != Host_IO::kFMode_IsFolder ) return false; + + aFolder.folder = Host_IO::OpenFolder ( tempPath.c_str() ); + int numChildrenFound = 0; + std::string childPath; + + while ( ( Host_IO::GetNextChild ( aFolder.folder, &childName ) && ( numChildrenFound < kNumRequiredContentFolders ) ) ) { // Make sure the children of CONTENTS are legit. + if ( CheckContentFolderName ( childName ) ) { + childPath = tempPath; + childPath += kDirChar; + childPath += childName; + if ( Host_IO::GetFileMode ( childPath.c_str() ) != Host_IO::kFMode_IsFolder ) return false; + ++numChildrenFound; + } + } + aFolder.Close(); + + // Make sure the clip's .XML file exists. + + InternalMakeClipFilePath ( &tempPath, rootPath, clipName, ".XML" ); + if ( Host_IO::GetFileMode ( tempPath.c_str() ) != Host_IO::kFMode_IsFile ) return false; + + // Make a bogus path to pass the root path and clip name to the handler. A bit of a hack, but + // the only way to get info from here to there. + + + tempPath = rootPath; + tempPath += kDirChar; + tempPath += clipName; + + size_t pathLen = tempPath.size() + 1; // Include a terminating nul. + parent->tempPtr = malloc ( pathLen ); + if ( parent->tempPtr == 0 ) XMP_Throw ( "No memory for P2 clip path", kXMPErr_NoMemory ); + memcpy ( parent->tempPtr, tempPath.c_str(), pathLen ); // AUDIT: Safe, allocated above. + + return true; + +} // P2_CheckFormat + +// ================================================================================================= + +static void* CreatePseudoClipPath ( const std::string & clientPath ) { + + // Used to create the clip pseudo path when the CheckFormat function is skipped. + + std::string pseudoPath = clientPath; + + size_t pathLen; + void* tempPtr = 0; + + if ( Host_IO::Exists ( pseudoPath.c_str() ) ) { + + // The client passed a physical path. The logical clip name is the leaf name, with the + // extension removed. Files in the AUDIO and VOICE folders have an extra 2 digits appended + // to the clip name. The movie root path ends two levels up. + + std::string clipName, parentName, ignored; + + XIO::SplitLeafName ( &pseudoPath, &clipName ); // Extract the logical clip name. + XIO::SplitFileExtension ( &clipName, &ignored ); + + XIO::SplitLeafName ( &pseudoPath, &parentName ); // Remove the 2 intermediate folder levels. + XIO::SplitLeafName ( &pseudoPath, &ignored ); + + if ( (parentName == "AUDIO") | (parentName == "VOICE") ) { + if ( clipName.size() >= 3 ) clipName.erase ( clipName.size() - 2 ); + } + + pseudoPath += kDirChar; + pseudoPath += clipName; + + } + + pathLen = pseudoPath.size() + 1; // Include a terminating nul. + tempPtr = malloc ( pathLen ); + if ( tempPtr == 0 ) XMP_Throw ( "No memory for P2 clip info", kXMPErr_NoMemory ); + memcpy ( tempPtr, pseudoPath.c_str(), pathLen ); + + return tempPtr; + +} // CreatePseudoClipPath + +// ================================================================================================= +// P2_MetaHandlerCTor +// ================== + +XMPFileHandler * P2_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new P2_MetaHandler ( parent ); + +} // P2_MetaHandlerCTor + +// ================================================================================================= +// P2_MetaHandler::P2_MetaHandler +// ============================== + +P2_MetaHandler::P2_MetaHandler ( XMPFiles * _parent ) : expat(0), clipMetadata(0), clipContent(0) +{ + + this->parent = _parent; // Inherited, can't set in the prefix. + this->handlerFlags = kP2_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + + // Extract the root path and clip name from tempPtr. + + if ( this->parent->tempPtr == 0 ) { + // The CheckFormat call might have been skipped. + this->parent->tempPtr = CreatePseudoClipPath ( this->parent->filePath ); + } + + this->rootPath.assign ( (char*) this->parent->tempPtr ); + free ( this->parent->tempPtr ); + this->parent->tempPtr = 0; + + XIO::SplitLeafName ( &this->rootPath, &this->clipName ); + +} // P2_MetaHandler::P2_MetaHandler + +// ================================================================================================= +// P2_MetaHandler::~P2_MetaHandler +// =============================== + +P2_MetaHandler::~P2_MetaHandler() +{ + + this->CleanupLegacyXML(); + if ( this->parent->tempPtr != 0 ) { + free ( this->parent->tempPtr ); + this->parent->tempPtr = 0; + } + +} // P2_MetaHandler::~P2_MetaHandler + +// ================================================================================================= +// P2_MetaHandler::MakeClipFilePath +// ================================ + +bool P2_MetaHandler::MakeClipFilePath ( std::string * path, XMP_StringPtr suffix, bool checkFile /* = false */ ) +{ + + InternalMakeClipFilePath ( path, this->rootPath, this->clipName, suffix ); + if ( ! checkFile ) return true; + + return Host_IO::Exists ( path->c_str() ); + +} // P2_MetaHandler::MakeClipFilePath + +// ================================================================================================= +// P2_MetaHandler::CleanupLegacyXML +// ================================ + +void P2_MetaHandler::CleanupLegacyXML() +{ + + if ( this->expat != 0 ) { delete ( this->expat ); this->expat = 0; } + + clipMetadata = 0; // ! Was a pointer into the expat tree. + clipContent = 0; // ! Was a pointer into the expat tree. + +} // P2_MetaHandler::CleanupLegacyXML + +// ================================================================================================= +// P2_MetaHandler::DigestLegacyItem +// ================================ + +void P2_MetaHandler::DigestLegacyItem ( MD5_CTX & md5Context, XML_NodePtr legacyContext, XMP_StringPtr legacyPropName ) +{ + XML_NodePtr legacyProp = legacyContext->GetNamedElement ( this->p2NS.c_str(), legacyPropName ); + + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() && (! legacyProp->content.empty()) ) { + const XML_Node * xmlValue = legacyProp->content[0]; + MD5Update ( &md5Context, (XMP_Uns8*)xmlValue->value.c_str(), (unsigned int)xmlValue->value.size() ); + } + +} // P2_MetaHandler::DigestLegacyItem + +// ================================================================================================= +// P2_MetaHandler::DigestLegacyRelations +// ===================================== + +void P2_MetaHandler::DigestLegacyRelations ( MD5_CTX & md5Context ) +{ + XMP_StringPtr p2NS = this->p2NS.c_str(); + XML_Node * legacyContext = this->clipContent->GetNamedElement ( p2NS, "Relation" ); + + if ( legacyContext != 0 ) { + + this->DigestLegacyItem ( md5Context, legacyContext, "GlobalShotID" ); + XML_Node * legacyConnectionContext = legacyContext = this->clipContent->GetNamedElement ( p2NS, "Connection" ); + + if ( legacyConnectionContext != 0 ) { + + legacyContext = legacyConnectionContext->GetNamedElement ( p2NS, "Top" ); + + if ( legacyContext != 0 ) { + this->DigestLegacyItem ( md5Context, legacyContext, "GlobalClipID" ); + } + + legacyContext = legacyConnectionContext->GetNamedElement ( p2NS, "Previous" ); + + if ( legacyContext != 0 ) { + this->DigestLegacyItem ( md5Context, legacyContext, "GlobalClipID" ); + } + + legacyContext = legacyConnectionContext->GetNamedElement ( p2NS, "Next" ); + + if ( legacyContext != 0 ) { + this->DigestLegacyItem ( md5Context, legacyContext, "GlobalClipID" ); + } + + } + + } + +} // P2_MetaHandler::DigestLegacyRelations + +// ================================================================================================= +// P2_MetaHandler::SetXMPPropertyFromLegacyXML +// =========================================== + +void P2_MetaHandler::SetXMPPropertyFromLegacyXML ( bool digestFound, + XML_NodePtr legacyContext, + XMP_StringPtr schemaNS, + XMP_StringPtr propName, + XMP_StringPtr legacyPropName, + bool isLocalized ) +{ + + if ( digestFound || (! this->xmpObj.DoesPropertyExist ( schemaNS, propName )) ) { + + XMP_StringPtr p2NS = this->p2NS.c_str(); + XML_NodePtr legacyProp = legacyContext->GetNamedElement ( p2NS, legacyPropName ); + + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() ) { + if ( isLocalized ) { + this->xmpObj.SetLocalizedText ( schemaNS, propName, "", "x-default", legacyProp->GetLeafContentValue(), kXMP_DeleteExisting ); + } else { + this->xmpObj.SetProperty ( schemaNS, propName, legacyProp->GetLeafContentValue(), kXMP_DeleteExisting ); + } + this->containsXMP = true; + } + + } + +} // P2_MetaHandler::SetXMPPropertyFromLegacyXML + +// ================================================================================================= +// P2_MetaHandler::SetRelationsFromLegacyXML +// ========================================= + +void P2_MetaHandler::SetRelationsFromLegacyXML ( bool digestFound ) +{ + XMP_StringPtr p2NS = this->p2NS.c_str(); + XML_NodePtr legacyRelationContext = this->clipContent->GetNamedElement ( p2NS, "Relation" ); + + // P2 Relation blocks are optional -- they're only present when a clip is part of a multi-clip shot. + + if ( legacyRelationContext != 0 ) { + + if ( digestFound || (! this->xmpObj.DoesPropertyExist ( kXMP_NS_DC, "relation" )) ) { + + XML_NodePtr legacyProp = legacyRelationContext->GetNamedElement ( p2NS, "GlobalShotID" ); + std::string relationString; + + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() ) { + + this->xmpObj.DeleteProperty ( kXMP_NS_DC, "relation" ); + relationString = std::string("globalShotID:") + legacyProp->GetLeafContentValue(); + this->xmpObj.AppendArrayItem ( kXMP_NS_DC, "relation", kXMP_PropArrayIsUnordered, relationString ); + this->containsXMP = true; + + XML_NodePtr legacyConnectionContext = legacyRelationContext->GetNamedElement ( p2NS, "Connection" ); + + if ( legacyConnectionContext != 0 ) { + + XML_NodePtr legacyContext = legacyConnectionContext->GetNamedElement ( p2NS, "Top" ); + + if ( legacyContext != 0 ) { + legacyProp = legacyContext->GetNamedElement ( p2NS, "GlobalClipID" ); + + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() ) { + relationString = std::string("topGlobalClipID:") + legacyProp->GetLeafContentValue(); + this->xmpObj.AppendArrayItem ( kXMP_NS_DC, "relation", kXMP_PropArrayIsUnordered, relationString ); + } + } + + legacyContext = legacyConnectionContext->GetNamedElement ( p2NS, "Previous" ); + + if ( legacyContext != 0 ) { + legacyProp = legacyContext->GetNamedElement ( p2NS, "GlobalClipID" ); + + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() ) { + relationString = std::string("previousGlobalClipID:") + legacyProp->GetLeafContentValue(); + this->xmpObj.AppendArrayItem ( kXMP_NS_DC, "relation", kXMP_PropArrayIsUnordered, relationString ); + } + } + + legacyContext = legacyConnectionContext->GetNamedElement ( p2NS, "Next" ); + + if ( legacyContext != 0 ) { + legacyProp = legacyContext->GetNamedElement ( p2NS, "GlobalClipID" ); + + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() ) { + relationString = std::string("nextGlobalClipID:") + legacyProp->GetLeafContentValue(); + this->xmpObj.AppendArrayItem ( kXMP_NS_DC, "relation", kXMP_PropArrayIsUnordered, relationString ); + } + } + + } + + } + + } + + } + +} // P2_MetaHandler::SetRelationsFromLegacyXML + +// ================================================================================================= +// P2_MetaHandler::SetAudioInfoFromLegacyXML +// ========================================= + +void P2_MetaHandler::SetAudioInfoFromLegacyXML ( bool digestFound ) +{ + XMP_StringPtr p2NS = this->p2NS.c_str(); + XML_NodePtr legacyAudioContext = this->clipContent->GetNamedElement ( p2NS, "EssenceList" ); + + if ( legacyAudioContext != 0 ) { + + legacyAudioContext = legacyAudioContext->GetNamedElement ( p2NS, "Audio" ); + + if ( legacyAudioContext != 0 ) { + + this->SetXMPPropertyFromLegacyXML ( digestFound, legacyAudioContext, kXMP_NS_DM, "audioSampleRate", "SamplingRate", false ); + + if ( digestFound || (! this->xmpObj.DoesPropertyExist ( kXMP_NS_DM, "audioSampleType" )) ) { + XML_NodePtr legacyProp = legacyAudioContext->GetNamedElement ( p2NS, "BitsPerSample" ); + + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() ) { + + const std::string p2BitsPerSample = legacyProp->GetLeafContentValue(); + std::string dmSampleType; + + if ( p2BitsPerSample == "16" ) { + dmSampleType = "16Int"; + } else if ( p2BitsPerSample == "24" ) { + dmSampleType = "32Int"; + } + + if ( ! dmSampleType.empty() ) { + this->xmpObj.SetProperty ( kXMP_NS_DM, "audioSampleType", dmSampleType, kXMP_DeleteExisting ); + this->containsXMP = true; + } + + } + + } + + } + + } + +} // P2_MetaHandler::SetAudioInfoFromLegacyXML + +// ================================================================================================= +// P2_MetaHandler::SetVideoInfoFromLegacyXML +// ========================================= + +void P2_MetaHandler::SetVideoInfoFromLegacyXML ( bool digestFound ) +{ + XMP_StringPtr p2NS = this->p2NS.c_str(); + XML_NodePtr legacyVideoContext = this->clipContent->GetNamedElement ( p2NS, "EssenceList" ); + + if ( legacyVideoContext != 0 ) { + + legacyVideoContext = legacyVideoContext->GetNamedElement ( p2NS, "Video" ); + + if ( legacyVideoContext != 0 ) { + this->SetVideoFrameInfoFromLegacyXML ( legacyVideoContext, digestFound ); + this->SetStartTimecodeFromLegacyXML ( legacyVideoContext, digestFound ); + this->SetXMPPropertyFromLegacyXML ( digestFound, legacyVideoContext, kXMP_NS_DM, "videoFrameRate", "FrameRate", false ); + } + + } + +} // P2_MetaHandler::SetVideoInfoFromLegacyXML + +// ================================================================================================= +// P2_MetaHandler::SetDurationFromLegacyXML +// ======================================== + +void P2_MetaHandler::SetDurationFromLegacyXML ( bool digestFound ) +{ + + if ( digestFound || (! this->xmpObj.DoesPropertyExist ( kXMP_NS_DM, "duration" )) ) { + + XMP_StringPtr p2NS = this->p2NS.c_str(); + XML_NodePtr legacyDurationProp = this->clipContent->GetNamedElement ( p2NS, "Duration" ); + XML_NodePtr legacyEditUnitProp = this->clipContent->GetNamedElement ( p2NS, "EditUnit" ); + + if ( (legacyDurationProp != 0) && ( legacyEditUnitProp != 0 ) && + legacyDurationProp->IsLeafContentNode() && legacyEditUnitProp->IsLeafContentNode() ) { + + this->xmpObj.DeleteProperty ( kXMP_NS_DM, "duration" ); + this->xmpObj.SetStructField ( kXMP_NS_DM, "duration", + kXMP_NS_DM, "value", legacyDurationProp->GetLeafContentValue() ); + + this->xmpObj.SetStructField ( kXMP_NS_DM, "duration", + kXMP_NS_DM, "scale", legacyEditUnitProp->GetLeafContentValue() ); + this->containsXMP = true; + + } + + } + +} // P2_MetaHandler::SetDurationFromLegacyXML + +// ================================================================================================= +// P2_MetaHandler::SetVideoFrameInfoFromLegacyXML +// ============================================== + +void P2_MetaHandler::SetVideoFrameInfoFromLegacyXML ( XML_NodePtr legacyVideoContext, bool digestFound ) +{ + + // Map the P2 Codec field to various dynamic media schema fields. + if ( digestFound || (! this->xmpObj.DoesPropertyExist ( kXMP_NS_DM, "videoFrameSize" )) ) { + + XMP_StringPtr p2NS = this->p2NS.c_str(); + XML_NodePtr legacyProp = legacyVideoContext->GetNamedElement ( p2NS, "Codec" ); + + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() ) { + + const std::string p2Codec = legacyProp->GetLeafContentValue(); + std::string dmPixelAspectRatio, dmVideoCompressor, dmWidth, dmHeight; + + if ( p2Codec == "DV25_411" ) { + dmWidth = "720"; + dmVideoCompressor = "DV25 4:1:1"; + } else if ( p2Codec == "DV25_420" ) { + dmWidth = "720"; + dmVideoCompressor = "DV25 4:2:0"; + } else if ( p2Codec == "DV50_422" ) { + dmWidth = "720"; + dmVideoCompressor = "DV50 4:2:2"; + } else if ( ( p2Codec == "DV100_1080/59.94i" ) || ( p2Codec == "DV100_1080/50i" ) ) { + dmVideoCompressor = "DV100"; + dmHeight = "1080"; + + if ( p2Codec == "DV100_1080/59.94i" ) { + dmWidth = "1280"; + dmPixelAspectRatio = "3/2"; + } else { + dmWidth = "1440"; + dmPixelAspectRatio = "1920/1440"; + } + } else if ( ( p2Codec == "DV100_720/59.94p" ) || ( p2Codec == "DV100_720/50p" ) ) { + dmVideoCompressor = "DV100"; + dmHeight = "720"; + dmWidth = "960"; + dmPixelAspectRatio = "1920/1440"; + } else if ( ( p2Codec.compare ( 0, 6, "AVC-I_" ) == 0 ) ) { + + // This is AVC-Intra footage. The framerate and PAR depend on the "class" attribute in the P2 XML. + const XMP_StringPtr codecClass = legacyProp->GetAttrValue( "Class" ); + + if ( XMP_LitMatch ( codecClass, "100" ) ) { + + dmVideoCompressor = "AVC-Intra 100"; + dmPixelAspectRatio = "1/1"; + + if ( p2Codec.compare ( 6, 4, "1080" ) == 0 ) { + dmHeight = "1080"; + dmWidth = "1920"; + } else if ( p2Codec.compare ( 6, 3, "720" ) == 0 ) { + dmHeight = "720"; + dmWidth = "1280"; + } + + } else if ( XMP_LitMatch ( codecClass, "50" ) ) { + + dmVideoCompressor = "AVC-Intra 50"; + dmPixelAspectRatio = "1920/1440"; + + if ( p2Codec.compare ( 6, 4, "1080" ) == 0 ) { + dmHeight = "1080"; + dmWidth = "1440"; + } else if ( p2Codec.compare ( 6, 3, "720" ) == 0 ) { + dmHeight = "720"; + dmWidth = "960"; + } + + } else { + // Unknown codec class -- we don't have enough info to determine the + // codec, PAR, or aspect ratio + dmVideoCompressor = "AVC-Intra"; + } + } + + if ( dmWidth == "720" ) { + + // This is SD footage -- calculate the frame height and pixel aspect ratio using the legacy P2 + // FrameRate and AspectRatio fields. + + legacyProp = legacyVideoContext->GetNamedElement ( p2NS, "FrameRate" ); + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() ) { + + const std::string p2FrameRate = legacyProp->GetLeafContentValue(); + + legacyProp = legacyVideoContext->GetNamedElement ( p2NS, "AspectRatio" ); + + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() ) { + const std::string p2AspectRatio = legacyProp->GetLeafContentValue(); + + if ( p2FrameRate == "50i" ) { + // Standard Definition PAL. + dmHeight = "576"; + if ( p2AspectRatio == "4:3" ) { + dmPixelAspectRatio = "768/702"; + } else if ( p2AspectRatio == "16:9" ) { + dmPixelAspectRatio = "1024/702"; + } + } else if ( p2FrameRate == "59.94i" ) { + // Standard Definition NTSC. + dmHeight = "480"; + if ( p2AspectRatio == "4:3" ) { + dmPixelAspectRatio = "10/11"; + } else if ( p2AspectRatio == "16:9" ) { + dmPixelAspectRatio = "40/33"; + } + } + + } + } + } + + if ( ! dmPixelAspectRatio.empty() ) { + this->xmpObj.SetProperty ( kXMP_NS_DM, "videoPixelAspectRatio", dmPixelAspectRatio, kXMP_DeleteExisting ); + this->containsXMP = true; + } + + if ( ! dmVideoCompressor.empty() ) { + this->xmpObj.SetProperty ( kXMP_NS_DM, "videoCompressor", dmVideoCompressor, kXMP_DeleteExisting ); + this->containsXMP = true; + } + + if ( ( ! dmWidth.empty() ) && ( ! dmHeight.empty() ) ) { + this->xmpObj.SetStructField ( kXMP_NS_DM, "videoFrameSize", kXMP_NS_XMP_Dimensions, "w", dmWidth, 0 ); + this->xmpObj.SetStructField ( kXMP_NS_DM, "videoFrameSize", kXMP_NS_XMP_Dimensions, "h", dmHeight, 0 ); + this->xmpObj.SetStructField ( kXMP_NS_DM, "videoFrameSize", kXMP_NS_XMP_Dimensions, "unit", "pixel", 0 ); + this->containsXMP = true; + } + + } + + } + +} // P2_MetaHandler::SetVideoFrameInfoFromLegacyXML + +// ================================================================================================= +// P2_MetaHandler::SetStartTimecodeFromLegacyXML +// ============================================= + +void P2_MetaHandler::SetStartTimecodeFromLegacyXML ( XML_NodePtr legacyVideoContext, bool digestFound ) +{ + + // Translate start timecode to the format specified by the dynamic media schema. + if ( digestFound || (! this->xmpObj.DoesPropertyExist ( kXMP_NS_DM, "startTimecode" )) ) { + + XMP_StringPtr p2NS = this->p2NS.c_str(); + XML_NodePtr legacyProp = legacyVideoContext->GetNamedElement ( p2NS, "StartTimecode" ); + + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() ) { + + std::string p2StartTimecode = legacyProp->GetLeafContentValue(); + + legacyProp = legacyVideoContext->GetNamedElement ( p2NS, "FrameRate" ); + + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() ) { + + const std::string p2FrameRate = legacyProp->GetLeafContentValue(); + XMP_StringPtr p2DropFrameFlag = legacyProp->GetAttrValue ( "DropFrameFlag" ); + if ( p2DropFrameFlag == 0 ) p2DropFrameFlag = ""; // Make tests easier. + std::string dmTimeFormat; + + if ( ( p2FrameRate == "50i" ) || ( p2FrameRate == "25p" ) ) { + + dmTimeFormat = "25Timecode"; + + } else if ( p2FrameRate == "23.98p" ) { + + dmTimeFormat = "23976Timecode"; + + } else if ( p2FrameRate == "50p" ) { + + dmTimeFormat = "50Timecode"; + + } else if ( p2FrameRate == "59.94p" ) { + + if ( XMP_LitMatch ( p2DropFrameFlag, "true" ) ) { + dmTimeFormat = "5994DropTimecode"; + } else if ( XMP_LitMatch ( p2DropFrameFlag, "false" ) ) { + dmTimeFormat = "5994NonDropTimecode"; + } + + } else if ( (p2FrameRate == "59.94i") || (p2FrameRate == "29.97p") ) { + + if ( p2DropFrameFlag != 0 ) { + + if ( XMP_LitMatch ( p2DropFrameFlag, "false" ) ) { + + dmTimeFormat = "2997NonDropTimecode"; + + } else if ( XMP_LitMatch ( p2DropFrameFlag, "true" ) ) { + + // Drop frame NTSC timecode uses semicolons instead of colons as separators. + std::string::iterator currCharIt = p2StartTimecode.begin(); + const std::string::iterator charsEndIt = p2StartTimecode.end(); + + for ( ; currCharIt != charsEndIt; ++currCharIt ) { + if ( *currCharIt == ':' ) *currCharIt = ';'; + } + + dmTimeFormat = "2997DropTimecode"; + + } + + } + + } + + if ( ( ! p2StartTimecode.empty() ) && ( ! dmTimeFormat.empty() ) ) { + this->xmpObj.SetStructField ( kXMP_NS_DM, "startTimecode", kXMP_NS_DM, "timeValue", p2StartTimecode, 0 ); + this->xmpObj.SetStructField ( kXMP_NS_DM, "startTimecode", kXMP_NS_DM, "timeFormat", dmTimeFormat, 0 ); + this->containsXMP = true; + } + + } + + } + + } + +} // P2_MetaHandler::SetStartTimecodeFromLegacyXML + + +// ================================================================================================= +// P2_MetaHandler::SetGPSPropertyFromLegacyXML +// =========================================== + +void P2_MetaHandler::SetGPSPropertyFromLegacyXML ( XML_NodePtr legacyLocationContext, bool digestFound, XMP_StringPtr propName, XMP_StringPtr legacyPropName ) +{ + + if ( digestFound || (! this->xmpObj.DoesPropertyExist ( kXMP_NS_EXIF, propName )) ) { + + XMP_StringPtr p2NS = this->p2NS.c_str(); + XML_NodePtr legacyGPSProp = legacyLocationContext->GetNamedElement ( p2NS, legacyPropName ); + + if ( ( legacyGPSProp != 0 ) && legacyGPSProp->IsLeafContentNode() ) { + + this->xmpObj.DeleteProperty ( kXMP_NS_EXIF, propName ); + + const std::string legacyGPSValue = legacyGPSProp->GetLeafContentValue(); + + if ( ! legacyGPSValue.empty() ) { + + // Convert from decimal to sexagesimal GPS coordinates + char direction = '\0'; + double degrees = 0.0; + const int numFieldsRead = sscanf ( legacyGPSValue.c_str(), "%c%lf", &direction, °rees ); + + if ( numFieldsRead == 2 ) { + double wholeDegrees = 0.0; + const double fractionalDegrees = modf ( degrees, &wholeDegrees ); + const double minutes = fractionalDegrees * 60.0; + char xmpValue [128]; + + sprintf ( xmpValue, "%d,%.5lf%c", static_cast<int>(wholeDegrees), minutes, direction ); + this->xmpObj.SetProperty ( kXMP_NS_EXIF, propName, xmpValue ); + this->containsXMP = true; + + } + + } + + } + + } + +} // P2_MetaHandler::SetGPSPropertyFromLegacyXML + +// ================================================================================================= +// P2_MetaHandler::SetAltitudeFromLegacyXML +// ======================================== + +void P2_MetaHandler::SetAltitudeFromLegacyXML ( XML_NodePtr legacyLocationContext, bool digestFound ) +{ + + if ( digestFound || (! this->xmpObj.DoesPropertyExist ( kXMP_NS_EXIF, "GPSAltitude" )) ) { + + XMP_StringPtr p2NS = this->p2NS.c_str(); + XML_NodePtr legacyAltitudeProp = legacyLocationContext->GetNamedElement ( p2NS, "Altitude" ); + + if ( ( legacyAltitudeProp != 0 ) && legacyAltitudeProp->IsLeafContentNode() ) { + + this->xmpObj.DeleteProperty ( kXMP_NS_EXIF, "GPSAltitude" ); + + const std::string legacyGPSValue = legacyAltitudeProp->GetLeafContentValue(); + + if ( ! legacyGPSValue.empty() ) { + + int altitude = 0; + + if ( sscanf ( legacyGPSValue.c_str(), "%d", &altitude ) == 1) { + + if ( altitude >= 0 ) { + // At or above sea level. + this->xmpObj.SetProperty ( kXMP_NS_EXIF, "GPSAltitudeRef", "0" ); + } else { + // Below sea level. + altitude = -altitude; + this->xmpObj.SetProperty ( kXMP_NS_EXIF, "GPSAltitudeRef", "1" ); + } + + char xmpValue [128]; + + sprintf ( xmpValue, "%d/1", altitude ); + this->xmpObj.SetProperty ( kXMP_NS_EXIF, "GPSAltitude", xmpValue ); + this->containsXMP = true; + + } + + } + + } + + } + +} // P2_MetaHandler::SetAltitudeFromLegacyXML + +// ================================================================================================= +// P2_MetaHandler::ForceChildElement +// ================================= + +XML_Node * P2_MetaHandler::ForceChildElement ( XML_Node * parent, XMP_StringPtr localName, int indent /* = 0 */ ) +{ + XML_Node * wsNode; + XML_Node * childNode = parent->GetNamedElement ( this->p2NS.c_str(), localName ); + + if ( childNode == 0 ) { + + // The indenting is a hack, assuming existing 2 spaces per level. + + wsNode = new XML_Node ( parent, "", kCDataNode ); + wsNode->value = " "; // Add 2 spaces to the existing WS before the parent's close tag. + parent->content.push_back ( wsNode ); + + childNode = new XML_Node ( parent, localName, kElemNode ); + childNode->ns = parent->ns; + childNode->nsPrefixLen = parent->nsPrefixLen; + childNode->name.insert ( 0, parent->name, 0, parent->nsPrefixLen ); + parent->content.push_back ( childNode ); + + wsNode = new XML_Node ( parent, "", kCDataNode ); + wsNode->value = '\n'; + for ( ; indent > 1; --indent ) wsNode->value += " "; // Indent less 1, to "outdent" the parent's close. + parent->content.push_back ( wsNode ); + + } + + return childNode; + +} // P2_MetaHandler::ForceChildElement + +// ================================================================================================= +// P2_MetaHandler::MakeLegacyDigest +// ================================= + +// *** Early hack version. + +#define kHexDigits "0123456789ABCDEF" + +void P2_MetaHandler::MakeLegacyDigest ( std::string * digestStr ) +{ + digestStr->erase(); + if ( this->clipMetadata == 0 ) return; // Bail if we don't have any legacy XML. + XMP_Assert ( this->expat != 0 ); + + XMP_StringPtr p2NS = this->p2NS.c_str(); + XML_NodePtr legacyContext; + MD5_CTX md5Context; + unsigned char digestBin [16]; + MD5Init ( &md5Context ); + + legacyContext = this->clipContent; + this->DigestLegacyItem ( md5Context, legacyContext, "ClipName" ); + this->DigestLegacyItem ( md5Context, legacyContext, "GlobalClipID" ); + this->DigestLegacyItem ( md5Context, legacyContext, "Duration" ); + this->DigestLegacyItem ( md5Context, legacyContext, "EditUnit" ); + this->DigestLegacyRelations ( md5Context ); + + legacyContext = this->clipContent->GetNamedElement ( p2NS, "EssenceList" ); + + if ( legacyContext != 0 ) { + + XML_NodePtr videoContext = legacyContext->GetNamedElement ( p2NS, "Video" ); + + if ( videoContext != 0 ) { + this->DigestLegacyItem ( md5Context, videoContext, "AspectRatio" ); + this->DigestLegacyItem ( md5Context, videoContext, "Codec" ); + this->DigestLegacyItem ( md5Context, videoContext, "FrameRate" ); + this->DigestLegacyItem ( md5Context, videoContext, "StartTimecode" ); + } + + XML_NodePtr audioContext = legacyContext->GetNamedElement ( p2NS, "Audio" ); + + if ( audioContext != 0 ) { + this->DigestLegacyItem ( md5Context, audioContext, "SamplingRate" ); + this->DigestLegacyItem ( md5Context, audioContext, "BitsPerSample" ); + } + + } + + legacyContext = this->clipMetadata; + this->DigestLegacyItem ( md5Context, legacyContext, "UserClipName" ); + this->DigestLegacyItem ( md5Context, legacyContext, "ShotMark" ); + + legacyContext = this->clipMetadata->GetNamedElement ( p2NS, "Access" ); + /* Rather return than create the digest because the "Access" element is listed as "required" in the P2 spec. + So a P2 file without an "Access" element does not follow the spec and might be corrupt.*/ + if ( legacyContext == 0 ) return; + + this->DigestLegacyItem ( md5Context, legacyContext, "Creator" ); + this->DigestLegacyItem ( md5Context, legacyContext, "CreationDate" ); + this->DigestLegacyItem ( md5Context, legacyContext, "LastUpdateDate" ); + + legacyContext = this->clipMetadata->GetNamedElement ( p2NS, "Shoot" ); + + if ( legacyContext != 0 ) { + this->DigestLegacyItem ( md5Context, legacyContext, "Shooter" ); + + legacyContext = legacyContext->GetNamedElement ( p2NS, "Location" ); + + if ( legacyContext != 0 ) { + this->DigestLegacyItem ( md5Context, legacyContext, "PlaceName" ); + this->DigestLegacyItem ( md5Context, legacyContext, "Longitude" ); + this->DigestLegacyItem ( md5Context, legacyContext, "Latitude" ); + this->DigestLegacyItem ( md5Context, legacyContext, "Altitude" ); + } + } + + legacyContext = this->clipMetadata->GetNamedElement ( p2NS, "Scenario" ); + + if ( legacyContext != 0 ) { + this->DigestLegacyItem ( md5Context, legacyContext, "SceneNo." ); + this->DigestLegacyItem ( md5Context, legacyContext, "TakeNo." ); + } + + legacyContext = this->clipMetadata->GetNamedElement ( p2NS, "Device" ); + + if ( legacyContext != 0 ) { + this->DigestLegacyItem ( md5Context, legacyContext, "Manufacturer" ); + this->DigestLegacyItem ( md5Context, legacyContext, "SerialNo." ); + this->DigestLegacyItem ( md5Context, legacyContext, "ModelName" ); + } + + MD5Final ( digestBin, &md5Context ); + + char buffer [40]; + for ( int in = 0, out = 0; in < 16; in += 1, out += 2 ) { + XMP_Uns8 byte = digestBin[in]; + buffer[out] = kHexDigits [ byte >> 4 ]; + buffer[out+1] = kHexDigits [ byte & 0xF ]; + } + buffer[32] = 0; + digestStr->append ( buffer ); + +} // P2_MetaHandler::MakeLegacyDigest + +// ================================================================================================= +// P2_MetaHandler::GetFileModDate +// ============================== + +static inline bool operator< ( const XMP_DateTime & left, const XMP_DateTime & right ) { + int compare = SXMPUtils::CompareDateTime ( left, right ); + return (compare < 0); +} + +bool P2_MetaHandler::GetFileModDate ( XMP_DateTime * modDate ) +{ + + // The P2 locations of metadata: + // CONTENTS/ + // CLIP/ + // 0001AB.XML + // 0001AB.XMP + + bool ok, haveDate = false; + std::string fullPath; + XMP_DateTime oneDate, junkDate; + if ( modDate == 0 ) modDate = &junkDate; + + ok = this->MakeClipFilePath ( &fullPath, ".XML", true /* checkFile */ ); + if ( ok ) ok = Host_IO::GetModifyDate ( fullPath.c_str(), &oneDate ); + if ( ok ) { + if ( (! haveDate) || (*modDate < oneDate) ) *modDate = oneDate; + haveDate = true; + } + + ok = this->MakeClipFilePath ( &fullPath, ".XMP", true /* checkFile */ ); + if ( ok ) ok = Host_IO::GetModifyDate ( fullPath.c_str(), &oneDate ); + if ( ok ) { + if ( (! haveDate) || (*modDate < oneDate) ) *modDate = oneDate; + haveDate = true; + } + + return haveDate; + +} // P2_MetaHandler::GetFileModDate + +// ================================================================================================= +// P2_MetaHandler::CacheFileData +// ============================= + +void P2_MetaHandler::CacheFileData() +{ + XMP_Assert ( ! this->containsXMP ); + + if ( this->parent->UsesClientIO() ) { + XMP_Throw ( "XDCAM cannot be used with client-managed I/O", kXMPErr_InternalFailure ); + } + + // Make sure the clip's .XMP file exists. + + std::string xmpPath; + this->MakeClipFilePath ( &xmpPath, ".XMP" ); + if ( Host_IO::GetFileMode ( xmpPath.c_str() ) != Host_IO::kFMode_IsFile ) return; // No XMP. + + // Read the entire .XMP file. + + bool readOnly = XMP_OptionIsClear ( this->parent->openFlags, kXMPFiles_OpenForUpdate ); + + XMP_Assert ( this->parent->ioRef == 0 ); + XMPFiles_IO* xmpFile = XMPFiles_IO::New_XMPFiles_IO ( xmpPath.c_str(), readOnly ); + if ( xmpFile == 0 ) return; // The open failed. + this->parent->ioRef = xmpFile; + + XMP_Int64 xmpLen = xmpFile->Length(); + if ( xmpLen > 100*1024*1024 ) { + XMP_Throw ( "P2 XMP is outrageously large", kXMPErr_InternalFailure ); // Sanity check. + } + + this->xmpPacket.erase(); + this->xmpPacket.append ( (size_t)xmpLen, ' ' ); + + XMP_Int32 ioCount = xmpFile->ReadAll ( (void*)this->xmpPacket.data(), (XMP_Int32)xmpLen ); + + this->packetInfo.offset = 0; + this->packetInfo.length = (XMP_Int32)xmpLen; + FillPacketInfo ( this->xmpPacket, &this->packetInfo ); + + this->containsXMP = true; + +} // P2_MetaHandler::CacheFileData + +// ================================================================================================= +// P2_MetaHandler::ProcessXMP +// ========================== + +void P2_MetaHandler::ProcessXMP() +{ + + // Some versions of gcc can't tolerate goto's across declarations. + // *** Better yet, avoid this cruft with self-cleaning objects. + #define CleanupAndExit \ + { \ + bool openForUpdate = XMP_OptionIsSet ( this->parent->openFlags, kXMPFiles_OpenForUpdate ); \ + if ( ! openForUpdate ) this->CleanupLegacyXML(); \ + return; \ + } + + if ( this->processedXMP ) return; + this->processedXMP = true; // Make sure only called once. + + if ( this->containsXMP ) { + this->xmpObj.ParseFromBuffer ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + } + + std::string xmlPath; + this->MakeClipFilePath ( &xmlPath, ".XML" ); + + Host_IO::FileRef hostRef = Host_IO::Open ( xmlPath.c_str(), Host_IO::openReadOnly ); + if ( hostRef == Host_IO::noFileRef ) return; // The open failed. + XMPFiles_IO xmlFile ( hostRef, xmlPath.c_str(), Host_IO::openReadOnly ); + + this->expat = XMP_NewExpatAdapter ( ExpatAdapter::kUseLocalNamespaces ); + if ( this->expat == 0 ) XMP_Throw ( "P2_MetaHandler: Can't create Expat adapter", kXMPErr_NoMemory ); + + XMP_Uns8 buffer [64*1024]; + while ( true ) { + XMP_Int32 ioCount = xmlFile.Read ( buffer, sizeof(buffer) ); + if ( ioCount == 0 ) break; + this->expat->ParseBuffer ( buffer, ioCount, false /* not the end */ ); + } + this->expat->ParseBuffer ( 0, 0, true ); // End the parse. + + xmlFile.Close(); + + // The root element should be P2Main in some namespace. At least 2 different namespaces are in + // use (ending in "v3.0" and "v3.1"). Take whatever this file uses. + + XML_Node & xmlTree = this->expat->tree; + XML_NodePtr rootElem = 0; + + for ( size_t i = 0, limit = xmlTree.content.size(); i < limit; ++i ) { + if ( xmlTree.content[i]->kind == kElemNode ) { + rootElem = xmlTree.content[i]; + } + } + + if ( rootElem == 0 ) CleanupAndExit + XMP_StringPtr rootLocalName = rootElem->name.c_str() + rootElem->nsPrefixLen; + if ( ! XMP_LitMatch ( rootLocalName, "P2Main" ) ) CleanupAndExit + + this->p2NS = rootElem->ns; + + // Now find ClipMetadata element and check the legacy digest. + + XMP_StringPtr p2NS = this->p2NS.c_str(); + XML_NodePtr legacyContext, legacyProp; + + legacyContext = rootElem->GetNamedElement ( p2NS, "ClipContent" ); + if ( legacyContext == 0 ) CleanupAndExit + + this->clipContent = legacyContext; // ! Save the ClipContext pointer for other use. + + legacyContext = legacyContext->GetNamedElement ( p2NS, "ClipMetadata" ); + if ( legacyContext == 0 ) CleanupAndExit + + this->clipMetadata = legacyContext; // ! Save the ClipMetadata pointer for other use. + + std::string oldDigest, newDigest; + bool digestFound = this->xmpObj.GetStructField ( kXMP_NS_XMP, "NativeDigests", kXMP_NS_XMP, "P2", &oldDigest, 0 ); + if ( digestFound ) { + this->MakeLegacyDigest ( &newDigest ); + if ( oldDigest == newDigest ) CleanupAndExit + } + + // If we get here we need find and import the actual legacy elements using the current namespace. + // Either there is no old digest in the XMP, or the digests differ. In the former case keep any + // existing XMP, in the latter case take new legacy values. + this->SetXMPPropertyFromLegacyXML ( digestFound, this->clipContent, kXMP_NS_DC, "title", "ClipName", true ); + this->SetXMPPropertyFromLegacyXML ( digestFound, this->clipContent, kXMP_NS_DC, "identifier", "GlobalClipID", false ); + this->SetDurationFromLegacyXML (digestFound ); + this->SetRelationsFromLegacyXML ( digestFound ); + this->SetXMPPropertyFromLegacyXML ( digestFound, this->clipMetadata, kXMP_NS_DM, "shotName", "UserClipName", false ); + this->SetAudioInfoFromLegacyXML ( digestFound ); + this->SetVideoInfoFromLegacyXML ( digestFound ); + + legacyContext = this->clipMetadata->GetNamedElement ( p2NS, "Access" ); + if ( legacyContext == 0 ) CleanupAndExit + + if ( digestFound || (! this->xmpObj.DoesPropertyExist ( kXMP_NS_DC, "creator" )) ) { + legacyProp = legacyContext->GetNamedElement ( p2NS, "Creator" ); + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() ) { + this->xmpObj.DeleteProperty ( kXMP_NS_DC, "creator" ); + this->xmpObj.AppendArrayItem ( kXMP_NS_DC, "creator", kXMP_PropArrayIsOrdered, + legacyProp->GetLeafContentValue() ); + this->containsXMP = true; + } + } + + this->SetXMPPropertyFromLegacyXML ( digestFound, legacyContext, kXMP_NS_XMP, "CreateDate", "CreationDate", false ); + this->SetXMPPropertyFromLegacyXML ( digestFound, legacyContext, kXMP_NS_XMP, "ModifyDate", "LastUpdateDate", false ); + + if ( digestFound || (! this->xmpObj.DoesPropertyExist ( kXMP_NS_DM, "good" )) ) { + legacyProp = this->clipMetadata->GetNamedElement ( p2NS, "ShotMark" ); + if ( (legacyProp == 0) || (! legacyProp->IsLeafContentNode()) ) { + this->xmpObj.DeleteProperty ( kXMP_NS_DM, "good" ); + } else { + XMP_StringPtr markValue = legacyProp->GetLeafContentValue(); + if ( markValue == 0 ) { + this->xmpObj.DeleteProperty ( kXMP_NS_DM, "good" ); + } else if ( XMP_LitMatch ( markValue, "true" ) || XMP_LitMatch ( markValue, "1" ) ) { + this->xmpObj.SetProperty_Bool ( kXMP_NS_DM, "good", true, kXMP_DeleteExisting ); + this->containsXMP = true; + } else if ( XMP_LitMatch ( markValue, "false" ) || XMP_LitMatch ( markValue, "0" ) ) { + this->xmpObj.SetProperty_Bool ( kXMP_NS_DM, "good", false, kXMP_DeleteExisting ); + this->containsXMP = true; + } + } + } + + legacyContext = this->clipMetadata->GetNamedElement ( p2NS, "Shoot" ); + if ( legacyContext != 0 ) { + this->SetXMPPropertyFromLegacyXML ( digestFound, legacyContext, kXMP_NS_TIFF, "Artist", "Shooter", false ); + legacyContext = legacyContext->GetNamedElement ( p2NS, "Location" ); + } + + if ( legacyContext != 0 ) { + this->SetXMPPropertyFromLegacyXML ( digestFound, legacyContext, kXMP_NS_DM, "shotLocation", "PlaceName", false ); + this->SetGPSPropertyFromLegacyXML ( legacyContext, digestFound, "GPSLongitude", "Longitude" ); + this->SetGPSPropertyFromLegacyXML ( legacyContext, digestFound, "GPSLatitude", "Latitude" ); + this->SetAltitudeFromLegacyXML ( legacyContext, digestFound ); + } + + legacyContext = this->clipMetadata->GetNamedElement ( p2NS, "Device" ); + if ( legacyContext != 0 ) { + this->SetXMPPropertyFromLegacyXML ( digestFound, legacyContext, kXMP_NS_TIFF, "Make", "Manufacturer", false ); + this->SetXMPPropertyFromLegacyXML ( digestFound, legacyContext, kXMP_NS_EXIF_Aux, "SerialNumber", "SerialNo.", false ); + this->SetXMPPropertyFromLegacyXML ( digestFound, legacyContext, kXMP_NS_TIFF, "Model", "ModelName", false ); + } + + legacyContext = this->clipMetadata->GetNamedElement ( p2NS, "Scenario" ); + if ( legacyContext != 0 ) { + this->SetXMPPropertyFromLegacyXML ( digestFound, legacyContext, kXMP_NS_DM, "scene", "SceneNo.", false ); + this->SetXMPPropertyFromLegacyXML ( digestFound, legacyContext, kXMP_NS_DM, "takeNumber", "TakeNo.", false ); + } + + CleanupAndExit + #undef CleanupAndExit + +} // P2_MetaHandler::ProcessXMP + +// ================================================================================================= +// P2_MetaHandler::UpdateFile +// ========================== +// +// Note that UpdateFile is only called from XMPFiles::CloseFile, so it is OK to close the file here. + +void P2_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + if ( ! this->needsUpdate ) return; + this->needsUpdate = false; // Make sure only called once. + + XMP_Assert ( this->parent->UsesLocalIO() ); + + // Update the internal legacy XML tree if we have one, and set the digest in the XMP. + + bool updateLegacyXML = false; + + if ( this->clipMetadata != 0 ) { + + XMP_Assert ( this->expat != 0 ); + + bool xmpFound; + std::string xmpValue; + XML_Node * xmlNode; + + xmpFound = this->xmpObj.GetLocalizedText ( kXMP_NS_DC, "title", "", "x-default", 0, &xmpValue, 0 ); + + if ( xmpFound ) { + + xmlNode = this->ForceChildElement ( this->clipContent, "ClipName", 3 ); + + if ( xmpValue != xmlNode->GetLeafContentValue() ) { + xmlNode->SetLeafContentValue ( xmpValue.c_str() ); + updateLegacyXML = true; + } + + } + + xmpFound = this->xmpObj.GetArrayItem ( kXMP_NS_DC, "creator", 1, &xmpValue, 0 ); + + if ( xmpFound ) { + xmlNode = this->ForceChildElement ( this->clipMetadata, "Access", 3 ); + xmlNode = this->ForceChildElement ( xmlNode, "Creator", 4 ); + if ( xmpValue != xmlNode->GetLeafContentValue() ) { + xmlNode->SetLeafContentValue ( xmpValue.c_str() ); + updateLegacyXML = true; + } + } + + } + + std::string newDigest; + this->MakeLegacyDigest ( &newDigest ); + this->xmpObj.SetStructField ( kXMP_NS_XMP, "NativeDigests", kXMP_NS_XMP, "P2", newDigest.c_str(), kXMP_DeleteExisting ); + + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, this->GetSerializeOptions() ); + + // ----------------------------------------------------------------------- + // Update the XMP file first, don't let legacy XML failures block the XMP. + + std::string xmpPath; + this->MakeClipFilePath ( &xmpPath, ".XMP" ); + + bool haveXMP = Host_IO::Exists ( xmpPath.c_str() ); + if ( ! haveXMP ) { + XMP_Assert ( this->parent->ioRef == 0 ); + Host_IO::Create ( xmpPath.c_str() ); + this->parent->ioRef = XMPFiles_IO::New_XMPFiles_IO ( xmpPath.c_str(), Host_IO::openReadWrite ); + if ( this->parent->ioRef == 0 ) XMP_Throw ( "Failure opening P2 XMP file", kXMPErr_ExternalFailure ); + } + + XMP_IO* xmpFile = this->parent->ioRef; + XMP_Assert ( xmpFile != 0 ); + XIO::ReplaceTextFile ( xmpFile, this->xmpPacket, (haveXMP & doSafeUpdate) ); + + // -------------------------------------------- + // Now update the legacy XML file if necessary. + + if ( updateLegacyXML ) { + + std::string legacyXML, xmlPath; + this->expat->tree.Serialize ( &legacyXML ); + this->MakeClipFilePath ( &xmlPath, ".XML" ); + + bool haveXML = Host_IO::Exists ( xmlPath.c_str() ); + if ( ! haveXML ) Host_IO::Create ( xmlPath.c_str() ); + + Host_IO::FileRef hostRef = Host_IO::Open ( xmlPath.c_str(), Host_IO::openReadWrite ); + if ( hostRef == Host_IO::noFileRef ) XMP_Throw ( "Failure opening P2 legacy XML file", kXMPErr_ExternalFailure ); + XMPFiles_IO origXML ( hostRef, xmlPath.c_str(), Host_IO::openReadWrite ); + XIO::ReplaceTextFile ( &origXML, legacyXML, (haveXML & doSafeUpdate) ); + origXML.Close(); + + } + +} // P2_MetaHandler::UpdateFile + +// ================================================================================================= +// P2_MetaHandler::WriteTempFile +// ============================= + +void P2_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + + // ! WriteTempFile is not supposed to be called for handlers that own the file. + XMP_Throw ( "P2_MetaHandler::WriteTempFile should not be called", kXMPErr_InternalFailure ); + +} // P2_MetaHandler::WriteTempFile + +// ================================================================================================= diff --git a/XMPFiles/source/FileHandlers/P2_Handler.hpp b/XMPFiles/source/FileHandlers/P2_Handler.hpp new file mode 100644 index 0000000..93b2672 --- /dev/null +++ b/XMPFiles/source/FileHandlers/P2_Handler.hpp @@ -0,0 +1,110 @@ +#ifndef __P2_Handler_hpp__ +#define __P2_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2007 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. + +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +#include "source/ExpatAdapter.hpp" + +#include "third-party/zuid/interfaces/MD5.h" + +// ================================================================================================= +/// \file P2_Handler.hpp +/// \brief Folder format handler for P2. +/// +/// This header ... +/// +// ================================================================================================= + +// *** Could derive from Basic_Handler - buffer file tail in a temp file. + +extern XMPFileHandler * P2_MetaHandlerCTor ( XMPFiles * parent ); + +extern bool P2_CheckFormat ( XMP_FileFormat format, + const std::string & rootPath, + const std::string & gpName, + const std::string & parentName, + const std::string & leafName, + XMPFiles * parent ); + +static const XMP_OptionBits kP2_HandlerFlags = (kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_CanRewrite | + kXMPFiles_PrefersInPlace | + kXMPFiles_CanReconcile | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_HandlerOwnsFile | + kXMPFiles_AllowsSafeUpdate | + kXMPFiles_UsesSidecarXMP | + kXMPFiles_FolderBasedFormat); + +class P2_MetaHandler : public XMPFileHandler +{ +public: + + bool GetFileModDate ( XMP_DateTime * modDate ); + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + XMP_OptionBits GetSerializeOptions() // *** These should be standard for standalone XMP files. + { return (kXMP_UseCompactFormat | kXMP_OmitPacketWrapper); }; + + P2_MetaHandler ( XMPFiles * _parent ); + virtual ~P2_MetaHandler(); + +private: + + P2_MetaHandler() : expat(0), clipMetadata(0), clipContent(0) {}; // Hidden on purpose. + + bool MakeClipFilePath ( std::string * path, XMP_StringPtr suffix, bool checkFile = false ); + void MakeLegacyDigest ( std::string * digestStr ); + void CleanupLegacyXML(); + + void DigestLegacyItem ( MD5_CTX & md5Context, XML_NodePtr legacyContext, XMP_StringPtr legacyPropName ); + void DigestLegacyRelations ( MD5_CTX & md5Context ); + + void SetXMPPropertyFromLegacyXML ( bool digestFound, + XML_NodePtr legacyContext, + XMP_StringPtr schemaNS, + XMP_StringPtr propName, + XMP_StringPtr legacyPropName, + bool isLocalized ); + + void SetRelationsFromLegacyXML ( bool digestFound ); + void SetAudioInfoFromLegacyXML ( bool digestFound ); + void SetVideoInfoFromLegacyXML ( bool digestFound ); + void SetDurationFromLegacyXML ( bool digestFound ); + + void SetVideoFrameInfoFromLegacyXML ( XML_NodePtr legacyVideoContext, bool digestFound ); + void SetStartTimecodeFromLegacyXML ( XML_NodePtr legacyVideoContext, bool digestFound ); + void SetGPSPropertyFromLegacyXML ( XML_NodePtr legacyLocationContext, bool digestFound, XMP_StringPtr propName, XMP_StringPtr legacyPropName ); + void SetAltitudeFromLegacyXML ( XML_NodePtr legacyLocationContext, bool digestFound ); + + XML_Node * ForceChildElement ( XML_Node * parent, XMP_StringPtr localName, int indent = 0 ); + + std::string rootPath, clipName, p2NS; + + ExpatAdapter * expat; + XML_Node * clipMetadata; // ! Don't delete, points into the Expat tree. + XML_Node * clipContent; // ! Don't delete, points into the Expat tree. + +}; // P2_MetaHandler + +// ================================================================================================= + +#endif /* __P2_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/PNG_Handler.cpp b/XMPFiles/source/FileHandlers/PNG_Handler.cpp new file mode 100644 index 0000000..52b65ff --- /dev/null +++ b/XMPFiles/source/FileHandlers/PNG_Handler.cpp @@ -0,0 +1,248 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FileHandlers/PNG_Handler.hpp" +#include "XMPFiles/source/FormatSupport/PNG_Support.hpp" + +#include "source/XIO.hpp" + +using namespace std; + +// ================================================================================================= +/// \file PNG_Handler.hpp +/// \brief File format handler for PNG. +/// +/// This handler ... +/// +// ================================================================================================= + +// ================================================================================================= +// PNG_MetaHandlerCTor +// ==================== + +XMPFileHandler * PNG_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new PNG_MetaHandler ( parent ); + +} // PNG_MetaHandlerCTor + +// ================================================================================================= +// PNG_CheckFormat +// =============== + +bool PNG_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ) +{ + IgnoreParam(format); IgnoreParam(fileRef); IgnoreParam(parent); + XMP_Assert ( format == kXMP_PNGFile ); + + IOBuffer ioBuf; + + fileRef->Rewind(); + if ( ! CheckFileSpace ( fileRef, &ioBuf, PNG_SIGNATURE_LEN ) ) return false; // We need at least 8, the buffer is filled anyway. + + if ( ! CheckBytes ( ioBuf.ptr, PNG_SIGNATURE_DATA, PNG_SIGNATURE_LEN ) ) return false; + + return true; + +} // PNG_CheckFormat + +// ================================================================================================= +// PNG_MetaHandler::PNG_MetaHandler +// ================================== + +PNG_MetaHandler::PNG_MetaHandler ( XMPFiles * _parent ) +{ + this->parent = _parent; + this->handlerFlags = kPNG_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + +} + +// ================================================================================================= +// PNG_MetaHandler::~PNG_MetaHandler +// =================================== + +PNG_MetaHandler::~PNG_MetaHandler() +{ +} + +// ================================================================================================= +// PNG_MetaHandler::CacheFileData +// =============================== + +void PNG_MetaHandler::CacheFileData() +{ + + this->containsXMP = false; + + XMP_IO* fileRef ( this->parent->ioRef ); + if ( fileRef == 0) return; + + PNG_Support::ChunkState chunkState; + long numChunks = PNG_Support::OpenPNG ( fileRef, chunkState ); + if ( numChunks == 0 ) return; + + if (chunkState.xmpLen != 0) + { + // XMP present + + this->xmpPacket.reserve(chunkState.xmpLen); + this->xmpPacket.assign(chunkState.xmpLen, ' '); + + if (PNG_Support::ReadBuffer ( fileRef, chunkState.xmpPos, chunkState.xmpLen, const_cast<char *>(this->xmpPacket.data()) )) + { + this->packetInfo.offset = chunkState.xmpPos; + this->packetInfo.length = chunkState.xmpLen; + this->containsXMP = true; + } + } + else + { + // no XMP + } + +} // PNG_MetaHandler::CacheFileData + +// ================================================================================================= +// PNG_MetaHandler::ProcessXMP +// ============================ +// +// Process the raw XMP and legacy metadata that was previously cached. + +void PNG_MetaHandler::ProcessXMP() +{ + this->processedXMP = true; // Make sure we only come through here once. + + // Process the XMP packet. + + if ( ! this->xmpPacket.empty() ) { + + XMP_Assert ( this->containsXMP ); + XMP_StringPtr packetStr = this->xmpPacket.c_str(); + XMP_StringLen packetLen = (XMP_StringLen)this->xmpPacket.size(); + + this->xmpObj.ParseFromBuffer ( packetStr, packetLen ); + + this->containsXMP = true; + + } + +} // PNG_MetaHandler::ProcessXMP + +// ================================================================================================= +// PNG_MetaHandler::UpdateFile +// ============================ + +void PNG_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + bool updated = false; + + if ( ! this->needsUpdate ) return; + if ( doSafeUpdate ) XMP_Throw ( "PNG_MetaHandler::UpdateFile: Safe update not supported", kXMPErr_Unavailable ); + + XMP_StringPtr packetStr = xmpPacket.c_str(); + XMP_StringLen packetLen = (XMP_StringLen)xmpPacket.size(); + if ( packetLen == 0 ) return; + + XMP_IO* fileRef(this->parent->ioRef); + if ( fileRef == 0 ) return; + + PNG_Support::ChunkState chunkState; + long numChunks = PNG_Support::OpenPNG ( fileRef, chunkState ); + if ( numChunks == 0 ) return; + + // write/update chunk + if (chunkState.xmpLen == 0) + { + // no current chunk -> inject + updated = SafeWriteFile(); + } + else if (chunkState.xmpLen >= packetLen ) + { + // current chunk size is sufficient -> write and update CRC (in place update) + updated = PNG_Support::WriteBuffer(fileRef, chunkState.xmpPos, packetLen, packetStr ); + PNG_Support::UpdateChunkCRC(fileRef, chunkState.xmpChunk ); + } + else if (chunkState.xmpLen < packetLen) + { + // XMP is too large for current chunk -> expand + updated = SafeWriteFile(); + } + + if ( ! updated )return; // If there's an error writing the chunk, bail. + + this->needsUpdate = false; + +} // PNG_MetaHandler::UpdateFile + +// ================================================================================================= +// PNG_MetaHandler::WriteTempFile +// ============================== + +void PNG_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + XMP_IO* originalRef = this->parent->ioRef; + + PNG_Support::ChunkState chunkState; + long numChunks = PNG_Support::OpenPNG ( originalRef, chunkState ); + if ( numChunks == 0 ) return; + + tempRef->Truncate ( 0 ); + tempRef->Write ( PNG_SIGNATURE_DATA, PNG_SIGNATURE_LEN ); + + PNG_Support::ChunkIterator curPos = chunkState.chunks.begin(); + PNG_Support::ChunkIterator endPos = chunkState.chunks.end(); + + for (; (curPos != endPos); ++curPos) + { + PNG_Support::ChunkData chunk = *curPos; + + // discard existing XMP chunk + if (chunk.xmp) + continue; + + // copy any other chunk + PNG_Support::CopyChunk(originalRef, tempRef, chunk); + + // place XMP chunk immediately after IHDR-chunk + if (PNG_Support::CheckIHDRChunkHeader(chunk)) + { + XMP_StringPtr packetStr = xmpPacket.c_str(); + XMP_StringLen packetLen = (XMP_StringLen)xmpPacket.size(); + + PNG_Support::WriteXMPChunk(tempRef, packetLen, packetStr ); + } + } + +} // PNG_MetaHandler::WriteTempFile + +// ================================================================================================= +// PNG_MetaHandler::SafeWriteFile +// ============================== + +bool PNG_MetaHandler::SafeWriteFile() +{ + XMP_IO* originalFile = this->parent->ioRef; + XMP_IO* tempFile = originalFile->DeriveTemp(); + if ( tempFile == 0 ) XMP_Throw ( "Failure creating PNG temp file", kXMPErr_InternalFailure ); + + this->WriteTempFile ( tempFile ); + originalFile->AbsorbTemp(); + + return true; + +} // PNG_MetaHandler::SafeWriteFile + +// ================================================================================================= diff --git a/XMPFiles/source/FileHandlers/PNG_Handler.hpp b/XMPFiles/source/FileHandlers/PNG_Handler.hpp new file mode 100644 index 0000000..9298141 --- /dev/null +++ b/XMPFiles/source/FileHandlers/PNG_Handler.hpp @@ -0,0 +1,62 @@ +#ifndef __PNG_Handler_hpp__ +#define __PNG_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/PNG_Support.hpp" +#include "source/XIO.hpp" + +// ================================================================================================= +/// \file PNG_Handler.hpp +/// \brief File format handler for PNG. +/// +/// This header ... +/// +// ================================================================================================= + +// *** Could derive from Basic_Handler - buffer file tail in a temp file. + +extern XMPFileHandler* PNG_MetaHandlerCTor ( XMPFiles* parent ); + +extern bool PNG_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO * fileRef, + XMPFiles * parent ); + +static const XMP_OptionBits kPNG_HandlerFlags = ( kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_PrefersInPlace | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_NeedsReadOnlyPacket ); + +class PNG_MetaHandler : public XMPFileHandler +{ +public: + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + bool SafeWriteFile(); + + PNG_MetaHandler ( XMPFiles* parent ); + virtual ~PNG_MetaHandler(); + +}; // PNG_MetaHandler + +// ================================================================================================= + +#endif /* __PNG_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/PSD_Handler.cpp b/XMPFiles/source/FileHandlers/PSD_Handler.cpp new file mode 100644 index 0000000..d57f91c --- /dev/null +++ b/XMPFiles/source/FileHandlers/PSD_Handler.cpp @@ -0,0 +1,417 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/PSD_Handler.hpp" + +#include "XMPFiles/source/FormatSupport/TIFF_Support.hpp" +#include "XMPFiles/source/FormatSupport/PSIR_Support.hpp" +#include "XMPFiles/source/FormatSupport/IPTC_Support.hpp" +#include "XMPFiles/source/FormatSupport/ReconcileLegacy.hpp" +#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp" + +#include "third-party/zuid/interfaces/MD5.h" + +using namespace std; + +// ================================================================================================= +/// \file PSD_Handler.cpp +/// \brief File format handler for PSD (Photoshop). +/// +/// This handler ... +/// +// ================================================================================================= + +// ================================================================================================= +// PSD_CheckFormat +// =============== + +// For PSD we just check the "8BPS" signature, the following version, and that the file is at least +// 34 bytes long. This covers the 26 byte header, the 4 byte color mode section length (which might +// be 0), and the 4 byte image resource section length (which might be 0). The parsing logic in +// CacheFileData will do further checks that the image resources actually exist. Those checks are +// not needed to decide if this is a PSD file though, instead they decide if this is valid PSD. + +// ! The CheckXyzFormat routines don't track the filePos, that is left to ScanXyzFile. + +bool PSD_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ) +{ + IgnoreParam(format); IgnoreParam(filePath); IgnoreParam(parent); + XMP_Assert ( format == kXMP_PhotoshopFile ); + + IOBuffer ioBuf; + + fileRef->Rewind ( ); + if ( ! CheckFileSpace ( fileRef, &ioBuf, 34 ) ) return false; // 34 = header plus 2 lengths + + if ( ! CheckBytes ( ioBuf.ptr, "8BPS", 4 ) ) return false; + ioBuf.ptr += 4; // Move to the version. + XMP_Uns16 version = GetUns16BE ( ioBuf.ptr ); + if ( (version != 1) && (version != 2) ) return false; + + return true; + +} // PSD_CheckFormat + +// ================================================================================================= +// PSD_MetaHandlerCTor +// =================== + +XMPFileHandler * PSD_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new PSD_MetaHandler ( parent ); + +} // PSD_MetaHandlerCTor + +// ================================================================================================= +// PSD_MetaHandler::PSD_MetaHandler +// ================================ + +PSD_MetaHandler::PSD_MetaHandler ( XMPFiles * _parent ) : iptcMgr(0), exifMgr(0), skipReconcile(false) +{ + this->parent = _parent; + this->handlerFlags = kPSD_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + +} // PSD_MetaHandler::PSD_MetaHandler + +// ================================================================================================= +// PSD_MetaHandler::~PSD_MetaHandler +// ================================= + +PSD_MetaHandler::~PSD_MetaHandler() +{ + + if ( this->iptcMgr != 0 ) delete ( this->iptcMgr ); + if ( this->exifMgr != 0 ) delete ( this->exifMgr ); + +} // PSD_MetaHandler::~PSD_MetaHandler + +// ================================================================================================= +// PSD_MetaHandler::CacheFileData +// ============================== +// +// Find and parse the image resource section, everything we want is in there. Don't simply capture +// the whole section, there could be lots of stuff we don't care about. + +// *** This implementation simply returns when an invalid file is encountered. Should we throw instead? + +void PSD_MetaHandler::CacheFileData() +{ + XMP_IO* fileRef = this->parent->ioRef; + XMP_PacketInfo & packetInfo = this->packetInfo; + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + XMP_Assert ( ! this->containsXMP ); + // Set containsXMP to true here only if the XMP image resource is found. + + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "PSD_MetaHandler::CacheFileData - User abort", kXMPErr_UserAbort ); + } + + XMP_Uns8 psdHeader[30]; + XMP_Uns32 ioLen, cmLen, psirLen; + + XMP_Int64 filePos = 0; + fileRef->Rewind ( ); + + ioLen = fileRef->Read ( psdHeader, 30 ); + if ( ioLen != 30 ) return; // Throw? + + this->imageHeight = GetUns32BE ( &psdHeader[14] ); + this->imageWidth = GetUns32BE ( &psdHeader[18] ); + + cmLen = GetUns32BE ( &psdHeader[26] ); + + XMP_Int64 psirOrigin = 26 + 4 + cmLen; + + filePos = fileRef->Seek ( psirOrigin, kXMP_SeekFromStart ); + if ( filePos != psirOrigin ) return; // Throw? + + ioLen = fileRef->Read ( psdHeader, 4 ); + if ( ioLen != 4 ) return; // Throw? + + psirLen = GetUns32BE ( &psdHeader[0] ); + + this->psirMgr.ParseFileResources ( fileRef, psirLen ); + + PSIR_Manager::ImgRsrcInfo xmpInfo; + bool found = this->psirMgr.GetImgRsrc ( kPSIR_XMP, &xmpInfo ); + + if ( found ) { + + // printf ( "PSD_MetaHandler::CacheFileData - XMP packet offset %d (0x%X), size %d\n", + // xmpInfo.origOffset, xmpInfo.origOffset, xmpInfo.dataLen ); + this->packetInfo.offset = xmpInfo.origOffset; + this->packetInfo.length = xmpInfo.dataLen; + this->packetInfo.padSize = 0; // Assume for now, set these properly in ProcessXMP. + this->packetInfo.charForm = kXMP_CharUnknown; + this->packetInfo.writeable = true; + + this->xmpPacket.assign ( (XMP_StringPtr)xmpInfo.dataPtr, xmpInfo.dataLen ); + + this->containsXMP = true; + + } + +} // PSD_MetaHandler::CacheFileData + +// ================================================================================================= +// PSD_MetaHandler::ProcessXMP +// =========================== +// +// Process the raw XMP and legacy metadata that was previously cached. + +void PSD_MetaHandler::ProcessXMP() +{ + + this->processedXMP = true; // Make sure we only come through here once. + + // Set up everything for the legacy import, but don't do it yet. This lets us do a forced legacy + // import if the XMP packet gets parsing errors. + + bool readOnly = ((this->parent->openFlags & kXMPFiles_OpenForUpdate) == 0); + + if ( readOnly ) { + this->iptcMgr = new IPTC_Reader(); + this->exifMgr = new TIFF_MemoryReader(); + } else { + this->iptcMgr = new IPTC_Writer(); // ! Parse it later. + this->exifMgr = new TIFF_FileWriter(); + } + + PSIR_Manager & psir = this->psirMgr; // Give the compiler help in recognizing non-aliases. + IPTC_Manager & iptc = *this->iptcMgr; + TIFF_Manager & exif = *this->exifMgr; + + PSIR_Manager::ImgRsrcInfo iptcInfo, exifInfo; + bool haveIPTC = psir.GetImgRsrc ( kPSIR_IPTC, &iptcInfo ); + bool haveExif = psir.GetImgRsrc ( kPSIR_Exif, &exifInfo ); + int iptcDigestState = kDigestMatches; + + if ( haveExif ) exif.ParseMemoryStream ( exifInfo.dataPtr, exifInfo.dataLen ); + + if ( haveIPTC ) { + + bool haveDigest = false; + PSIR_Manager::ImgRsrcInfo digestInfo; + haveDigest = psir.GetImgRsrc ( kPSIR_IPTCDigest, &digestInfo ); + if ( digestInfo.dataLen != 16 ) haveDigest = false; + + if ( ! haveDigest ) { + iptcDigestState = kDigestMissing; + } else { + iptcDigestState = PhotoDataUtils::CheckIPTCDigest ( iptcInfo.dataPtr, iptcInfo.dataLen, digestInfo.dataPtr ); + } + + } + + XMP_OptionBits options = 0; + if ( this->containsXMP ) options |= k2XMP_FileHadXMP; + if ( haveIPTC ) options |= k2XMP_FileHadIPTC; + if ( haveExif ) options |= k2XMP_FileHadExif; + + // Process the XMP packet. If it fails to parse, do a forced legacy import but still throw an + // exception. This tells the caller that an error happened, but gives them recovered legacy + // should they want to proceed with that. + + bool haveXMP = false; + + if ( ! this->xmpPacket.empty() ) { + XMP_Assert ( this->containsXMP ); + // Common code takes care of packetInfo.charForm, .padSize, and .writeable. + XMP_StringPtr packetStr = this->xmpPacket.c_str(); + XMP_StringLen packetLen = (XMP_StringLen)this->xmpPacket.size(); + try { + this->xmpObj.ParseFromBuffer ( packetStr, packetLen ); + haveXMP = true; + } catch ( ... ) { + XMP_ClearOption ( options, k2XMP_FileHadXMP ); + if ( haveIPTC ) iptc.ParseMemoryDataSets ( iptcInfo.dataPtr, iptcInfo.dataLen ); + if ( iptcDigestState == kDigestMatches ) iptcDigestState = kDigestMissing; + ImportPhotoData ( exif, iptc, psir, iptcDigestState, &this->xmpObj, options ); + throw; // ! Rethrow the exception, don't absorb it. + } + } + + // Process the legacy metadata. + + if ( haveIPTC && (! haveXMP) && (iptcDigestState == kDigestMatches) ) iptcDigestState = kDigestMissing; + bool parseIPTC = (iptcDigestState != kDigestMatches) || (! readOnly); + if ( parseIPTC ) iptc.ParseMemoryDataSets ( iptcInfo.dataPtr, iptcInfo.dataLen ); + ImportPhotoData ( exif, iptc, psir, iptcDigestState, &this->xmpObj, options ); + this->containsXMP = true; // Assume we now have something in the XMP. + +} // PSD_MetaHandler::ProcessXMP + +// ================================================================================================= +// PSD_MetaHandler::UpdateFile +// =========================== + +void PSD_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + XMP_Assert ( ! doSafeUpdate ); // This should only be called for "unsafe" updates. + + XMP_Int64 oldPacketOffset = this->packetInfo.offset; + XMP_Int32 oldPacketLength = this->packetInfo.length; + + if ( oldPacketOffset == kXMPFiles_UnknownOffset ) oldPacketOffset = 0; // ! Simplify checks. + if ( oldPacketLength == kXMPFiles_UnknownLength ) oldPacketLength = 0; + + bool fileHadXMP = ((oldPacketOffset != 0) && (oldPacketLength != 0)); + + // Update the IPTC-IIM and native TIFF/Exif metadata. ExportPhotoData also trips the tiff: and + // exif: copies from the XMP, so reserialize the now final XMP packet. + + ExportPhotoData ( kXMP_PhotoshopFile, &this->xmpObj, this->exifMgr, this->iptcMgr, &this->psirMgr ); + + try { + XMP_OptionBits options = kXMP_UseCompactFormat; + if ( fileHadXMP ) options |= kXMP_ExactPacketLength; + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, options, oldPacketLength ); + } catch ( ... ) { + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, kXMP_UseCompactFormat ); + } + + // Decide whether to do an in-place update. This can only happen if all of the following are true: + // - There is an XMP packet in the file. + // - The are no changes to the legacy image resources. (The IPTC and EXIF are in the PSIR.) + // - The new XMP can fit in the old space. + + bool doInPlace = (fileHadXMP && (this->xmpPacket.size() <= (size_t)oldPacketLength)); + if ( this->psirMgr.IsLegacyChanged() ) doInPlace = false; + + if ( doInPlace ) { + + #if GatherPerformanceData + sAPIPerf->back().extraInfo += ", PSD in-place update"; + #endif + + if ( this->xmpPacket.size() < (size_t)this->packetInfo.length ) { + // They ought to match, cheap to be sure. + size_t extraSpace = (size_t)this->packetInfo.length - this->xmpPacket.size(); + this->xmpPacket.append ( extraSpace, ' ' ); + } + + XMP_IO* liveFile = this->parent->ioRef; + + XMP_Assert ( this->xmpPacket.size() == (size_t)oldPacketLength ); // ! Done by common PutXMP logic. + + // printf ( "PSD_MetaHandler::UpdateFile - XMP in-place packet offset %lld (0x%llX), size %d\n", + // oldPacketOffset, oldPacketOffset, this->xmpPacket.size() ); + liveFile->Seek ( oldPacketOffset, kXMP_SeekFromStart ); + liveFile->Write ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + + } else { + + #if GatherPerformanceData + sAPIPerf->back().extraInfo += ", PSD copy update"; + #endif + + XMP_IO* origRef = this->parent->ioRef; + XMP_IO* tempRef = origRef->DeriveTemp(); + + try { + XMP_Assert ( ! this->skipReconcile ); + this->skipReconcile = true; + this->WriteTempFile ( tempRef ); + this->skipReconcile = false; + } catch ( ... ) { + this->skipReconcile = false; + origRef->DeleteTemp(); + throw; + } + + origRef->AbsorbTemp(); + + } + + this->needsUpdate = false; + +} // PSD_MetaHandler::UpdateFile + +// ================================================================================================= +// PSD_MetaHandler::WriteTempFile +// ============================== + +// The metadata parts of a Photoshop file are all in the image resources. The PSIR_Manager's +// UpdateFileResources method will take care of the image resource portion of the file, updating +// those resources that have changed and preserving those that have not. + +void PSD_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + XMP_IO* origRef = this->parent->ioRef; + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + XMP_Uns64 sourceLen = origRef->Length(); + if ( sourceLen == 0 ) return; // Tolerate empty files. + + // Reconcile the legacy metadata, unless this is called from UpdateFile. Reserialize the XMP to + // get standard padding, PutXMP has probably done an in-place serialize. Set the XMP image resource. + + if ( ! skipReconcile ) { + // Update the IPTC-IIM and native TIFF/Exif metadata, and reserialize the now final XMP packet. + ExportPhotoData ( kXMP_JPEGFile, &this->xmpObj, this->exifMgr, this->iptcMgr, &this->psirMgr ); + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, kXMP_UseCompactFormat ); + } + + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, kXMP_UseCompactFormat ); + this->packetInfo.offset = kXMPFiles_UnknownOffset; + this->packetInfo.length = (XMP_StringLen)this->xmpPacket.size(); + FillPacketInfo ( this->xmpPacket, &this->packetInfo ); + + this->psirMgr.SetImgRsrc ( kPSIR_XMP, this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + + // Copy the file header and color mode section, then write the updated image resource section, + // and copy the tail of the source file (layer and mask section to EOF). + + origRef->Rewind ( ); + tempRef->Truncate ( 0 ); + + XIO::Copy ( origRef, tempRef, 26 ); // Copy the file header. + + XMP_Uns32 cmLen; + origRef->Read ( &cmLen, 4 ); + tempRef->Write ( &cmLen, 4 ); // Copy the color mode section length. + cmLen = GetUns32BE ( &cmLen ); + XIO::Copy ( origRef, tempRef, cmLen ); // Copy the color mode section contents. + + XMP_Uns32 irLen; + origRef->Read ( &irLen, 4 ); // Get the source image resource section length. + irLen = GetUns32BE ( &irLen ); + + this->psirMgr.UpdateFileResources ( origRef, tempRef, 0, abortProc, abortArg ); + + XMP_Uns64 tailOffset = 26 + 4 + cmLen + 4 + irLen; + XMP_Uns64 tailLength = sourceLen - tailOffset; + + origRef->Seek ( tailOffset, kXMP_SeekFromStart ); + tempRef->Seek ( 0, kXMP_SeekFromEnd ); + XIO::Copy ( origRef, tempRef, tailLength ); // Copy the tail of the file. + + this->needsUpdate = false; + +} // PSD_MetaHandler::WriteTempFile + +// ================================================================================================= diff --git a/XMPFiles/source/FileHandlers/PSD_Handler.hpp b/XMPFiles/source/FileHandlers/PSD_Handler.hpp new file mode 100644 index 0000000..a64b6df --- /dev/null +++ b/XMPFiles/source/FileHandlers/PSD_Handler.hpp @@ -0,0 +1,72 @@ +#ifndef __PSD_Handler_hpp__ +#define __PSD_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "XMPFiles/source/FormatSupport/TIFF_Support.hpp" +#include "XMPFiles/source/FormatSupport/PSIR_Support.hpp" +#include "XMPFiles/source/FormatSupport/IPTC_Support.hpp" + +// ================================================================================================= +/// \file PSD_Handler.hpp +/// \brief File format handler for PSD (Photoshop). +/// +/// This header ... +/// +// ================================================================================================= + +// *** Could derive from Basic_Handler - buffer file tail in a temp file. + +extern XMPFileHandler * PSD_MetaHandlerCTor ( XMPFiles * parent ); + +extern bool PSD_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ); + +static const XMP_OptionBits kPSD_HandlerFlags = (kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_CanRewrite | + kXMPFiles_PrefersInPlace | + kXMPFiles_CanReconcile | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_AllowsSafeUpdate); + +class PSD_MetaHandler : public XMPFileHandler +{ +public: + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + bool skipReconcile; // ! Used between UpdateFile and WriteFile. + + PSD_MetaHandler ( XMPFiles * parent ); + virtual ~PSD_MetaHandler(); + +private: + + PSD_MetaHandler() : iptcMgr(0), exifMgr(0), skipReconcile(false) {}; // Hidden on purpose. + + PSIR_FileWriter psirMgr; // Don't need a pointer, the PSIR part is always file-based. + IPTC_Manager * iptcMgr; // Need to use pointers so we can properly select between read-only + TIFF_Manager * exifMgr; // and read-write modes of usage. + + XMP_Uns32 imageWidth, imageHeight; // Pixel dimensions, used with thumbnail info. + +}; // PSD_MetaHandler + +// ================================================================================================= + +#endif /* __PSD_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/PostScript_Handler.cpp b/XMPFiles/source/FileHandlers/PostScript_Handler.cpp new file mode 100644 index 0000000..bd5d4f7 --- /dev/null +++ b/XMPFiles/source/FileHandlers/PostScript_Handler.cpp @@ -0,0 +1,574 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/PostScript_Handler.hpp" + +#include "XMPFiles/source/FormatSupport/XMPScanner.hpp" +#include "XMPFiles/source/FileHandlers/Scanner_Handler.hpp" + +using namespace std; + +// ================================================================================================= +/// \file PostScript_Handler.cpp +/// \brief File format handler for PostScript and EPS files. +/// +/// This header ... +/// +// ================================================================================================= + +static const char * kPSFileTag = "%!PS-Adobe-"; +static const size_t kPSFileTagLen = strlen ( kPSFileTag ); + +// ================================================================================================= +// PostScript_MetaHandlerCTor +// ========================== + +XMPFileHandler * PostScript_MetaHandlerCTor ( XMPFiles * parent ) +{ + XMPFileHandler * newHandler = new PostScript_MetaHandler ( parent ); + + return newHandler; + +} // PostScript_MetaHandlerCTor + +// ================================================================================================= +// PostScript_CheckFormat +// ====================== + +bool PostScript_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ) +{ + IgnoreParam(filePath); IgnoreParam(parent); + XMP_Assert ( (format == kXMP_EPSFile) || (format == kXMP_PostScriptFile) ); + + IOBuffer ioBuf; + + XMP_Int64 psOffset; + size_t psLength; + XMP_Uns32 temp1, temp2; + + // Check for the binary EPSF preview header. + + fileRef->Rewind(); + if ( ! CheckFileSpace ( fileRef, &ioBuf, 4 ) ) return false; + temp1 = GetUns32BE ( ioBuf.ptr ); + + if ( temp1 == 0xC5D0D3C6 ) { + + if ( ! CheckFileSpace ( fileRef, &ioBuf, 30 ) ) return false; + + psOffset = GetUns32LE ( ioBuf.ptr+4 ); // PostScript offset. + psLength = GetUns32LE ( ioBuf.ptr+8 ); // PostScript length. + + FillBuffer ( fileRef, psOffset, &ioBuf ); // Make sure buffer starts at psOffset for length check. + if ( (ioBuf.len < kIOBufferSize) && (ioBuf.len < psLength) ) return false; // Not enough PostScript. + + } + + // Check the start of the PostScript DSC header comment. + + if ( ! CheckFileSpace ( fileRef, &ioBuf, (kPSFileTagLen + 3 + 1) ) ) return false; + if ( ! CheckBytes ( ioBuf.ptr, Uns8Ptr(kPSFileTag), kPSFileTagLen ) ) return false; + ioBuf.ptr += kPSFileTagLen; + + // Check the PostScript DSC major version number. + + temp1 = 0; + while ( (ioBuf.ptr < ioBuf.limit) && ('0' <= *ioBuf.ptr) && (*ioBuf.ptr <= '9') ) { + temp1 = (temp1 * 10) + (*ioBuf.ptr - '0'); + if ( temp1 > 1000 ) return false; // Overflow. + ioBuf.ptr += 1; + } + if ( temp1 < 3 ) return false; // The version must be at least 3.0. + + if ( ! CheckFileSpace ( fileRef, &ioBuf, 3 ) ) return false; + if ( *ioBuf.ptr != '.' ) return false; // No minor number. + ioBuf.ptr += 1; + + // Check the PostScript DSC minor version number. + + temp2 = 0; + while ( (ioBuf.ptr < ioBuf.limit) && ('0' <= *ioBuf.ptr) && (*ioBuf.ptr <= '9') ) { + temp2 = (temp2 * 10) + (*ioBuf.ptr - '0'); + if ( temp2 > 1000 ) return false; // Overflow. + ioBuf.ptr += 1; + } + // We don't care about the actual minor version number. + + if ( format == kXMP_PostScriptFile ) { + + // Almost done for plain PostScript, check for whitespace. + + if ( ! CheckFileSpace ( fileRef, &ioBuf, 1 ) ) return false; + if ( (*ioBuf.ptr != ' ') && (*ioBuf.ptr != kLF) && (*ioBuf.ptr != kCR) ) return false; + ioBuf.ptr += 1; + + } else { + + // Check for the EPSF keyword on the header comment. + + if ( ! CheckFileSpace ( fileRef, &ioBuf, 6+3+1 ) ) return false; + if ( ! CheckBytes ( ioBuf.ptr, Uns8Ptr(" EPSF-"), 6 ) ) return false; + ioBuf.ptr += 6; + + // Check the EPS major version number. + + temp1 = 0; + while ( (ioBuf.ptr < ioBuf.limit) && ('0' <= *ioBuf.ptr) && (*ioBuf.ptr <= '9') ) { + temp1 = (temp1 * 10) + (*ioBuf.ptr - '0'); + if ( temp1 > 1000 ) return false; // Overflow. + ioBuf.ptr += 1; + } + if ( temp1 < 3 ) return false; // The version must be at least 3.0. + + if ( ! CheckFileSpace ( fileRef, &ioBuf, 3 ) ) return false; + if ( *ioBuf.ptr != '.' ) return false; // No minor number. + ioBuf.ptr += 1; + + // Check the EPS minor version number. + + temp2 = 0; + while ( (ioBuf.ptr < ioBuf.limit) && ('0' <= *ioBuf.ptr) && (*ioBuf.ptr <= '9') ) { + temp2 = (temp2 * 10) + (*ioBuf.ptr - '0'); + if ( temp2 > 1000 ) return false; // Overflow. + ioBuf.ptr += 1; + } + // We don't care about the actual minor version number. + + if ( ! CheckFileSpace ( fileRef, &ioBuf, 1 ) ) return false; + if ( (*ioBuf.ptr != kLF) && (*ioBuf.ptr != kCR) ) return false; + ioBuf.ptr += 1; + + } + + return true; + +} // PostScript_CheckFormat + +// ================================================================================================= +// PostScript_MetaHandler::PostScript_MetaHandler +// ============================================== + +PostScript_MetaHandler::PostScript_MetaHandler ( XMPFiles * _parent ) +{ + this->parent = _parent; + this->handlerFlags = kPostScript_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + this->psHint = kPSHint_NoMarker; + +} // PostScript_MetaHandler::PostScript_MetaHandler + +// ================================================================================================= +// PostScript_MetaHandler::~PostScript_MetaHandler +// =============================================== + +PostScript_MetaHandler::~PostScript_MetaHandler() +{ + // ! Inherit the base cleanup. + +} // PostScript_MetaHandler::~PostScript_MetaHandler + +// ================================================================================================= +// PostScript_MetaHandler::FindPostScriptHint +// ========================================== +// +// Search for "%ADO_ContainsXMP:" at the beginning of a line, it must be before "%%EndComments". If +// the XMP marker is found, look for the MainFirst/MainLast/NoMain options. + +static const char * kPSContainsXMPString = "%ADO_ContainsXMP:"; +static const size_t kPSContainsXMPLength = strlen ( kPSContainsXMPString ); + +static const char * kPSEndCommentString = "%%EndComments"; // ! Assumed shorter than kPSContainsXMPString. +static const size_t kPSEndCommentLength = strlen ( kPSEndCommentString ); + +int PostScript_MetaHandler::FindPostScriptHint() +{ + bool found = false; + IOBuffer ioBuf; + XMP_Uns8 ch; + + XMP_IO* fileRef = this->parent->ioRef; + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + // Check for the binary EPSF preview header. + + fileRef->Rewind(); + if ( ! CheckFileSpace ( fileRef, &ioBuf, 4 ) ) return false; + XMP_Uns32 temp1 = GetUns32BE ( ioBuf.ptr ); + + if ( temp1 == 0xC5D0D3C6 ) { + + if ( ! CheckFileSpace ( fileRef, &ioBuf, 30 ) ) return false; + + XMP_Uns32 psOffset = GetUns32LE ( ioBuf.ptr+4 ); // PostScript offset. + XMP_Uns32 psLength = GetUns32LE ( ioBuf.ptr+8 ); // PostScript length. + + MoveToOffset ( fileRef, psOffset, &ioBuf ); + + } + + // Look for the ContainsXMP comment. + + while ( true ) { + + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "PostScript_MetaHandler::FindPostScriptHint - User abort", kXMPErr_UserAbort ); + } + + if ( ! CheckFileSpace ( fileRef, &ioBuf, kPSContainsXMPLength ) ) return kPSHint_NoMarker; + + if ( CheckBytes ( ioBuf.ptr, Uns8Ptr(kPSEndCommentString), kPSEndCommentLength ) ) { + + // Found "%%EndComments", don't look any further. + return kPSHint_NoMarker; + + } else if ( ! CheckBytes ( ioBuf.ptr, Uns8Ptr(kPSContainsXMPString), kPSContainsXMPLength ) ) { + + // Not "%%EndComments" or "%ADO_ContainsXMP:", skip past the end of this line. + do { + if ( ! CheckFileSpace ( fileRef, &ioBuf, 1 ) ) return kPSHint_NoMarker; + ch = *ioBuf.ptr; + ++ioBuf.ptr; + } while ( ! IsNewline ( ch ) ); + + } else { + + // Found "%ADO_ContainsXMP:", look for the main packet location option. + + ioBuf.ptr += kPSContainsXMPLength; + int xmpHint = kPSHint_NoMain; // ! From here on, a failure means "no main", not "no marker". + if ( ! CheckFileSpace ( fileRef, &ioBuf, 1 ) ) return kPSHint_NoMain; + if ( ! IsSpaceOrTab ( *ioBuf.ptr ) ) return kPSHint_NoMain; + + while ( true ) { + + while ( true ) { // Skip leading spaces and tabs. + if ( ! CheckFileSpace ( fileRef, &ioBuf, 1 ) ) return kPSHint_NoMain; + if ( ! IsSpaceOrTab ( *ioBuf.ptr ) ) break; + ++ioBuf.ptr; + } + if ( IsNewline ( *ioBuf.ptr ) ) return kPSHint_NoMain; // Reached the end of the ContainsXMP comment. + + if ( ! CheckFileSpace ( fileRef, &ioBuf, 6 ) ) return kPSHint_NoMain; + + if ( CheckBytes ( ioBuf.ptr, Uns8Ptr("NoMain"), 6 ) ) { + + ioBuf.ptr += 6; + xmpHint = kPSHint_NoMain; + break; + + } else if ( CheckBytes ( ioBuf.ptr, Uns8Ptr("MainFi"), 6 ) ) { + + ioBuf.ptr += 6; + if ( ! CheckFileSpace ( fileRef, &ioBuf, 3 ) ) return kPSHint_NoMain; + if ( CheckBytes ( ioBuf.ptr, Uns8Ptr("rst"), 3 ) ) { + ioBuf.ptr += 3; + xmpHint = kPSHint_MainFirst; + } + break; + + } else if ( CheckBytes ( ioBuf.ptr, Uns8Ptr("MainLa"), 6 ) ) { + + ioBuf.ptr += 6; + if ( ! CheckFileSpace ( fileRef, &ioBuf, 2 ) ) return kPSHint_NoMain; + if ( CheckBytes ( ioBuf.ptr, Uns8Ptr("st"), 2 ) ) { + ioBuf.ptr += 2; + xmpHint = kPSHint_MainLast; + } + break; + + } else { + + while ( true ) { // Skip until whitespace. + if ( ! CheckFileSpace ( fileRef, &ioBuf, 1 ) ) return kPSHint_NoMain; + if ( IsWhitespace ( *ioBuf.ptr ) ) break; + ++ioBuf.ptr; + } + + } + + } // Look for the main packet location option. + + // Make sure we found exactly a known option. + if ( ! CheckFileSpace ( fileRef, &ioBuf, 1 ) ) return kPSHint_NoMain; + if ( ! IsWhitespace ( *ioBuf.ptr ) ) return kPSHint_NoMain; + return xmpHint; + + } // Found "%ADO_ContainsXMP:". + + } // Outer marker loop. + + return kPSHint_NoMarker; // Should never reach here. + +} // PostScript_MetaHandler::FindPostScriptHint + + +// ================================================================================================= +// PostScript_MetaHandler::FindFirstPacket +// ======================================= +// +// Run the packet scanner until we find a valid packet. The first one is the main. For simplicity, +// the state of all snips is checked after each buffer is read. In theory only the last of the +// previous snips might change from partial to valid, but then we would have to special case the +// first pass when there is no previous set of snips. Since we have to get a full report to look at +// the last snip anyway, it costs virtually nothing extra to recheck all of the snips. + +bool PostScript_MetaHandler::FindFirstPacket() +{ + int snipCount; + bool found = false; + size_t bufPos, bufLen; + + XMP_IO* fileRef = this->parent->ioRef; + XMP_Int64 fileLen = fileRef->Length(); + XMP_PacketInfo & packetInfo = this->packetInfo; + + XMPScanner scanner ( fileLen ); + XMPScanner::SnipInfoVector snips; + + enum { kBufferSize = 64*1024 }; + XMP_Uns8 buffer [kBufferSize]; + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + bufPos = 0; + bufLen = 0; + + fileRef->Rewind(); // Seek back to the beginning of the file. + + while ( true ) { + + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "PostScript_MetaHandler::FindFirstPacket - User abort", kXMPErr_UserAbort ); + } + + bufPos += bufLen; + bufLen = fileRef->Read ( buffer, kBufferSize ); + if ( bufLen == 0 ) return false; // Must be at EoF, no packets found. + + scanner.Scan ( buffer, bufPos, bufLen ); + snipCount = scanner.GetSnipCount(); + scanner.Report ( snips ); + + for ( int i = 0; i < snipCount; ++i ) { + if ( snips[i].fState == XMPScanner::eValidPacketSnip ) { + if ( snips[i].fLength > 0x7FFFFFFF ) XMP_Throw ( "PostScript_MetaHandler::FindFirstPacket: Oversize packet", kXMPErr_BadXMP ); + packetInfo.offset = snips[i].fOffset; + packetInfo.length = (XMP_Int32)snips[i].fLength; + packetInfo.charForm = snips[i].fCharForm; + packetInfo.writeable = (snips[i].fAccess == 'w'); + return true; + } + } + + } + + return false; + +} // FindFirstPacket + + +// ================================================================================================= +// PostScript_MetaHandler::FindLastPacket +// ====================================== +// +// Run the packet scanner backwards until we find the start of a packet, or a valid packet. If we +// found a packet start, resume forward scanning to see if it is a valid packet. For simplicity, all +// of the snips are checked on each pass, for much the same reasons as in FindFirstPacket. + +#if 1 + +// *** Doing this right (as described above) requires out of order scanning support which isn't +// *** implemented yet. For now we scan the whole file and pick the last valid packet. + +bool PostScript_MetaHandler::FindLastPacket() +{ + int pkt; + size_t bufPos, bufLen; + + XMP_IO* fileRef = this->parent->ioRef; + XMP_Int64 fileLen = fileRef->Length(); + XMP_PacketInfo & packetInfo = this->packetInfo; + + // ------------------------------------------------------ + // Scan the entire file to find all of the valid packets. + + XMPScanner scanner ( fileLen ); + + enum { kBufferSize = 64*1024 }; + XMP_Uns8 buffer [kBufferSize]; + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + fileRef->Rewind(); // Seek back to the beginning of the file. + + for ( bufPos = 0; bufPos < (size_t)fileLen; bufPos += bufLen ) { + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "PostScript_MetaHandler::FindLastPacket - User abort", kXMPErr_UserAbort ); + } + bufLen = fileRef->Read ( buffer, kBufferSize ); + if ( bufLen == 0 ) XMP_Throw ( "PostScript_MetaHandler::FindLastPacket: Read failure", kXMPErr_ExternalFailure ); + scanner.Scan ( buffer, bufPos, bufLen ); + } + + // ------------------------------- + // Pick the last the valid packet. + + int snipCount = scanner.GetSnipCount(); + + XMPScanner::SnipInfoVector snips ( snipCount ); + scanner.Report ( snips ); + + for ( pkt = snipCount-1; pkt >= 0; --pkt ) { + if ( snips[pkt].fState == XMPScanner::eValidPacketSnip ) break; + } + + if ( pkt >= 0 ) { + if ( snips[pkt].fLength > 0x7FFFFFFF ) XMP_Throw ( "PostScript_MetaHandler::FindLastPacket: Oversize packet", kXMPErr_BadXMP ); + packetInfo.offset = snips[pkt].fOffset; + packetInfo.length = (XMP_Int32)snips[pkt].fLength; + packetInfo.charForm = snips[pkt].fCharForm; + packetInfo.writeable = (snips[pkt].fAccess == 'w'); + return true; + } + + return false; + +} // PostScript_MetaHandler::FindLastPacket + +#else + +bool PostScript_MetaHandler::FindLastPacket() +{ + int err, snipCount; + bool found = false; + XMP_Int64 backPos, backLen; + size_t ioCount; + + XMP_IO* fileRef = this->parent->fileRef; + XMP_Int64 fileLen = fileRef->Length(); + XMP_PacketInfo & packetInfo = this->packetInfo; + + XMPScanner scanner ( fileLen ); + XMPScanner::SnipInfoVector snips; + + enum { kBufferSize = 64*1024 }; + XMP_Uns8 buffer [kBufferSize]; + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + backPos = fileLen; + backLen = 0; + + while ( true ) { + + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "PostScript_MetaHandler::FindLastPacket - User abort", kXMPErr_UserAbort ); + } + + backLen = kBufferSize; + if ( backPos < kBufferSize ) backLen = backPos; + if ( backLen == 0 ) return false; // Must be at BoF, no packets found. + + backPos -= backLen; + fileRef->Seek ( backPos, kXMP_SeekFromStart ); // Seek back to the start of the next buffer. + + #error "ioCount is 32 bits, backLen is 64" + ioCount = fileRef->Read ( buffer, backLen ); + if ( ioCount != backLen ) XMP_Throw ( "PostScript_MetaHandler::FindLastPacket: Read failure", kXMPErr_ExternalFailure ); + + scanner.Scan ( buffer, backPos, backLen ); + snipCount = scanner.GetSnipCount(); + scanner.Report ( snips ); + + for ( int i = snipCount-1; i >= 0; --i ) { + + if ( snips[i].fState == XMPScanner::eValidPacketSnip ) { + + return VerifyMainPacket ( fileRef, snips[i].fOffset, snips[i].fLength, format, beLenient, mainInfo ); + + } else if ( snips[i].fState == XMPScanner::ePartialPacketSnip ) { + + // This part is a tad tricky. We have a partial packet, so we need to scan + // forward from its ending to see if it is a valid packet. Snips won't recombine, + // the partial snip will change state. Be careful with the I/O to not clobber the + // backward scan positions, so that it can be resumed if necessary. + + size_t fwdPos = snips[i].fOffset + snips[i].fLength; + fileRef->Seek ( fwdPos, kXMP_SeekFromStart ); // Seek to the end of the partial snip. + + while ( (fwdPos < fileLen) && (snips[i].fState == XMPScanner::ePartialPacketSnip) ) { + ioCount = fileRef->Read ( buffer, kBufferSize ); + if ( ioCount == 0 ) XMP_Throw ( "PostScript_MetaHandler::FindLastPacket: Read failure", kXMPErr_ExternalFailure ); + scanner.Scan ( buffer, fwdPos, ioCount ); + scanner.Report ( snips ); + fwdPos += ioCount; + } + + if ( snips[i].fState == XMPScanner::eValidPacketSnip ) { + if ( snips[i].fLength > 0x7FFFFFFF ) XMP_Throw ( "PostScript_MetaHandler::FindLastPacket: Oversize packet", kXMPErr_BadXMP ); + packetInfo.offset = snips[i].fOffset; + packetInfo.length = (XMP_Int32)snips[i].fLength; + packetInfo.charForm = snips[i].fCharForm; + packetInfo.writeable = (snips[i].fAccess == 'w'); + return true; + } + + } + + } // Backwards snip loop. + + } // Backwards read loop. + + return false; // Should never get here. + +} // PostScript_MetaHandler::FindLastPacket + +#endif + +// ================================================================================================= +// PostScript_MetaHandler::CacheFileData +// ===================================== + +void PostScript_MetaHandler::CacheFileData() +{ + this->containsXMP = false; + this->psHint = FindPostScriptHint(); + + if ( this->psHint == kPSHint_MainFirst ) { + this->containsXMP = FindFirstPacket(); + } else if ( this->psHint == kPSHint_MainLast ) { + this->containsXMP = FindLastPacket(); + } + + if ( this->containsXMP ) ReadXMPPacket ( this ); + +} // PostScript_MetaHandler::CacheFileData + +// ================================================================================================= diff --git a/XMPFiles/source/FileHandlers/PostScript_Handler.hpp b/XMPFiles/source/FileHandlers/PostScript_Handler.hpp new file mode 100644 index 0000000..8813336 --- /dev/null +++ b/XMPFiles/source/FileHandlers/PostScript_Handler.hpp @@ -0,0 +1,68 @@ +#ifndef __PostScript_Handler_hpp__ +#define __PostScript_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/FileHandlers/Trivial_Handler.hpp" + +// ================================================================================================= +/// \file PostScript_Handler.hpp +/// \brief File format handler for PostScript and EPS files. +/// +/// This header ... +/// +// ================================================================================================= + +// *** This probably could be derived from Basic_Handler, buffer the file tail in a temp file. + +extern XMPFileHandler * PostScript_MetaHandlerCTor ( XMPFiles * parent ); + +extern bool PostScript_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO * fileRef, + XMPFiles * parent ); + +static const XMP_OptionBits kPostScript_HandlerFlags = kTrivial_HandlerFlags; + +enum { + kPSHint_NoMarker = 0, + kPSHint_NoMain = 1, + kPSHint_MainFirst = 2, + kPSHint_MainLast = 3 +}; + +class PostScript_MetaHandler : public Trivial_MetaHandler +{ +public: + + PostScript_MetaHandler ( XMPFiles * parent ); + ~PostScript_MetaHandler(); + + void CacheFileData(); + + int psHint; + +protected: + + int FindPostScriptHint(); + + bool FindFirstPacket(); + bool FindLastPacket(); + +}; // PostScript_MetaHandler + +// ================================================================================================= + +#endif /* __PostScript_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/RIFF_Handler.cpp b/XMPFiles/source/FileHandlers/RIFF_Handler.cpp new file mode 100644 index 0000000..7d6fdb3 --- /dev/null +++ b/XMPFiles/source/FileHandlers/RIFF_Handler.cpp @@ -0,0 +1,356 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2009 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/RIFF.hpp" +#include "XMPFiles/source/FileHandlers/RIFF_Handler.hpp" +#include "source/XIO.hpp" + +using namespace std; + +// ================================================================================================= +/// \file RIFF_Handler.cpp +/// \brief File format handler for RIFF. +// ================================================================================================= + +// ================================================================================================= +// RIFF_MetaHandlerCTor +// ==================== + +XMPFileHandler * RIFF_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new RIFF_MetaHandler ( parent ); +} + +// ================================================================================================= +// RIFF_CheckFormat +// =============== +// +// An RIFF file must begin with "RIFF", a 4 byte length, then the chunkType (AVI or WAV) +// The specified length MUST (in practice: SHOULD) match fileSize-8, but we don't bother checking this here. + +bool RIFF_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* file, + XMPFiles* parent ) +{ + IgnoreParam(format); IgnoreParam(parent); + XMP_Assert ( (format == kXMP_AVIFile) || (format == kXMP_WAVFile) ); + + if ( file->Length() < 12 ) return false; + file ->Rewind(); + + XMP_Uns8 chunkID[12]; + file->ReadAll ( chunkID, 12 ); + if ( ! CheckBytes( &chunkID[0], "RIFF", 4 )) return false; + + if ( CheckBytes(&chunkID[8],"AVI ",4) && format == kXMP_AVIFile ) return true; + if ( CheckBytes(&chunkID[8],"WAVE",4) && format == kXMP_WAVFile ) return true; + + return false; + +} // RIFF_CheckFormat + +// ================================================================================================= +// RIFF_MetaHandler::RIFF_MetaHandler +// ================================ + +RIFF_MetaHandler::RIFF_MetaHandler ( XMPFiles * _parent ) +{ + this->parent = _parent; + this->handlerFlags = kRIFF_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + + this->oldFileSize = this->newFileSize = this->trailingGarbageSize = 0; + this->level = 0; + this->listInfoChunk = this->listTdatChunk = 0; + this->dispChunk = this->bextChunk = this->cr8rChunk = this->prmlChunk = 0; + this->xmpChunk = 0; + this->lastChunk = 0; + this->hasListInfoINAM = false; +} + +// ================================================================================================= +// RIFF_MetaHandler::~RIFF_MetaHandler +// ================================= + +RIFF_MetaHandler::~RIFF_MetaHandler() +{ + while ( ! this->riffChunks.empty() ) + { + delete this->riffChunks.back(); + this->riffChunks.pop_back(); + } +} + +// ================================================================================================= +// RIFF_MetaHandler::CacheFileData +// ============================== + +void RIFF_MetaHandler::CacheFileData() +{ + this->containsXMP = false; //assume for now + + XMP_IO* file = this->parent->ioRef; + this->oldFileSize = file ->Length(); + if ( (this->parent->format == kXMP_WAVFile) && (this->oldFileSize > 0xFFFFFFFF) ) + XMP_Throw ( "RIFF_MetaHandler::CacheFileData: WAV Files larger 4GB not supported", kXMPErr_Unimplemented ); + + file ->Rewind(); + this->level = 0; + + // parse top-level chunks (most likely just one, except large avi files) + XMP_Int64 filePos = 0; + while ( filePos < this->oldFileSize ) + { + + this->riffChunks.push_back( (RIFF::ContainerChunk*) RIFF::getChunk( NULL, this ) ); + + // Tolerate limited forms of trailing garbage in a file. Some apps append private data. + + filePos = file->Offset(); + XMP_Int64 fileTail = this->oldFileSize - filePos; + + if ( fileTail != 0 ) { + + if ( fileTail < 12 ) { + + this->oldFileSize = filePos; // Pretend the file is smaller. + this->trailingGarbageSize = fileTail; + + } else if ( this->parent->format == kXMP_WAVFile ) { + + if ( fileTail < 1024*1024 ) { + this->oldFileSize = filePos; // Pretend the file is smaller. + this->trailingGarbageSize = fileTail; + } else { + XMP_Throw ( "Excessive garbage at end of file", kXMPErr_BadFileFormat ) + } + + } else { + + XMP_Int32 chunkInfo [3]; + file->ReadAll ( &chunkInfo, 12 ); + file->Seek ( -12, kXMP_SeekFromCurrent ); + if ( (GetUns32LE ( &chunkInfo[0] ) != RIFF::kChunk_RIFF) || (GetUns32LE ( &chunkInfo[2] ) != RIFF::kType_AVIX) ) { + if ( fileTail < 1024*1024 ) { + this->oldFileSize = filePos; // Pretend the file is smaller. + this->trailingGarbageSize = fileTail; + } else { + XMP_Throw ( "Excessive garbage at end of file", kXMPErr_BadFileFormat ) + } + } + + } + + } + + } + + // covered before => internal error if it occurs + XMP_Validate( file->Offset() == this->oldFileSize, + "RIFF_MetaHandler::CacheFileData: unknown data at end of file", + kXMPErr_InternalFailure ); + +} // RIFF_MetaHandler::CacheFileData + +// ================================================================================================= +// RIFF_MetaHandler::ProcessXMP +// ============================ + +void RIFF_MetaHandler::ProcessXMP() +{ + SXMPUtils::RemoveProperties ( &this->xmpObj, 0, 0, kXMPUtil_DoAllProperties ); + // process physical packet first + if ( this->containsXMP ) this->xmpObj.ParseFromBuffer ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + // then import native properties: + RIFF::importProperties( this ); + this->processedXMP = true; +} + +// ================================================================================================= +// RIFF_MetaHandler::UpdateFile +// =========================== + +void RIFF_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + XMP_Validate( this->needsUpdate, "nothing to update", kXMPErr_InternalFailure ); + + //////////////////////////////////////////////////////////////////////////////////////// + //////////// PASS 1: basics, exports, packet reserialze + XMP_IO* file = this->parent->ioRef; + RIFF::containerVect *rc = &this->riffChunks; + + //temptemp + //printf( "BEFORE:\n%s\n", rc->at(0)->toString().c_str() ); + + XMP_Enforce( rc->size() >= 1); + RIFF::ContainerChunk* mainChunk = rc->at(0); + this->lastChunk = rc->at( rc->size() - 1 ); // (may or may not coincide with mainChunk: ) + XMP_Enforce( mainChunk != NULL ); + + RIFF::relocateWronglyPlacedXMPChunk( this ); + // [2435625] lifted disablement for AVI + RIFF::exportAndRemoveProperties( this ); + + // always rewrite both LISTs (implicit size changes, e.g. through 0-term corrections may + // have very well led to size changes...) + // set XMP packet info, re-serialize + this->packetInfo.charForm = stdCharForm; + this->packetInfo.writeable = true; + this->packetInfo.offset = kXMPFiles_UnknownOffset; + this->packetInfo.length = kXMPFiles_UnknownLength; + + // re-serialization ( needed because of above exportAndRemoveProperties() ) + try { + if ( this->xmpChunk == 0 ) // new chunk? pad with 2K + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, kXMP_NoOptions , 2048 ); + else // otherwise try to match former size + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, kXMP_ExactPacketLength , (XMP_Uns32) this->xmpChunk->oldSize-8 ); + } catch ( ... ) { // if that fails, be happy with whatever. + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, kXMP_NoOptions ); + } + + if ( (this->xmpPacket.size() & 1) == 1 ) this->xmpPacket += ' '; // Force the XMP to have an even size. + + // if missing, add xmp packet at end: + if( this->xmpChunk == 0 ) + this->xmpChunk = new RIFF::XMPChunk( this->lastChunk ); + // * parenting happens within this call. + // * size computation will happen in XMPChunk::changesAndSize() + // * actual data will be set in XMPChunk::write() + + //////////////////////////////////////////////////////////////////////////////////////// + // PASS 2: compute sizes, optimize container structure (no writing yet) + { + this->newFileSize = 0; + + // note: going through forward (not vice versa) is an important condition, + // so that parking LIST:Tdat,Cr8r, PrmL to last chunk is doable + // when encountered en route + for ( XMP_Uns32 chunkNo = 0; chunkNo < rc->size(); chunkNo++ ) + { + RIFF::Chunk* cur = rc->at(chunkNo); + cur->changesAndSize( this ); + this->newFileSize += cur->newSize; + if ( this->newFileSize % 2 == 1 ) this->newFileSize++; // pad byte + } + this->newFileSize += this->trailingGarbageSize; + } // PASS2 + + //////////////////////////////////////////////////////////////////////////////////////// + // PASS 2a: verify no chunk violates 2GB boundaries + switch( this->parent->format ) + { + // NB: <4GB for ALL chunks asserted before in ContainerChunk::changesAndSize() + + case kXMP_AVIFile: + // ensure no chunk (single or multi, no matter) crosses 2 GB ... + for ( XMP_Int32 chunkNo = 0; chunkNo < (XMP_Int32)rc->size(); chunkNo++ ) + { + if ( rc->at(chunkNo)->oldSize <= 0x80000000LL ) // ... if <2GB before + XMP_Validate( rc->at(chunkNo)->newSize <= 0x80000000LL, + "Chunk grew beyond 2 GB", kXMPErr_Unimplemented ); + } + + // compatibility: if single-chunk AND below <1GB, ensure <1GB + if ( ( rc->size() > 1 ) && ( rc->at(0)->oldSize < 0x40000000 ) ) + { + XMP_Validate( rc->at(0)->newSize < 0x40000000LL, "compatibility: mainChunk must remain < 1GB" , kXMPErr_Unimplemented ); + } + + // [2473381] compatibility: if single-chunk AND >2GB,<4GB, ensure <4GB + if ( ( rc->size() > 1 ) && + ( rc->at(0)->oldSize > 0x80000000LL ) && // 2GB + ( rc->at(0)->oldSize < 0x100000000LL ) ) // 4GB + { + XMP_Validate( rc->at(0)->newSize < 0x100000000LL, "compatibility: mainChunk must remain < 4GB" , kXMPErr_Unimplemented ); + } + + break; + + case kXMP_WAVFile: + XMP_Validate( 1 == rc->size(), "WAV must be single-chunk", kXMPErr_InternalFailure ); + XMP_Validate( rc->at(0)->newSize <= 0xFFFFFFFFLL, "WAV above 4 GB not supported", kXMPErr_Unimplemented ); + break; + + default: + XMP_Throw( "unknown format", kXMPErr_InternalFailure ); + } + + //////////////////////////////////////////////////////////////////////////////////////// + // PASS 3: write avix chunk(s) if applicable (shrinks or stays) + // and main chunk. -- operation order depends on mainHasShrunk. + { + // if needed, extend file beforehand + if ( this->newFileSize > this->oldFileSize ) { + file->Seek ( newFileSize, kXMP_SeekFromStart ); + file->Rewind(); + } + + RIFF::Chunk* mainChunk = rc->at(0); + + XMP_Int64 mainGrowth = mainChunk->newSize - mainChunk->oldSize; + XMP_Enforce( mainGrowth >= 0 ); // main always stays or grows + + //temptemp + //printf( "AFTER:\n%s\n", rc->at(0)->toString().c_str() ); + + if ( rc->size() > 1 ) // [2457482] + XMP_Validate( mainGrowth == 0, "mainChunk must not grow, if multiple RIFF chunks", kXMPErr_InternalFailure ); + + // back to front: + + XMP_Int64 avixStart = newFileSize; // count from the back + + if ( this->trailingGarbageSize != 0 ) { + XMP_Int64 goodDataEnd = this->newFileSize - this->trailingGarbageSize; + XIO::Move ( file, this->oldFileSize, file, goodDataEnd, this->trailingGarbageSize ); + avixStart = goodDataEnd; + } + + for ( XMP_Int32 chunkNo = ((XMP_Int32)rc->size()) -1; chunkNo >= 0; chunkNo-- ) + { + RIFF::Chunk* cur = rc->at(chunkNo); + + avixStart -= cur->newSize; + if ( avixStart % 2 == 1 ) // rewind one more + avixStart -= 1; + + file->Seek ( avixStart , kXMP_SeekFromStart ); + + if ( cur->hasChange ) // need explicit write-out ? + cur->write( this, file, chunkNo == 0 ); + else // or will a simple move do? + { + XMP_Enforce( cur->oldSize == cur->newSize ); + if ( cur->oldPos != avixStart ) // important optimization: only move if there's a need to + XIO::Move( file, cur->oldPos, file, avixStart, cur->newSize ); + } + } + + // if needed, shrink file afterwards + if ( this->newFileSize < this->oldFileSize ) file->Truncate ( this->newFileSize ); + } // PASS 3 + + this->needsUpdate = false; //do last for safety +} // RIFF_MetaHandler::UpdateFile + +// ================================================================================================= +// RIFF_MetaHandler::WriteTempFile +// =============================== + +void RIFF_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + IgnoreParam( tempRef ); + XMP_Throw ( "RIFF_MetaHandler::WriteTempFile: Not supported (must go through UpdateFile", kXMPErr_Unavailable ); +} + diff --git a/XMPFiles/source/FileHandlers/RIFF_Handler.hpp b/XMPFiles/source/FileHandlers/RIFF_Handler.hpp new file mode 100644 index 0000000..d8c83a2 --- /dev/null +++ b/XMPFiles/source/FileHandlers/RIFF_Handler.hpp @@ -0,0 +1,73 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2009 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= +#ifndef __RIFF_Handler_hpp__ +#define __RIFF_Handler_hpp__ 1 + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/RIFF_Support.hpp" +#include "source/XIO.hpp" + +// ================================================================================================= +/// \file RIFF_Handler.hpp +/// \brief File format handler for RIFF (AVI, WAV). +// ================================================================================================= + +extern XMPFileHandler * RIFF_MetaHandlerCTor ( XMPFiles * parent ); + +extern bool RIFF_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO * fileRef, + XMPFiles * parent ); + +static const XMP_OptionBits kRIFF_HandlerFlags = (kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_PrefersInPlace | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_CanReconcile + ); + +class RIFF_MetaHandler : public XMPFileHandler +{ +public: + RIFF_MetaHandler ( XMPFiles* parent ); + ~RIFF_MetaHandler(); + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + + //////////////////////////////////////////////////////////////////////////////////// + // instance vars + // most often just one RIFF:* (except for AVI,[AVIX] >1 GB) + std::vector<RIFF::ContainerChunk*> riffChunks; + XMP_Int64 oldFileSize, newFileSize, trailingGarbageSize; + + // state variables, needed during parsing + XMP_Uns8 level; + + RIFF::ContainerChunk *listInfoChunk, *listTdatChunk; + RIFF::ValueChunk* dispChunk; + RIFF::ValueChunk* bextChunk; + RIFF::ValueChunk* cr8rChunk; + RIFF::ValueChunk* prmlChunk; + RIFF::XMPChunk* xmpChunk; + RIFF::ContainerChunk* lastChunk; + bool hasListInfoINAM; // needs to be known for the special 3-way merge around dc:title + +}; // RIFF_MetaHandler + +// ================================================================================================= + +#endif /* __RIFF_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/SWF_Handler.cpp b/XMPFiles/source/FileHandlers/SWF_Handler.cpp new file mode 100644 index 0000000..a0554ce --- /dev/null +++ b/XMPFiles/source/FileHandlers/SWF_Handler.cpp @@ -0,0 +1,330 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/SWF_Handler.hpp" +#include "XMPFiles/source/FormatSupport/SWF_Support.hpp" + +using namespace std; + +// ================================================================================================= +/// \file SWF_Handler.hpp +/// \brief File format handler for SWF. +/// +/// This handler ... +/// +// ================================================================================================= + +// ================================================================================================= +// SWF_MetaHandlerCTor +// =================== + +XMPFileHandler * SWF_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new SWF_MetaHandler ( parent ); + +} // SWF_MetaHandlerCTor + +// ================================================================================================= +// SWF_CheckFormat +// =============== + +bool SWF_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO * fileRef, + XMPFiles * parent ) +{ + IgnoreParam(format); IgnoreParam(fileRef); IgnoreParam(parent); + XMP_Assert ( format == kXMP_SWFFile ); + + // Make sure the file is long enough for an empty SWF stream. Check the signature. + + if ( fileRef->Length() < SWF_IO::HeaderPrefixSize ) return false; + + fileRef->Rewind(); + XMP_Uns8 buffer [4]; + fileRef->ReadAll ( buffer, 4 ); + XMP_Uns32 signature = GetUns32LE ( &buffer[0] ) & 0xFFFFFF; // Discard the version byte. + + return ( (signature == SWF_IO::CompressedSignature) || (signature == SWF_IO::ExpandedSignature) ); + +} // SWF_CheckFormat + +// ================================================================================================= +// SWF_MetaHandler::SWF_MetaHandler +// ================================ + +SWF_MetaHandler::SWF_MetaHandler ( XMPFiles * _parent ) + : isCompressed(false), hasFileAttributes(false), hasMetadata(false), brokenSWF(false), expandedSize(0), firstTagOffset(0) +{ + this->parent = _parent; + this->handlerFlags = kSWF_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + +} + +// ================================================================================================= +// SWF_MetaHandler::~SWF_MetaHandler +// ================================= + +SWF_MetaHandler::~SWF_MetaHandler() +{ + // Nothing to do at this time. +} + +// ================================================================================================= +// SWF_MetaHandler::CacheFileData +// ============================== +// +// SWF files are pretty small, have simple metadata, and often have ZIP compression. Because they +// are small and often compressed, we always cache the fully expanded SWF in memory. That is used +// for both reading and updating. Note that SWF_CheckFormat has already done basic checks on the +// size and signature, they don't need to be repeated here. +// +// Try to find the FileAttributes and Metadata tags, saving their offsets for later use if updating +// the file. We need to be tolerant when reading, allowing the FileAttributes tag to be anywhere and +// allowing Metadata without FileAttributes or with the HasMetadata flag clear. The SWF spec is not +// clear enough about the rules for SWF 7 and earlier, there are 3rd party tools that don't put +// FileAttributes first. + +void SWF_MetaHandler::CacheFileData() { + + XMP_Assert ( (! this->processedXMP) && (! this->containsXMP) ); + XMP_Assert ( this->expandedSWF.empty() ); + + XMP_IO * fileRef = this->parent->ioRef; + XMP_Int64 fileLength = fileRef->Length(); + XMP_Enforce ( fileLength <= SWF_IO::MaxExpandedSize ); + + // Get the uncompressed SWF stream into memory. + + fileRef->Rewind(); + XMP_Uns8 buffer [SWF_IO::HeaderPrefixSize]; // Read the uncompressed file header prefix. + fileRef->ReadAll ( buffer, SWF_IO::HeaderPrefixSize ); + + XMP_Uns32 signature = GetUns32LE ( &buffer[0] ) & 0xFFFFFF; // Discard the version byte. + this->expandedSize = GetUns32LE ( &buffer[4] ); + if ( signature == SWF_IO::CompressedSignature ) this->isCompressed = true; + + if ( this->isCompressed ) { + + // Expand the SWF file into memory. + this->expandedSWF.reserve ( this->expandedSize ); // Try to avoid reallocations. + SWF_IO::DecompressFileToMemory ( fileRef, &this->expandedSWF ); + this->expandedSize = this->expandedSWF.size(); // Use the true length. + + } else { + + // Read the entire uncompressed file into memory. + this->expandedSize = (XMP_Uns32)fileLength; // Use the true length. + this->expandedSWF.insert ( this->expandedSWF.end(), (size_t)fileLength, 0 ); + fileRef->Rewind(); + fileRef->ReadAll ( &this->expandedSWF[0], (XMP_Uns32)fileLength ); + + } + + // Look for the FileAttributes and Metadata tags. + + this->firstTagOffset = SWF_IO::FileHeaderSize ( this->expandedSWF[SWF_IO::HeaderPrefixSize] ); + + XMP_Uns32 currOffset = this->firstTagOffset; + SWF_IO::TagInfo currTag; + + for ( ; currOffset < this->expandedSize; currOffset = SWF_IO::NextTagOffset(currTag) ) { + + bool ok = SWF_IO::GetTagInfo ( this->expandedSWF, currOffset, &currTag ); + if ( ! ok ) { + this->brokenSWF = true; // Let the read finish, but refuse to update. + break; + } + + if ( currTag.tagID == SWF_IO::FileAttributesTagID ) { + this->fileAttributesTag = currTag; + this->hasFileAttributes = true; + if ( this->hasMetadata ) break; // Exit if we have both. + } + + if ( currTag.tagID == SWF_IO::MetadataTagID ) { + this->metadataTag = currTag; + this->hasMetadata = true; + if ( this->hasFileAttributes ) break; // Exit if we have both. + } + + } + + if ( this->hasMetadata ) { + this->packetInfo.offset = SWF_IO::ContentOffset ( this->metadataTag ); + this->packetInfo.length = this->metadataTag.contentLength; + this->xmpPacket.assign ( (char*)&this->expandedSWF[(size_t)this->packetInfo.offset], (size_t)this->packetInfo.length ); + FillPacketInfo ( this->xmpPacket, &this->packetInfo ); + this->containsXMP = true; + } + +} // SWF_MetaHandler::CacheFileData + +// ================================================================================================= +// SWF_MetaHandler::ProcessXMP +// =========================== + +void SWF_MetaHandler::ProcessXMP() +{ + + this->processedXMP = true; // Make sure we only come through here once. + + if ( ! this->xmpPacket.empty() ) { + XMP_Assert ( this->containsXMP ); + XMP_StringPtr packetStr = this->xmpPacket.c_str(); + XMP_StringLen packetLen = (XMP_StringLen)this->xmpPacket.size(); + this->xmpObj.ParseFromBuffer ( packetStr, packetLen ); + } + +} // SWF_MetaHandler::ProcessXMP + +// ================================================================================================= +// XMPFileHandler::GetSerializeOptions +// =================================== +// +// Override default implementation to ensure omitting XMP wrapper. + +XMP_OptionBits SWF_MetaHandler::GetSerializeOptions() +{ + + return (kXMP_OmitPacketWrapper | kXMP_OmitAllFormatting | kXMP_OmitXMPMetaElement); + +} // XMPFileHandler::GetSerializeOptions + +// ================================================================================================= +// SWF_MetaHandler::UpdateFile +// =========================== +// +// Update the expanded SWF in memory, then write it to the file. + +void SWF_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + + if ( doSafeUpdate ) XMP_Throw ( "SWF_MetaHandler::UpdateFile: Safe update not supported", kXMPErr_Unavailable ); + + if ( ! this->needsUpdate ) return; + this->needsUpdate = false; // Don't come through here twice, even if there are errors. + + if ( this->brokenSWF ) { + XMP_Throw ( "SWF is broken, can't update.", kXMPErr_BadFileFormat ); + } + + // Make sure there is a FileAttributes tag at the front, with the HasMetadata flag set. + + if ( ! this->hasFileAttributes ) { + + // Insert a new FileAttributes tag as the first tag. + + XMP_Uns8 buffer [6]; // Two byte header plus four byte content. + PutUns16LE ( ((SWF_IO::FileAttributesTagID << 6) | 4), &buffer[0] ); + PutUns32LE ( SWF_IO::HasMetadataMask, &buffer[2] ); + + this->expandedSWF.insert ( (this->expandedSWF.begin() + this->firstTagOffset), 6, 0 ); + memcpy ( &this->expandedSWF[this->firstTagOffset], &buffer[0], 6 ); + + this->hasFileAttributes = true; + bool ok = SWF_IO::GetTagInfo ( this->expandedSWF, this->firstTagOffset, &this->fileAttributesTag ); + XMP_Assert ( ok ); + + if ( this->hasMetadata ) this->metadataTag.tagOffset += 6; // The Metadata tag is now further back. + + } else { + + // Make sure the HasMetadata flag is set. + if ( this->fileAttributesTag.contentLength > 0 ) { + XMP_Uns32 flagsOffset = SWF_IO::ContentOffset ( this->fileAttributesTag ); + this->expandedSWF[flagsOffset] |= SWF_IO::HasMetadataMask; + } + + // Make sure the FileAttributes tag is the first tag. + if ( this->fileAttributesTag.tagOffset != this->firstTagOffset ) { + + RawDataBlock attrTag; + XMP_Uns32 attrTagLength = SWF_IO::FullTagLength ( this->fileAttributesTag ); + attrTag.assign ( attrTagLength, 0 ); + memcpy ( &attrTag[0], &this->expandedSWF[this->fileAttributesTag.tagOffset], attrTagLength ); + + RawDataBlock::iterator attrTagPos = this->expandedSWF.begin() + this->fileAttributesTag.tagOffset; + RawDataBlock::iterator attrTagEnd = attrTagPos + attrTagLength; + this->expandedSWF.erase ( attrTagPos, attrTagEnd ); // Remove the old FileAttributes tag; + + if ( this->hasMetadata && (this->metadataTag.tagOffset < this->fileAttributesTag.tagOffset) ) { + this->metadataTag.tagOffset += attrTagLength; // The FileAttributes tag will become in front. + } + + this->expandedSWF.insert ( (this->expandedSWF.begin() + this->firstTagOffset), attrTagLength, 0 ); + memcpy ( &this->expandedSWF[this->firstTagOffset], &attrTag[0], attrTagLength ); + + this->fileAttributesTag.tagOffset = this->firstTagOffset; + + } + + } + + // Make sure the XMP is as small as possible. Write the XMP as the second tag. + + XMP_Assert ( this->hasFileAttributes ); + + XMP_OptionBits smallOptions = kXMP_OmitPacketWrapper | kXMP_UseCompactFormat | kXMP_OmitAllFormatting | kXMP_OmitXMPMetaElement; + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, smallOptions ); + + if ( this->hasMetadata ) { + // Remove the old XMP, the size and location have probably changed. + XMP_Uns32 oldMetaLength = SWF_IO::FullTagLength ( this->metadataTag ); + RawDataBlock::iterator oldMetaPos = this->expandedSWF.begin() + this->metadataTag.tagOffset; + RawDataBlock::iterator oldMetaEnd = oldMetaPos + oldMetaLength; + this->expandedSWF.erase ( oldMetaPos, oldMetaEnd ); + } + + this->metadataTag.hasLongHeader = true; + this->metadataTag.tagID = SWF_IO::MetadataTagID; + this->metadataTag.tagOffset = SWF_IO::NextTagOffset ( this->fileAttributesTag ); + this->metadataTag.contentLength = this->xmpPacket.size(); + + XMP_Uns32 newMetaLength = 6 + this->metadataTag.contentLength; // Always use a long tag header. + this->expandedSWF.insert ( (this->expandedSWF.begin() + this->metadataTag.tagOffset), newMetaLength, 0 ); + + PutUns16LE ( ((SWF_IO::MetadataTagID << 6) | SWF_IO::TagLengthMask), &this->expandedSWF[this->metadataTag.tagOffset] ); + PutUns32LE ( this->metadataTag.contentLength, &this->expandedSWF[this->metadataTag.tagOffset+2] ); + memcpy ( &this->expandedSWF[this->metadataTag.tagOffset+6], this->xmpPacket.c_str(), this->metadataTag.contentLength ); + + this->hasMetadata = true; + + // Rewrite the file. + + XMP_IO * fileRef = this->parent->ioRef; + fileRef->Rewind(); + fileRef->Truncate ( 0 ); + fileRef->Write ( &this->expandedSWF[0], this->expandedSWF.size() ); + +} // SWF_MetaHandler::UpdateFile + +// ================================================================================================= +// SWF_MetaHandler::WriteTempFile +// ============================== + +// ! See important notes in SWF_Handler.hpp about file handling. + +void SWF_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + + // ! WriteTempFile is not supposed to be called for SWF. + XMP_Throw ( "SWF_MetaHandler::WriteTempFile should not be called", kXMPErr_InternalFailure ); + +} // SWF_MetaHandler::WriteTempFile diff --git a/XMPFiles/source/FileHandlers/SWF_Handler.hpp b/XMPFiles/source/FileHandlers/SWF_Handler.hpp new file mode 100644 index 0000000..f2ca7cb --- /dev/null +++ b/XMPFiles/source/FileHandlers/SWF_Handler.hpp @@ -0,0 +1,72 @@ +#ifndef __SWF_Handler_hpp__ +#define __SWF_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "XMPFiles/source/FormatSupport/SWF_Support.hpp" + +// ================================================================================================= +/// \file SWF_Handler.hpp +/// \brief File format handler for SWF. +/// +/// This header ... +/// +// ================================================================================================= + +extern XMPFileHandler* SWF_MetaHandlerCTor ( XMPFiles* parent ); + +extern bool SWF_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO * fileRef, + XMPFiles * parent ); + +static const XMP_OptionBits kSWF_HandlerFlags = ( kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_PrefersInPlace | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket ); + +class SWF_MetaHandler : public XMPFileHandler { + +public: + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + XMP_OptionBits GetSerializeOptions(); + + SWF_MetaHandler ( XMPFiles* parent ); + virtual ~SWF_MetaHandler(); + +private: + + SWF_MetaHandler() : isCompressed(false), hasFileAttributes(false), hasMetadata(false), brokenSWF(false), + expandedSize(0), firstTagOffset(0) {}; + + bool isCompressed, hasFileAttributes, hasMetadata, brokenSWF; + XMP_Uns32 expandedSize, firstTagOffset; + RawDataBlock expandedSWF; + + SWF_IO::TagInfo fileAttributesTag, metadataTag; + +}; // SWF_MetaHandler + +// ================================================================================================= + +#endif /* __SWF_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/Scanner_Handler.cpp b/XMPFiles/source/FileHandlers/Scanner_Handler.cpp new file mode 100644 index 0000000..2d6308d --- /dev/null +++ b/XMPFiles/source/FileHandlers/Scanner_Handler.cpp @@ -0,0 +1,347 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FormatSupport/XMPScanner.hpp" +#include "XMPFiles/source/FileHandlers/Scanner_Handler.hpp" + +#include <vector> + +using namespace std; + +#if EnablePacketScanning + +// ================================================================================================= +/// \file Scanner_Handler.cpp +/// \brief File format handler for packet scanning. +/// +/// This header ... +/// +// ================================================================================================= + +struct CandidateInfo { + XMP_PacketInfo packetInfo; + std::string xmpPacket; + SXMPMeta * xmpObj; +}; + +// ================================================================================================= +// Scanner_MetaHandlerCTor +// ======================= + +XMPFileHandler * Scanner_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new Scanner_MetaHandler ( parent ); + +} // Scanner_MetaHandlerCTor + +// ================================================================================================= +// Scanner_MetaHandler::Scanner_MetaHandler +// ======================================== + +Scanner_MetaHandler::Scanner_MetaHandler ( XMPFiles * _parent ) +{ + this->parent = _parent; + this->handlerFlags = kScanner_HandlerFlags; + +} // Scanner_MetaHandler::Scanner_MetaHandler + +// ================================================================================================= +// Scanner_MetaHandler::~Scanner_MetaHandler +// ========================================= + +Scanner_MetaHandler::~Scanner_MetaHandler() +{ + // ! Inherit the base cleanup. + +} // Scanner_MetaHandler::~Scanner_MetaHandler + +// ================================================================================================= +// PickMainPacket +// ============== +// +// Pick the main packet from the vector of candidates. The rules: +// 1. Use the manifest find containment. Prune contained packets. +// 2. Use the metadata date to pick the most recent. +// 3. if lenient, pick the last writeable packet, or the last if all are read only. + +static int +PickMainPacket ( std::vector<CandidateInfo>& candidates, bool beLenient ) +{ + int pkt; // ! Must be signed. + int main = -1; // Assume the worst. + XMP_OptionBits options; + + int metaCount = (int)candidates.size(); + if ( metaCount == 0 ) return -1; + if ( metaCount == 1 ) return 0; + + // --------------------------------------------------------------------------------------------- + // 1. Look at each packet to see if it has a manifest. If it does, prune all of the others that + // this one says it contains. Hopefully we'll end up with just one packet. Note that we have to + // mark all the children first, then prune. Pruning on the fly means that we won't do a proper + // tree discovery if we prune a parent before a child. This would happen if we happened to visit + // a grandparent first. + + int child; + + std::vector<bool> pruned ( metaCount, false ); + + for ( pkt = 0; pkt < (int)candidates.size(); ++pkt ) { + + // First see if this candidate has a manifest. + + try { + std::string voidValue; + bool found = candidates[pkt].xmpObj->GetProperty ( kXMP_NS_XMP_MM, "Manifest", &voidValue, &options ); + if ( (! found) || (! XMP_PropIsArray ( options )) ) continue; // No manifest, or not an array. + } catch ( ... ) { + continue; // No manifest. + }; + + // Mark all other candidates that are referred to in this manifest. + + for ( child = 0; child < (int)candidates.size(); ++child ) { + if ( pruned[child] || (child == pkt) ) continue; // Skip already pruned ones and self. + } + + } + + // Go ahead and actually remove the marked packets. + + for ( pkt = 0; pkt < (int)candidates.size(); ++pkt ) { + if ( pruned[pkt] ) { + delete candidates[pkt].xmpObj; + candidates[pkt].xmpObj = 0; + metaCount -= 1; + } + } + + // We're done if the containment pruning left us with 0 or 1 candidate. + + if ( metaCount == 0 ) { + XMP_Throw ( "GetMainPacket/PickMainPacket: Recursive containment", kXMPErr_BadXMP ); + } else if ( metaCount == 1 ) { + for ( pkt = 0; pkt < (int)candidates.size(); ++pkt ) { + if ( candidates[pkt].xmpObj != 0 ) { + main = pkt; + break; + } + } + } + + if ( main != -1 ) return main; // We found the main. + + // ------------------------------------------------------------------------------------------- + // 2. Pick the packet with the most recent metadata date. If we are being lenient then missing + // dates are older than any real date, and equal dates pick the last packet. If we are being + // strict then any missing or equal dates mean we can't pick. + + XMP_DateTime latestTime, currTime; + + for ( pkt = 0; pkt < (int)candidates.size(); ++pkt ) { + + if ( candidates[pkt].xmpObj == 0 ) continue; // This was pruned in the manifest stage. + + bool haveDate = candidates[pkt].xmpObj->GetProperty_Date ( kXMP_NS_XMP, "MetadataDate", &currTime, &options ); + + if ( ! haveDate ) { + + if ( ! beLenient ) return -1; + if ( main == -1 ) { + main = pkt; + memset ( &latestTime, 0, sizeof(latestTime) ); + } + + } else if ( main == -1 ) { + + main = pkt; + latestTime = currTime; + + } else { + + int timeOp = SXMPUtils::CompareDateTime ( currTime, latestTime ); + + if ( timeOp > 0 ) { + main = pkt; + latestTime = currTime; + } else if ( timeOp == 0 ) { + if ( ! beLenient ) return -1; + main = pkt; + latestTime = currTime; + } + + } + + } + + if ( main != -1 ) return main; // We found the main. + + // -------------------------------------------------------------------------------------------- + // 3. If we're being lenient, pick the last writeable packet, or the last if all are read only. + + if ( beLenient ) { + + for ( pkt = (int)candidates.size()-1; pkt >= 0; --pkt ) { + if ( candidates[pkt].xmpObj == 0 ) continue; // This was pruned in the manifest stage. + if ( candidates[pkt].packetInfo.writeable ) { + main = pkt; + break; + } + } + + if ( main == -1 ) { + for ( pkt = (int)candidates.size()-1; pkt >= 0; --pkt ) { + if ( candidates[pkt].xmpObj != 0 ) { + main = pkt; + break; + } + } + } + + } + + return main; + +} // PickMainPacket + +// ================================================================================================= +// Scanner_MetaHandler::CacheFileData +// ================================== + +void Scanner_MetaHandler::CacheFileData() +{ + XMP_IO* fileRef = this->parent->ioRef; + bool beLenient = XMP_OptionIsClear ( this->parent->openFlags, kXMPFiles_OpenStrictly ); + + int pkt; + XMP_Int64 bufPos; + size_t bufLen; + SXMPMeta * newMeta; + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + std::vector<CandidateInfo> candidates; // ! These have SXMPMeta* fields, don't leak on exceptions. + + this->containsXMP = false; + + try { + + // ------------------------------------------------------ + // Scan the entire file to find all of the valid packets. + + XMP_Int64 fileLen = fileRef->Length(); + XMPScanner scanner ( fileLen ); + + enum { kBufferSize = 64*1024 }; + XMP_Uns8 buffer [kBufferSize]; + + fileRef->Rewind(); + + for ( bufPos = 0; bufPos < fileLen; bufPos += bufLen ) { + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "Scanner_MetaHandler::LocateXMP - User abort", kXMPErr_UserAbort ); + } + bufLen = fileRef->Read ( buffer, kBufferSize ); + if ( bufLen == 0 ) XMP_Throw ( "Scanner_MetaHandler::LocateXMP: Read failure", kXMPErr_ExternalFailure ); + scanner.Scan ( buffer, bufPos, bufLen ); + } + + // -------------------------------------------------------------- + // Parse the valid packet snips, building a vector of candidates. + + long snipCount = scanner.GetSnipCount(); + + XMPScanner::SnipInfoVector snips ( snipCount ); + scanner.Report ( snips ); + + for ( pkt = 0; pkt < snipCount; ++pkt ) { + + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "Scanner_MetaHandler::LocateXMP - User abort", kXMPErr_UserAbort ); + } + + // Seek to the packet then try to parse it. + + if ( snips[pkt].fState != XMPScanner::eValidPacketSnip ) continue; + fileRef->Seek ( snips[pkt].fOffset, kXMP_SeekFromStart ); + newMeta = new SXMPMeta(); + std::string xmpPacket; + xmpPacket.reserve ( (size_t)snips[pkt].fLength ); + + try { + for ( bufPos = 0; bufPos < snips[pkt].fLength; bufPos += bufLen ) { + bufLen = kBufferSize; + if ( (bufPos + bufLen) > (size_t)snips[pkt].fLength ) bufLen = size_t ( snips[pkt].fLength - bufPos ); + (void) fileRef->ReadAll ( buffer, (XMP_Int32)bufLen ); + xmpPacket.append ( (const char *)buffer, bufLen ); + newMeta->ParseFromBuffer ( (char *)buffer, (XMP_StringLen)bufLen, kXMP_ParseMoreBuffers ); + } + newMeta->ParseFromBuffer ( 0, 0, kXMP_NoOptions ); + } catch ( ... ) { + delete newMeta; + if ( beLenient ) continue; // Skip if we're being lenient, else rethrow. + throw; + } + + // It parsed OK, add it to the array of candidates. + + candidates.push_back ( CandidateInfo() ); + CandidateInfo & newInfo = candidates.back(); + newInfo.xmpObj = newMeta; + newInfo.xmpPacket.swap ( xmpPacket ); + newInfo.packetInfo.offset = snips[pkt].fOffset; + newInfo.packetInfo.length = (XMP_Int32)snips[pkt].fLength; + newInfo.packetInfo.charForm = snips[pkt].fCharForm; + newInfo.packetInfo.writeable = (snips[pkt].fAccess == 'w'); + + } + + // ---------------------------------------- + // Figure out which packet is the main one. + + int main = PickMainPacket ( candidates, beLenient ); + + if ( main != -1 ) { + this->packetInfo = candidates[main].packetInfo; + this->xmpPacket.swap ( candidates[main].xmpPacket ); + this->xmpObj = *candidates[main].xmpObj; + this->containsXMP = true; + this->processedXMP = true; + } + + for ( pkt = 0; pkt < (int)candidates.size(); ++pkt ) { + if ( candidates[pkt].xmpObj != 0 ) delete candidates[pkt].xmpObj; + } + + } catch ( ... ) { + + // Clean up the SXMPMeta* fields from the vector of candidates. + for ( pkt = 0; pkt < (int)candidates.size(); ++pkt ) { + if ( candidates[pkt].xmpObj != 0 ) delete candidates[pkt].xmpObj; + } + throw; + + } + +} // Scanner_MetaHandler::CacheFileData + +// ================================================================================================= + +#endif // IncludePacketScanning diff --git a/XMPFiles/source/FileHandlers/Scanner_Handler.hpp b/XMPFiles/source/FileHandlers/Scanner_Handler.hpp new file mode 100644 index 0000000..53c4820 --- /dev/null +++ b/XMPFiles/source/FileHandlers/Scanner_Handler.hpp @@ -0,0 +1,42 @@ +#ifndef __Scanner_Handler_hpp__ +#define __Scanner_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "XMPFiles/source/FileHandlers/Trivial_Handler.hpp" + +// ================================================================================================= +/// \file Scanner_Handler.hpp +/// \brief File format handler for packet scanning. +/// +/// This header ... +/// +// ================================================================================================= + +extern XMPFileHandler * Scanner_MetaHandlerCTor ( XMPFiles * parent ); + +static const XMP_OptionBits kScanner_HandlerFlags = kTrivial_HandlerFlags; + +class Scanner_MetaHandler : public Trivial_MetaHandler +{ +public: + + Scanner_MetaHandler () {}; + Scanner_MetaHandler ( XMPFiles * parent ); + + ~Scanner_MetaHandler(); + + void CacheFileData(); + +}; // Scanner_MetaHandler + +// ================================================================================================= + +#endif /* __Scanner_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/SonyHDV_Handler.cpp b/XMPFiles/source/FileHandlers/SonyHDV_Handler.cpp new file mode 100644 index 0000000..863eab7 --- /dev/null +++ b/XMPFiles/source/FileHandlers/SonyHDV_Handler.cpp @@ -0,0 +1,848 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2007 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/SonyHDV_Handler.hpp" + +#include "third-party/zuid/interfaces/MD5.h" + +#if XMP_WinBuild + #pragma warning ( disable : 4996 ) // '...' was declared deprecated +#endif + +using namespace std; + +// ================================================================================================= +/// \file SonyHDV_Handler.cpp +/// \brief Folder format handler for Sony HDV. +/// +/// This handler is for the Sony HDV video format. This is a pseudo-package, visible files but with +/// a very well-defined layout and naming rules. +/// +/// A typical Sony HDV layout looks like: +/// +/// .../MyMovie/ +/// VIDEO/ +/// HVR/ +/// 00_0001_2007-08-06_165555.IDX +/// 00_0001_2007-08-06_165555.M2T +/// 00_0001_2007-08-06_171740.M2T +/// 00_0001_2007-08-06_171740.M2T.ese +/// tracks.dat +/// +/// The logical clip name can be "00_0001" or "00_0001_" plus anything. We'll find the .IDX file, +/// which defines the existence of the clip. Full file names as input will pull out the camera/clip +/// parts and match in the same way. The .XMP file will use the date/time suffix from the .IDX file. +// ================================================================================================= + +// ================================================================================================= +// SonyHDV_CheckFormat +// =================== +// +// This version does fairly simple checks. The top level folder (.../MyMovie) must contain the +// VIDEO/HVR subtree. The HVR folder must contain a .IDX file for the desired clip. The name checks +// are case insensitive. +// +// The state of the string parameters depends on the form of the path passed by the client. If the +// client passed a logical clip path, like ".../MyMovie/00_0001", the parameters are: +// rootPath - ".../MyMovie" +// gpName - empty +// parentName - empty +// leafName - "00_0001" +// +// If the client passed a full file path, like ".../MyMovie/VIDEO/HVR/00_0001_2007-08-06_165555.M2T", +// they are: +// rootPath - ".../MyMovie" +// gpName - "VIDEO" +// parentName - "HVR" +// leafName - "00_0001_2007-08-06_165555.M2T" +// +// The logical clip name can be short like "00_0001", or long like "00_0001_2007-08-06_165555". We +// only key off of the portion before a second underscore. + +// ! The common code has shifted the gpName, parentName, and leafName strings to upper case. It has +// ! also made sure that for a logical clip path the rootPath is an existing folder, and that the +// ! file exists for a full file path. + +bool SonyHDV_CheckFormat ( XMP_FileFormat format, + const std::string & rootPath, + const std::string & gpName, + const std::string & parentName, + const std::string & leafName, + XMPFiles * parent ) +{ + // Do some basic checks on the root path and component names. + + if ( gpName.empty() != parentName.empty() ) return false; // Must be both empty or both non-empty. + + std::string tempPath = rootPath; + tempPath += kDirChar; + tempPath += "VIDEO"; + + if ( gpName.empty() ) { + // This is the logical clip path case. Look for VIDEO/HVR subtree. + if ( Host_IO::GetChildMode ( tempPath.c_str(), "HVR" ) != Host_IO::kFMode_IsFolder ) return false; + } else { + // This is the existing file case. Check the parent and grandparent names. + if ( (gpName != "VIDEO") || (parentName != "HVR") ) return false; + } + + // Look for the clip's .IDX file. If found use that as the full clip name. + + tempPath += kDirChar; + tempPath += "HVR"; + + std::string clipName = leafName; + +#if 0 + + // Disabled until Sony HDV clip spanning is supported. Since segments of spanned clips are + // currently considered separate entities, information such as frame count needs to be + // considered on a per segment basis. + + int usCount = 0; + size_t i, limit = leafName.size(); + for ( i = 0; i < limit; ++i ) { + if ( clipName[i] == '_' ) { + ++usCount; + if ( usCount == 2 ) break; + } + } + if ( i < limit ) clipName.erase ( i ); + clipName += '_'; // Make sure a final '_' is there for the search comparisons. + + Host_IO::AutoFolder aFolder; + std::string childName; + bool found = false; + + aFolder.folder = Host_IO::OpenFolder ( tempPath.c_str() ); + while ( (! found) && Host_IO::GetNextChild ( aFolder.folder, &childName ) ) { + size_t childLen = childName.size(); + if ( childLen < 4 ) continue; + MakeUpperCase ( &childName ); + if ( childName.compare ( childLen-4, 4, ".IDX" ) != 0 ) continue; + if ( childName.compare ( 0, clipName.size(), clipName ) == 0 ) { + found = true; + clipName = childName; + clipName.erase ( childLen-4 ); + } + } + aFolder.Close(); + if ( ! found ) return false; + +#endif + + tempPath = rootPath; + tempPath += kDirChar; + tempPath += clipName; + + size_t pathLen = tempPath.size() + 1; // Include a terminating nul. + parent->tempPtr = malloc ( pathLen ); + if ( parent->tempPtr == 0 ) XMP_Throw ( "No memory for SonyHDV clip info", kXMPErr_NoMemory ); + memcpy ( parent->tempPtr, tempPath.c_str(), pathLen ); // AUDIT: Safe, allocated above. + + return true; + +} // SonyHDV_CheckFormat + +// ================================================================================================= + +static void* CreatePseudoClipPath ( const std::string & clientPath ) { + + // Used to create the clip pseudo path when the CheckFormat function is skipped. + + std::string pseudoPath = clientPath; + + size_t pathLen; + void* tempPtr = 0; + + if ( Host_IO::Exists ( pseudoPath.c_str() ) ) { + + // The client passed a physical path. The logical clip name is the leaf name, with the + // extension removed. There are no extra suffixes on Sony HDV files. The movie root path ends + // two levels up. + + std::string clipName, ignored; + + XIO::SplitLeafName ( &pseudoPath, &clipName ); // Extract the logical clip name. + XIO::SplitFileExtension ( &clipName, &ignored ); + + XIO::SplitLeafName ( &pseudoPath, &ignored ); // Remove the 2 intermediate folder levels. + XIO::SplitLeafName ( &pseudoPath, &ignored ); + + pseudoPath += kDirChar; + pseudoPath += clipName; + + } + + pathLen = pseudoPath.size() + 1; // Include a terminating nul. + tempPtr = malloc ( pathLen ); + if ( tempPtr == 0 ) XMP_Throw ( "No memory for SonyHDV clip info", kXMPErr_NoMemory ); + memcpy ( tempPtr, pseudoPath.c_str(), pathLen ); + + return tempPtr; + +} // CreatePseudoClipPath + +// ================================================================================================= +// ReadIDXFile +// =========== + +#define ExtractTimeCodeByte(ch,mask) ( (((ch & mask) >> 4) * 10) + (ch & 0xF) ) + +static bool ReadIDXFile ( const std::string& idxPath, + const std::string& clipName, + SXMPMeta* xmpObj, + bool& containsXMP, + MD5_CTX* md5Context, + bool digestFound ) +{ + bool result = true; + containsXMP = false; + + if ( clipName.size() != 25 ) return false; + + try { + + + Host_IO::FileRef hostRef = Host_IO::Open ( idxPath.c_str(), Host_IO::openReadOnly ); + if ( hostRef == Host_IO::noFileRef ) return false; // The open failed. + XMPFiles_IO idxFile ( hostRef, idxPath.c_str(), Host_IO::openReadOnly ); + + struct SHDV_HeaderBlock + { + char mHeader[8]; + unsigned char mValidFlag; + unsigned char mReserved; + unsigned char mECCTB; + unsigned char mSignalMode; + unsigned char mFileThousands; + unsigned char mFileHundreds; + unsigned char mFileTens; + unsigned char mFileUnits; + }; + + SHDV_HeaderBlock hdvHeaderBlock; + memset ( &hdvHeaderBlock, 0, sizeof(SHDV_HeaderBlock) ); + + idxFile.ReadAll ( hdvHeaderBlock.mHeader, 8 ); + idxFile.ReadAll ( &hdvHeaderBlock.mValidFlag, 1 ); + idxFile.ReadAll ( &hdvHeaderBlock.mReserved, 1 ); + idxFile.ReadAll ( &hdvHeaderBlock.mECCTB, 1 ); + idxFile.ReadAll ( &hdvHeaderBlock.mSignalMode, 1 ); + idxFile.ReadAll ( &hdvHeaderBlock.mFileThousands, 1 ); + idxFile.ReadAll ( &hdvHeaderBlock.mFileHundreds, 1 ); + idxFile.ReadAll ( &hdvHeaderBlock.mFileTens, 1 ); + idxFile.ReadAll ( &hdvHeaderBlock.mFileUnits, 1 ); + + const int fileCount = (hdvHeaderBlock.mFileThousands - '0') * 1000 + + (hdvHeaderBlock.mFileHundreds - '0') * 100 + + (hdvHeaderBlock.mFileTens - '0') * 10 + + (hdvHeaderBlock.mFileUnits - '0'); + + // Read file info block. + struct SHDV_FileBlock + { + char mDT[2]; + unsigned char mFileNameYear; + unsigned char mFileNameMonth; + unsigned char mFileNameDay; + unsigned char mFileNameHour; + unsigned char mFileNameMinute; + unsigned char mFileNameSecond; + unsigned char mStartTimeCode[4]; + unsigned char mTotalFrame[4]; + }; + + SHDV_FileBlock hdvFileBlock; + memset ( &hdvFileBlock, 0, sizeof(SHDV_FileBlock) ); + + char filenameBuffer[256]; + std::string fileDateAndTime = clipName.substr(8); + + bool foundFileBlock = false; + + for ( int i=0; ((i < fileCount) && (! foundFileBlock)); ++i ) { + + idxFile.ReadAll ( hdvFileBlock.mDT, 2 ); + idxFile.ReadAll ( &hdvFileBlock.mFileNameYear, 1 ); + idxFile.ReadAll ( &hdvFileBlock.mFileNameMonth, 1 ); + idxFile.ReadAll ( &hdvFileBlock.mFileNameDay, 1 ); + idxFile.ReadAll ( &hdvFileBlock.mFileNameHour, 1 ); + idxFile.ReadAll ( &hdvFileBlock.mFileNameMinute, 1 ); + idxFile.ReadAll ( &hdvFileBlock.mFileNameSecond, 1 ); + idxFile.ReadAll ( &hdvFileBlock.mStartTimeCode, 4 ); + idxFile.ReadAll ( &hdvFileBlock.mTotalFrame, 4 ); + + // Compose file name we expect from file contents and break out on match. + sprintf ( filenameBuffer, "%02d-%02d-%02d_%02d%02d%02d", + hdvFileBlock.mFileNameYear + 2000, + hdvFileBlock.mFileNameMonth, + hdvFileBlock.mFileNameDay, + hdvFileBlock.mFileNameHour, + hdvFileBlock.mFileNameMinute, + hdvFileBlock.mFileNameSecond ); + + foundFileBlock = (fileDateAndTime==filenameBuffer); + + } + + idxFile.Close(); + if ( ! foundFileBlock ) return false; + + // If digest calculation requested, calculate it and return. + if ( md5Context != 0 ) { + MD5Update ( md5Context, (XMP_Uns8*)(&hdvHeaderBlock), sizeof(SHDV_HeaderBlock) ); + MD5Update ( md5Context, (XMP_Uns8*)(&hdvFileBlock), sizeof(SHDV_FileBlock) ); + } + + // The xmpObj parameter must be provided in order to extract XMP + if ( xmpObj == 0 ) return (md5Context != 0); + + // Standard def? + const bool isSD = ((hdvHeaderBlock.mSignalMode == 0x80) || (hdvHeaderBlock.mSignalMode == 0)); + + // Progressive vs interlaced extracted from high bit of ECCTB byte + const bool clipIsProgressive = ((hdvHeaderBlock.mECCTB & 0x80) != 0); + + // Lowest three bits contain frame rate information + const int sfr = (hdvHeaderBlock.mECCTB & 7) + (clipIsProgressive ? 0 : 8); + + // Sample scale and sample size. + int clipSampleScale = 0; + int clipSampleSize = 0; + std::string frameRate; + + // Frame rate + switch ( sfr ) { + case 0 : break; // Not valid in spec, but it's happening in test files. + case 1 : clipSampleScale = 24000; clipSampleSize = 1001; frameRate = "23.98p"; break; + case 3 : clipSampleScale = 25; clipSampleSize = 1; frameRate = "25p"; break; + case 4 : clipSampleScale = 30000; clipSampleSize = 1001; frameRate = "29.97p"; break; + case 11 : clipSampleScale = 25; clipSampleSize = 1; frameRate = "50i"; break; + case 12 : clipSampleScale = 30000; clipSampleSize = 1001; frameRate = "59.94i"; break; + } + + containsXMP = true; + + // Frame size and PAR for HD (not clear on SD yet). + std::string xmpString; + XMP_StringPtr xmpValue = 0; + + if ( ! isSD ) { + + if ( digestFound || (! xmpObj->DoesPropertyExist ( kXMP_NS_DM, "videoFrameSize" )) ) { + + xmpValue = "1440"; + xmpObj->GetStructField ( kXMP_NS_DM, "videoFrameSize", kXMP_NS_DM, "w", &xmpString, 0 ); + if ( xmpString != xmpValue ) { + xmpObj->SetStructField ( kXMP_NS_DM, "videoFrameSize", kXMP_NS_XMP_Dimensions, "w", xmpValue, 0 ); + } + + xmpValue = "1080"; + xmpObj->GetStructField ( kXMP_NS_DM, "videoFrameSize", kXMP_NS_DM, "h", &xmpString, 0 ); + if ( xmpString != xmpValue ) { + xmpObj->SetStructField ( kXMP_NS_DM, "videoFrameSize", kXMP_NS_XMP_Dimensions, "h", xmpValue, 0 ); + } + + xmpValue = "pixels"; + xmpObj->GetStructField ( kXMP_NS_DM, "videoFrameSize", kXMP_NS_DM, "unit", &xmpString, 0 ); + if ( xmpString != xmpValue ) { + xmpObj->SetStructField ( kXMP_NS_DM, "videoFrameSize", kXMP_NS_XMP_Dimensions, "unit", xmpValue, 0 ); + } + } + + xmpValue = "4/3"; + if ( digestFound || (! xmpObj->DoesPropertyExist ( kXMP_NS_DM, "videoPixelAspectRatio" )) ) { + xmpObj->SetProperty ( kXMP_NS_DM, "videoPixelAspectRatio", xmpValue, kXMP_DeleteExisting ); + } + + } + + // Sample size and scale. + if ( clipSampleScale != 0 ) { + + char buffer[255]; + + if ( digestFound || (! xmpObj->DoesPropertyExist ( kXMP_NS_DM, "startTimeScale" )) ) { + sprintf(buffer, "%d", clipSampleScale); + xmpValue = buffer; + xmpObj->SetProperty ( kXMP_NS_DM, "startTimeScale", xmpValue, kXMP_DeleteExisting ); + } + + if ( digestFound || (! xmpObj->DoesPropertyExist ( kXMP_NS_DM, "startTimeSampleSize" )) ) { + sprintf(buffer, "%d", clipSampleSize); + xmpValue = buffer; + xmpObj->SetProperty ( kXMP_NS_DM, "startTimeSampleSize", xmpValue, kXMP_DeleteExisting ); + } + + if ( digestFound || (! xmpObj->DoesPropertyExist ( kXMP_NS_DM, "duration" )) ) { + + const int frameCount = (hdvFileBlock.mTotalFrame[0] << 24) + (hdvFileBlock.mTotalFrame[1] << 16) + + (hdvFileBlock.mTotalFrame[2] << 8) + hdvFileBlock.mTotalFrame[3]; + + sprintf ( buffer, "%d", frameCount ); + xmpValue = buffer; + xmpObj->SetStructField ( kXMP_NS_DM, "duration", kXMP_NS_DM, "value", xmpValue, 0 ); + + sprintf ( buffer, "%d/%d", clipSampleSize, clipSampleScale ); + xmpValue = buffer; + xmpObj->SetStructField ( kXMP_NS_DM, "duration", kXMP_NS_DM, "scale", xmpValue, 0 ); + + } + + } + + // Time Code. + if ( digestFound || (! xmpObj->DoesPropertyExist ( kXMP_NS_DM, "startTimecode" )) ) { + + if ( (clipSampleScale != 0) && (clipSampleSize != 0) ) { + + const bool dropFrame = ( (0x40 & hdvFileBlock.mStartTimeCode[0]) != 0 ) && ( sfr == 4 || sfr == 12 ); + const char chDF = dropFrame ? ';' : ':'; + const int tcFrames = ExtractTimeCodeByte ( hdvFileBlock.mStartTimeCode[0], 0x30 ); + const int tcSeconds = ExtractTimeCodeByte ( hdvFileBlock.mStartTimeCode[1], 0x70 ); + const int tcMinutes = ExtractTimeCodeByte ( hdvFileBlock.mStartTimeCode[2], 0x70 ); + const int tcHours = ExtractTimeCodeByte ( hdvFileBlock.mStartTimeCode[3], 0x30 ); + + // HH:MM:SS:FF or HH;MM;SS;FF + char timecode[256]; + sprintf ( timecode, "%02d%c%02d%c%02d%c%02d", tcHours, chDF, tcMinutes, chDF, tcSeconds, chDF, tcFrames ); + std::string sonyTimeString = timecode; + + xmpObj->GetStructField ( kXMP_NS_DM, "startTimecode", kXMP_NS_DM, "timeValue", &xmpString, 0 ); + if ( xmpString != sonyTimeString ) { + + xmpObj->SetStructField ( kXMP_NS_DM, "startTimecode", kXMP_NS_DM, "timeValue", sonyTimeString, 0 ); + + std::string timeFormat; + if ( clipSampleSize == 1 ) { + + // 24, 25, 40, 50, 60 + switch ( clipSampleScale ) { + case 24 : timeFormat = "24"; break; + case 25 : timeFormat = "25"; break; + case 50 : timeFormat = "50"; break; + default : XMP_Assert ( false ); + } + + timeFormat += "Timecode"; + + } else { + + // 23.976, 29.97, 59.94 + XMP_Assert ( clipSampleSize == 1001 ); + switch ( clipSampleScale ) { + case 24000 : timeFormat = "23976"; break; + case 30000 : timeFormat = "2997"; break; + case 60000 : timeFormat = "5994"; break; + default : XMP_Assert( false ); break; + } + + timeFormat += dropFrame ? "DropTimecode" : "NonDropTimecode"; + + } + + xmpObj->SetStructField ( kXMP_NS_DM, "startTimecode", kXMP_NS_DM, "timeFormat", timeFormat, 0 ); + + } + + } + + } + + if ( digestFound || (! xmpObj->DoesPropertyExist ( kXMP_NS_DM, "CreateDate" )) ) { + + // Clip has date and time in the case of DT (otherwise date and time haven't been set). + bool clipHasDate = ((hdvFileBlock.mDT[0] == 'D') && (hdvFileBlock.mDT[1] == 'T')); + + // Creation date + if ( clipHasDate ) { + + // YYYY-MM-DDThh:mm:ssZ + char date[256]; + sprintf ( date, "%4d-%02d-%02dT%02d:%02d:%02dZ", + hdvFileBlock.mFileNameYear + 2000, + hdvFileBlock.mFileNameMonth, + hdvFileBlock.mFileNameDay, + hdvFileBlock.mFileNameHour, + hdvFileBlock.mFileNameMinute, + hdvFileBlock.mFileNameSecond ); + + XMP_StringPtr xmpDate = date; + xmpObj->SetProperty ( kXMP_NS_XMP, "CreateDate", xmpDate, kXMP_DeleteExisting ); + + } + + } + + // Frame rate. + if ( digestFound || (! xmpObj->DoesPropertyExist ( kXMP_NS_DM, "videoFrameRate" )) ) { + + if ( frameRate.size() != 0 ) { + xmpString = frameRate; + xmpObj->SetProperty ( kXMP_NS_DM, "videoFrameRate", xmpString, kXMP_DeleteExisting ); + } + + } + + } catch ( ... ) { + + result = false; + + } + + return result; + +} // ReadIDXFile + +// ================================================================================================= +// SonyHDV_MetaHandlerCTor +// ======================= + +XMPFileHandler * SonyHDV_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new SonyHDV_MetaHandler ( parent ); + +} // SonyHDV_MetaHandlerCTor + +// ================================================================================================= +// SonyHDV_MetaHandler::SonyHDV_MetaHandler +// ======================================== + +SonyHDV_MetaHandler::SonyHDV_MetaHandler ( XMPFiles * _parent ) +{ + + this->parent = _parent; // Inherited, can't set in the prefix. + this->handlerFlags = kSonyHDV_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + + // Extract the root path and clip name. + + if ( this->parent->tempPtr == 0 ) { + // The CheckFormat call might have been skipped. + this->parent->tempPtr = CreatePseudoClipPath ( this->parent->filePath ); + } + + this->rootPath.assign ( (char*) this->parent->tempPtr ); + free ( this->parent->tempPtr ); + this->parent->tempPtr = 0; + + XIO::SplitLeafName ( &this->rootPath, &this->clipName ); + +} // SonyHDV_MetaHandler::SonyHDV_MetaHandler + +// ================================================================================================= +// SonyHDV_MetaHandler::~SonyHDV_MetaHandler +// ========================================= + +SonyHDV_MetaHandler::~SonyHDV_MetaHandler() +{ + + if ( this->parent->tempPtr != 0 ) { + free ( this->parent->tempPtr ); + this->parent->tempPtr = 0; + } + +} // SonyHDV_MetaHandler::~SonyHDV_MetaHandler + +// ================================================================================================= +// SonyHDV_MetaHandler::MakeClipFilePath +// ===================================== + +bool SonyHDV_MetaHandler::MakeClipFilePath ( std::string * path, XMP_StringPtr suffix, bool checkFile /* = false */ ) +{ + + *path = this->rootPath; + *path += kDirChar; + *path += "VIDEO"; + *path += kDirChar; + *path += "HVR"; + *path += kDirChar; + *path += this->clipName; + *path += suffix; + + if ( ! checkFile ) return true; + return Host_IO::Exists ( path->c_str() ); + +} // SonyHDV_MetaHandler::MakeClipFilePath + +// ================================================================================================= +// SonyHDV_MetaHandler::MakeIndexFilePath +// ====================================== + +bool SonyHDV_MetaHandler::MakeIndexFilePath ( std::string& idxPath, const std::string& rootPath, const std::string& leafName ) +{ + std::string tempPath; + tempPath = rootPath; + tempPath += kDirChar; + tempPath += "VIDEO"; + tempPath += kDirChar; + tempPath += "HVR"; + + idxPath = tempPath; + idxPath += kDirChar; + idxPath += leafName; + idxPath += ".IDX"; + + // Default case + if ( Host_IO::GetFileMode ( idxPath.c_str() ) == Host_IO::kFMode_IsFile ) return true; + + // Spanned clip case + + // Scanning code taken from SonyHDV_CheckFormat + // Can be isolated to a separate function. + + std::string clipName = leafName; + int usCount = 0; + size_t i, limit = leafName.size(); + + for ( i = 0; i < limit; ++i ) { + if ( clipName[i] == '_' ) { + ++usCount; + if ( usCount == 2 ) break; + } + } + + if ( i < limit ) clipName.erase ( i ); + clipName += '_'; // Make sure a final '_' is there for the search comparisons. + + Host_IO::AutoFolder aFolder; + std::string childName; + bool found = false; + + aFolder.folder = Host_IO::OpenFolder ( tempPath.c_str() ); + while ( (! found) && Host_IO::GetNextChild ( aFolder.folder, &childName ) ) { + size_t childLen = childName.size(); + if ( childLen < 4 ) continue; + MakeUpperCase ( &childName ); + if ( childName.compare ( childLen-4, 4, ".IDX" ) != 0 ) continue; + if ( childName.compare ( 0, clipName.size(), clipName ) == 0 ) { + found = true; + clipName = childName; + clipName.erase ( childLen-4 ); + } + } + aFolder.Close(); + if ( ! found ) return false; + + idxPath = tempPath; + idxPath += kDirChar; + idxPath += clipName; + idxPath += ".IDX"; + + return true; + +} + +// ================================================================================================= +// SonyHDV_MetaHandler::MakeLegacyDigest +// ===================================== + +#define kHexDigits "0123456789ABCDEF" + +void SonyHDV_MetaHandler::MakeLegacyDigest ( std::string * digestStr ) +{ + std::string idxPath; + if ( ! this->MakeIndexFilePath ( idxPath, this->rootPath, this->clipName ) ) return; + + MD5_CTX context; + unsigned char digestBin [16]; + bool dummy = false; + MD5Init ( &context ); + ReadIDXFile ( idxPath, this->clipName, 0, dummy, &context, false ); + MD5Final ( digestBin, &context ); + + char buffer [40]; + for ( int in = 0, out = 0; in < 16; in += 1, out += 2 ) { + XMP_Uns8 byte = digestBin[in]; + buffer[out] = kHexDigits [ byte >> 4 ]; + buffer[out+1] = kHexDigits [ byte & 0xF ]; + } + buffer[32] = 0; + digestStr->erase(); + digestStr->append ( buffer, 32 ); + +} // MakeLegacyDigest + +// ================================================================================================= +// SonyHDV_MetaHandler::GetFileModDate +// =================================== + +static inline bool operator< ( const XMP_DateTime & left, const XMP_DateTime & right ) { + int compare = SXMPUtils::CompareDateTime ( left, right ); + return (compare < 0); +} + +bool SonyHDV_MetaHandler::GetFileModDate ( XMP_DateTime * modDate ) +{ + + // The Sony HDV locations of metadata: + // VIDEO/ + // HVR/ + // 00_0001_2007-08-06_165555.IDX + // 00_0001_2007-08-06_165555.XMP + + bool ok, haveDate = false; + std::string fullPath; + XMP_DateTime oneDate, junkDate; + if ( modDate == 0 ) modDate = &junkDate; + + ok = this->MakeIndexFilePath ( fullPath, this->rootPath, this->clipName ); + if ( ok ) ok = Host_IO::GetModifyDate ( fullPath.c_str(), &oneDate ); + if ( ok ) { + if ( (! haveDate) || (*modDate < oneDate) ) *modDate = oneDate; + haveDate = true; + } + + ok = this->MakeClipFilePath ( &fullPath, ".XMP", true /* checkFile */ ); + if ( ok ) ok = Host_IO::GetModifyDate ( fullPath.c_str(), &oneDate ); + if ( ok ) { + if ( (! haveDate) || (*modDate < oneDate) ) *modDate = oneDate; + haveDate = true; + } + + return haveDate; + +} // SonyHDV_MetaHandler::GetFileModDate + +// ================================================================================================= +// SonyHDV_MetaHandler::CacheFileData +// ================================== + +void SonyHDV_MetaHandler::CacheFileData() +{ + XMP_Assert ( ! this->containsXMP ); + + if ( this->parent->UsesClientIO() ) { + XMP_Throw ( "XDCAM cannot be used with client-managed I/O", kXMPErr_InternalFailure ); + } + + // See if the clip's .XMP file exists. + + std::string xmpPath; + this->MakeClipFilePath ( &xmpPath, ".XMP" ); + if ( Host_IO::GetFileMode ( xmpPath.c_str() ) != Host_IO::kFMode_IsFile ) return; // No XMP. + + // Read the entire .XMP file. + + bool readOnly = XMP_OptionIsClear ( this->parent->openFlags, kXMPFiles_OpenForUpdate ); + + XMP_Assert ( this->parent->ioRef == 0 ); + XMPFiles_IO* xmpFile = XMPFiles_IO::New_XMPFiles_IO ( xmpPath.c_str(), readOnly ); + if ( xmpFile == 0 ) return; // The open failed. + this->parent->ioRef = xmpFile; + + XMP_Int64 xmpLen = xmpFile->Length(); + if ( xmpLen > 100*1024*1024 ) { + XMP_Throw ( "SonyHDV XMP is outrageously large", kXMPErr_InternalFailure ); // Sanity check. + } + + this->xmpPacket.erase(); + this->xmpPacket.append ( (size_t)xmpLen, ' ' ); + + XMP_Int32 ioCount = xmpFile->ReadAll ( (void*)this->xmpPacket.data(), (XMP_Int32)xmpLen ); + + this->packetInfo.offset = 0; + this->packetInfo.length = (XMP_Int32)xmpLen; + FillPacketInfo ( this->xmpPacket, &this->packetInfo ); + + this->containsXMP = true; + +} // SonyHDV_MetaHandler::CacheFileData + +// ================================================================================================= +// SonyHDV_MetaHandler::ProcessXMP +// =============================== + +void SonyHDV_MetaHandler::ProcessXMP() +{ + if ( this->processedXMP ) return; + this->processedXMP = true; // Make sure only called once. + + if ( this->containsXMP ) { + this->xmpObj.ParseFromBuffer ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + } + + // Check the legacy digest. + std::string oldDigest, newDigest; + bool digestFound; + digestFound = this->xmpObj.GetStructField ( kXMP_NS_XMP, "NativeDigests", kXMP_NS_XMP, "SonyHDV", &oldDigest, 0 ); + if ( digestFound ) { + this->MakeLegacyDigest ( &newDigest ); + if ( oldDigest == newDigest ) return; + } + + // Read the IDX legacy. + std::string idxPath; + if ( ! this->MakeIndexFilePath ( idxPath, this->rootPath, this->clipName ) ) return; + ReadIDXFile ( idxPath, this->clipName, &this->xmpObj, this->containsXMP, 0, digestFound ); + +} // SonyHDV_MetaHandler::ProcessXMP + +// ================================================================================================= +// SonyHDV_MetaHandler::UpdateFile +// =============================== +// +// Note that UpdateFile is only called from XMPFiles::CloseFile, so it is OK to close the file here. + +void SonyHDV_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + if ( ! this->needsUpdate ) return; + this->needsUpdate = false; // Make sure only called once. + + XMP_Assert ( this->parent->UsesLocalIO() ); + + std::string newDigest; + this->MakeLegacyDigest ( &newDigest ); + this->xmpObj.SetStructField ( kXMP_NS_XMP, "NativeDigests", kXMP_NS_XMP, "SonyHDV", newDigest.c_str(), kXMP_DeleteExisting ); + + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, this->GetSerializeOptions() ); + + // ------------------------------------------------- + // Update just the XMP file not the native IDX file. + + std::string xmpPath; + this->MakeClipFilePath ( &xmpPath, ".XMP" ); + + bool haveXMP = Host_IO::Exists ( xmpPath.c_str() ); + if ( ! haveXMP ) { + XMP_Assert ( this->parent->ioRef == 0 ); + Host_IO::Create ( xmpPath.c_str() ); + this->parent->ioRef = XMPFiles_IO::New_XMPFiles_IO ( xmpPath.c_str(), Host_IO::openReadWrite ); + if ( this->parent->ioRef == 0 ) XMP_Throw ( "Failure opening SonyHDV XMP file", kXMPErr_ExternalFailure ); + } + + XMP_IO* xmpFile = this->parent->ioRef; + XMP_Assert ( xmpFile != 0 ); + XIO::ReplaceTextFile ( xmpFile, this->xmpPacket, (haveXMP & doSafeUpdate) ); + +} // SonyHDV_MetaHandler::UpdateFile + +// ================================================================================================= +// SonyHDV_MetaHandler::WriteTempFile +// ================================== + +void SonyHDV_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + + // ! WriteTempFile is not supposed to be called for handlers that own the file. + XMP_Throw ( "SonyHDV_MetaHandler::WriteTempFile should not be called", kXMPErr_InternalFailure ); + +} // SonyHDV_MetaHandler::WriteTempFile + +// ================================================================================================= diff --git a/XMPFiles/source/FileHandlers/SonyHDV_Handler.hpp b/XMPFiles/source/FileHandlers/SonyHDV_Handler.hpp new file mode 100644 index 0000000..ac27c66 --- /dev/null +++ b/XMPFiles/source/FileHandlers/SonyHDV_Handler.hpp @@ -0,0 +1,79 @@ +#ifndef __SonyHDV_Handler_hpp__ +#define __SonyHDV_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2007 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. + +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +#include "source/ExpatAdapter.hpp" + +// ================================================================================================= +/// \file SonyHDV_Handler.hpp +/// \brief Folder format handler for SonyHDV. +/// +/// This header ... +/// +// ================================================================================================= + +extern XMPFileHandler * SonyHDV_MetaHandlerCTor ( XMPFiles * parent ); + +extern bool SonyHDV_CheckFormat ( XMP_FileFormat format, + const std::string & rootPath, + const std::string & gpName, + const std::string & parentName, + const std::string & leafName, + XMPFiles * parent ); + +static const XMP_OptionBits kSonyHDV_HandlerFlags = (kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_CanRewrite | + kXMPFiles_PrefersInPlace | + kXMPFiles_CanReconcile | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_HandlerOwnsFile | + kXMPFiles_AllowsSafeUpdate | + kXMPFiles_FolderBasedFormat); + +class SonyHDV_MetaHandler : public XMPFileHandler +{ +public: + + bool GetFileModDate ( XMP_DateTime * modDate ); + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + XMP_OptionBits GetSerializeOptions() // *** These should be standard for standalone XMP files. + { return (kXMP_UseCompactFormat | kXMP_OmitPacketWrapper); }; + + SonyHDV_MetaHandler ( XMPFiles * _parent ); + virtual ~SonyHDV_MetaHandler(); + +private: + + SonyHDV_MetaHandler() {}; // Hidden on purpose. + + bool MakeClipFilePath ( std::string * path, XMP_StringPtr suffix, bool checkFile = false ); + bool MakeIndexFilePath ( std::string& idxPath, const std::string& rootPath, const std::string& leafName ); + void MakeLegacyDigest ( std::string * digestStr ); + + std::string rootPath, clipName; + +}; // SonyHDV_MetaHandler + +// ================================================================================================= + +#endif /* __SonyHDV_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/TIFF_Handler.cpp b/XMPFiles/source/FileHandlers/TIFF_Handler.cpp new file mode 100644 index 0000000..109fe43 --- /dev/null +++ b/XMPFiles/source/FileHandlers/TIFF_Handler.cpp @@ -0,0 +1,403 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/TIFF_Handler.hpp" + +#include "XMPFiles/source/FormatSupport/TIFF_Support.hpp" +#include "XMPFiles/source/FormatSupport/PSIR_Support.hpp" +#include "XMPFiles/source/FormatSupport/IPTC_Support.hpp" +#include "XMPFiles/source/FormatSupport/ReconcileLegacy.hpp" +#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp" + +#include "third-party/zuid/interfaces/MD5.h" + +using namespace std; + +// ================================================================================================= +/// \file TIFF_Handler.cpp +/// \brief File format handler for TIFF. +/// +/// This handler ... +/// +// ================================================================================================= + +// ================================================================================================= +// TIFF_CheckFormat +// ================ + +// For TIFF we just check for the II/42 or MM/42 in the first 4 bytes and that there are at least +// 26 bytes of data (4+4+2+12+4). +// +// ! The CheckXyzFormat routines don't track the filePos, that is left to ScanXyzFile. + +bool TIFF_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ) +{ + IgnoreParam(format); IgnoreParam(filePath); IgnoreParam(parent); + XMP_Assert ( format == kXMP_TIFFFile ); + + enum { kMinimalTIFFSize = 4+4+2+12+4 }; // Header plus IFD with 1 entry. + + IOBuffer ioBuf; + + fileRef->Rewind ( ); + if ( ! CheckFileSpace ( fileRef, &ioBuf, kMinimalTIFFSize ) ) return false; + + bool leTIFF = CheckBytes ( ioBuf.ptr, "\x49\x49\x2A\x00", 4 ); + bool beTIFF = CheckBytes ( ioBuf.ptr, "\x4D\x4D\x00\x2A", 4 ); + + return (leTIFF | beTIFF); + +} // TIFF_CheckFormat + +// ================================================================================================= +// TIFF_MetaHandlerCTor +// ==================== + +XMPFileHandler * TIFF_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new TIFF_MetaHandler ( parent ); + +} // TIFF_MetaHandlerCTor + +// ================================================================================================= +// TIFF_MetaHandler::TIFF_MetaHandler +// ================================== + +TIFF_MetaHandler::TIFF_MetaHandler ( XMPFiles * _parent ) : psirMgr(0), iptcMgr(0) +{ + this->parent = _parent; + this->handlerFlags = kTIFF_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + +} // TIFF_MetaHandler::TIFF_MetaHandler + +// ================================================================================================= +// TIFF_MetaHandler::~TIFF_MetaHandler +// =================================== + +TIFF_MetaHandler::~TIFF_MetaHandler() +{ + + if ( this->psirMgr != 0 ) delete ( this->psirMgr ); + if ( this->iptcMgr != 0 ) delete ( this->iptcMgr ); + +} // TIFF_MetaHandler::~TIFF_MetaHandler + +// ================================================================================================= +// TIFF_MetaHandler::CacheFileData +// =============================== +// +// The data caching for TIFF is easy to explain and implement, but does more processing than one +// might at first expect. This seems unavoidable given the need to close the disk file after calling +// CacheFileData. We parse the TIFF stream and cache the values for all tags of interest, and note +// whether XMP is present. We do not parse the XMP, Photoshop image resources, or IPTC datasets. + +// *** This implementation simply returns when invalid TIFF is encountered. Should we throw instead? + +void TIFF_MetaHandler::CacheFileData() +{ + XMP_IO* fileRef = this->parent->ioRef; + XMP_PacketInfo & packetInfo = this->packetInfo; + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + const bool checkAbort = (abortProc != 0); + + XMP_Assert ( ! this->containsXMP ); + // Set containsXMP to true here only if the XMP tag is found. + + if ( checkAbort && abortProc(abortArg) ) { + XMP_Throw ( "TIFF_MetaHandler::CacheFileData - User abort", kXMPErr_UserAbort ); + } + + this->tiffMgr.ParseFileStream ( fileRef ); + + TIFF_Manager::TagInfo dngInfo; + if ( this->tiffMgr.GetTag ( kTIFF_PrimaryIFD, kTIFF_DNGVersion, &dngInfo ) ) { + + // Reject DNG files that are version 2.0 or beyond, this is being written at the time of + // DNG version 1.2. The DNG team says it is OK to use 2.0, not strictly 1.2. Use the + // DNGBackwardVersion if it is present, else the DNGVersion. Note that the version value is + // supposed to be type BYTE, so the file order is always essentially big endian. + + XMP_Uns8 majorVersion = *((XMP_Uns8*)dngInfo.dataPtr); // Start with DNGVersion. + if ( this->tiffMgr.GetTag ( kTIFF_PrimaryIFD, kTIFF_DNGBackwardVersion, &dngInfo ) ) { + majorVersion = *((XMP_Uns8*)dngInfo.dataPtr); // Use DNGBackwardVersion if possible. + } + if ( majorVersion > 1 ) XMP_Throw ( "DNG version beyond 1.x", kXMPErr_BadTIFF ); + + } + + TIFF_Manager::TagInfo xmpInfo; + bool found = this->tiffMgr.GetTag ( kTIFF_PrimaryIFD, kTIFF_XMP, &xmpInfo ); + + if ( found ) { + + this->packetInfo.offset = this->tiffMgr.GetValueOffset ( kTIFF_PrimaryIFD, kTIFF_XMP ); + this->packetInfo.length = xmpInfo.dataLen; + this->packetInfo.padSize = 0; // Assume for now, set these properly in ProcessXMP. + this->packetInfo.charForm = kXMP_CharUnknown; + this->packetInfo.writeable = true; + + this->xmpPacket.assign ( (XMP_StringPtr)xmpInfo.dataPtr, xmpInfo.dataLen ); + + this->containsXMP = true; + + } + +} // TIFF_MetaHandler::CacheFileData + +// ================================================================================================= +// TIFF_MetaHandler::ProcessXMP +// ============================ +// +// Process the raw XMP and legacy metadata that was previously cached. The legacy metadata in TIFF +// is messy because there are 2 copies of the IPTC and because of a Photoshop 6 bug/quirk in the way +// Exif metadata is saved. + +void TIFF_MetaHandler::ProcessXMP() +{ + + this->processedXMP = true; // Make sure we only come through here once. + + // Set up everything for the legacy import, but don't do it yet. This lets us do a forced legacy + // import if the XMP packet gets parsing errors. + + // ! Photoshop 6 wrote annoyingly wacky TIFF files. It buried a lot of the Exif metadata inside + // ! image resource 1058, itself inside of tag 34377 in the 0th IFD. Take care of this before + // ! doing any of the legacy metadata presence or priority analysis. Delete image resource 1058 + // ! to get rid of the buried Exif, but don't mark the XMPFiles object as changed. This change + // ! should not trigger an update, but should be included as part of a normal update. + + bool found; + bool readOnly = ((this->parent->openFlags & kXMPFiles_OpenForUpdate) == 0); + + if ( readOnly ) { + this->psirMgr = new PSIR_MemoryReader(); + this->iptcMgr = new IPTC_Reader(); + } else { + this->psirMgr = new PSIR_FileWriter(); + this->iptcMgr = new IPTC_Writer(); // ! Parse it later. + } + + TIFF_Manager & tiff = this->tiffMgr; // Give the compiler help in recognizing non-aliases. + PSIR_Manager & psir = *this->psirMgr; + IPTC_Manager & iptc = *this->iptcMgr; + + TIFF_Manager::TagInfo psirInfo; + bool havePSIR = tiff.GetTag ( kTIFF_PrimaryIFD, kTIFF_PSIR, &psirInfo ); + + if ( havePSIR ) { // ! Do the Photoshop 6 integration before other legacy analysis. + psir.ParseMemoryResources ( psirInfo.dataPtr, psirInfo.dataLen ); + PSIR_Manager::ImgRsrcInfo buriedExif; + found = psir.GetImgRsrc ( kPSIR_Exif, &buriedExif ); + if ( found ) { + tiff.IntegrateFromPShop6 ( buriedExif.dataPtr, buriedExif.dataLen ); + if ( ! readOnly ) psir.DeleteImgRsrc ( kPSIR_Exif ); + } + } + + TIFF_Manager::TagInfo iptcInfo; + bool haveIPTC = tiff.GetTag ( kTIFF_PrimaryIFD, kTIFF_IPTC, &iptcInfo ); // The TIFF IPTC tag. + int iptcDigestState = kDigestMatches; + + if ( haveIPTC ) { + + bool haveDigest = false; + PSIR_Manager::ImgRsrcInfo digestInfo; + if ( havePSIR ) haveDigest = psir.GetImgRsrc ( kPSIR_IPTCDigest, &digestInfo ); + if ( digestInfo.dataLen != 16 ) haveDigest = false; + + if ( ! haveDigest ) { + + iptcDigestState = kDigestMissing; + + } else { + + // Older versions of Photoshop wrote tag 33723 with type LONG, but ignored the trailing + // zero padding for the IPTC digest. If the full digest differs, recheck without the padding. + + iptcDigestState = PhotoDataUtils::CheckIPTCDigest ( iptcInfo.dataPtr, iptcInfo.dataLen, digestInfo.dataPtr ); + + if ( (iptcDigestState == kDigestDiffers) && (kTIFF_TypeSizes[iptcInfo.type] > 1) ) { + XMP_Uns8 * endPtr = (XMP_Uns8*)iptcInfo.dataPtr + iptcInfo.dataLen - 1; + XMP_Uns8 * minPtr = endPtr - kTIFF_TypeSizes[iptcInfo.type] + 1; + while ( (endPtr >= minPtr) && (*endPtr == 0) ) --endPtr; + XMP_Uns32 unpaddedLen = (XMP_Uns32) (endPtr - (XMP_Uns8*)iptcInfo.dataPtr + 1); + iptcDigestState = PhotoDataUtils::CheckIPTCDigest ( iptcInfo.dataPtr, unpaddedLen, digestInfo.dataPtr ); + } + + } + + } + + XMP_OptionBits options = k2XMP_FileHadExif; // TIFF files are presumed to have Exif legacy. + if ( haveIPTC ) options |= k2XMP_FileHadIPTC; + if ( this->containsXMP ) options |= k2XMP_FileHadXMP; + + // Process the XMP packet. If it fails to parse, do a forced legacy import but still throw an + // exception. This tells the caller that an error happened, but gives them recovered legacy + // should they want to proceed with that. + + bool haveXMP = false; + + if ( ! this->xmpPacket.empty() ) { + XMP_Assert ( this->containsXMP ); + // Common code takes care of packetInfo.charForm, .padSize, and .writeable. + XMP_StringPtr packetStr = this->xmpPacket.c_str(); + XMP_StringLen packetLen = (XMP_StringLen)this->xmpPacket.size(); + try { + this->xmpObj.ParseFromBuffer ( packetStr, packetLen ); + haveXMP = true; + } catch ( ... ) { + XMP_ClearOption ( options, k2XMP_FileHadXMP ); + if ( haveIPTC ) iptc.ParseMemoryDataSets ( iptcInfo.dataPtr, iptcInfo.dataLen ); + if ( iptcDigestState == kDigestMatches ) iptcDigestState = kDigestMissing; + ImportPhotoData ( tiff, iptc, psir, iptcDigestState, &this->xmpObj, options ); + throw; // ! Rethrow the exception, don't absorb it. + } + } + + // Process the legacy metadata. + + if ( haveIPTC && (! haveXMP) && (iptcDigestState == kDigestMatches) ) iptcDigestState = kDigestMissing; + bool parseIPTC = (iptcDigestState != kDigestMatches) || (! readOnly); + if ( parseIPTC ) iptc.ParseMemoryDataSets ( iptcInfo.dataPtr, iptcInfo.dataLen ); + ImportPhotoData ( tiff, iptc, psir, iptcDigestState, &this->xmpObj, options ); + + this->containsXMP = true; // Assume we now have something in the XMP. + +} // TIFF_MetaHandler::ProcessXMP + +// ================================================================================================= +// TIFF_MetaHandler::UpdateFile +// ============================ +// +// There is very little to do directly in UpdateFile. ExportXMPtoJTP takes care of setting all of +// the necessary TIFF tags, including things like the 2nd copy of the IPTC in the Photoshop image +// resources in tag 34377. TIFF_FileWriter::UpdateFileStream does all of the update-by-append I/O. + +// *** Need to pass the abort proc and arg to TIFF_FileWriter::UpdateFileStream. + +void TIFF_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + XMP_Assert ( ! doSafeUpdate ); // This should only be called for "unsafe" updates. + + XMP_IO* destRef = this->parent->ioRef; + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + + XMP_Int64 oldPacketOffset = this->packetInfo.offset; + XMP_Int32 oldPacketLength = this->packetInfo.length; + + if ( oldPacketOffset == kXMPFiles_UnknownOffset ) oldPacketOffset = 0; // ! Simplify checks. + if ( oldPacketLength == kXMPFiles_UnknownLength ) oldPacketLength = 0; + + bool fileHadXMP = ((oldPacketOffset != 0) && (oldPacketLength != 0)); + + // Update the IPTC-IIM and native TIFF/Exif metadata. ExportPhotoData also trips the tiff: and + // exif: copies from the XMP, so reserialize the now final XMP packet. + + ExportPhotoData ( kXMP_TIFFFile, &this->xmpObj, &this->tiffMgr, this->iptcMgr, this->psirMgr ); + + try { + XMP_OptionBits options = kXMP_UseCompactFormat; + if ( fileHadXMP ) options |= kXMP_ExactPacketLength; + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, options, oldPacketLength ); + } catch ( ... ) { + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, kXMP_UseCompactFormat ); + } + + // Decide whether to do an in-place update. This can only happen if all of the following are true: + // - There is an XMP packet in the file. + // - The are no changes to the legacy tags. (The IPTC and PSIR are in the TIFF tags.) + // - The new XMP can fit in the old space. + + bool doInPlace = (fileHadXMP && (this->xmpPacket.size() <= (size_t)oldPacketLength)); + if ( this->tiffMgr.IsLegacyChanged() ) doInPlace = false; + + if ( ! doInPlace ) { + + #if GatherPerformanceData + sAPIPerf->back().extraInfo += ", TIFF append update"; + #endif + + this->tiffMgr.SetTag ( kTIFF_PrimaryIFD, kTIFF_XMP, kTIFF_UndefinedType, (XMP_Uns32)this->xmpPacket.size(), this->xmpPacket.c_str() ); + this->tiffMgr.UpdateFileStream ( destRef ); + + } else { + + #if GatherPerformanceData + sAPIPerf->back().extraInfo += ", TIFF in-place update"; + #endif + + if ( this->xmpPacket.size() < (size_t)this->packetInfo.length ) { + // They ought to match, cheap to be sure. + size_t extraSpace = (size_t)this->packetInfo.length - this->xmpPacket.size(); + this->xmpPacket.append ( extraSpace, ' ' ); + } + + XMP_IO* liveFile = this->parent->ioRef; + + XMP_Assert ( this->xmpPacket.size() == (size_t)oldPacketLength ); // ! Done by common PutXMP logic. + + liveFile->Seek ( oldPacketOffset, kXMP_SeekFromStart ); + liveFile->Write ( this->xmpPacket.c_str(), (XMP_Int32)this->xmpPacket.size() ); + + } + + this->needsUpdate = false; + +} // TIFF_MetaHandler::UpdateFile + +// ================================================================================================= +// TIFF_MetaHandler::WriteTempFile +// =============================== +// +// The structure of TIFF makes it hard to do a sequential source-to-dest copy with interleaved +// updates. So, copy the existing source to the destination and call UpdateFile. + +void TIFF_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + XMP_IO* origRef = this->parent->ioRef; + + XMP_AbortProc abortProc = this->parent->abortProc; + void * abortArg = this->parent->abortArg; + + XMP_Int64 fileLen = origRef->Length(); + if ( fileLen > 0xFFFFFFFFLL ) { // Check before making a copy of the file. + XMP_Throw ( "TIFF fles can't exceed 4GB", kXMPErr_BadTIFF ); + } + + origRef->Rewind ( ); + tempRef->Truncate ( 0 ); + XIO::Copy ( origRef, tempRef, fileLen, abortProc, abortArg ); + + try { + this->parent->ioRef = tempRef; // ! Make UpdateFile update the temp. + this->UpdateFile ( false ); + this->parent->ioRef = origRef; + } catch ( ... ) { + this->parent->ioRef = origRef; + throw; + } + +} // TIFF_MetaHandler::WriteTempFile + +// ================================================================================================= diff --git a/XMPFiles/source/FileHandlers/TIFF_Handler.hpp b/XMPFiles/source/FileHandlers/TIFF_Handler.hpp new file mode 100644 index 0000000..40dc50a --- /dev/null +++ b/XMPFiles/source/FileHandlers/TIFF_Handler.hpp @@ -0,0 +1,68 @@ +#ifndef __TIFF_Handler_hpp__ +#define __TIFF_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "XMPFiles/source/FormatSupport/TIFF_Support.hpp" +#include "XMPFiles/source/FormatSupport/PSIR_Support.hpp" +#include "XMPFiles/source/FormatSupport/IPTC_Support.hpp" + +// ================================================================================================= +/// \file TIFF_Handler.hpp +/// \brief File format handler for TIFF. +/// +/// This header ... +/// +// ================================================================================================= + +// *** Could derive from Basic_Handler - buffer file tail in a temp file. + +extern XMPFileHandler * TIFF_MetaHandlerCTor ( XMPFiles * parent ); + +extern bool TIFF_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ); + +static const XMP_OptionBits kTIFF_HandlerFlags = (kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_CanRewrite | + kXMPFiles_PrefersInPlace | + kXMPFiles_CanReconcile | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_AllowsSafeUpdate); + +class TIFF_MetaHandler : public XMPFileHandler +{ +public: + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + TIFF_MetaHandler ( XMPFiles * parent ); + virtual ~TIFF_MetaHandler(); + +private: + + TIFF_MetaHandler() : psirMgr(0), iptcMgr(0) {}; // Hidden on purpose. + + TIFF_FileWriter tiffMgr; // The TIFF part is always file-based. + PSIR_Manager * psirMgr; // Need to use pointers so we can properly select between read-only and + IPTC_Manager * iptcMgr; // read-write modes of usage. + +}; // TIFF_MetaHandler + +// ================================================================================================= + +#endif /* __TIFF_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/Trivial_Handler.cpp b/XMPFiles/source/FileHandlers/Trivial_Handler.cpp new file mode 100644 index 0000000..ac8b468 --- /dev/null +++ b/XMPFiles/source/FileHandlers/Trivial_Handler.cpp @@ -0,0 +1,74 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/Trivial_Handler.hpp" + +using namespace std; + +// ================================================================================================= +/// \file Trivial_Handler.cpp +/// \brief Base class for trivial handlers that only process in-place XMP. +/// +/// This header ... +/// +// ================================================================================================= + +// ================================================================================================= +// Trivial_MetaHandler::~Trivial_MetaHandler +// ========================================= + +Trivial_MetaHandler::~Trivial_MetaHandler() +{ + // Nothing to do. + +} // Trivial_MetaHandler::~Trivial_MetaHandler + +// ================================================================================================= +// Trivial_MetaHandler::UpdateFile +// =============================== + +void Trivial_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + IgnoreParam ( doSafeUpdate ); + XMP_Assert ( ! doSafeUpdate ); // Not supported at this level. + if ( ! this->needsUpdate ) return; + + XMP_IO* fileRef = this->parent->ioRef; + XMP_PacketInfo & packetInfo = this->packetInfo; + std::string & xmpPacket = this->xmpPacket; + + fileRef->Seek ( packetInfo.offset, kXMP_SeekFromStart ); + fileRef->Write ( xmpPacket.c_str(), packetInfo.length ); + XMP_Assert ( xmpPacket.size() == (size_t)packetInfo.length ); + + this->needsUpdate = false; + +} // Trivial_MetaHandler::UpdateFile + +// ================================================================================================= +// Trivial_MetaHandler::WriteTempFile +// ================================== + +void Trivial_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + IgnoreParam ( tempRef ); + + XMP_Throw ( "Trivial_MetaHandler::WriteTempFile: Not supported", kXMPErr_Unavailable ); + +} // Trivial_MetaHandler::WriteTempFile + +// ================================================================================================= diff --git a/XMPFiles/source/FileHandlers/Trivial_Handler.hpp b/XMPFiles/source/FileHandlers/Trivial_Handler.hpp new file mode 100644 index 0000000..d9e0e15 --- /dev/null +++ b/XMPFiles/source/FileHandlers/Trivial_Handler.hpp @@ -0,0 +1,47 @@ +#ifndef __Trivial_Handler_hpp__ +#define __Trivial_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +// ================================================================================================= +/// \file Trivial_Handler.hpp +/// \brief Base class for trivial handlers that only process in-place XMP. +/// +/// This header ... +/// +/// \note There is no general promise here about crash-safe I/O. An update to an existing file might +/// have invalid partial state while rewriting existing XMP in-place. Crash-safe updates are managed +/// at a higher level of XMPFiles, using a temporary file and final swap of file content. +/// +// ================================================================================================= + +static const XMP_OptionBits kTrivial_HandlerFlags = ( kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_AllowsSafeUpdate ); + +class Trivial_MetaHandler : public XMPFileHandler +{ +public: + + Trivial_MetaHandler() {}; + ~Trivial_MetaHandler(); + + virtual void CacheFileData() = 0; + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + +}; // Trivial_MetaHandler + +// ================================================================================================= + +#endif /* __Trivial_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/UCF_Handler.cpp b/XMPFiles/source/FileHandlers/UCF_Handler.cpp new file mode 100644 index 0000000..c9f63b9 --- /dev/null +++ b/XMPFiles/source/FileHandlers/UCF_Handler.cpp @@ -0,0 +1,856 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2007 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// =============================================================================================== + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/UCF_Handler.hpp" + +#include "third-party/zlib/zlib.h" + +#include <time.h> + +#ifdef DYNAMIC_CRC_TABLE + #error "unexpectedly DYNAMIC_CRC_TABLE defined." + //Must implement get_crc_table prior to any multi-threading (see notes there) +#endif + +#if XMP_WinBuild + #pragma warning ( disable : 4996 ) // '...' was declared deprecated +#endif + +using namespace std; + +// ================================================================================================= +/// \file UCF_Handler.cpp +/// \brief UCF handler class +// ================================================================================================= +const XMP_Uns16 xmpFilenameLen = 21; +const char* xmpFilename = "META-INF/metadata.xml"; + +// ================================================================================================= +// UCF_MetaHandlerCTor +// ==================== +XMPFileHandler* UCF_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new UCF_MetaHandler ( parent ); +} // UCF_MetaHandlerCTor + +// ================================================================================================= +// UCF_CheckFormat +// ================ +// * lenght must at least be 114 bytes +// * first bytes must be \x50\x4B\x03\x04 for *any* zip file +// * at offset 30 it must spell "mimetype" + +#define MIN_UCF_LENGTH 114 +// zip minimum considerations: +// the shortest legal zip is 100 byte: +// 30+1* bytes file header +//+ 0 byte content file (uncompressed) +//+ 46+1* bytes central directory file header +//+ 22 byte end of central directory record +//------- +//100 bytes +// +//1 byte is the shortest legal filename. anything below is no valid zip. +// +//==> the mandatory+first "mimetype" content file has a filename length of 8 bytes, +// thus even if empty (arguably incorrect but tolerable), +// the shortest legal UCF is 114 bytes (30 + 8 + 0 + 46 + 8 + 22 ) +// anything below is with certainty not a valid ucf. + +bool UCF_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ) +{ + // *not* using buffer functionality here, all we need + // to detect UCF securely is in the first 38 bytes... + IgnoreParam(filePath); IgnoreParam(parent); //suppress warnings + XMP_Assert ( format == kXMP_UCFFile ); //standard assert + + XMP_Uns8 buffer[MIN_UCF_LENGTH]; + + fileRef->Rewind(); + if ( MIN_UCF_LENGTH != fileRef->Read ( buffer, MIN_UCF_LENGTH) ) //NO requireall (->no throw), just return false + return false; + if ( !CheckBytes ( &buffer[0], "\x50\x4B\x03\x04", 4 ) ) // "PK 03 04" + return false; + // UCF spec says: there must be a content file mimetype, and be first and be uncompressed... + if ( !CheckBytes ( &buffer[30], "mimetype", 8 ) ) + return false; + + ////////////////////////////////////////////////////////////////////////////// + //figure out mimetype, decide on writeability + // grab mimetype + fileRef->Seek ( 18, kXMP_SeekFromStart ); + XMP_Uns32 mimeLength = XIO::ReadUns32_LE ( fileRef ); + XMP_Uns32 mimeCompressedLength = XIO::ReadUns32_LE ( fileRef ); // must be same since uncompressed + + XMP_Validate( mimeLength == mimeCompressedLength, + "mimetype compressed and uncompressed length differ", + kXMPErr_BadFileFormat ); + + XMP_Validate( mimeLength != 0, "0-byte mimetype", kXMPErr_BadFileFormat ); + + // determine writability based on mimetype + fileRef->Seek ( 30 + 8, kXMP_SeekFromStart ); + char* mimetype = new char[ mimeLength + 1 ]; + fileRef->ReadAll ( mimetype, mimeLength ); + mimetype[mimeLength] = '\0'; + + bool okMimetype; + + // be lenient on extraneous CR (0xA) [non-XMP bug #16980028] + if ( mimeLength > 0 ) //avoid potential crash (will properly fail below anyhow) + if ( mimetype[mimeLength-1] == 0xA ) + mimetype[mimeLength-1] = '\0'; + + if ( + XMP_LitMatch( mimetype, "application/vnd.adobe.xfl" ) || //Flash Diesel team + XMP_LitMatch( mimetype, "application/vnd.adobe.xfl+zip") || //Flash Diesel team + XMP_LitMatch( mimetype, "application/vnd.adobe.x-mars" ) || //Mars plugin(labs only), Acrobat8 + XMP_LitMatch( mimetype, "application/vnd.adobe.pdfxml" ) || //Mars plugin(labs only), Acrobat 9 + XMP_LitMatch( mimetype, "vnd.adobe.x-asnd" ) || //Adobe Sound Document (Soundbooth Team) + XMP_LitMatch( mimetype, "application/vnd.adobe.indesign-idml-package" ) || //inCopy (inDesign) IDML Document + XMP_LitMatch( mimetype, "application/vnd.adobe.incopy-package" ) || // InDesign Document + XMP_LitMatch( mimetype, "application/vnd.adobe.indesign-package" ) || // InDesign Document + + false ) // "sentinel" + + // *** ==> unknown are also treated as not acceptable + okMimetype = true; + else + okMimetype = false; + + // not accepted (neither read nor write + //.air - Adobe Air Files + //application/vnd.adobe.air-application-installer-package+zip + //.airi - temporary Adobe Air Files + //application/vnd.adobe.air-application-intermediate-package+zip + + delete [] mimetype; + return okMimetype; + +} // UCF_CheckFormat + +// ================================================================================================= +// UCF_MetaHandler::UCF_MetaHandler +// ================================== + +UCF_MetaHandler::UCF_MetaHandler ( XMPFiles * _parent ) +{ + this->parent = _parent; + this->handlerFlags = kUCF_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; +} // UCF_MetaHandler::UCF_MetaHandler + +// ================================================================================================= +// UCF_MetaHandler::~UCF_MetaHandler +// ===================================== + +UCF_MetaHandler::~UCF_MetaHandler() +{ + // nothing +} + +// ================================================================================================= +// UCF_MetaHandler::CacheFileData +// =============================== +// +void UCF_MetaHandler::CacheFileData() +{ + //*** abort procedures + this->containsXMP = false; //assume no XMP for now (beware of exceptions...) + XMP_IO* file = this->parent->ioRef; + XMP_PacketInfo &packetInfo = this->packetInfo; + + // clear file positioning info --------------------------------------------------- + b=0;b2=0;x=0;x2=0;cd=0;cd2=0;cdx=0;cdx2=0;h=0;h2=0,fl=0;f2l=0; + al=0;bl=0;xl=0;x2l=0;cdl=0;cd2l=0;cdxl=0;cdx2l=0;hl=0,z=0,z2=0,z2l=0; + numCF=0;numCF2=0; + wasCompressed = false; + + // ------------------------------------------------------------------------------- + fl=file ->Length(); + if ( fl < MIN_UCF_LENGTH ) XMP_Throw("file too short, can't be correct UCF",kXMPErr_Unimplemented); + + ////////////////////////////////////////////////////////////////////////////// + // find central directory before optional comment + // things have to go bottom-up, since description headers are allowed in UCF + // "scan backwards" until feasible field found (plus sig sanity check) + // OS buffering should be smart enough, so not doing anything on top + // plus almost all comments will be zero or rather short + + //no need to check anything but the 21 chars of "METADATA-INF/metadata.xml" + char filenameToTest[22]; + filenameToTest[21]='\0'; + + XMP_Int32 zipCommentLen = 0; + for ( ; zipCommentLen <= EndOfDirectory::COMMENT_MAX; zipCommentLen++ ) + { + file->Seek ( -zipCommentLen -2, kXMP_SeekFromEnd ); + if ( XIO::ReadUns16_LE( file ) == zipCommentLen ) //found it? + { + //double check, might just look like comment length (actually be 'evil' comment) + file ->Seek ( - EndOfDirectory::FIXED_SIZE, kXMP_SeekFromCurrent ); + if ( XIO::ReadUns32_LE( file ) == EndOfDirectory::ID ) break; //heureka, directory ID + // 'else': pretend nothing happended, just go on + } + } + //was it a break or just not found ? + if ( zipCommentLen > EndOfDirectory::COMMENT_MAX ) XMP_Throw( "zip broken near end or invalid comment" , kXMPErr_BadFileFormat ); + + //////////////////////////////////////////////////////////////////////////// + //read central directory + hl = zipCommentLen + EndOfDirectory::FIXED_SIZE; + h = fl - hl; + file ->Seek ( h , kXMP_SeekFromStart ); + + if ( XIO::ReadUns32_LE( file ) != EndOfDirectory::ID ) + XMP_Throw("directory header id not found. or broken comment",kXMPErr_BadFileFormat); + if ( XIO::ReadUns16_LE( file ) != 0 ) + XMP_Throw("UCF must be 'first' zip volume",kXMPErr_BadFileFormat); + if ( XIO::ReadUns16_LE( file ) != 0 ) + XMP_Throw("UCF must be single-volume zip",kXMPErr_BadFileFormat); + + numCF = XIO::ReadUns16_LE( file ); //number of content files + if ( numCF != XIO::ReadUns16_LE( file ) ) + XMP_Throw( "per volume and total number of dirs differ" , kXMPErr_BadFileFormat ); + cdl = XIO::ReadUns32_LE( file ); + cd = XIO::ReadUns32_LE( file ); + file->Seek ( 2, kXMP_SeekFromCurrent ); //skip comment len, needed since next LFA is kXMP_SeekFromCurrent ! + + ////////////////////////////////////////////////////////////////////////////// + // check for zip64-end-of-CD-locator/ zip64-end-of-CD + // to to central directory + if ( cd == 0xffffffff ) + { // deal with zip 64, otherwise continue + XMP_Int64 tmp = file->Seek ( -(EndOfDirectory::FIXED_SIZE + Zip64Locator::TOTAL_SIZE), + kXMP_SeekFromCurrent ); //go to begining of zip64 locator + //relative movement , absolute would imho only require another -zipCommentLen + + if ( Zip64Locator::ID == XIO::ReadUns32_LE(file) ) // prevent 'coincidental length' ffffffff + { + XMP_Validate( 0 == XIO::ReadUns32_LE(file), + "zip64 CD disk must be 0", kXMPErr_BadFileFormat ); + + z = XIO::ReadUns64_LE(file); + XMP_Validate( z < 0xffffffffffffLL, "file in terrabyte range?", kXMPErr_BadFileFormat ); // 3* ffff, sanity test + + XMP_Uns32 totalNumOfDisks = XIO::ReadUns32_LE(file); + /* tolerated while pkglib bug #1742179 */ + XMP_Validate( totalNumOfDisks == 0 || totalNumOfDisks == 1, + "zip64 total num of disks must be 0", kXMPErr_BadFileFormat ); + + /////////////////////////////////////////////// + /// on to end-of-CD itself + file->Seek ( z, kXMP_SeekFromStart ); + XMP_Validate( Zip64EndOfDirectory::ID == XIO::ReadUns32_LE(file), + "invalid zip64 end of CD sig", kXMPErr_BadFileFormat ); + + XMP_Int64 sizeOfZip64EOD = XIO::ReadUns64_LE(file); + file->Seek ( 12, kXMP_SeekFromCurrent ); + //yes twice "total" and "per disk" + XMP_Int64 tmp64 = XIO::ReadUns64_LE(file); + XMP_Validate( tmp64 == numCF, "num of content files differs to zip64 (1)", kXMPErr_BadFileFormat ); + tmp64 = XIO::ReadUns64_LE(file); + XMP_Validate( tmp64 == numCF, "num of content files differs to zip64 (2)", kXMPErr_BadFileFormat ); + // cd length verification + tmp64 = XIO::ReadUns64_LE(file); + XMP_Validate( tmp64 == cdl, "CD length differs in zip64", kXMPErr_BadFileFormat ); + + cd = XIO::ReadUns64_LE(file); // wipe out invalid 0xffffffff with the real thing + //ignoring "extensible data sector (would need fullLength - fixed length) for now + } + } // of zip64 fork + ///////////////////////////////////////////////////////////////////////////// + // parse central directory + // 'foundXMP' <=> cdx != 0 + + file->Seek ( cd, kXMP_SeekFromStart ); + XMP_Int64 cdx_suspect=0; + XMP_Int64 cdxl_suspect=0; + CDFileHeader curCDHeader; + + for ( XMP_Uns16 entryNum=1 ; entryNum <= numCF ; entryNum++ ) + { + cdx_suspect = file->Offset(); //just suspect for now + curCDHeader.read( file ); + + if ( GetUns32LE( &curCDHeader.fields[CDFileHeader::o_sig] ) != 0x02014b50 ) + XMP_Throw("&invalid file header",kXMPErr_BadFileFormat); + + cdxl_suspect = curCDHeader.FIXED_SIZE + + GetUns16LE(&curCDHeader.fields[CDFileHeader::o_fileNameLength]) + + GetUns16LE(&curCDHeader.fields[CDFileHeader::o_extraFieldLength]) + + GetUns16LE(&curCDHeader.fields[CDFileHeader::o_commentLength]); + + // we only look 21 characters, that's META-INF/metadata.xml, no \0 attached + if ( curCDHeader.filenameLen == xmpFilenameLen /*21*/ ) + if( XMP_LitNMatch( curCDHeader.filename , "META-INF/metadata.xml", 21 ) ) + { + cdx = cdx_suspect; + cdxl = cdxl_suspect; + break; + } + //hop to next + file->Seek ( cdx_suspect + cdxl_suspect , kXMP_SeekFromStart ); + } //for-loop, iterating *all* central directory headers (also beyond found) + + if ( !cdx ) // not found xmp + { + // b and bl remain 0, x and xl remain 0 + // ==> a is everything before directory + al = cd; + return; + } + + // from here is if-found-only + ////////////////////////////////////////////////////////////////////////////// + //CD values needed, most serve counter-validation purposes (below) only + // read whole object (incl. all 3 fields) again properly + // to get extra Fields, etc + file->Seek ( cdx, kXMP_SeekFromStart ); + xmpCDHeader.read( file ); + + XMP_Validate( xmpFilenameLen == GetUns16LE( &xmpCDHeader.fields[CDFileHeader::o_fileNameLength]), + "content file length not ok", kXMPErr_BadFileFormat ); + + XMP_Uns16 CD_compression = GetUns16LE( &xmpCDHeader.fields[CDFileHeader::o_compression] ); + XMP_Validate(( CD_compression == 0 || CD_compression == 0x08), + "illegal compression, must be flate or none", kXMPErr_BadFileFormat ); + XMP_Uns16 CD_flags = GetUns16LE( &xmpCDHeader.fields[CDFileHeader::o_flags] ); + XMP_Uns32 CD_crc = GetUns32LE( &xmpCDHeader.fields[CDFileHeader::o_crc32] ); + + // parse (actual, non-CD!) file header //////////////////////////////////////////////// + x = xmpCDHeader.offsetLocalHeader; + file ->Seek ( x , kXMP_SeekFromStart ); + xmpFileHeader.read( file ); + xl = xmpFileHeader.sizeHeader() + xmpCDHeader.sizeCompressed; + + //values needed + XMP_Uns16 fileNameLength = GetUns16LE( &xmpFileHeader.fields[FileHeader::o_fileNameLength] ); + XMP_Uns16 extraFieldLength = GetUns16LE( &xmpFileHeader.fields[FileHeader::o_extraFieldLength] ); + XMP_Uns16 compression = GetUns16LE( &xmpFileHeader.fields[FileHeader::o_compression] ); + XMP_Uns32 sig = GetUns32LE( &xmpFileHeader.fields[FileHeader::o_sig] ); + XMP_Uns16 flags = GetUns16LE( &xmpFileHeader.fields[FileHeader::o_flags] ); + XMP_Uns32 sizeCompressed = GetUns32LE( &xmpFileHeader.fields[FileHeader::o_sizeCompressed] ); + XMP_Uns32 sizeUncompressed = GetUns32LE( &xmpFileHeader.fields[FileHeader::o_sizeUncompressed] ); + XMP_Uns32 crc = GetUns32LE( &xmpFileHeader.fields[FileHeader::o_crc32] ); + + // check filename + XMP_Validate( fileNameLength == 21, "filename size contradiction" , kXMPErr_BadFileFormat ); + XMP_Enforce ( xmpFileHeader.filename != 0 ); + XMP_Validate( !memcmp( "META-INF/metadata.xml", xmpFileHeader.filename , xmpFilenameLen ) , "filename is cf header is not META-INF/metadata.xml" , kXMPErr_BadFileFormat ); + + // deal with data descriptor if needed + if ( flags & FileHeader::kdataDescriptorFlag ) + { + if ( sizeCompressed!=0 || sizeUncompressed!=0 || crc!=0 ) XMP_Throw("data descriptor must mean 3x zero",kXMPErr_BadFileFormat); + file->Seek ( xmpCDHeader.sizeCompressed + fileNameLength + xmpCDHeader.extraFieldLen, kXMP_SeekFromCurrent ); //skip actual data to get to descriptor + crc = XIO::ReadUns32_LE( file ); + if ( crc == 0x08074b50 ) //data descriptor may or may not have signature (see spec) + { + crc = XIO::ReadUns32_LE( file ); //if it does, re-read + } + sizeCompressed = XIO::ReadUns32_LE( file ); + sizeUncompressed = XIO::ReadUns32_LE( file ); + // *** cater for zip64 plus 'streamed' data-descriptor stuff + } + + // more integrity checks (post data descriptor handling) + if ( sig != 0x04034b50 ) XMP_Throw("invalid content file header",kXMPErr_BadFileFormat); + if ( compression != CD_compression ) XMP_Throw("compression contradiction",kXMPErr_BadFileFormat); + if ( sizeUncompressed != xmpCDHeader.sizeUncompressed ) XMP_Throw("contradicting uncompressed lengths",kXMPErr_BadFileFormat); + if ( sizeCompressed != xmpCDHeader.sizeCompressed ) XMP_Throw("contradicting compressed lengths",kXMPErr_BadFileFormat); + if ( sizeUncompressed == 0 ) XMP_Throw("0-byte uncompressed size", kXMPErr_BadFileFormat ); + + //////////////////////////////////////////////////////////////////// + // packet Info + this->packetInfo.charForm = stdCharForm; + this->packetInfo.writeable = false; + this->packetInfo.offset = kXMPFiles_UnknownOffset; // checksum!, hide position to not give funny ideas + this->packetInfo.length = kXMPFiles_UnknownLength; + + //////////////////////////////////////////////////////////////////// + // prepare packet (compressed or not) + this->xmpPacket.erase(); + this->xmpPacket.reserve( sizeUncompressed ); + this->xmpPacket.append( sizeUncompressed, ' ' ); + XMP_StringPtr packetStr = XMP_StringPtr ( xmpPacket.c_str() ); // only set after reserving the space! + + // go to packet offset + file->Seek ( x + xmpFileHeader.FIXED_SIZE + fileNameLength + extraFieldLength , kXMP_SeekFromStart); + + // compression fork -------------------------------------------------- + switch (compression) + { + case 0x8: // FLATE + { + wasCompressed = true; + XMP_Uns32 bytesRead = 0; + XMP_Uns32 bytesWritten = 0; // for writing into packetString + const unsigned int CHUNK = 16384; + + int ret; + unsigned int have; //added type + z_stream strm; + unsigned char in[CHUNK]; + unsigned char out[CHUNK]; + // does need this intermediate stage, no direct compressio to packetStr possible, + // since also partially filled buffers must be picked up. That's how it works. + // in addition: internal zlib variables might have 16 bit limits... + + /* allocate inflate state */ + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.avail_in = 0; + strm.next_in = Z_NULL; + + /* must use windowBits = -15, for raw inflate, no zlib header */ + ret = inflateInit2(&strm,-MAX_WBITS); + + if (ret != Z_OK) + XMP_Throw("zlib error ",kXMPErr_ExternalFailure); + + /* decompress until deflate stream ends or end of file */ + do { + // must take care here not to read too much, thus whichever is smaller: + XMP_Int32 bytesRemaining = sizeCompressed - bytesRead; + if ( (XMP_Int32)CHUNK < bytesRemaining ) bytesRemaining = (XMP_Int32)CHUNK; + strm.avail_in=file ->ReadAll ( in , bytesRemaining ); + bytesRead += strm.avail_in; // NB: avail_in is "unsigned_int", so might be 16 bit (not harmfull) + + if (strm.avail_in == 0) break; + strm.next_in = in; + + do { + strm.avail_out = CHUNK; + strm.next_out = out; + ret = inflate(&strm, Z_NO_FLUSH); + XMP_Assert( ret != Z_STREAM_ERROR ); /* state not clobbered */ + switch (ret) + { + case Z_NEED_DICT: + (void)inflateEnd(&strm); + XMP_Throw("zlib error: Z_NEED_DICT",kXMPErr_ExternalFailure); + case Z_DATA_ERROR: + (void)inflateEnd(&strm); + XMP_Throw("zlib error: Z_DATA_ERROR",kXMPErr_ExternalFailure); + case Z_MEM_ERROR: + (void)inflateEnd(&strm); + XMP_Throw("zlib error: Z_MEM_ERROR",kXMPErr_ExternalFailure); + } + + have = CHUNK - strm.avail_out; + memcpy( (unsigned char*) packetStr + bytesWritten , out , have ); + bytesWritten += have; + + } while (strm.avail_out == 0); + + /* it's done when inflate() says it's done */ + } while (ret != Z_STREAM_END); + + /* clean up and return */ + (void)inflateEnd(&strm); + if (ret != Z_STREAM_END) + XMP_Throw("zlib error ",kXMPErr_ExternalFailure); + break; + } + case 0x0: // no compression - read directly into the right place + { + wasCompressed = false; + XMP_Enforce( file->ReadAll ( (char*)packetStr, sizeUncompressed ) ); + break; + } + default: + { + XMP_Throw("illegal zip compression method (not none, not flate)",kXMPErr_BadFileFormat); + } + } + this->containsXMP = true; // do this last, after all possible failure/execptions +} + + +// ================================================================================================= +// UCF_MetaHandler::ProcessXMP +// ============================ + +void UCF_MetaHandler::ProcessXMP() +{ + // we have no legacy, CacheFileData did all that was needed + // ==> default implementation is fine + XMPFileHandler::ProcessXMP(); +} + +// ================================================================================================= +// UCF_MetaHandler::UpdateFile +// ============================= + +// TODO: xmp packet with data descriptor + +void UCF_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + //sanity + XMP_Enforce( (x!=0) == (cdx!=0) ); + if (!cdx) + xmpCDHeader.setXMPFilename(); //if new, set filename (impacts length, thus before computation) + if ( ! this->needsUpdate ) + return; + + // *** + if ( doSafeUpdate ) XMP_Throw ( "UCF_MetaHandler::UpdateFile: Safe update not supported", kXMPErr_Unavailable ); + + XMP_IO* file = this->parent->ioRef; + + // final may mean compressed or not, whatever is to-be-embedded + uncomprPacketStr = xmpPacket.c_str(); + uncomprPacketLen = (XMP_StringLen) xmpPacket.size(); + finalPacketStr = uncomprPacketStr; // will be overriden if compressedXMP==true + finalPacketLen = uncomprPacketLen; + std::string compressedPacket; // moot if non-compressed, still here for scope reasons (having to keep a .c_str() alive) + + if ( !x ) // if new XMP... + { + xmpFileHeader.clear(); + xmpFileHeader.setXMPFilename(); + // ZIP64 TODO: extra Fields, impact on cdxl2 and x2l + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // COMPRESSION DECISION + + // for large files compression is bad: + // a) size of XMP becomes irrelevant on large files ==> why worry over compression ? + // b) more importantly: no such thing as padding possible, compression == ever changing sizes + // => never in-place rewrites, *ugly* performance impact on large files + inPlacePossible = false; //assume for now + + if ( !x ) // no prior XMP? -> decide on filesize + compressXMP = ( fl > 1024*50 /* 100 kB */ ) ? false : true; + else + compressXMP = wasCompressed; // don't change a thing + + if ( !wasCompressed && !compressXMP && + ( GetUns32LE( &xmpFileHeader.fields[FileHeader::o_sizeUncompressed] ) == uncomprPacketLen )) + { + inPlacePossible = true; + } + //////////////////////////////////////////////////////////////////////////////////////////////// + // COMPRESS XMP + if ( compressXMP ) + { + const unsigned int CHUNK = 16384; + int ret, flush; + unsigned int have; + z_stream strm; + unsigned char out[CHUNK]; + + /* allocate deflate state */ + strm.zalloc = Z_NULL; strm.zfree = Z_NULL; strm.opaque = Z_NULL; + if ( deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8 /*memlevel*/, Z_DEFAULT_STRATEGY) ) + XMP_Throw("zlib error ",kXMPErr_ExternalFailure); + + //write at once, since we got it in mem anyway: + strm.avail_in = uncomprPacketLen; + flush = Z_FINISH; // that's all, folks + strm.next_in = (unsigned char*) uncomprPacketStr; + + do { + strm.avail_out = CHUNK; + strm.next_out = out; + + ret = deflate(&strm, flush); /* no bad return value (!=0 acceptable) */ + XMP_Enforce(ret != Z_STREAM_ERROR); /* state not clobbered */ + //fwrite(buffer,size,count,file) + have = CHUNK - strm.avail_out; + compressedPacket.append( (const char*) out, have); + } while (strm.avail_out == 0); + + if (ret != Z_STREAM_END) + XMP_Throw("zlib stream incomplete ",kXMPErr_ExternalFailure); + XMP_Enforce(strm.avail_in == 0); // all input will be used + (void)deflateEnd(&strm); //clean up (do prior to checks) + + finalPacketStr = compressedPacket.c_str(); + finalPacketLen = (XMP_StringLen)compressedPacket.size(); + } + + PutUns32LE ( uncomprPacketLen, &xmpFileHeader.fields[FileHeader::o_sizeUncompressed] ); + PutUns32LE ( finalPacketLen, &xmpFileHeader.fields[FileHeader::o_sizeCompressed] ); + PutUns16LE ( compressXMP ? 8:0, &xmpFileHeader.fields[FileHeader::o_compression] ); + + //////////////////////////////////////////////////////////////////////////////////////////////// + // CRC (always of uncompressed data) + XMP_Uns32 crc = crc32( 0 , (Bytef*)uncomprPacketStr, uncomprPacketLen ); + PutUns32LE( crc, &xmpFileHeader.fields[FileHeader::o_crc32] ); + + //////////////////////////////////////////////////////////////////////////////////////////////// + // TIME calculation for timestamp + // will be applied both to xmp content file and CD header + XMP_Uns16 lastModTime, lastModDate; + XMP_DateTime time; + SXMPUtils::CurrentDateTime( &time ); + + if ( (time.year - 1900) < 80) + { + lastModTime = 0; // 1.1.1980 00:00h + lastModDate = 21; + } + + // typedef unsigned short ush; //2 bytes + lastModDate = (XMP_Uns16) (((time.year) - 1980 ) << 9 | ((time.month) << 5) | time.day); + lastModTime = ((XMP_Uns16)time.hour << 11) | ((XMP_Uns16)time.minute << 5) | ((XMP_Uns16)time.second >> 1); + + PutUns16LE ( lastModDate, &xmpFileHeader.fields[FileHeader::o_lastmodDate] ); + PutUns16LE ( lastModTime, &xmpFileHeader.fields[FileHeader::o_lastmodTime] ); + + //////////////////////////////////////////////////////////////////////////////////////////////// + // adjustments depending on 4GB Border, + // decisions on in-place update + // so far only z, zl have been determined + + // Zip64 related assurances, see (15) + XMP_Enforce(!z2); + XMP_Enforce(h+hl == fl ); + + //////////////////////////////////////////////////////////////////////////////////////////////// + // COMPUTE MISSING VARIABLES + // A - based on xmp existence + // + // already known: x, xl, cd + // most left side vars, + // + // finalPacketStr, finalPacketLen + + if ( x ) // previous xmp? + { + al = x; + b = x + xl; + bl = cd - b; + } + else + { + al = cd; + //b,bl left at zero + } + + if ( inPlacePossible ) + { // leave xmp right after A + x2 = al; + x2l = xmpFileHeader.sizeTotalCF(); //COULDDO: assert (x2l == xl) + if (b) b2 = x2 + x2l; // b follows x as last content part + cd2 = b2 + bl; // CD follows B2 + } + else + { // move xmp to end + if (b) b2 = al; // b follows + // x follows as last content part (B existing or not) + x2 = al + bl; + x2l = xmpFileHeader.sizeTotalCF(); + cd2 = x2 + x2l; // CD follows X + } + + /// create new XMP header /////////////////////////////////////////////////// + // written into actual fields + generation of extraField at .write()-time... + // however has impact on .size() computation -- thus enter before cdx2l computation + xmpCDHeader.sizeUncompressed = uncomprPacketLen; + xmpCDHeader.sizeCompressed = finalPacketLen; + xmpCDHeader.offsetLocalHeader = x2; + PutUns32LE ( crc, &xmpCDHeader.fields[CDFileHeader::o_crc32] ); + PutUns16LE ( compressXMP ? 8:0, &xmpCDHeader.fields[CDFileHeader::o_compression] ); + PutUns16LE ( lastModDate, &xmpCDHeader.fields[CDFileHeader::o_lastmodDate] ); + PutUns16LE ( lastModTime, &xmpCDHeader.fields[CDFileHeader::o_lastmodTime] ); + + // for + if ( inPlacePossible ) + { + cdx2 = cdx; //same, same + writeOut( file, file, false, true ); + return; + } + + //////////////////////////////////////////////////////////////////////// + // temporarily store (those few, small) trailing things that might not survive the move around: + file->Seek ( cd, kXMP_SeekFromStart ); // seek to central directory + cdEntries.clear(); //mac precaution + + ////////////////////////////////////////////////////////////////////////////// + // parse headers + // * stick together output header list + cd2l = 0; //sum up below + + CDFileHeader tempHeader; + for( XMP_Uns16 pos=1 ; pos <= numCF ; pos++ ) + { + if ( (cdx) && (file->Offset() == cdx) ) + { + tempHeader.read( file ); //read, even if not use, to advance file pointer + } + else + { + tempHeader.read( file ); + // adjust b2 offset for files that were behind the xmp: + // may (if xmp moved to back) + // or may not (inPlace Update) make a difference + if ( (x) && ( tempHeader.offsetLocalHeader > x) ) // if xmp existed before and this was a file behind it + tempHeader.offsetLocalHeader += b2 - b; + cd2l += tempHeader.size(); // prior offset change might have impact + cdEntries.push_back( tempHeader ); + } + } + + //push in XMP packet as last one (new or not) + cdEntries.push_back( xmpCDHeader ); + cdx2l = xmpCDHeader.size(); + cd2l += cdx2l; // true, no matter which order + + //OLD cd2l = : cdl - cdxl + cdx2l; // (NB: cdxl might be 0) + numCF2 = numCF + ( (cdx)?0:1 ); //xmp packet for the first time? -> add one more CF + + XMP_Validate( numCF2 > 0, "no content files", kXMPErr_BadFileFormat ); + XMP_Validate( numCF2 <= 0xFFFE, "max number of 0xFFFE entries reached", kXMPErr_BadFileFormat ); + + cdx2 = cd2 + cd2l - cdx2l; // xmp content entry comes last (since beyond inPlace Update) + + // zip64 decision + if ( ( cd2 + cd2l + hl ) > 0xffffffff ) // predict non-zip size ==> do we need a zip-64? + { + z2 = cd2 + cd2l; + z2l = Zip64EndOfDirectory::FIXED_SIZE + Zip64Locator::TOTAL_SIZE; + } + + // header and output length, + h2 = cd2 + cd2l + z2l; // (z2l might be 0) + f2l = h2 + hl; + + //////////////////////////////////////////////////////////////////////////////////////////////// + // read H (endOfCD), correct offset + file->Seek ( h, kXMP_SeekFromStart ); + + endOfCD.read( file ); + if ( cd2 <= 0xffffffff ) + PutUns32LE( (XMP_Int32) cd2 , &endOfCD.fields[ endOfCD.o_CdOffset ] ); + else + PutUns32LE( 0xffffffff , &endOfCD.fields[ endOfCD.o_CdOffset ] ); + PutUns16LE( numCF2, &endOfCD.fields[ endOfCD.o_CdNumEntriesDisk ] ); + PutUns16LE( numCF2, &endOfCD.fields[ endOfCD.o_CdNumEntriesTotal ] ); + + XMP_Enforce( cd2l <= 0xffffffff ); // _size_ of directory itself certainly under 4GB + PutUns32LE( (XMP_Uns32)cd2l, &endOfCD.fields[ endOfCD.o_CdSize ] ); + + //////////////////////////////////////////////////////////////////////////////////////////////// + // MOVING + writeOut( file, file, false, false ); + + this->needsUpdate = false; //do last for safety reasons +} // UCF_MetaHandler::UpdateFile + +// ================================================================================================= +// UCF_MetaHandler::WriteTempFile +// ============================== +void UCF_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + IgnoreParam ( tempRef ); + XMP_Throw ( "UCF_MetaHandler::WriteTempFile: TO BE IMPLEMENTED", kXMPErr_Unimplemented ); +} + +// ================================================================================================= +// own approach to unify Update and WriteFile: +// ============================ + +void UCF_MetaHandler::writeOut( XMP_IO* sourceFile, XMP_IO* targetFile, bool isRewrite, bool isInPlace) +{ + // isInPlace is only possible when it's not a complete rewrite + XMP_Enforce( (!isInPlace) || (!isRewrite) ); + + ///////////////////////////////////////////////////////// + // A + if (isRewrite) //move over A block + XIO::Move( sourceFile , 0 , targetFile, 0 , al ); + + ///////////////////////////////////////////////////////// + // B / X (not necessarily in this order) + if ( !isInPlace ) // B does not change a thing (important optimization) + { + targetFile ->Seek ( b2 , kXMP_SeekFromStart ); + XIO::Move( sourceFile , b , targetFile, b2 , bl ); + } + + targetFile ->Seek ( x2 , kXMP_SeekFromStart ); + xmpFileHeader.write( targetFile ); + targetFile->Write ( finalPacketStr, finalPacketLen ); + //TODO: cover reverse case / inplace ... + + ///////////////////////////////////////////////////////// + // CD + // No Seek here on purpose. + // This assert must still be valid + + // if inPlace, the only thing that needs still correction is the CRC in CDX: + if ( isInPlace ) + { + XMP_Uns32 crc; //TEMP, not actually needed + crc = GetUns32LE( &xmpFileHeader.fields[FileHeader::o_crc32] ); + + // go there, + // do the job (take value directly from (non-CD-)fileheader), + // end of story. + targetFile ->Seek ( cdx2 + CDFileHeader::o_crc32 , kXMP_SeekFromStart ); + targetFile->Write ( &xmpFileHeader.fields[FileHeader::o_crc32], 4 ); + + return; + } + + targetFile ->Seek ( cd2 , kXMP_SeekFromStart ); + + std::vector<CDFileHeader>::iterator iter; + int tmptmp=1; + for( iter = cdEntries.begin(); iter != cdEntries.end(); iter++ ) { + CDFileHeader* p=&(*iter); + XMP_Int64 before = targetFile->Offset(); + p->write( targetFile ); + XMP_Int64 total = targetFile->Offset() - before; + XMP_Int64 tmpSize = p->size(); + tmptmp++; + } + + ///////////////////////////////////////////////////////// + // Z + if ( z2 ) // yes, that simple + { + XMP_Assert( z2 == targetFile->Offset()); + targetFile ->Seek ( z2 , kXMP_SeekFromStart ); + + //no use in copying, always construct from scratch + Zip64EndOfDirectory zip64EndOfDirectory( cd2, cd2l, numCF2) ; + Zip64Locator zip64Locator( z2 ); + + zip64EndOfDirectory.write( targetFile ); + zip64Locator.write( targetFile ); + } + + ///////////////////////////////////////////////////////// + // H + XMP_Assert( h2 == targetFile->Offset()); + endOfCD.write( targetFile ); + + XMP_Assert( f2l == targetFile->Offset()); + if ( f2l< fl) + targetFile->Truncate ( f2l ); //file may have shrunk +} diff --git a/XMPFiles/source/FileHandlers/UCF_Handler.hpp b/XMPFiles/source/FileHandlers/UCF_Handler.hpp new file mode 100644 index 0000000..a9b9b55 --- /dev/null +++ b/XMPFiles/source/FileHandlers/UCF_Handler.hpp @@ -0,0 +1,722 @@ +#ifndef __UCF_Handler_hpp__ +#define __UCF_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2007 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" + +// ================================================================================================= +/// \file UCF_Handler.hpp +// +// underlying math: +// __ 0 ______ 0 ______ __ +// | A | | A | +// | | | | +// al | | (a2l)| | +// x |------| b2 |------| +// xl | X | | B |_ +// b |------| (b2l)| | | +// | B | x2 |------| | B2 could also be +// bl | | x2l | X2 | | _after_ X2 +// cd |------| cd2 |------|<' +// |//CD//| |//CD2/| +// cdx |------| cdx2 |------| +// cdxl|------| cdx2l|------| +// cdl|//////| cd2l|//////| +// z |------| z2|------| +// | | | | +// [zl]| Z | z2l | Z2 | +// h |------| h2 |------| +// fl | H | | H2 | f2l +// __ hl |______| (h2l)|______| __ +// +// fl file length pre (2 = post) +// numCf number of content files prior to injection +// numCf2 " post +// +// +// l length variable, all else offset +// [ ] variable is not needed +// ( ) variable is identical to left +// a content files prior to xmp (possibly: all) +// b content files behind xmp (possibly: 0) +// x xmp packet (possibly: 0) +// cd central directory +// h end of central directory +// +// z zip64 record and locator (if existing) +// +// general rules: +// the bigger A, the less rewrite effort. +// (also within the CD) +// putting XMP at the end maximizes A. +// +// bool previousXMP == x!=0 +// +// (x==0) == (cdx==0) == (xl==0) == (cdxl==0) +// +// std::vector<XMP_Uns32> cdOffsetsPre; +// +// ----------------- +// asserts: +//( 1) a == a2 == 0, making these variables obsolete +//( 2) a2l == al, this block is not touched +//( 3) b2 <= b, b is only moved closer to the beginning of file +//( 4) b2l == bl, b does not change in size +//( 5) x2 >= x, b is only moved further down in the file +// +//( 6) x != 0, x2l != 0, cd != 0, cdl != 0 +// none of these blocks is at the beginning ('mimetype' by spec), +// nor is any of them zero byte long +//( 7) h!=0, hl >= 22 header is not at the beginning, minimum size 22 +// +// file size computation: +//( 8) al + bl + xl +cdl +hl = fl +//( 9) al + bl + x2l+cd2l+hl = fl2 +// +//(10) ( x==0 ) <=> ( cdx == 0 ) +// if there's a packet in the pre-file, or there isn't +//(11) (x==0) => xl=0 +//(12) (cdx==0)=> cdx=0 +// +//(13) x==0 ==> b,bl,b2,b2l==0 +// if there is no pre-xmp, B does not exist +//(14) x!=0 ==> al:=x, b:=x+xl, bl:=cd-b +// +// zip 64: +//(15) zl and z2l are basically equal, except _one_ of them is 0 : +// +//(16) b2l is indeed never different t +// +// FIXED_SIZE means the fixed (minimal) portion of a struct +// TOTAL_SIZE indicates, that this struct indeed has a fixed, known total length +// +// ================================================================================================= + +extern XMPFileHandler* UCF_MetaHandlerCTor ( XMPFiles * parent ); + +extern bool UCF_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ); + +static const XMP_OptionBits kUCF_HandlerFlags = ( + kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_CanRewrite | + /* kXMPFiles_PrefersInPlace | removed, only reasonable for formats where difference is significant */ + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + // *** kXMPFiles_AllowsSafeUpdate | + kXMPFiles_NeedsReadOnlyPacket //UCF/zip has checksums... + ); + +enum { // data descriptor + // may or may not have a signature: 0x08074b50 + kUCF_DD_crc32 = 0, + kUCF_DD_sizeCompressed = 4, + kUCF_DD_sizeUncompressed = 8, +}; + +class UCF_MetaHandler : public XMPFileHandler +{ +public: + UCF_MetaHandler ( XMPFiles * _parent ); + ~UCF_MetaHandler(); + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + +protected: + const static XMP_Uns16 xmpFilenameLen = 21; + const static char* xmpFilename; + +private: + class Zip64EndOfDirectory { + private: + + public: + const static XMP_Uns16 o_sig = 0; // 0x06064b50 + const static XMP_Uns16 o_size = 4; // of this, excluding leading 12 bytes + // == FIXED_SIZE -12, since we're never creating the extensible data sector... + const static XMP_Uns16 o_VersionMade = 12; + const static XMP_Uns16 o_VersionNeededExtr = 14; + const static XMP_Uns16 o_numDisk = 16; // force 0 + const static XMP_Uns16 o_numCDDisk = 20; // force 0 + const static XMP_Uns16 o_numCFsThisDisk = 24; + const static XMP_Uns16 o_numCFsTotal = 32; // force equal + const static XMP_Uns16 o_sizeOfCD = 40; // (regular one, not Z64) + const static XMP_Uns16 o_offsetCD = 48; // " + + const static XMP_Int32 FIXED_SIZE = 56; + char fields[FIXED_SIZE]; + + const static XMP_Uns32 ID = 0x06064b50; + + Zip64EndOfDirectory( XMP_Int64 offsetCD, XMP_Int64 sizeOfCD, XMP_Uns64 numCFs ) + { + memset(fields,'\0',FIXED_SIZE); + + PutUns32LE(ID ,&fields[o_sig] ); + PutUns64LE(FIXED_SIZE - 12, &fields[o_size] ); //see above + PutUns16LE( 45 ,&fields[o_VersionMade] ); + PutUns16LE( 45 ,&fields[o_VersionNeededExtr] ); + // fine at 0: o_numDisk + // fine at 0: o_numCDDisk + PutUns64LE( numCFs, &fields[o_numCFsThisDisk] ); + PutUns64LE( numCFs, &fields[o_numCFsTotal] ); + PutUns64LE( sizeOfCD, &fields[o_sizeOfCD] ); + PutUns64LE( offsetCD, &fields[o_offsetCD] ); + } + + void write(XMP_IO* file) + { + XMP_Validate( ID == GetUns32LE( &this->fields[o_sig] ), "invalid header on write", kXMPErr_BadFileFormat ); + file ->Write ( fields , FIXED_SIZE ); + } + + }; + + class Zip64Locator { + public: + const static XMP_Uns16 o_sig = 0; // 0x07064b50 + const static XMP_Uns16 o_numDiskZ64CD = 4; // force 0 + const static XMP_Uns16 o_offsZ64EOD = 8; + const static XMP_Uns16 o_numDisks = 16; // set 1, tolerate 0 + + const static XMP_Int32 TOTAL_SIZE = 20; + char fields[TOTAL_SIZE]; + + const static XMP_Uns32 ID = 0x07064b50; + + Zip64Locator( XMP_Int64 offsetZ64EOD ) + { + memset(fields,'\0',TOTAL_SIZE); + PutUns32LE(ID, &fields[Zip64Locator::o_sig] ); + PutUns32LE(0, &fields[Zip64Locator::o_numDiskZ64CD] ); + PutUns64LE(offsetZ64EOD, &fields[Zip64Locator::o_offsZ64EOD] ); + PutUns32LE(1, &fields[Zip64Locator::o_numDisks] ); + } + + // writes structure to file (starting at current position) + void write(XMP_IO* file) + { + XMP_Validate( ID == GetUns32LE( &this->fields[o_sig] ), "invalid header on write", kXMPErr_BadFileFormat ); + file ->Write ( fields , TOTAL_SIZE ); + } + }; + + struct EndOfDirectory { + public: + const static XMP_Int32 FIXED_SIZE = 22; //32 bit type is important to not overrun on maxcomment + const static XMP_Uns32 ID = 0x06054b50; + const static XMP_Int32 COMMENT_MAX = 0xFFFF; + //offsets + const static XMP_Int32 o_CentralDirectorySize = 12; + const static XMP_Int32 o_CentralDirectoryOffset = 16; + }; + + class FileHeader { + private: + //TODO intergrate in clear() + void release() // avoid terminus free() since subject to a #define (mem-leak-check) + { + if (filename) delete filename; + if (extraField) delete extraField; + filename=0; + extraField=0; + } + + public: + const static XMP_Uns32 SIG = 0x04034b50; + const static XMP_Uns16 kdataDescriptorFlag = 0x8; + + const static XMP_Uns16 o_sig = 0; + const static XMP_Uns16 o_extractVersion = 4; + const static XMP_Uns16 o_flags = 6; + const static XMP_Uns16 o_compression = 8; + const static XMP_Uns16 o_lastmodTime = 10; + const static XMP_Uns16 o_lastmodDate = 12; + const static XMP_Uns16 o_crc32 = 14; + const static XMP_Uns16 o_sizeCompressed = 18; + const static XMP_Uns16 o_sizeUncompressed = 22; + const static XMP_Uns16 o_fileNameLength = 26; + const static XMP_Uns16 o_extraFieldLength = 28; + // total 30 + + const static int FIXED_SIZE = 30; + char fields[FIXED_SIZE]; + + char* filename; + char* extraField; + XMP_Uns16 filenameLen; + XMP_Uns16 extraFieldLen; + + void clear() + { + this->release(); + memset(fields,'\0',FIXED_SIZE); + //arm with minimal default values: + PutUns32LE(0x04034b50, &fields[FileHeader::o_sig] ); + PutUns16LE(0x14, &fields[FileHeader::o_extractVersion] ); + } + + FileHeader() : filename(0),filenameLen(0),extraField(0),extraFieldLen(0) + { + clear(); + }; + + // reads entire *FileHeader* structure from file (starting at current position) + void read(XMP_IO* file) + { + this->release(); + + file ->ReadAll ( fields , FIXED_SIZE ); + + XMP_Uns32 tmp32 = GetUns32LE( &this->fields[FileHeader::o_sig] ); + XMP_Validate( SIG == tmp32, "invalid header", kXMPErr_BadFileFormat ); + filenameLen = GetUns16LE( &this->fields[FileHeader::o_fileNameLength] ); + extraFieldLen = GetUns16LE( &this->fields[FileHeader::o_extraFieldLength] ); + + // nb unlike the CDFileHeader the FileHeader will in practice never have + // extra fields. Reasoning: File headers never carry (their own) offsets, + // (un)compressed size of XMP will hardly ever reach 4 GB + + if (filenameLen) { + filename = new char[filenameLen]; + file->ReadAll ( filename, filenameLen ); + } + if (extraFieldLen) { + extraField = new char[extraFieldLen]; + file->ReadAll ( extraField, extraFieldLen ); + // *** NB: this WOULD need parsing for content files that are + // compressed or uncompressed >4GB (VERY unlikely for XMP) + } + } + + // writes structure to file (starting at current position) + void write(XMP_IO* file) + { + XMP_Validate( SIG == GetUns32LE( &this->fields[FileHeader::o_sig] ), "invalid header on write", kXMPErr_BadFileFormat ); + + filenameLen = GetUns16LE( &this->fields[FileHeader::o_fileNameLength] ); + extraFieldLen = GetUns16LE( &this->fields[FileHeader::o_extraFieldLength] ); + + file ->Write ( fields , FIXED_SIZE ); + if (filenameLen) file->Write ( filename, filenameLen ); + if (extraFieldLen) file->Write ( extraField, extraFieldLen ); + } + + void transfer(const FileHeader &orig) + { + memcpy(fields,orig.fields,FIXED_SIZE); + if (orig.extraField) + { + extraFieldLen=orig.extraFieldLen; + extraField = new char[extraFieldLen]; + memcpy(extraField,orig.extraField,extraFieldLen); + } + if (orig.filename) + { + filenameLen=orig.filenameLen; + filename = new char[filenameLen]; + memcpy(filename,orig.filename,filenameLen); + } + }; + + void setXMPFilename() + { + // only needed for fresh structs, thus enforcing rather than catering to memory issues + XMP_Enforce( (filenameLen==0) && (extraFieldLen == 0) ); + filenameLen = xmpFilenameLen; + PutUns16LE(filenameLen, &fields[FileHeader::o_fileNameLength] ); + filename = new char[xmpFilenameLen]; + memcpy(filename,"META-INF/metadata.xml",xmpFilenameLen); + } + + XMP_Uns32 sizeHeader() + { + return this->FIXED_SIZE + this->filenameLen + this->extraFieldLen; + } + + XMP_Uns32 sizeTotalCF() + { + //*** not zip64 bit safe yet, use only for non-large xmp packet + return this->sizeHeader() + GetUns32LE( &fields[FileHeader::o_sizeCompressed] ); + } + + ~FileHeader() + { + this->release(); + }; + + }; //class FileHeader + + ////// yes, this needs an own class + ////// offsets must be extracted, added, modified, + ////// come&go depending on being >0xffffff + ////class extraField { + //// private: + + + class CDFileHeader { + private: + void release() //*** needed or can go? + { + if (filename) delete filename; + if (extraField) delete extraField; + if (comment) delete comment; + filename=0; filenameLen=0; + extraField=0; extraFieldLen=0; + comment=0; commentLen=0; + } + + const static XMP_Uns32 SIG = 0x02014b50; + + public: + const static XMP_Uns16 o_sig = 0; //0x02014b50 + const static XMP_Uns16 o_versionMadeBy = 4; + const static XMP_Uns16 o_extractVersion = 6; + const static XMP_Uns16 o_flags = 8; + const static XMP_Uns16 o_compression = 10; + const static XMP_Uns16 o_lastmodTime = 12; + const static XMP_Uns16 o_lastmodDate = 14; + const static XMP_Uns16 o_crc32 = 16; + const static XMP_Uns16 o_sizeCompressed = 20; // 16bit stub + const static XMP_Uns16 o_sizeUncompressed = 24; // 16bit stub + const static XMP_Uns16 o_fileNameLength = 28; + const static XMP_Uns16 o_extraFieldLength = 30; + const static XMP_Uns16 o_commentLength = 32; + const static XMP_Uns16 o_diskNo = 34; + const static XMP_Uns16 o_internalAttribs = 36; + const static XMP_Uns16 o_externalAttribs = 38; + const static XMP_Uns16 o_offsetLocalHeader = 42; // 16bit stub + // total size is 4+12+12+10+8=46 + + const static int FIXED_SIZE = 46; + char fields[FIXED_SIZE]; + + // do not bet on any zero-freeness, + // certainly no zero termination (pascal strings), + // treat as data blocks + char* filename; + char* extraField; + char* comment; + XMP_Uns16 filenameLen; + XMP_Uns16 extraFieldLen; + XMP_Uns16 commentLen; + + // full, real, parsed 64 bit values + XMP_Int64 sizeUncompressed; + XMP_Int64 sizeCompressed; + XMP_Int64 offsetLocalHeader; + + CDFileHeader() : filename(0),extraField(0),comment(0),filenameLen(0), + extraFieldLen(0),commentLen(0),sizeUncompressed(0),sizeCompressed(0),offsetLocalHeader(0) + { + memset(fields,'\0',FIXED_SIZE); + //already arm with appropriate values where applicable: + PutUns32LE(0x02014b50, &fields[CDFileHeader::o_sig] ); + PutUns16LE(0x14, &fields[CDFileHeader::o_extractVersion] ); + }; + + // copy constructor + CDFileHeader(const CDFileHeader& orig) : filename(0),extraField(0),comment(0),filenameLen(0), + extraFieldLen(0),commentLen(0),sizeUncompressed(0),sizeCompressed(0),offsetLocalHeader(0) + { + memcpy(fields,orig.fields,FIXED_SIZE); + if (orig.extraField) + { + extraFieldLen=orig.extraFieldLen; + extraField = new char[extraFieldLen]; + memcpy(extraField , orig.extraField , extraFieldLen); + } + if (orig.filename) + { + filenameLen=orig.filenameLen; + filename = new char[filenameLen]; + memcpy(filename , orig.filename , filenameLen); + } + if (orig.comment) + { + commentLen=orig.commentLen; + comment = new char[commentLen]; + memcpy(comment , orig.comment , commentLen); + } + + filenameLen = orig.filenameLen; + extraFieldLen = orig.extraFieldLen; + commentLen = orig.commentLen; + + sizeUncompressed = orig.sizeUncompressed; + sizeCompressed = orig.sizeCompressed; + offsetLocalHeader = orig.offsetLocalHeader; + } + + // Assignment operator + CDFileHeader& operator=(const CDFileHeader& obj) + { + XMP_Throw("not supported",kXMPErr_Unimplemented); + } + + // reads entire structure from file (starting at current position) + void read(XMP_IO* file) + { + this->release(); + + file->ReadAll ( fields, FIXED_SIZE ); + XMP_Validate( SIG == GetUns32LE( &this->fields[CDFileHeader::o_sig] ), "invalid header", kXMPErr_BadFileFormat ); + + filenameLen = GetUns16LE( &this->fields[CDFileHeader::o_fileNameLength] ); + extraFieldLen = GetUns16LE( &this->fields[CDFileHeader::o_extraFieldLength] ); + commentLen = GetUns16LE( &this->fields[CDFileHeader::o_commentLength] ); + + if (filenameLen) { + filename = new char[filenameLen]; + file->ReadAll ( filename, filenameLen ); + } + if (extraFieldLen) { + extraField = new char[extraFieldLen]; + file->ReadAll ( extraField, extraFieldLen ); + } + if (commentLen) { + comment = new char[commentLen]; + file->ReadAll ( comment, commentLen ); + } + + ////// GET ACTUAL 64 BIT VALUES ////////////////////////////////////////////// + // get 32bit goodies first, correct later + sizeUncompressed = GetUns32LE( &fields[o_sizeUncompressed] ); + sizeCompressed = GetUns32LE( &fields[o_sizeCompressed] ); + offsetLocalHeader = GetUns32LE( &fields[o_offsetLocalHeader] ); + + XMP_Int32 offset = 0; + while ( offset < extraFieldLen ) + { + XMP_Validate( (extraFieldLen - offset) >= 4, "need 4 bytes for next header ID+len", kXMPErr_BadFileFormat); + XMP_Uns16 headerID = GetUns16LE( &extraField[offset] ); + XMP_Uns16 dataSize = GetUns16LE( &extraField[offset+2] ); + offset += 4; + + XMP_Validate( (extraFieldLen - offset) <= dataSize, + "actual field lenght not given", kXMPErr_BadFileFormat); + if ( headerID == 0x1 ) //we only care about "Zip64 extended information extra field" + { + XMP_Validate( offset < extraFieldLen, "extra field too short", kXMPErr_BadFileFormat); + if (sizeUncompressed == 0xffffffff) + { + sizeUncompressed = GetUns64LE( &extraField[offset] ); + offset += 8; + } + if (sizeCompressed == 0xffffffff) + { + sizeCompressed = GetUns64LE( &extraField[offset] ); + offset += 8; + } + if (offsetLocalHeader == 0xffffffff) + { + offsetLocalHeader = GetUns64LE( &extraField[offset] ); + offset += 8; + } + } + else + { + offset += dataSize; + } // if + } // while + } // read() + + // writes structure to file (starting at current position) + void write(XMP_IO* file) + { + //// WRITE BACK REAL 64 BIT VALUES, CREATE EXTRA FIELD /////////////// + //may only wipe extra field after obtaining all Info from it + if (extraField) delete extraField; + extraFieldLen=0; + + if ( ( sizeUncompressed > 0xffffffff ) || + ( sizeCompressed > 0xffffffff ) || + ( offsetLocalHeader > 0xffffffff ) ) + { + extraField = new char[64]; // actual maxlen is 32 + extraFieldLen = 4; //first fields are for ID, size + if ( sizeUncompressed > 0xffffffff ) + { + PutUns64LE( sizeUncompressed, &extraField[extraFieldLen] ); + extraFieldLen += 8; + sizeUncompressed = 0xffffffff; + } + if ( sizeCompressed > 0xffffffff ) + { + PutUns64LE( sizeCompressed, &extraField[extraFieldLen] ); + extraFieldLen += 8; + sizeCompressed = 0xffffffff; + } + if ( offsetLocalHeader > 0xffffffff ) + { + PutUns64LE( offsetLocalHeader, &extraField[extraFieldLen] ); + extraFieldLen += 8; + offsetLocalHeader = 0xffffffff; + } + + //write ID, dataSize + PutUns16LE( 0x0001, &extraField[0] ); + PutUns16LE( extraFieldLen-4, &extraField[2] ); + //extraFieldSize + PutUns16LE( extraFieldLen, &this->fields[CDFileHeader::o_extraFieldLength] ); + } + + // write out 32-bit ('ff-stubs' or not) + PutUns32LE( (XMP_Uns32)sizeUncompressed, &fields[o_sizeUncompressed] ); + PutUns32LE( (XMP_Uns32)sizeCompressed, &fields[o_sizeCompressed] ); + PutUns32LE( (XMP_Uns32)offsetLocalHeader, &fields[o_offsetLocalHeader] ); + + /// WRITE ///////////////////////////////////////////////////////////////// + XMP_Enforce( SIG == GetUns32LE( &this->fields[CDFileHeader::o_sig] ) ); + + file ->Write ( fields , FIXED_SIZE ); + if (filenameLen) file->Write ( filename , filenameLen ); + if (extraFieldLen) file->Write ( extraField , extraFieldLen ); + if (commentLen) file->Write ( extraField , extraFieldLen ); + } + + void setXMPFilename() + { + if (filename) delete filename; + filenameLen = xmpFilenameLen; + filename = new char[xmpFilenameLen]; + PutUns16LE(filenameLen, &fields[CDFileHeader::o_fileNameLength] ); + memcpy(filename,"META-INF/metadata.xml",xmpFilenameLen); + } + + XMP_Int64 size() + { + XMP_Int64 r = this->FIXED_SIZE + this->filenameLen + this->commentLen; + // predict serialization size + if ( (sizeUncompressed > 0xffffffff)||(sizeCompressed > 0xffffffff)||(offsetLocalHeader>0xffffffff) ) + { + r += 4; //extra fields necessary + if (sizeUncompressed > 0xffffffff) r += 8; + if (sizeCompressed > 0xffffffff) r += 8; + if (offsetLocalHeader > 0xffffffff) r += 8; + } + return r; + } + + ~CDFileHeader() + { + this->release(); + }; + }; // class CDFileHeader + + class EndOfCD { + private: + const static XMP_Uns32 SIG = 0x06054b50; + void UCFECD_Free() + { + if(commentLen) delete comment; + commentLen = 0; + } + public: + const static XMP_Int32 o_Sig = 0; + const static XMP_Int32 o_CdNumEntriesDisk = 8; // same-same for UCF, since single-volume + const static XMP_Int32 o_CdNumEntriesTotal = 10;// must update both + const static XMP_Int32 o_CdSize = 12; + const static XMP_Int32 o_CdOffset = 16; + const static XMP_Int32 o_CommentLen = 20; + + const static int FIXED_SIZE = 22; + char fields[FIXED_SIZE]; + + char* comment; + XMP_Uns16 commentLen; + + EndOfCD() : comment(0), commentLen(0) + { + //nothing + }; + + void read (XMP_IO* file) + { + UCFECD_Free(); + + file->ReadAll ( fields, FIXED_SIZE ); + XMP_Validate( this->SIG == GetUns32LE( &this->fields[o_Sig] ), "invalid header", kXMPErr_BadFileFormat ); + + commentLen = GetUns16LE( &this->fields[o_CommentLen] ); + if(commentLen) + { + comment = new char[commentLen]; + file->ReadAll ( comment, commentLen ); + } + }; + + void write(XMP_IO* file) + { + XMP_Enforce( this->SIG == GetUns32LE( &this->fields[o_Sig] ) ); + commentLen = GetUns16LE( &this->fields[o_CommentLen] ); + file ->Write ( fields , FIXED_SIZE ); + if (commentLen) + file->Write ( comment, commentLen ); + } + + ~EndOfCD() + { + if (comment) delete comment; + }; + }; //class EndOfCD + + //////////////////////////////////////////////////////////////////////////////////// + // EMBEDDING MATH + // + // a = content files before xmp (always 0 thus ommited) + // b/b2 = content files behind xmp (before/after injection) + // x/x2 = offset xmp content header + content file (before/after injection) + // cd/cd = central directory + // h/h2 = end of central directory record + XMP_Int64 b,b2,x,x2,cd,cd2,cdx,cdx2,z,z2,h,h2, + // length thereof ('2' only where possibly different) + // using XMP_Int64 here also for length (not XMP_Int32), + // to be prepared for zip64, our LFA functions might need things in multiple chunks... + al,bl,xl,x2l,cdl,cd2l,cdxl,cdx2l,z2l,hl,fl,f2l; + XMP_Uns16 numCF,numCF2; + + bool wasCompressed; // ..before, false if no prior xmp + bool compressXMP; // compress this time? + bool inPlacePossible; + /* bool isZip64; <=> z2 != 0 */ + + FileHeader xmpFileHeader; + CDFileHeader xmpCDHeader; + + XMP_StringPtr uncomprPacketStr; + XMP_StringLen uncomprPacketLen; + XMP_StringPtr finalPacketStr; + XMP_StringLen finalPacketLen; + std::vector<CDFileHeader> cdEntries; + EndOfCD endOfCD; + void writeOut( XMP_IO* sourceFile, XMP_IO* targetFile, bool isRewrite, bool isInPlace); + +}; // UCF_MetaHandler + +// ================================================================================================= + +#endif /* __UCF_Handler_hpp__ */ + + diff --git a/XMPFiles/source/FileHandlers/WAVE_Handler.cpp b/XMPFiles/source/FileHandlers/WAVE_Handler.cpp new file mode 100644 index 0000000..5f1b330 --- /dev/null +++ b/XMPFiles/source/FileHandlers/WAVE_Handler.cpp @@ -0,0 +1,480 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FileHandlers/WAVE_Handler.hpp" +#include "XMPFiles/source/FormatSupport/WAVE/WAVEBehavior.h" +#include "XMPFiles/source/FormatSupport/WAVE/WAVEReconcile.h" +#include "XMPFiles/source/NativeMetadataSupport/MetadataSet.h" +#include "source/XIO.hpp" + +using namespace IFF_RIFF; + +// ================================================================================================= +/// \file WAVE_Handler.cpp +/// \brief File format handler for WAVE. +// ================================================================================================= + + +// ================================================================================================= +// WAVE_MetaHandlerCTor +// ==================== + +XMPFileHandler * WAVE_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new WAVE_MetaHandler ( parent ); +} + +// ================================================================================================= +// WAVE_CheckFormat +// =============== +// +// Checks if the given file is a valid WAVE file. +// The first 12 bytes are checked. The first 4 must be "RIFF" +// Bytes 8 to 12 must be "WAVE" + +bool WAVE_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* file, + XMPFiles* parent ) +{ + // Reset file pointer position + file->Rewind(); + + XMP_Uns8 buffer[12]; + XMP_Int32 got = file->Read ( buffer, 12 ); + // Reset file pointer position + file->Rewind(); + + // Need to have at least ID, size and Type of first chunk + if ( got < 12 ) + { + return false; + } + + XMP_Uns32 type = WAVE_MetaHandler::whatRIFFFormat( buffer ); + if ( type != kChunk_RIFF && type != kChunk_RF64 ) + { + return false; + } + + const BigEndian& endian = BigEndian::getInstance(); + if ( endian.getUns32(&buffer[8]) == kType_WAVE ) + { + return true; + } + + return false; +} // WAVE_CheckFormat + + +// ================================================================================================= +// WAVE_MetaHandler::whatRIFFFormat +// =============== + +XMP_Uns32 WAVE_MetaHandler::whatRIFFFormat( XMP_Uns8* buffer ) +{ + XMP_Uns32 type = 0; + + const BigEndian& endian = BigEndian::getInstance(); + + if( buffer != 0 ) + { + if( endian.getUns32( buffer ) == kChunk_RIFF ) + { + type = kChunk_RIFF; + } + else if( endian.getUns32( buffer ) == kChunk_RF64 ) + { + type = kChunk_RF64; + } + } + + return type; +} // whatRIFFFormat + + +// Static inits + +// ChunkIdentifier +// RIFF:WAVE/PMX_ +const ChunkIdentifier WAVE_MetaHandler::kRIFFXMP[2] = { { kChunk_RIFF, kType_WAVE }, { kChunk_XMP, kType_NONE} }; +// RIFF:WAVE/LIST:INFO +const ChunkIdentifier WAVE_MetaHandler::kRIFFInfo[2] = { { kChunk_RIFF, kType_WAVE }, { kChunk_LIST, kType_INFO } }; +// RIFF:WAVE/DISP +const ChunkIdentifier WAVE_MetaHandler::kRIFFDisp[2] = { { kChunk_RIFF, kType_WAVE }, { kChunk_DISP, kType_NONE } }; +// RIFF:WAVE/BEXT +const ChunkIdentifier WAVE_MetaHandler::kRIFFBext[2] = { { kChunk_RIFF, kType_WAVE }, { kChunk_bext, kType_NONE } }; +// RIFF:WAVE/cart +const ChunkIdentifier WAVE_MetaHandler::kRIFFCart[2] = { { kChunk_RIFF, kType_WAVE }, { kChunk_cart, kType_NONE } }; +// cr8r is not yet required for WAVE +// RIFF:WAVE/Cr8r +// const ChunkIdentifier WAVE_MetaHandler::kWAVECr8r[2] = { { kChunk_RIFF, kType_WAVE }, { kChunk_Cr8r, kType_NONE } }; +// RF64:WAVE/PMX_ +const ChunkIdentifier WAVE_MetaHandler::kRF64XMP[2] = { { kChunk_RF64, kType_WAVE }, { kChunk_XMP, kType_NONE} }; +// RF64:WAVE/LIST:INFO +const ChunkIdentifier WAVE_MetaHandler::kRF64Info[2] = { { kChunk_RF64, kType_WAVE }, { kChunk_LIST, kType_INFO } }; +// RF64:WAVE/DISP +const ChunkIdentifier WAVE_MetaHandler::kRF64Disp[2] = { { kChunk_RF64, kType_WAVE }, { kChunk_DISP, kType_NONE } }; +// RF64:WAVE/BEXT +const ChunkIdentifier WAVE_MetaHandler::kRF64Bext[2] = { { kChunk_RF64, kType_WAVE }, { kChunk_bext, kType_NONE } }; +// RF64:WAVE/cart +const ChunkIdentifier WAVE_MetaHandler::kRF64Cart[2] = { { kChunk_RF64, kType_WAVE }, { kChunk_cart, kType_NONE } }; +// cr8r is not yet required for WAVE +// RF64:WAVE/Cr8r +// const ChunkIdentifier WAVE_MetaHandler::kRF64Cr8r[2] = { { kChunk_RF64, kType_WAVE }, { kChunk_Cr8r, kType_NONE } }; + +// ================================================================================================= +// WAVE_MetaHandler::WAVE_MetaHandler +// ================================ + +WAVE_MetaHandler::WAVE_MetaHandler ( XMPFiles * _parent ) + : mChunkBehavior(NULL), mChunkController(NULL), + mINFOMeta(), mBEXTMeta(), mCartMeta(), mDISPMeta(), + mXMPChunk(NULL), mINFOChunk(NULL), + mBEXTChunk(NULL), mCartChunk(NULL), mDISPChunk(NULL) +{ + this->parent = _parent; + this->handlerFlags = kWAVE_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + + this->mChunkBehavior = new WAVEBehavior(); + this->mChunkController = new ChunkController( mChunkBehavior, false ); + +} // WAVE_MetaHandler::WAVE_MetaHandler + + +// ================================================================================================= +// WAVE_MetaHandler::~WAVE_MetaHandler +// ================================= + +WAVE_MetaHandler::~WAVE_MetaHandler() +{ + if( mChunkController != NULL ) + { + delete mChunkController; + } + + if( mChunkBehavior != NULL ) + { + delete mChunkBehavior; + } +} // WAVE_MetaHandler::~WAVE_MetaHandler + + +// ================================================================================================= +// WAVE_MetaHandler::CacheFileData +// ============================== + +void WAVE_MetaHandler::CacheFileData() +{ + // Need to determine the file type, need the first four bytes of the file + + // Reset file pointer position + this->parent->ioRef->Rewind(); + + XMP_Uns8 buffer[4]; + XMP_Int32 got = this->parent->ioRef->Read ( buffer, 4 ); + XMP_Assert( got == 4 ); + + XMP_Uns32 type = WAVE_MetaHandler::whatRIFFFormat( buffer ); + XMP_Assert( type == kChunk_RIFF || type == kChunk_RF64 ); + + // Reset file pointer position + this->parent->ioRef->Rewind(); + + // Add the relevant chunk paths for the determined RIFF format + if( type == kChunk_RIFF ) + { + mWAVEXMPChunkPath.append( kRIFFXMP, SizeOfCIArray(kRIFFXMP) ); + mWAVEInfoChunkPath.append( kRIFFInfo, SizeOfCIArray(kRIFFInfo) ); + mWAVEDispChunkPath.append( kRIFFDisp, SizeOfCIArray(kRIFFDisp) ); + mWAVEBextChunkPath.append( kRIFFBext, SizeOfCIArray(kRIFFBext) ); + mWAVECartChunkPath.append( kRIFFCart, SizeOfCIArray(kRIFFCart) ); + // cr8r is not yet required for WAVE + //mWAVECr8rChunkPath.append( kWAVECr8r, SizeOfCIArray(kWAVECr8r) ); + } + else // RF64 + { + mWAVEXMPChunkPath.append( kRF64XMP, SizeOfCIArray(kRF64XMP) ); + mWAVEInfoChunkPath.append( kRF64Info, SizeOfCIArray(kRF64Info) ); + mWAVEDispChunkPath.append( kRF64Disp, SizeOfCIArray(kRF64Disp) ); + mWAVEBextChunkPath.append( kRF64Bext, SizeOfCIArray(kRF64Bext) ); + mWAVECartChunkPath.append( kRF64Cart, SizeOfCIArray(kRF64Cart) ); + // cr8r is not yet required for WAVE + //mWAVECr8rChunkPath.append( kRF64Cr8r, SizeOfCIArray(kRF64Cr8r) ); + } + + mChunkController->addChunkPath( mWAVEXMPChunkPath ); + mChunkController->addChunkPath( mWAVEInfoChunkPath ); + mChunkController->addChunkPath( mWAVEDispChunkPath ); + mChunkController->addChunkPath( mWAVEBextChunkPath ); + mChunkController->addChunkPath( mWAVECartChunkPath ); + // cr8r is not yet required for WAVE + //mChunkController->addChunkPath( mWAVECr8rChunkPath ); + + // Parse the given file + // Throws exception if the file cannot be parsed + mChunkController->parseFile( this->parent->ioRef, &this->parent->openFlags ); + + // Retrieve the file type, it must have at least FORM:WAVE + std::vector<XMP_Uns32> typeList = mChunkController->getTopLevelTypes(); + + // If file is neither WAVE, throw exception + XMP_Validate( typeList.at(0) == kType_WAVE , "File is not of type WAVE", kXMPErr_BadFileFormat ); + + // Check if the file contains XMP (last if there are duplicates) + mXMPChunk = mChunkController->getChunk( mWAVEXMPChunkPath, true ); + + + // Retrieve XMP packet info + if( mXMPChunk != NULL ) + { + this->packetInfo.length = static_cast<XMP_Int32>(mXMPChunk->getSize()); + this->packetInfo.charForm = kXMP_Char8Bit; + this->packetInfo.writeable = true; + + // Get actual the XMP packet + this->xmpPacket.assign ( mXMPChunk->getString( this->packetInfo.length) ); + + // set state + this->containsXMP = true; + } +} // WAVE_MetaHandler::CacheFileData + + +// ================================================================================================= +// WAVE_MetaHandler::ProcessXMP +// ============================ + +void WAVE_MetaHandler::ProcessXMP() +{ + // Must be done only once + if ( this->processedXMP ) + { + return; + } + // Set the status at start, in case something goes wrong in this method + this->processedXMP = true; + + // Parse the XMP + if ( ! this->xmpPacket.empty() ) { + + XMP_Assert ( this->containsXMP ); + + FillPacketInfo ( this->xmpPacket, &this->packetInfo ); + + this->xmpObj.ParseFromBuffer ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + + this->containsXMP = true; + } + + // Then import native properties + MetadataSet metaSet; + WAVEReconcile recon; + + // Parse the WAVE metadata object with values + + const XMP_Uns8* buffer = NULL; // temporary buffer + XMP_Uns64 size = 0; + // Get LIST:INFO legacy chunk + mINFOChunk = mChunkController->getChunk( mWAVEInfoChunkPath, true ); + if( mINFOChunk != NULL ) + { + size = mINFOChunk->getData( &buffer ); + mINFOMeta.parse( buffer, size ); + } + + // Parse Bext legacy chunk + mBEXTChunk = mChunkController->getChunk( mWAVEBextChunkPath, true ); + if( mBEXTChunk != NULL ) + { + size = mBEXTChunk->getData( &buffer ); + mBEXTMeta.parse( buffer, size ); + } + + // Parse cart legacy chunk + mCartChunk = mChunkController->getChunk( mWAVECartChunkPath, true ); + if( mCartChunk != NULL ) + { + size = mCartChunk->getData( &buffer ); + mCartMeta.parse( buffer, size ); + } + + // Parse DISP legacy chunk + const std::vector<IChunkData*>& disps = mChunkController->getChunks( mWAVEDispChunkPath ); + + if( ! disps.empty() ) + { + for( std::vector<IChunkData*>::const_reverse_iterator iter=disps.rbegin(); iter!=disps.rend(); iter++ ) + { + size = (*iter)->getData( &buffer ); + + if( DISPMetadata::isValidDISP( buffer, size ) ) + { + mDISPChunk = (*iter); + break; + } + } + } + + if( mDISPChunk != NULL ) + { + size = mDISPChunk->getData( &buffer ); + mDISPMeta.parse( buffer, size ); + } + + //cr8r is not yet required for WAVE + //// Parse Cr8r legacy chunk + //mCr8rChunk = mChunkController->getChunk( mWAVECr8rChunkPath ); + //if( mCr8rChunk != NULL ) + //{ + // size = mCr8rChunk->getData( &buffer ); + // mCr8rMeta.parse( buffer, size ); + //} + + // app legacy to the metadata list + metaSet.append( &mINFOMeta ); + metaSet.append( &mBEXTMeta ); + metaSet.append( &mCartMeta ); + metaSet.append( &mDISPMeta ); + + // cr8r is not yet required for WAVE + // metaSet.append( &mCr8rMeta ); + + // Do the import + if( recon.importToXMP( this->xmpObj, metaSet ) ) + { + // Remember if anything has changed + this->containsXMP = true; + } + +} // WAVE_MetaHandler::ProcessXMP + + +// ================================================================================================= +// RIFF_MetaHandler::UpdateFile +// =========================== + +void WAVE_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + if ( ! this->needsUpdate ) { // If needsUpdate is set then at least the XMP changed. + return; + } + + if ( doSafeUpdate ) + { + XMP_Throw ( "WAVE_MetaHandler::UpdateFile: Safe update not supported", kXMPErr_Unavailable ); + } + + // Export XMP to legacy chunks. Create/delete them if necessary + MetadataSet metaSet; + WAVEReconcile recon; + + metaSet.append( &mINFOMeta ); + metaSet.append( &mBEXTMeta ); + metaSet.append( &mCartMeta ); + metaSet.append( &mDISPMeta ); + + // cr8r is not yet required for WAVE + // metaSet.append( &mCr8rMeta ); + + // If anything changes, update/create/delete the legacy chunks + if( recon.exportFromXMP( metaSet, this->xmpObj ) ) + { + if ( mINFOMeta.hasChanged( )) + { + updateLegacyChunk( &mINFOChunk, kChunk_LIST, kType_INFO, mINFOMeta ); + } + + if ( mBEXTMeta.hasChanged( )) + { + updateLegacyChunk( &mBEXTChunk, kChunk_bext, kType_NONE, mBEXTMeta ); + } + + if ( mCartMeta.hasChanged( )) + { + updateLegacyChunk( &mCartChunk, kChunk_cart, kType_NONE, mCartMeta ); + } + + if ( mDISPMeta.hasChanged( )) + { + updateLegacyChunk( &mDISPChunk, kChunk_DISP, kType_NONE, mDISPMeta ); + } + + //cr8r is not yet required for WAVE + //if ( mCr8rMeta.hasChanged( )) + //{ + // updateLegacyChunk( &mCr8rChunk, kChunk_Cr8r, kType_NONE, mCr8rMeta ); + //} + } + + //update/create XMP chunk + if( this->containsXMP ) + { + this->xmpObj.SerializeToBuffer ( &(this->xmpPacket) ); + + if( mXMPChunk != NULL ) + { + mXMPChunk->setData( reinterpret_cast<const XMP_Uns8 *>(this->xmpPacket.c_str()), this->xmpPacket.length() ); + } + else // create XMP chunk + { + mXMPChunk = mChunkController->createChunk( kChunk_XMP, kType_NONE ); + mXMPChunk->setData( reinterpret_cast<const XMP_Uns8 *>(this->xmpPacket.c_str()), this->xmpPacket.length() ); + mChunkController->insertChunk( mXMPChunk ); + } + } + // XMP Packet is never completely removed from the file. + + //write tree back to file + mChunkController->writeFile( this->parent->ioRef ); + + this->needsUpdate = false; // Make sure this is only called once. +} // WAVE_MetaHandler::UpdateFile + + +void WAVE_MetaHandler::updateLegacyChunk( IChunkData **chunk, XMP_Uns32 chunkID, XMP_Uns32 chunkType, IMetadata &legacyData ) +{ + // If there is a legacy value, update/create the appropriate chunk + if( ! legacyData.isEmpty() ) + { + XMP_Uns8* buffer = NULL; + XMP_Uns64 size = legacyData.serialize( &buffer ); + + if( *chunk != NULL ) + { + (*chunk)->setData( buffer, size, false ); + } + else + { + *chunk = mChunkController->createChunk( chunkID, chunkType ); + (*chunk)->setData( buffer, size, false ); + mChunkController->insertChunk( *chunk ); + } + + delete[] buffer; + } + else //delete chunk if existing + { + mChunkController->removeChunk ( *chunk ); + } +}//updateLegacyChunk + + +// ================================================================================================= +// RIFF_MetaHandler::WriteFile +// ========================== + +void WAVE_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + XMP_Throw( "WAVE_MetaHandler::WriteTempFile is not Implemented!", kXMPErr_Unimplemented ); +}//WAVE_MetaHandler::WriteFile diff --git a/XMPFiles/source/FileHandlers/WAVE_Handler.hpp b/XMPFiles/source/FileHandlers/WAVE_Handler.hpp new file mode 100644 index 0000000..49f873c --- /dev/null +++ b/XMPFiles/source/FileHandlers/WAVE_Handler.hpp @@ -0,0 +1,172 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef __WAVE_Handler_hpp__ +#define __WAVE_Handler_hpp__ 1 + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/IFF/ChunkController.h" +#include "XMPFiles/source/FormatSupport/IFF/IChunkBehavior.h" +#include "XMPFiles/source/FormatSupport/IFF/IChunkData.h" +#include "source/Endian.h" +#include "XMPFiles/source/FormatSupport/IFF/ChunkPath.h" +#include "XMPFiles/source/FormatSupport/WAVE/BEXTMetadata.h" +#include "XMPFiles/source/FormatSupport/WAVE/CartMetadata.h" +#include "XMPFiles/source/FormatSupport/WAVE/DISPMetadata.h" +#include "XMPFiles/source/FormatSupport/WAVE/INFOMetadata.h" +#include "source/XIO.hpp" +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +using namespace IFF_RIFF; + +// ================================================================================================= +/// \file WAV_Handler.hpp +/// \brief File format handler for AIFF. +// ================================================================================================= + +/** + * Contructor for the handler. + */ +extern XMPFileHandler * WAVE_MetaHandlerCTor ( XMPFiles * parent ); + +/** + * Checks the format of the file, see common code. + */ +extern bool WAVE_CheckFormat ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO* fileRef, + XMPFiles * parent ); + +/** WAVE does not need kXMPFiles_CanRewrite as we can always use UpdateFile to either do + * in-place update or append to the file. */ +static const XMP_OptionBits kWAVE_HandlerFlags = (kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_PrefersInPlace | + kXMPFiles_CanReconcile | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_AllowsSafeUpdate + ); + +/** + * Main class for the the WAVE file handler. + */ +class WAVE_MetaHandler : public XMPFileHandler +{ +public: + WAVE_MetaHandler ( XMPFiles* parent ); + ~WAVE_MetaHandler(); + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + /** + * Checks if the first 4 bytes of the given buffer are either type RIFF or RF64 + * @param buffer a byte buffer that must contain at least 4 bytes and point to the correct byte + * @return Either kChunk_RIFF, kChunk_RF64 0 if no type could be determined + */ + static XMP_Uns32 whatRIFFFormat( XMP_Uns8* buffer ); + +private: + /** + * Updates/creates/deletes a given legacy chunk depending on the given new legacy value + * If the Chunk exists and is not empty, it is updated. If it is empty the + * Chunk is removed from the tree. If the Chunk does not exist but a value is given, it is created + * and initialized with that value + * + * @param chunk OUT pointer to the legacy chunk + * @param chunkID Id of the Chunk if it needs to be created + * @param chunkType Type of the Chunk if it needs to be created + * @param legacyData the new legacy metadata object (can be empty) + */ + void updateLegacyChunk( IChunkData **chunk, XMP_Uns32 chunkID, XMP_Uns32 chunkType, IMetadata &legacyData ); + + + /** private standard Ctor, not to be used */ + WAVE_MetaHandler (): mChunkController(NULL), mChunkBehavior(NULL), + mINFOMeta(), mBEXTMeta(), mCartMeta(), mDISPMeta(), + mXMPChunk(NULL), mINFOChunk(NULL), + mBEXTChunk(NULL), mCartChunk(NULL), mDISPChunk(NULL) {}; + + // ----- MEMBERS ----- // + + /** Controls the parsing and writing of the passed stream. */ + ChunkController *mChunkController; + /** Represents the rules how chunks are added, removed or rearranged */ + IChunkBehavior *mChunkBehavior; + /** container for Legacy metadata */ + INFOMetadata mINFOMeta; + BEXTMetadata mBEXTMeta; + CartMetadata mCartMeta; + DISPMetadata mDISPMeta; + + // cr8r is not yet required for WAVE + // Cr8rMetadata mCr8rMeta; + + /** pointer to the XMP chunk */ + IChunkData *mXMPChunk; + /** pointer to legacy chunks */ + IChunkData *mINFOChunk; + IChunkData *mBEXTChunk; + IChunkData *mCartChunk; + IChunkData *mDISPChunk; + + // cr8r is not yet required for WAVE + // IChunkData *mCr8rChunk; + + // ----- CONSTANTS ----- // + + /** Chunk path identifier of interest in WAVE */ + static const ChunkIdentifier kRIFFXMP[2]; + static const ChunkIdentifier kRIFFInfo[2]; + static const ChunkIdentifier kRIFFDisp[2]; + static const ChunkIdentifier kRIFFBext[2]; + static const ChunkIdentifier kRIFFCart[2]; + + // cr8r is not yet required for WAVE + // static const ChunkIdentifier kWAVECr8r[2]; + + /** Chunk path identifier of interest in RF64 */ + static const ChunkIdentifier kRF64XMP[2]; + static const ChunkIdentifier kRF64Info[2]; + static const ChunkIdentifier kRF64Disp[2]; + static const ChunkIdentifier kRF64Bext[2]; + static const ChunkIdentifier kRF64Cart[2]; + + // cr8r is not yet required for WAVE + // static const ChunkIdentifier kRF64Cr8r[2]; + + /** Path to XMP chunk */ + ChunkPath mWAVEXMPChunkPath; + + /** Path to INFO chunk */ + ChunkPath mWAVEInfoChunkPath; + + /** Path to DISP chunk */ + ChunkPath mWAVEDispChunkPath; + + /** Path to BEXT chunk */ + ChunkPath mWAVEBextChunkPath; + + /** Path to cart chunk */ + ChunkPath mWAVECartChunkPath; + + //cr8r is not yet required for WAVE + ///** Path to Cr8r chunk */ + //const ChunkPath mWAVECr8rChunkPath; + +}; // WAVE_MetaHandler + +// ================================================================================================= + +#endif /* __WAVE_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/XDCAMEX_Handler.cpp b/XMPFiles/source/FileHandlers/XDCAMEX_Handler.cpp new file mode 100644 index 0000000..350ab1f --- /dev/null +++ b/XMPFiles/source/FileHandlers/XDCAMEX_Handler.cpp @@ -0,0 +1,890 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2008 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/XDCAMEX_Handler.hpp" +#include "XMPFiles/source/FormatSupport/XDCAM_Support.hpp" +#include "third-party/zuid/interfaces/MD5.h" + +using namespace std; + +// ================================================================================================= +/// \file XDCAMEX_Handler.cpp +/// \brief Folder format handler for XDCAMEX. +/// +/// This handler is for the XDCAMEX video format. +/// +/// .../MyMovie/ +/// BPAV/ +/// MEDIAPRO.XML +/// MEDIAPRO.BUP +/// CLPR/ +/// 709_001_01/ +/// 709_001_01.SMI +/// 709_001_01.MP4 +/// 709_001_01M01.XML +/// 709_001_01R01.BIM +/// 709_001_01I01.PPN +/// 709_001_02/ +/// 709_002_01/ +/// 709_003_01/ +/// TAKR/ +/// 709_001/ +/// 709_001.SMI +/// 709_001M01.XML +/// +/// The Backup files (.BUP) are optional. No files or directories other than those listed are +/// allowed in the BPAV directory. The CLPR (clip root) directory may contain only clip directories, +/// which may only contain the clip files listed. The TAKR (take root) direcory may contail only +/// take directories, which may only contain take files. The take root directory can be empty. +/// MEDIPRO.XML contains information on clip and take management. +/// +/// Each clip directory contains a media file (.MP4), a clip info file (.SMI), a real time metadata +/// file (.BIM), a non real time metadata file (.XML), and a picture pointer file (.PPN). A take +/// directory conatins a take info and non real time take metadata files. +// ================================================================================================= + +// ================================================================================================= +// XDCAMEX_CheckFormat +// =================== +// +// This version checks for the presence of a top level BPAV directory, and the required files and +// directories immediately within it. The CLPR and TAKR subfolders are required, as is MEDIAPRO.XML. +// +// The state of the string parameters depends on the form of the path passed by the client. If the +// client passed a logical clip path, like ".../MyMovie/012_3456_01", the parameters are: +// rootPath - ".../MyMovie" +// gpName - empty +// parentName - empty +// leafName - "012_3456_01" +// If the client passed a full file path, like ".../MyMovie/BPAV/CLPR/012_3456_01/012_3456_01M01.XML", they are: +// rootPath - ".../MyMovie/BPAV" +// gpName - "CLPR" +// parentName - "012_3456_01" +// leafName - "012_3456_01M01" + +// ! The common code has shifted the gpName, parentName, and leafName strings to upper case. It has +// ! also made sure that for a logical clip path the rootPath is an existing folder, and that the +// ! file exists for a full file path. + +// ! Using explicit '/' as a separator when creating paths, it works on Windows. + +bool XDCAMEX_CheckFormat ( XMP_FileFormat format, + const std::string & _rootPath, + const std::string & gpName, + const std::string & parentName, + const std::string & _leafName, + XMPFiles * parent ) +{ + std::string rootPath = _rootPath; + std::string clipName = _leafName; + std::string grandGPName; + + std::string bpavPath ( rootPath ); + + // Do some initial checks on the gpName and parentName. + + if ( gpName.empty() != parentName.empty() ) return false; // Must be both empty or both non-empty. + + if ( gpName.empty() ) { + + // This is the logical clip path case. Make sure .../MyMovie/BPAV/CLPR is a folder. + bpavPath += kDirChar; // The rootPath was just ".../MyMovie". + bpavPath += "BPAV"; + if ( Host_IO::GetChildMode ( bpavPath.c_str(), "CLPR" ) != Host_IO::kFMode_IsFolder ) return false; + + } else { + + // This is the explicit file case. Make sure the ancestry is OK, compare using the parent's + // length since the file can have a suffix like "M01". Use the leafName as the clipName to + // preserve lower case, but truncate to the parent's length to remove any suffix. + + if ( gpName != "CLPR" ) return false; + XIO::SplitLeafName ( &rootPath, &grandGPName ); + MakeUpperCase ( &grandGPName ); + if ( grandGPName != "BPAV" ) return false; + + if ( ! XMP_LitNMatch ( parentName.c_str(), clipName.c_str(), parentName.size() ) ) { + std::string tempName = clipName; + MakeUpperCase ( &tempName ); + if ( ! XMP_LitNMatch ( parentName.c_str(), tempName.c_str(), parentName.size() ) ) return false; + } + + clipName.erase ( parentName.size() ); + + } + + // Check the rest of the required general structure. + if ( Host_IO::GetChildMode ( bpavPath.c_str(), "TAKR" ) != Host_IO::kFMode_IsFolder ) return false; + if ( Host_IO::GetChildMode ( bpavPath.c_str(), "MEDIAPRO.XML" ) != Host_IO::kFMode_IsFile ) return false; + + // Make sure the clip's .MP4 and .SMI files exist. + std::string tempPath ( bpavPath ); + tempPath += kDirChar; + tempPath += "CLPR"; + tempPath += kDirChar; + tempPath += clipName; + tempPath += kDirChar; + tempPath += clipName; + tempPath += ".MP4"; + if ( Host_IO::GetFileMode ( tempPath.c_str() ) != Host_IO::kFMode_IsFile ) return false; + tempPath.erase ( tempPath.size()-3 ); + tempPath += "SMI"; + if ( Host_IO::GetFileMode ( tempPath.c_str() ) != Host_IO::kFMode_IsFile ) return false; + + // And now save the psuedo path for the handler object. + tempPath = rootPath; + tempPath += kDirChar; + tempPath += clipName; + size_t pathLen = tempPath.size() + 1; // Include a terminating nul. + parent->tempPtr = malloc ( pathLen ); + if ( parent->tempPtr == 0 ) XMP_Throw ( "No memory for XDCAMEX clip info", kXMPErr_NoMemory ); + memcpy ( parent->tempPtr, tempPath.c_str(), pathLen ); + + return true; + +} // XDCAMEX_CheckFormat + +// ================================================================================================= + +static void* CreatePseudoClipPath ( const std::string & clientPath ) +{ + + // Used to create the clip pseudo path when the CheckFormat function is skipped. + + std::string pseudoPath = clientPath; + + size_t pathLen; + void* tempPtr = 0; + + if ( Host_IO::Exists ( pseudoPath.c_str() ) ) { + + // The client passed a physical path. The logical clip name is the last folder name, the + // parent of the file. This is best since some files have suffixes. + + std::string clipName, ignored; + + XIO::SplitLeafName ( &pseudoPath, &ignored ); // Split the file name. + XIO::SplitLeafName ( &pseudoPath, &clipName ); // Use the parent folder name. + + XIO::SplitLeafName ( &pseudoPath, &ignored ); // Remove the 2 intermediate folder levels. + XIO::SplitLeafName ( &pseudoPath, &ignored ); + + pseudoPath += kDirChar; + pseudoPath += clipName; + + } + + pathLen = pseudoPath.size() + 1; // Include a terminating nul. + tempPtr = malloc ( pathLen ); + if ( tempPtr == 0 ) XMP_Throw ( "No memory for XDCAMEX clip info", kXMPErr_NoMemory ); + memcpy ( tempPtr, pseudoPath.c_str(), pathLen ); + + return tempPtr; + +} // CreatePseudoClipPath + +// ================================================================================================= +// XDCAMEX_MetaHandlerCTor +// ======================= + +XMPFileHandler * XDCAMEX_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new XDCAMEX_MetaHandler ( parent ); + +} // XDCAMEX_MetaHandlerCTor + +// ================================================================================================= +// XDCAMEX_MetaHandler::XDCAMEX_MetaHandler +// ======================================== + +XDCAMEX_MetaHandler::XDCAMEX_MetaHandler ( XMPFiles * _parent ) : expat(0) +{ + this->parent = _parent; // Inherited, can't set in the prefix. + this->handlerFlags = kXDCAMEX_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + + // Extract the root path and clip name from tempPtr. + + if ( this->parent->tempPtr == 0 ) { + // The CheckFormat call might have been skipped. + this->parent->tempPtr = CreatePseudoClipPath ( this->parent->filePath ); + } + + this->rootPath.assign ( (char*) this->parent->tempPtr ); + free ( this->parent->tempPtr ); + this->parent->tempPtr = 0; + + XIO::SplitLeafName ( &this->rootPath, &this->clipName ); + +} // XDCAMEX_MetaHandler::XDCAMEX_MetaHandler + +// ================================================================================================= +// XDCAMEX_MetaHandler::~XDCAMEX_MetaHandler +// ========================================= + +XDCAMEX_MetaHandler::~XDCAMEX_MetaHandler() +{ + + this->CleanupLegacyXML(); + if ( this->parent->tempPtr != 0 ) { + free ( this->parent->tempPtr ); + this->parent->tempPtr = 0; + } + +} // XDCAMEX_MetaHandler::~XDCAMEX_MetaHandler + +// ================================================================================================= +// XDCAMEX_MetaHandler::MakeClipFilePath +// ===================================== + +bool XDCAMEX_MetaHandler::MakeClipFilePath ( std::string * path, XMP_StringPtr suffix, bool checkFile /* = false */ ) +{ + + *path = this->rootPath; + *path += kDirChar; + *path += "BPAV"; + *path += kDirChar; + *path += "CLPR"; + *path += kDirChar; + *path += this->clipName; + *path += kDirChar; + *path += this->clipName; + *path += suffix; + + if ( ! checkFile ) return true; + return Host_IO::Exists ( path->c_str() ); + +} // XDCAMEX_MetaHandler::MakeClipFilePath + +// ================================================================================================= +// XDCAMEX_MetaHandler::MakeMediaproPath +// ===================================== + +bool XDCAMEX_MetaHandler::MakeMediaproPath ( std::string * path, bool checkFile /* = false */ ) +{ + + *path = this->rootPath; + *path += kDirChar; + *path += "BPAV"; + *path += kDirChar; + *path += "MEDIAPRO.XML"; + + if ( ! checkFile ) return true; + return Host_IO::Exists ( path->c_str() ); + +} // XDCAMEX_MetaHandler::MakeMediaproPath + +// ================================================================================================= +// XDCAMEX_MetaHandler::MakeLegacyDigest +// ===================================== + +// *** Early hack version. + +#define kHexDigits "0123456789ABCDEF" + +void XDCAMEX_MetaHandler::MakeLegacyDigest ( std::string * digestStr ) +{ + digestStr->erase(); + if ( this->clipMetadata == 0 ) return; // Bail if we don't have any legacy XML. + XMP_Assert ( this->expat != 0 ); + + XMP_StringPtr xdcNS = this->xdcNS.c_str(); + XML_NodePtr legacyContext, legacyProp; + + legacyContext = this->clipMetadata->GetNamedElement ( xdcNS, "Access" ); + if ( legacyContext == 0 ) return; + + MD5_CTX context; + unsigned char digestBin [16]; + MD5Init ( &context ); + + legacyProp = legacyContext->GetNamedElement ( xdcNS, "Creator" ); + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() && (! legacyProp->content.empty()) ) { + const XML_Node * xmlValue = legacyProp->content[0]; + MD5Update ( &context, (XMP_Uns8*)xmlValue->value.c_str(), (unsigned int)xmlValue->value.size() ); + } + + legacyProp = legacyContext->GetNamedElement ( xdcNS, "CreationDate" ); + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() && (! legacyProp->content.empty()) ) { + const XML_Node * xmlValue = legacyProp->content[0]; + MD5Update ( &context, (XMP_Uns8*)xmlValue->value.c_str(), (unsigned int)xmlValue->value.size() ); + } + + legacyProp = legacyContext->GetNamedElement ( xdcNS, "LastUpdateDate" ); + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() && (! legacyProp->content.empty()) ) { + const XML_Node * xmlValue = legacyProp->content[0]; + MD5Update ( &context, (XMP_Uns8*)xmlValue->value.c_str(), (unsigned int)xmlValue->value.size() ); + } + + MD5Final ( digestBin, &context ); + + char buffer [40]; + for ( int in = 0, out = 0; in < 16; in += 1, out += 2 ) { + XMP_Uns8 byte = digestBin[in]; + buffer[out] = kHexDigits [ byte >> 4 ]; + buffer[out+1] = kHexDigits [ byte & 0xF ]; + } + buffer[32] = 0; + digestStr->append ( buffer ); + +} // XDCAMEX_MetaHandler::MakeLegacyDigest + +// ================================================================================================= +// XDCAMEX_MetaHandler::CleanupLegacyXML +// ===================================== + +void XDCAMEX_MetaHandler::CleanupLegacyXML() +{ + + if ( this->expat != 0 ) { delete ( this->expat ); this->expat = 0; } + + clipMetadata = 0; // ! Was a pointer into the expat tree. + +} // XDCAMEX_MetaHandler::CleanupLegacyXML + +// ================================================================================================= +// XDCAMEX_MetaHandler::GetFileModDate +// =================================== + +static inline bool operator< ( const XMP_DateTime & left, const XMP_DateTime & right ) { + int compare = SXMPUtils::CompareDateTime ( left, right ); + return (compare < 0); +} + +bool XDCAMEX_MetaHandler::GetFileModDate ( XMP_DateTime * modDate ) +{ + + // The XDCAM EX locations of metadata: + // BPAV/ + // MEDIAPRO.XML // Has non-XMP metadata. + // CLPR/ + // 709_3001_01: + // 709_3001_01M01.XML // Has non-XMP metadata. + // 709_3001_01M01.XMP + + bool ok, haveDate = false; + std::string fullPath; + XMP_DateTime oneDate, junkDate; + if ( modDate == 0 ) modDate = &junkDate; + + ok = this->MakeMediaproPath ( &fullPath, true /* checkFile */ ); + if ( ok ) ok = Host_IO::GetModifyDate ( fullPath.c_str(), &oneDate ); + if ( ok ) { + if ( (! haveDate) || (*modDate < oneDate) ) *modDate = oneDate; + haveDate = true; + } + + ok = this->MakeClipFilePath ( &fullPath, "M01.XML", true /* checkFile */ ); + if ( ok ) ok = Host_IO::GetModifyDate ( fullPath.c_str(), &oneDate ); + if ( ok ) { + if ( (! haveDate) || (*modDate < oneDate) ) *modDate = oneDate; + haveDate = true; + } + + ok = this->MakeClipFilePath ( &fullPath, "M01.XMP", true /* checkFile */ ); + if ( ok ) ok = Host_IO::GetModifyDate ( fullPath.c_str(), &oneDate ); + if ( ok ) { + if ( (! haveDate) || (*modDate < oneDate) ) *modDate = oneDate; + haveDate = true; + } + + return haveDate; + +} // XDCAMEX_MetaHandler::GetFileModDate + +// ================================================================================================= +// XDCAMEX_MetaHandler::CacheFileData +// ================================== + +void XDCAMEX_MetaHandler::CacheFileData() +{ + XMP_Assert ( ! this->containsXMP ); + + if ( this->parent->UsesClientIO() ) { + XMP_Throw ( "XDCAMEX cannot be used with client-managed I/O", kXMPErr_InternalFailure ); + } + + // See if the clip's .XMP file exists. + + std::string xmpPath; + this->MakeClipFilePath ( &xmpPath, "M01.XMP" ); + if ( Host_IO::GetFileMode ( xmpPath.c_str() ) != Host_IO::kFMode_IsFile ) return; // No XMP. + + // Read the entire .XMP file. + + bool readOnly = XMP_OptionIsClear ( this->parent->openFlags, kXMPFiles_OpenForUpdate ); + + XMP_Assert ( this->parent->ioRef == 0 ); + XMPFiles_IO* xmpFile = XMPFiles_IO::New_XMPFiles_IO ( xmpPath.c_str(), readOnly ); + if ( xmpFile == 0 ) return; // The open failed. + this->parent->ioRef = xmpFile; + + XMP_Int64 xmpLen = xmpFile->Length(); + if ( xmpLen > 100*1024*1024 ) { + XMP_Throw ( "XDCAMEX XMP is outrageously large", kXMPErr_InternalFailure ); // Sanity check. + } + + this->xmpPacket.erase(); + this->xmpPacket.append ( (size_t)xmpLen, ' ' ); + + XMP_Int32 ioCount = xmpFile->ReadAll ( (void*)this->xmpPacket.data(), (XMP_Int32)xmpLen ); + + this->packetInfo.offset = 0; + this->packetInfo.length = (XMP_Int32)xmpLen; + FillPacketInfo ( this->xmpPacket, &this->packetInfo ); + + this->containsXMP = true; + +} // XDCAMEX_MetaHandler::CacheFileData + +// ================================================================================================= +// XDCAMEX_MetaHandler::GetTakeDuration +// ==================================== + +void XDCAMEX_MetaHandler::GetTakeDuration ( const std::string & takeURI, std::string & duration ) +{ + + // Some versions of gcc can't tolerate goto's across declarations. + // *** Better yet, avoid this cruft with self-cleaning objects. + #define CleanupAndExit \ + { \ + if ( expat != 0 ) delete expat; \ + takeXMLFile.Close(); \ + return; \ + } + + duration.clear(); + + // Build a directory string to the take .xml file. + + std::string takeDir ( takeURI ); + takeDir.erase ( 0, 1 ); // Change the leading "//" to "/", then all '/' to kDirChar. + if ( kDirChar != '/' ) { + for ( size_t i = 0, limit = takeDir.size(); i < limit; ++i ) { + if ( takeDir[i] == '/' ) takeDir[i] = kDirChar; + } + } + + std::string takePath ( this->rootPath ); + takePath += kDirChar; + takePath += "BPAV"; + takePath += takeDir; + + // Replace .SMI with M01.XML. + if ( takePath.size() > 4 ) { + takePath.erase ( takePath.size() - 4, 4 ); + takePath += "M01.XML"; + } + + // Parse MEDIAPRO.XML + + XML_NodePtr takeRootElem = 0; + XML_NodePtr context = 0; + + Host_IO::FileRef hostRef = Host_IO::Open ( takePath.c_str(), Host_IO::openReadOnly ); + if ( hostRef == Host_IO::noFileRef ) return; // The open failed. + XMPFiles_IO takeXMLFile ( hostRef, takePath.c_str(), Host_IO::openReadOnly ); + + ExpatAdapter * expat = XMP_NewExpatAdapter ( ExpatAdapter::kUseLocalNamespaces ); + if ( this->expat == 0 ) return; + + XMP_Uns8 buffer [64*1024]; + while ( true ) { + XMP_Int32 ioCount = takeXMLFile.Read ( buffer, sizeof(buffer) ); + if ( ioCount == 0 ) break; + expat->ParseBuffer ( buffer, ioCount, false /* not the end */ ); + } + + expat->ParseBuffer ( 0, 0, true ); // End the parse. + takeXMLFile.Close(); + + // Get the root node of the XML tree. + + XML_Node & mediaproXMLTree = expat->tree; + for ( size_t i = 0, limit = mediaproXMLTree.content.size(); i < limit; ++i ) { + if ( mediaproXMLTree.content[i]->kind == kElemNode ) { + takeRootElem = mediaproXMLTree.content[i]; + } + } + if ( takeRootElem == 0 ) CleanupAndExit + + XMP_StringPtr rlName = takeRootElem->name.c_str() + takeRootElem->nsPrefixLen; + if ( ! XMP_LitMatch ( rlName, "NonRealTimeMeta" ) ) CleanupAndExit + + // MediaProfile, Contents + XMP_StringPtr ns = takeRootElem->ns.c_str(); + context = takeRootElem->GetNamedElement ( ns, "Duration" ); + if ( context != 0 ) { + XMP_StringPtr durationValue = context->GetAttrValue ( "value" ); + if ( durationValue != 0 ) duration = durationValue; + } + + CleanupAndExit + #undef CleanupAndExit + +} // XDCAMEX_MetaHandler::GetTakeDuration + +// ================================================================================================= +// XDCAMEX_MetaHandler::GetMediaProMetadata +// ======================================== + +bool XDCAMEX_MetaHandler::GetMediaProMetadata ( SXMPMeta * xmpObjPtr, + const std::string& clipUMID, + bool digestFound ) +{ + // Build a directory string to the MEDIAPRO file. + + std::string mediaproPath; + this->MakeMediaproPath ( &mediaproPath ); + return XDCAM_Support::GetMediaProLegacyMetadata ( xmpObjPtr, clipUMID, mediaproPath, digestFound ); + +} // XDCAMEX_MetaHandler::GetMediaProMetadata + +// ================================================================================================= +// XDCAMEX_MetaHandler::GetTakeUMID +// ================================ + +void XDCAMEX_MetaHandler::GetTakeUMID ( const std::string& clipUMID, + std::string& takeUMID, + std::string& takeXMLURI ) +{ + + // Some versions of gcc can't tolerate goto's across declarations. + // *** Better yet, avoid this cruft with self-cleaning objects. + #define CleanupAndExit \ + { \ + if (expat != 0) delete expat; \ + mediaproXMLFile.Close(); \ + return; \ + } + + takeUMID.clear(); + takeXMLURI.clear(); + + // Build a directory string to the MEDIAPRO file. + + std::string mediapropath ( this->rootPath ); + mediapropath += kDirChar; + mediapropath += "BPAV"; + mediapropath += kDirChar; + mediapropath += "MEDIAPRO.XML"; + + // Parse MEDIAPRO.XML. + + XML_NodePtr mediaproRootElem = 0; + XML_NodePtr contentContext = 0, materialContext = 0; + + Host_IO::FileRef hostRef = Host_IO::Open ( mediapropath.c_str(), Host_IO::openReadOnly ); + if ( hostRef == Host_IO::noFileRef ) return; // The open failed. + XMPFiles_IO mediaproXMLFile ( hostRef, mediapropath.c_str(), Host_IO::openReadOnly ); + + ExpatAdapter * expat = XMP_NewExpatAdapter ( ExpatAdapter::kUseLocalNamespaces ); + if ( this->expat == 0 ) return; + + XMP_Uns8 buffer [64*1024]; + while ( true ) { + XMP_Int32 ioCount = mediaproXMLFile.Read ( buffer, sizeof(buffer) ); + if ( ioCount == 0 ) break; + expat->ParseBuffer ( buffer, ioCount, false /* not the end */ ); + } + + expat->ParseBuffer ( 0, 0, true ); // End the parse. + mediaproXMLFile.Close(); + + // Get the root node of the XML tree. + + XML_Node & mediaproXMLTree = expat->tree; + for ( size_t i = 0, limit = mediaproXMLTree.content.size(); i < limit; ++i ) { + if ( mediaproXMLTree.content[i]->kind == kElemNode ) { + mediaproRootElem = mediaproXMLTree.content[i]; + } + } + + if ( mediaproRootElem == 0 ) CleanupAndExit + XMP_StringPtr rlName = mediaproRootElem->name.c_str() + mediaproRootElem->nsPrefixLen; + if ( ! XMP_LitMatch ( rlName, "MediaProfile" ) ) CleanupAndExit + + // MediaProfile, Contents + + XMP_StringPtr ns = mediaproRootElem->ns.c_str(); + contentContext = mediaproRootElem->GetNamedElement ( ns, "Contents" ); + + if ( contentContext != 0 ) { + + size_t numMaterialElems = contentContext->CountNamedElements ( ns, "Material" ); + + for ( size_t i = 0; i < numMaterialElems; ++i ) { // Iterate over Material tags. + + XML_NodePtr materialElement = contentContext->GetNamedElement ( ns, "Material", i ); + XMP_Assert ( materialElement != 0 ); + + XMP_StringPtr umid = materialElement->GetAttrValue ( "umid" ); + XMP_StringPtr uri = materialElement->GetAttrValue ( "uri" ); + + if ( umid == 0 ) umid = ""; + if ( uri == 0 ) uri = ""; + + size_t numComponents = materialElement->CountNamedElements ( ns, "Component" ); + + for ( size_t j = 0; j < numComponents; ++j ) { + + XML_NodePtr componentElement = materialElement->GetNamedElement ( ns, "Component", j ); + XMP_Assert ( componentElement != 0 ); + + XMP_StringPtr compUMID = componentElement->GetAttrValue ( "umid" ); + + if ( (compUMID != 0) && (compUMID == clipUMID) ) { + takeUMID = umid; + takeXMLURI = uri; + break; + } + + } + + if ( ! takeUMID.empty() ) break; + + } + + } + + CleanupAndExit + #undef CleanupAndExit + +} + +// ================================================================================================= +// XDCAMEX_MetaHandler::ProcessXMP +// =============================== + +void XDCAMEX_MetaHandler::ProcessXMP() +{ + + // Some versions of gcc can't tolerate goto's across declarations. + // *** Better yet, avoid this cruft with self-cleaning objects. + #define CleanupAndExit \ + { \ + bool openForUpdate = XMP_OptionIsSet ( this->parent->openFlags, kXMPFiles_OpenForUpdate ); \ + if ( ! openForUpdate ) this->CleanupLegacyXML(); \ + xmlFile.Close(); \ + return; \ + } + + if ( this->processedXMP ) return; + this->processedXMP = true; // Make sure only called once. + + if ( this->containsXMP ) { + this->xmpObj.ParseFromBuffer ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + } + + // NonRealTimeMeta -> XMP by schema. + std::string thisUMID, takeUMID, takeXMLURI, takeDuration; + std::string xmlPath; + this->MakeClipFilePath ( &xmlPath, "M01.XML" ); + + Host_IO::FileRef hostRef = Host_IO::Open ( xmlPath.c_str(), Host_IO::openReadOnly ); + if ( hostRef == Host_IO::noFileRef ) return; // The open failed. + XMPFiles_IO xmlFile ( hostRef, xmlPath.c_str(), Host_IO::openReadOnly ); + + this->expat = XMP_NewExpatAdapter ( ExpatAdapter::kUseLocalNamespaces ); + if ( this->expat == 0 ) XMP_Throw ( "XDCAMEX_MetaHandler: Can't create Expat adapter", kXMPErr_NoMemory ); + + XMP_Uns8 buffer [64*1024]; + while ( true ) { + XMP_Int32 ioCount = xmlFile.Read ( buffer, sizeof(buffer) ); + if ( ioCount == 0 ) break; + this->expat->ParseBuffer ( buffer, ioCount, false /* not the end */ ); + } + this->expat->ParseBuffer ( 0, 0, true ); // End the parse. + + xmlFile.Close(); + + // The root element should be NonRealTimeMeta in some namespace. Take whatever this file uses. + + XML_Node & xmlTree = this->expat->tree; + XML_NodePtr rootElem = 0; + + for ( size_t i = 0, limit = xmlTree.content.size(); i < limit; ++i ) { + if ( xmlTree.content[i]->kind == kElemNode ) rootElem = xmlTree.content[i]; + } + + if ( rootElem == 0 ) CleanupAndExit + XMP_StringPtr rootLocalName = rootElem->name.c_str() + rootElem->nsPrefixLen; + if ( ! XMP_LitMatch ( rootLocalName, "NonRealTimeMeta" ) ) CleanupAndExit + + this->legacyNS = rootElem->ns; + + // Check the legacy digest. + + XMP_StringPtr legacyNS = this->legacyNS.c_str(); + this->clipMetadata = rootElem; // ! Save the NonRealTimeMeta pointer for other use. + + std::string oldDigest, newDigest; + bool digestFound = this->xmpObj.GetStructField ( kXMP_NS_XMP, "NativeDigests", kXMP_NS_XMP, "XDCAMEX", &oldDigest, 0 ); + if ( digestFound ) { + this->MakeLegacyDigest ( &newDigest ); + if ( oldDigest == newDigest ) CleanupAndExit + } + + // If we get here we need find and import the actual legacy elements using the current namespace. + // Either there is no old digest in the XMP, or the digests differ. In the former case keep any + // existing XMP, in the latter case take new legacy values. + this->containsXMP = XDCAM_Support::GetLegacyMetadata ( &this->xmpObj, rootElem, legacyNS, digestFound, thisUMID ); + + // If this clip is part of a take, add the take number to the relation field, and get the + // duration from the take metadata. + GetTakeUMID ( thisUMID, takeUMID, takeXMLURI ); + + // If this clip is part of a take, update the duration to reflect the take duration rather than + // the clip duration, and add the take name as a shot name. + + if ( ! takeXMLURI.empty() ) { + + // Update duration. This property already exists from clip legacy metadata. + GetTakeDuration ( takeXMLURI, takeDuration ); + if ( ! takeDuration.empty() ) { + this->xmpObj.SetStructField ( kXMP_NS_DM, "duration", kXMP_NS_DM, "value", takeDuration ); + containsXMP = true; + } + + if ( digestFound || (! this->xmpObj.DoesPropertyExist ( kXMP_NS_DM, "shotName" )) ) { + + std::string takeName; + XIO::SplitLeafName ( &takeXMLURI, &takeName ); + + // Check for the xml suffix, and delete if it exists. + size_t pos = takeName.rfind(".SMI"); + if ( pos != std::string::npos ) { + + takeName.erase ( pos ); + + // delete the take number suffix if it exists. + if ( takeName.size() > 3 ) { + + size_t suffix = takeName.size() - 3; + char c1 = takeName[suffix]; + char c2 = takeName[suffix+1]; + char c3 = takeName[suffix+2]; + if ( ('U' == c1) && ('0' <= c2) && (c2 <= '9') && ('0' <= c3) && (c3 <= '9') ) { + takeName.erase ( suffix ); + } + + this->xmpObj.SetProperty ( kXMP_NS_DM, "shotName", takeName, kXMP_DeleteExisting ); + containsXMP = true; + + } + + } + + } + + } + + if ( (! takeUMID.empty()) && + (digestFound || (! this->xmpObj.DoesPropertyExist ( kXMP_NS_DC, "relation" ))) ) { + this->xmpObj.DeleteProperty ( kXMP_NS_DC, "relation" ); + this->xmpObj.AppendArrayItem ( kXMP_NS_DC, "relation", kXMP_PropArrayIsUnordered, takeUMID ); + this->containsXMP = true; + } + + this->containsXMP |= GetMediaProMetadata ( &this->xmpObj, thisUMID, digestFound ); + + CleanupAndExit + #undef CleanupAndExit + +} // XDCAMEX_MetaHandler::ProcessXMP + + +// ================================================================================================= +// XDCAMEX_MetaHandler::UpdateFile +// =============================== +// +// Note that UpdateFile is only called from XMPFiles::CloseFile, so it is OK to close the file here. + +void XDCAMEX_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + if ( ! this->needsUpdate ) return; + this->needsUpdate = false; // Make sure only called once. + + XMP_Assert ( this->parent->UsesLocalIO() ); + + // Update the internal legacy XML tree if we have one, and set the digest in the XMP. + + bool updateLegacyXML = false; + + if ( this->clipMetadata != 0 ) { + updateLegacyXML = XDCAM_Support::SetLegacyMetadata ( this->clipMetadata, &this->xmpObj, this->legacyNS.c_str()); + } + + std::string newDigest; + this->MakeLegacyDigest ( &newDigest ); + this->xmpObj.SetStructField ( kXMP_NS_XMP, "NativeDigests", kXMP_NS_XMP, "XDCAMEX", newDigest.c_str(), kXMP_DeleteExisting ); + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, this->GetSerializeOptions() ); + + // ----------------------------------------------------------------------- + // Update the XMP file first, don't let legacy XML failures block the XMP. + + std::string xmpPath; + this->MakeClipFilePath ( &xmpPath, "M01.XMP" ); + + bool haveXMP = Host_IO::Exists ( xmpPath.c_str() ); + if ( ! haveXMP ) { + XMP_Assert ( this->parent->ioRef == 0 ); + Host_IO::Create ( xmpPath.c_str() ); + this->parent->ioRef = XMPFiles_IO::New_XMPFiles_IO ( xmpPath.c_str(), Host_IO::openReadWrite ); + if ( this->parent->ioRef == 0 ) XMP_Throw ( "Failure opening XDCAMEX XMP file", kXMPErr_ExternalFailure ); + } + + XMP_IO* xmpFile = this->parent->ioRef; + XMP_Assert ( xmpFile != 0 ); + XIO::ReplaceTextFile ( xmpFile, this->xmpPacket, (haveXMP & doSafeUpdate) ); + + // -------------------------------------------- + // Now update the legacy XML file if necessary. + + if ( updateLegacyXML ) { + + std::string legacyXML, xmlPath; + this->expat->tree.Serialize ( &legacyXML ); + this->MakeClipFilePath ( &xmlPath, "M01.XML" ); + + bool haveXML = Host_IO::Exists ( xmlPath.c_str() ); + if ( ! haveXML ) Host_IO::Create ( xmlPath.c_str() ); + + Host_IO::FileRef hostRef = Host_IO::Open ( xmlPath.c_str(), Host_IO::openReadWrite ); + if ( hostRef == Host_IO::noFileRef ) XMP_Throw ( "Failure opening XDCAMEX legacy XML file", kXMPErr_ExternalFailure ); + XMPFiles_IO origXML ( hostRef, xmlPath.c_str(), Host_IO::openReadWrite ); + XIO::ReplaceTextFile ( &origXML, legacyXML, (haveXML & doSafeUpdate) ); + origXML.Close(); + + } + +} // XDCAMEX_MetaHandler::UpdateFile + +// ================================================================================================= +// XDCAMEX_MetaHandler::WriteTempFile +// ================================== + +void XDCAMEX_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + + // ! WriteTempFile is not supposed to be called for handlers that own the file. + XMP_Throw ( "XDCAMEX_MetaHandler::WriteTempFile should not be called", kXMPErr_InternalFailure ); + +} // XDCAMEX_MetaHandler::WriteTempFile + +// ================================================================================================= diff --git a/XMPFiles/source/FileHandlers/XDCAMEX_Handler.hpp b/XMPFiles/source/FileHandlers/XDCAMEX_Handler.hpp new file mode 100644 index 0000000..a8ab89f --- /dev/null +++ b/XMPFiles/source/FileHandlers/XDCAMEX_Handler.hpp @@ -0,0 +1,85 @@ +#ifndef __XDCAMEX_Handler_hpp__ +#define __XDCAMEX_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2008 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. + +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +#include "source/ExpatAdapter.hpp" + +// ================================================================================================= +/// \file XDCAMEX_Handler.hpp +/// \brief Folder format handler for XDCAMEX. +// ================================================================================================= + +extern XMPFileHandler * XDCAMEX_MetaHandlerCTor ( XMPFiles * parent ); + +extern bool XDCAMEX_CheckFormat ( XMP_FileFormat format, + const std::string & rootPath, + const std::string & gpName, + const std::string & parentName, + const std::string & leafName, + XMPFiles * parent ); + +static const XMP_OptionBits kXDCAMEX_HandlerFlags = (kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_CanRewrite | + kXMPFiles_PrefersInPlace | + kXMPFiles_CanReconcile | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_HandlerOwnsFile | + kXMPFiles_AllowsSafeUpdate | + kXMPFiles_FolderBasedFormat); + +class XDCAMEX_MetaHandler : public XMPFileHandler +{ +public: + + bool GetFileModDate ( XMP_DateTime * modDate ); + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + XMP_OptionBits GetSerializeOptions() // *** These should be standard for standalone XMP files. + { return (kXMP_UseCompactFormat | kXMP_OmitPacketWrapper); }; + + XDCAMEX_MetaHandler ( XMPFiles * _parent ); + virtual ~XDCAMEX_MetaHandler(); + +private: + + XDCAMEX_MetaHandler() : expat(0) {}; // Hidden on purpose. + + bool MakeClipFilePath ( std::string * path, XMP_StringPtr suffix, bool checkFile = false ); + bool MakeMediaproPath ( std::string * path, bool checkFile = false ); + void MakeLegacyDigest ( std::string * digestStr ); + + void GetTakeUMID ( const std::string& clipUMID, std::string& takeUMID, std::string& takeXMLURI ); + void GetTakeDuration ( const std::string& takeUMID, std::string& duration ); + bool GetMediaProMetadata ( SXMPMeta * xmpObjPtr, const std::string& clipUMID, bool digestFound ); + + void CleanupLegacyXML(); + + std::string rootPath, clipName, xdcNS, legacyNS, clipUMID; + + ExpatAdapter * expat; + XML_Node * clipMetadata; + +}; // XDCAMEX_MetaHandler + +// ================================================================================================= + +#endif /* __XDCAMEX_Handler_hpp__ */ diff --git a/XMPFiles/source/FileHandlers/XDCAM_Handler.cpp b/XMPFiles/source/FileHandlers/XDCAM_Handler.cpp new file mode 100644 index 0000000..fab0925 --- /dev/null +++ b/XMPFiles/source/FileHandlers/XDCAM_Handler.cpp @@ -0,0 +1,837 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2007 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FileHandlers/XDCAM_Handler.hpp" +#include "XMPFiles/source/FormatSupport/XDCAM_Support.hpp" +#include "third-party/zuid/interfaces/MD5.h" + +using namespace std; + +// ================================================================================================= +/// \file XDCAM_Handler.cpp +/// \brief Folder format handler for XDCAM. +/// +/// This handler is for the XDCAM video format. This is a pseudo-package, visible files but with a very +/// well-defined layout and naming rules. There are 2 different layouts for XDCAM, called FAM and SAM. +/// The FAM layout is used by "normal" XDCAM devices. The SAM layout is used by XDCAM-EX devices. +/// +/// A typical FAM layout looks like (note mixed case for General, Clip, Edit, and Sub folders): +/// +/// .../MyMovie/ +/// INDEX.XML +/// DISCMETA.XML +/// MEDIAPRO.XML +/// General/ +/// unknown files +/// Clip/ +/// C0001.MXF +/// C0001M01.XML +/// C0001M01.XMP +/// C0002.MXF +/// C0002M01.XML +/// C0002M01.XMP +/// Sub/ +/// C0001S01.MXF +/// C0002S01.MXF +/// Edit/ +/// E0001E01.SMI +/// E0001M01.XML +/// E0002E01.SMI +/// E0002M01.XML +/// +/// A typical SAM layout looks like: +/// +/// .../MyMovie/ +/// GENERAL/ +/// unknown files +/// PROAV/ +/// INDEX.XML +/// INDEX.BUP +/// DISCMETA.XML +/// DISCINFO.XML +/// DISCINFO.BUP +/// CLPR/ +/// C0001/ +/// C0001C01.SMI +/// C0001V01.MXF +/// C0001A01.MXF +/// C0001A02.MXF +/// C0001R01.BIM +/// C0001I01.PPN +/// C0001M01.XML +/// C0001M01.XMP +/// C0001S01.MXF +/// C0002/ +/// ... +/// EDTR/ +/// E0001/ +/// E0001E01.SMI +/// E0001M01.XML +/// E0002/ +/// ... +/// +/// Note that the Sony documentation uses the folder names "General", "Clip", "Sub", and "Edit". We +/// use all caps here. Common code has already shifted the names, we want to be case insensitive. +/// +/// From the user's point of view, .../MyMovie contains XDCAM stuff, in this case 2 clips whose raw +/// names are C0001 and C0002. There may be mapping information for nicer clip names to the raw +/// names, but that can be ignored for now. Each clip is stored as a collection of files, each file +/// holding some specific aspect of the clip's data. +/// +/// The XDCAM handler operates on clips. The path from the client of XMPFiles can be either a logical +/// clip path, like ".../MyMovie/C0001", or a full path to one of the files. In the latter case the +/// handler must figure out the intended clip, it must not blindly use the named file. +/// +/// Once the XDCAM structure and intended clip are identified, the handler only deals with the .XMP +/// and .XML files in the CLIP or CLPR/<clip> folders. The .XMP file, if present, contains the XMP +/// for the clip. The .XML file must be present to define the existance of the clip. It contains a +/// variety of information about the clip, including some legacy metadata. +/// +// ================================================================================================= + +// ================================================================================================= +// XDCAM_CheckFormat +// ================= +// +// This version does fairly simple checks. The top level folder (.../MyMovie) must have exactly 1 +// child, a folder called CONTENTS. This must have a subfolder called CLIP. It may also have +// subfolders called VIDEO, AUDIO, ICON, VOICE, and PROXY. Any mixture of these additional folders +// is allowed, but no other children are allowed in CONTENTS. The CLIP folder must contain a .XML +// file for the desired clip. The name checks are case insensitive. +// +// The state of the string parameters depends on the form of the path passed by the client. If the +// client passed a logical clip path, like ".../MyMovie/C0001", the parameters are: +// rootPath - ".../MyMovie" +// gpName - empty +// parentName - empty +// leafName - "C0001" +// +// If the client passed a FAM file path, like ".../MyMovie/Edit/E0001E01.SMI", they are: +// rootPath - "..." +// gpName - "MyMovie" +// parentName - "EDIT" (common code has shifted the case) +// leafName - "E0001E01" +// +// If the client passed a SAM file path, like ".../MyMovie/PROAV/CLPR/C0001/C0001A02.MXF", they are: +// rootPath - ".../MyMovie/PROAV" +// gpName - "CLPR" +// parentName - "C0001" +// leafName - "C0001A02" +// +// For both FAM and SAM the leading character of the leafName for an existing file might be coerced +// to 'C' to form the logical clip name. And suffix such as "M01" must be removed for FAM. We don't +// need to worry about that for SAM, that uses the <clip> folder name. + +// ! The FAM format supports general clip file names through an ALIAS.XML mapping file. The simple +// ! existence check has an edge case bug, left to be fixed later. If the ALIAS.XML file exists, but +// ! some of the clips still have "raw" names, and we're passed an existing file path in the EDIT +// ! folder, we will fail to do the leading 'E' to 'C' coercion. We might also erroneously remove a +// ! suffix from a mapped essence file with a name like ClipX01.MXF. + +// ! The common code has shifted the gpName, parentName, and leafName strings to uppercase. It has +// ! also made sure that for a logical clip path the rootPath is an existing folder, and that the +// ! file exists for a full file path. + +bool XDCAM_CheckFormat ( XMP_FileFormat format, + const std::string & _rootPath, + const std::string & _gpName, + const std::string & parentName, + const std::string & leafName, + XMPFiles * parent ) +{ + std::string rootPath = _rootPath; // ! Need tweaking in the existing file cases (FAM and SAM). + std::string gpName = _gpName; + + bool isFAM = false; + + std::string tempPath, childName; + + std::string clipName = leafName; + + // Do some basic checks on the root path and component names. Decide if this is FAM or SAM. + + if ( gpName.empty() != parentName.empty() ) return false; // Must be both empty or both non-empty. + + if ( gpName.empty() ) { + + // This is the logical clip path case. Just look for PROAV to see if this is FAM or SAM. + if ( Host_IO::GetChildMode ( rootPath.c_str(), "PROAV" ) != Host_IO::kFMode_IsFolder ) isFAM = true; + + } else { + + // This is the existing file case. See if this is FAM or SAM, tweak the clip name as needed. + + if ( (parentName == "CLIP") || (parentName == "EDIT") || (parentName == "SUB") ) { + // ! The standard says Clip/Edit/Sub, but the caller has already shifted to upper case. + isFAM = true; + } else if ( (gpName != "CLPR") && (gpName != "EDTR") ) { + return false; + } + + if ( isFAM ) { + + // Put the proper root path together. Clean up the clip name if needed. + + if ( ! rootPath.empty() ) rootPath += kDirChar; + rootPath += gpName; + gpName.erase(); + + if ( Host_IO::GetChildMode ( rootPath.c_str(), "ALIAS.XML" ) != Host_IO::kFMode_IsFile ) { + clipName[0] = 'C'; // ! See notes above about pending bug. + } + + if ( clipName.size() > 3 ) { + size_t clipMid = clipName.size() - 3; + char c1 = clipName[clipMid]; + char c2 = clipName[clipMid+1]; + char c3 = clipName[clipMid+2]; + if ( ('A' <= c1) && (c1 <= 'Z') && + ('0' <= c2) && (c2 <= '9') && ('0' <= c3) && (c3 <= '9') ) { + clipName.erase ( clipMid ); + } + } + + } else { + + // Fix the clip name. Check for and strip the "PROAV" suffix on the root path. + + clipName = parentName; // ! We have a folder with the (almost) exact clip name. + clipName[0] = 'C'; + + std::string proav; + XIO::SplitLeafName ( &rootPath, &proav ); + MakeUpperCase ( &proav ); + if ( (rootPath.empty()) || (proav != "PROAV") ) return false; + + } + + } + + // Make sure the general XDCAM package structure is legit. Set tempPath as a bogus path of the + // form <root>/<FAM-or-SAM>/<clip>, e.g. ".../MyMovie/FAM/C0001". This is passed the handler via + // the tempPtr hackery. + + if ( isFAM ) { + + if ( (format != kXMP_XDCAM_FAMFile) && (format != kXMP_UnknownFile) ) return false; + + tempPath = rootPath; + + if ( Host_IO::GetChildMode ( tempPath.c_str(), "INDEX.XML" ) != Host_IO::kFMode_IsFile ) return false; + if ( Host_IO::GetChildMode ( tempPath.c_str(), "DISCMETA.XML" ) != Host_IO::kFMode_IsFile ) return false; + if ( Host_IO::GetChildMode ( tempPath.c_str(), "MEDIAPRO.XML" ) != Host_IO::kFMode_IsFile ) return false; + + tempPath += kDirChar; + tempPath += "Clip"; // ! Yes, mixed case. + tempPath += kDirChar; + tempPath += clipName; + tempPath += "M01.XML"; + if ( Host_IO::GetFileMode ( tempPath.c_str() ) != Host_IO::kFMode_IsFile ) return false; + + tempPath = rootPath; + tempPath += kDirChar; + tempPath += "FAM"; + tempPath += kDirChar; + tempPath += clipName; + + } else { + + if ( (format != kXMP_XDCAM_SAMFile) && (format != kXMP_UnknownFile) ) return false; + + // We already know about the PROAV folder, just check below it. + + tempPath = rootPath; + tempPath += kDirChar; + tempPath += "PROAV"; + + if ( Host_IO::GetChildMode ( tempPath.c_str(), "INDEX.XML" ) != Host_IO::kFMode_IsFile ) return false; + if ( Host_IO::GetChildMode ( tempPath.c_str(), "DISCMETA.XML" ) != Host_IO::kFMode_IsFile ) return false; + if ( Host_IO::GetChildMode ( tempPath.c_str(), "DISCINFO.XML" ) != Host_IO::kFMode_IsFile ) return false; + if ( Host_IO::GetChildMode ( tempPath.c_str(), "CLPR" ) != Host_IO::kFMode_IsFolder ) return false; + + tempPath += kDirChar; + tempPath += "CLPR"; + tempPath += kDirChar; + tempPath += clipName; + if ( Host_IO::GetFileMode ( tempPath.c_str() ) != Host_IO::kFMode_IsFolder ) return false; + + tempPath += kDirChar; + tempPath += clipName; + tempPath += "M01.XML"; + if ( Host_IO::GetFileMode ( tempPath.c_str() ) != Host_IO::kFMode_IsFile ) return false; + + tempPath = rootPath; + tempPath += kDirChar; + tempPath += "SAM"; + tempPath += kDirChar; + tempPath += clipName; + + } + + // Save the pseudo-path for the handler object. A bit of a hack, but the only way to get info + // from here to there. + + size_t pathLen = tempPath.size() + 1; // Include a terminating nul. + parent->tempPtr = malloc ( pathLen ); + if ( parent->tempPtr == 0 ) XMP_Throw ( "No memory for XDCAM clip info", kXMPErr_NoMemory ); + memcpy ( parent->tempPtr, tempPath.c_str(), pathLen ); // AUDIT: Safe, allocated above. + + return true; + +} // XDCAM_CheckFormat + +// ================================================================================================= + +static void* CreatePseudoClipPath ( const std::string & clientPath ) { + + // Used to create the clip pseudo path when the CheckFormat function is skipped. + + std::string pseudoPath = clientPath; + std::string clipName; + bool isSAM; + + size_t pathLen; + void* tempPtr = 0; + + if ( ! Host_IO::Exists ( pseudoPath.c_str() ) ) { + + // This is the logical clip path case. Look for PROAV to see if this is FAM or SAM. + + XIO::SplitLeafName ( &pseudoPath, &clipName ); // Extract the logical clip name, no extension. + isSAM = ( Host_IO::GetChildMode ( pseudoPath.c_str(), "PROAV" ) == Host_IO::kFMode_IsFolder ); + + } else { + + // The client passed a physical path. We have separate cases for FAM and SAM. If the last + // folder, the parent of the file, is Clip, Edit, or Sub (ignoring case) then this is FAM + // and things are a bit messy. For SAM, the parent folder is the almost clip name. + + std::string parentName, ignored; + + XIO::SplitLeafName ( &pseudoPath, &clipName ); // Extract the logical clip name. + XIO::SplitFileExtension ( &clipName, &ignored ); + + XIO::SplitLeafName ( &pseudoPath, &parentName ); + MakeUpperCase ( &parentName ); + isSAM = ( (parentName != "CLIP") && (parentName != "EDIT") && (parentName != "SUB") ); + + if ( isSAM ) { + + // SAM is easy, the parent name is almost the clip name, the first letter gets coerced + // to 'C'. There are 2 other folders to remove from the path. + + clipName = parentName; + clipName[0] = 'C'; + XIO::SplitLeafName ( &pseudoPath, &ignored ); // Remove the 2 intermediate folder levels. + XIO::SplitLeafName ( &pseudoPath, &ignored ); + + } else { + + // FAM is a bit messy, study the comments and code of XDCAM_CheckFormat for details. + + if ( Host_IO::GetChildMode ( pseudoPath.c_str(), "ALIAS.XML" ) != Host_IO::kFMode_IsFile ) { + clipName[0] = 'C'; // ! See notes in XDCAM_CheckFormat about pending bug. + } + + if ( clipName.size() > 3 ) { + size_t clipMid = clipName.size() - 3; + char c1 = clipName[clipMid]; + char c2 = clipName[clipMid+1]; + char c3 = clipName[clipMid+2]; + if ( ('A' <= c1) && (c1 <= 'Z') && + ('0' <= c2) && (c2 <= '9') && ('0' <= c3) && (c3 <= '9') ) { + clipName.erase ( clipMid ); + } + } + + } + + } + + pseudoPath += kDirChar; + if ( isSAM ) { + pseudoPath += "SAM"; + } else { + pseudoPath += "FAM"; + } + pseudoPath += kDirChar; + pseudoPath += clipName; + + pathLen = pseudoPath.size() + 1; // Include a terminating nul. + tempPtr = malloc ( pathLen ); + if ( tempPtr == 0 ) XMP_Throw ( "No memory for XDCAM clip info", kXMPErr_NoMemory ); + memcpy ( tempPtr, pseudoPath.c_str(), pathLen ); + + return tempPtr; + +} // CreatePseudoClipPath + +// ================================================================================================= +// XDCAM_MetaHandlerCTor +// ===================== + +XMPFileHandler * XDCAM_MetaHandlerCTor ( XMPFiles * parent ) +{ + return new XDCAM_MetaHandler ( parent ); + +} // XDCAM_MetaHandlerCTor + +// ================================================================================================= +// XDCAM_MetaHandler::XDCAM_MetaHandler +// ==================================== + +XDCAM_MetaHandler::XDCAM_MetaHandler ( XMPFiles * _parent ) : isFAM(false), expat(0) +{ + + this->parent = _parent; // Inherited, can't set in the prefix. + this->handlerFlags = kXDCAM_HandlerFlags; + this->stdCharForm = kXMP_Char8Bit; + + // Extract the root path, clip name, and FAM/SAM flag from tempPtr. + + if ( this->parent->tempPtr == 0 ) { + // The CheckFormat call might have been skipped. + this->parent->tempPtr = CreatePseudoClipPath ( this->parent->filePath ); + } + + this->rootPath.assign ( (char*) this->parent->tempPtr ); + free ( this->parent->tempPtr ); + this->parent->tempPtr = 0; + + XIO::SplitLeafName ( &this->rootPath, &this->clipName ); + + std::string temp; + XIO::SplitLeafName ( &this->rootPath, &temp ); + XMP_Assert ( (temp == "FAM") || (temp == "SAM") ); + if ( temp == "FAM" ) this->isFAM = true; + XMP_Assert ( this->isFAM ? (this->parent->format == kXMP_XDCAM_FAMFile) : (this->parent->format == kXMP_XDCAM_SAMFile) ); + +} // XDCAM_MetaHandler::XDCAM_MetaHandler + +// ================================================================================================= +// XDCAM_MetaHandler::~XDCAM_MetaHandler +// ===================================== + +XDCAM_MetaHandler::~XDCAM_MetaHandler() +{ + + this->CleanupLegacyXML(); + if ( this->parent->tempPtr != 0 ) { + free ( this->parent->tempPtr ); + this->parent->tempPtr = 0; + } + +} // XDCAM_MetaHandler::~XDCAM_MetaHandler + +// ================================================================================================= +// XDCAM_MetaHandler::MakeClipFilePath +// =================================== + +bool XDCAM_MetaHandler::MakeClipFilePath ( std::string * path, XMP_StringPtr suffix, bool checkFile /* = false */ ) +{ + + *path = this->rootPath; + *path += kDirChar; + + if ( this->isFAM ) { + *path += "Clip"; // ! Yes, mixed case. + } else { + *path += "PROAV"; + *path += kDirChar; + *path += "CLPR"; + *path += kDirChar; + *path += this->clipName; + } + + *path += kDirChar; + *path += this->clipName; + *path += suffix; + + if ( ! checkFile ) return true; + return Host_IO::Exists ( path->c_str() ); + +} // XDCAM_MetaHandler::MakeClipFilePath + +// ================================================================================================= +// XDCAM_MetaHandler::MakeMediaproPath +// =================================== + +bool XDCAM_MetaHandler::MakeMediaproPath ( std::string * path, bool checkFile /* = false */ ) +{ + + *path = this->rootPath; + *path += kDirChar; + *path += "MEDIAPRO.XML"; + + if ( ! checkFile ) return true; + return Host_IO::Exists ( path->c_str() ); + +} // XDCAM_MetaHandler::MakeMediaproPath + +// ================================================================================================= +// XDCAM_MetaHandler::MakeLegacyDigest +// =================================== + +// *** Early hack version. + +#define kHexDigits "0123456789ABCDEF" + +void XDCAM_MetaHandler::MakeLegacyDigest ( std::string * digestStr ) +{ + digestStr->erase(); + if ( this->clipMetadata == 0 ) return; // Bail if we don't have any legacy XML. + XMP_Assert ( this->expat != 0 ); + + XMP_StringPtr xdcNS = this->xdcNS.c_str(); + XML_NodePtr legacyContext, legacyProp; + + legacyContext = this->clipMetadata->GetNamedElement ( xdcNS, "Access" ); + if ( legacyContext == 0 ) return; + + MD5_CTX context; + unsigned char digestBin [16]; + MD5Init ( &context ); + + legacyProp = legacyContext->GetNamedElement ( xdcNS, "Creator" ); + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() && (! legacyProp->content.empty()) ) { + const XML_Node * xmlValue = legacyProp->content[0]; + MD5Update ( &context, (XMP_Uns8*)xmlValue->value.c_str(), (unsigned int)xmlValue->value.size() ); + } + + legacyProp = legacyContext->GetNamedElement ( xdcNS, "CreationDate" ); + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() && (! legacyProp->content.empty()) ) { + const XML_Node * xmlValue = legacyProp->content[0]; + MD5Update ( &context, (XMP_Uns8*)xmlValue->value.c_str(), (unsigned int)xmlValue->value.size() ); + } + + legacyProp = legacyContext->GetNamedElement ( xdcNS, "LastUpdateDate" ); + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() && (! legacyProp->content.empty()) ) { + const XML_Node * xmlValue = legacyProp->content[0]; + MD5Update ( &context, (XMP_Uns8*)xmlValue->value.c_str(), (unsigned int)xmlValue->value.size() ); + } + + MD5Final ( digestBin, &context ); + + char buffer [40]; + for ( int in = 0, out = 0; in < 16; in += 1, out += 2 ) { + XMP_Uns8 byte = digestBin[in]; + buffer[out] = kHexDigits [ byte >> 4 ]; + buffer[out+1] = kHexDigits [ byte & 0xF ]; + } + buffer[32] = 0; + digestStr->append ( buffer ); + +} // XDCAM_MetaHandler::MakeLegacyDigest + +// ================================================================================================= +// P2_MetaHandler::CleanupLegacyXML +// ================================ + +void XDCAM_MetaHandler::CleanupLegacyXML() +{ + + if ( this->expat != 0 ) { delete ( this->expat ); this->expat = 0; } + + clipMetadata = 0; // ! Was a pointer into the expat tree. + +} // XDCAM_MetaHandler::CleanupLegacyXML + +// ================================================================================================= +// XDCAM_MetaHandler::GetFileModDate +// ================================= + +static inline bool operator< ( const XMP_DateTime & left, const XMP_DateTime & right ) { + int compare = SXMPUtils::CompareDateTime ( left, right ); + return (compare < 0); +} + +bool XDCAM_MetaHandler::GetFileModDate ( XMP_DateTime * modDate ) +{ + + // The XDCAM FAM locations of metadata: + // MEDIAPRO.XML // Has non-XMP metadata. + // Clip: + // C0001_50i_DVCAM_43_4chM01.XML // Has non-XMP metadata. + // C0001_50i_DVCAM_43_4chM01.XMP + + // The XDCAM SAM locations of metadata: + // PROAV: + // CLPR: + // C0001: + // C0001M01.XML // Has non-XMP metadata. + // C0001M01.XMP + + bool ok, haveDate = false; + std::string fullPath; + XMP_DateTime oneDate, junkDate; + if ( modDate == 0 ) modDate = &junkDate; + + std::string mediaproPath; + ok = MakeMediaproPath ( &mediaproPath, true /* checkFile */ ); + if ( ok ) ok = Host_IO::GetModifyDate ( mediaproPath.c_str(), &oneDate ); + if ( ok ) { + if ( (! haveDate) || (*modDate < oneDate) ) *modDate = oneDate; + haveDate = true; + } + + ok = this->MakeClipFilePath ( &fullPath, "M01.XML", true /* checkFile */ ); + if ( ok ) ok = Host_IO::GetModifyDate ( fullPath.c_str(), &oneDate ); + if ( ok ) { + if ( (! haveDate) || (*modDate < oneDate) ) *modDate = oneDate; + haveDate = true; + } + + ok = this->MakeClipFilePath ( &fullPath, "M01.XMP", true /* checkFile */ ); + if ( ok ) ok = Host_IO::GetModifyDate ( fullPath.c_str(), &oneDate ); + if ( ok ) { + if ( (! haveDate) || (*modDate < oneDate) ) *modDate = oneDate; + haveDate = true; + } + + return haveDate; + +} // XDCAM_MetaHandler::GetFileModDate + +// ================================================================================================= +// XDCAM_MetaHandler::CacheFileData +// ================================ + +void XDCAM_MetaHandler::CacheFileData() +{ + XMP_Assert ( ! this->containsXMP ); + + if ( this->parent->UsesClientIO() ) { + XMP_Throw ( "XDCAM cannot be used with client-managed I/O", kXMPErr_InternalFailure ); + } + + // See if the clip's .XMP file exists. + + std::string xmpPath; + this->MakeClipFilePath ( &xmpPath, "M01.XMP" ); + if ( Host_IO::GetFileMode ( xmpPath.c_str() ) != Host_IO::kFMode_IsFile ) return; // No XMP. + + // Read the entire .XMP file. + + bool readOnly = XMP_OptionIsClear ( this->parent->openFlags, kXMPFiles_OpenForUpdate ); + + XMP_Assert ( this->parent->ioRef == 0 ); + XMPFiles_IO* xmpFile = XMPFiles_IO::New_XMPFiles_IO ( xmpPath.c_str(), readOnly ); + if ( xmpFile == 0 ) return; // The open failed. + this->parent->ioRef = xmpFile; + + XMP_Int64 xmpLen = xmpFile->Length(); + if ( xmpLen > 100*1024*1024 ) { + XMP_Throw ( "XDCAM XMP is outrageously large", kXMPErr_InternalFailure ); // Sanity check. + } + + this->xmpPacket.erase(); + this->xmpPacket.append ( (size_t)xmpLen, ' ' ); + + XMP_Int32 ioCount = xmpFile->ReadAll ( (void*)this->xmpPacket.data(), (XMP_Int32)xmpLen ); + + this->packetInfo.offset = 0; + this->packetInfo.length = (XMP_Int32)xmpLen; + FillPacketInfo ( this->xmpPacket, &this->packetInfo ); + + this->containsXMP = true; + +} // XDCAM_MetaHandler::CacheFileData + +// ================================================================================================= +// XDCAM_MetaHandler::GetMediaProMetadata +// ====================================== + +bool XDCAM_MetaHandler::GetMediaProMetadata ( SXMPMeta * xmpObjPtr, + const std::string& clipUMID, + bool digestFound ) +{ + if (!this->isFAM) return false; + + // Build a directory string to the MEDIAPRO file. + + std::string mediaproPath; + MakeMediaproPath ( &mediaproPath ); + return XDCAM_Support::GetMediaProLegacyMetadata ( xmpObjPtr, clipUMID, mediaproPath, digestFound ); + +} + +// ================================================================================================= +// XDCAM_MetaHandler::ProcessXMP +// ============================= + +void XDCAM_MetaHandler::ProcessXMP() +{ + + // Some versions of gcc can't tolerate goto's across declarations. + // *** Better yet, avoid this cruft with self-cleaning objects. + #define CleanupAndExit \ + { \ + bool openForUpdate = XMP_OptionIsSet ( this->parent->openFlags, kXMPFiles_OpenForUpdate ); \ + if ( ! openForUpdate ) this->CleanupLegacyXML(); \ + return; \ + } + + if ( this->processedXMP ) return; + this->processedXMP = true; // Make sure only called once. + + if ( this->containsXMP ) { + this->xmpObj.ParseFromBuffer ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + } + + // NonRealTimeMeta -> XMP by schema + std::string xmlPath, umid; + this->MakeClipFilePath ( &xmlPath, "M01.XML" ); + + Host_IO::FileRef hostRef = Host_IO::Open ( xmlPath.c_str(), Host_IO::openReadOnly ); + if ( hostRef == Host_IO::noFileRef ) return; // The open failed. + XMPFiles_IO xmlFile ( hostRef, xmlPath.c_str(), Host_IO::openReadOnly ); + + this->expat = XMP_NewExpatAdapter ( ExpatAdapter::kUseLocalNamespaces ); + if ( this->expat == 0 ) XMP_Throw ( "XDCAM_MetaHandler: Can't create Expat adapter", kXMPErr_NoMemory ); + + XMP_Uns8 buffer [64*1024]; + while ( true ) { + XMP_Int32 ioCount = xmlFile.Read ( buffer, sizeof(buffer) ); + if ( ioCount == 0 ) break; + this->expat->ParseBuffer ( buffer, ioCount, false /* not the end */ ); + } + this->expat->ParseBuffer ( 0, 0, true ); // End the parse. + + xmlFile.Close(); + + // The root element should be NonRealTimeMeta in some namespace. Take whatever this file uses. + + XML_Node & xmlTree = this->expat->tree; + XML_NodePtr rootElem = 0; + + for ( size_t i = 0, limit = xmlTree.content.size(); i < limit; ++i ) { + if ( xmlTree.content[i]->kind == kElemNode ) { + rootElem = xmlTree.content[i]; + } + } + + if ( rootElem == 0 ) CleanupAndExit + XMP_StringPtr rootLocalName = rootElem->name.c_str() + rootElem->nsPrefixLen; + if ( ! XMP_LitMatch ( rootLocalName, "NonRealTimeMeta" ) ) CleanupAndExit + + this->legacyNS = rootElem->ns; + + // Check the legacy digest. + + XMP_StringPtr legacyNS = this->legacyNS.c_str(); + + this->clipMetadata = rootElem; // ! Save the NonRealTimeMeta pointer for other use. + + std::string oldDigest, newDigest; + bool digestFound = this->xmpObj.GetStructField ( kXMP_NS_XMP, "NativeDigests", kXMP_NS_XMP, "XDCAM", &oldDigest, 0 ); + if ( digestFound ) { + this->MakeLegacyDigest ( &newDigest ); + if ( oldDigest == newDigest ) CleanupAndExit + } + + // If we get here we need find and import the actual legacy elements using the current namespace. + // Either there is no old digest in the XMP, or the digests differ. In the former case keep any + // existing XMP, in the latter case take new legacy values. + + this->containsXMP = XDCAM_Support::GetLegacyMetadata ( &this->xmpObj, rootElem, legacyNS, digestFound, umid ); + this->containsXMP |= GetMediaProMetadata ( &this->xmpObj, umid, digestFound ); + + CleanupAndExit + #undef CleanupAndExit + +} // XDCAM_MetaHandler::ProcessXMP + +// ================================================================================================= +// XDCAM_MetaHandler::UpdateFile +// ============================= +// +// Note that UpdateFile is only called from XMPFiles::CloseFile, so it is OK to close the file here. + +void XDCAM_MetaHandler::UpdateFile ( bool doSafeUpdate ) +{ + if ( ! this->needsUpdate ) return; + this->needsUpdate = false; // Make sure only called once. + + XMP_Assert ( this->parent->UsesLocalIO() ); + + // Update the internal legacy XML tree if we have one, and set the digest in the XMP. + + bool updateLegacyXML = false; + + if ( this->clipMetadata != 0 ) { + updateLegacyXML = XDCAM_Support::SetLegacyMetadata ( this->clipMetadata, &this->xmpObj, this->legacyNS.c_str()); + } + + std::string newDigest; + this->MakeLegacyDigest ( &newDigest ); + this->xmpObj.SetStructField ( kXMP_NS_XMP, "NativeDigests", kXMP_NS_XMP, "XDCAM", newDigest.c_str(), kXMP_DeleteExisting ); + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, this->GetSerializeOptions() ); + + // ----------------------------------------------------------------------- + // Update the XMP file first, don't let legacy XML failures block the XMP. + + std::string xmpPath; + this->MakeClipFilePath ( &xmpPath, "M01.XMP" ); + + bool haveXMP = Host_IO::Exists ( xmpPath.c_str() ); + if ( ! haveXMP ) { + XMP_Assert ( this->parent->ioRef == 0 ); + Host_IO::Create ( xmpPath.c_str() ); + this->parent->ioRef = XMPFiles_IO::New_XMPFiles_IO ( xmpPath.c_str(), Host_IO::openReadWrite ); + if ( this->parent->ioRef == 0 ) XMP_Throw ( "Failure opening XDCAM XMP file", kXMPErr_ExternalFailure ); + } + + XMP_IO* xmpFile = this->parent->ioRef; + XMP_Assert ( xmpFile != 0 ); + XIO::ReplaceTextFile ( xmpFile, this->xmpPacket, (haveXMP & doSafeUpdate) ); + + // -------------------------------------------- + // Now update the legacy XML file if necessary. + + if ( updateLegacyXML ) { + + std::string legacyXML, xmlPath; + this->expat->tree.Serialize ( &legacyXML ); + this->MakeClipFilePath ( &xmlPath, "M01.XML" ); + + bool haveXML = Host_IO::Exists ( xmlPath.c_str() ); + if ( ! haveXML ) Host_IO::Create ( xmlPath.c_str() ); + + Host_IO::FileRef hostRef = Host_IO::Open ( xmlPath.c_str(), Host_IO::openReadWrite ); + if ( hostRef == Host_IO::noFileRef ) XMP_Throw ( "Failure opening XDCAM XML file", kXMPErr_ExternalFailure ); + XMPFiles_IO origXML ( hostRef, xmlPath.c_str(), Host_IO::openReadWrite ); + XIO::ReplaceTextFile ( &origXML, legacyXML, (haveXML & doSafeUpdate) ); + origXML.Close(); + + } + +} // XDCAM_MetaHandler::UpdateFile + +// ================================================================================================= +// XDCAM_MetaHandler::WriteTempFile +// ================================ + +void XDCAM_MetaHandler::WriteTempFile ( XMP_IO* tempRef ) +{ + + // ! WriteTempFile is not supposed to be called for handlers that own the file. + XMP_Throw ( "XDCAM_MetaHandler::WriteTempFile should not be called", kXMPErr_InternalFailure ); + +} // XDCAM_MetaHandler::WriteTempFile + +// ================================================================================================= diff --git a/XMPFiles/source/FileHandlers/XDCAM_Handler.hpp b/XMPFiles/source/FileHandlers/XDCAM_Handler.hpp new file mode 100644 index 0000000..fbcc8bb --- /dev/null +++ b/XMPFiles/source/FileHandlers/XDCAM_Handler.hpp @@ -0,0 +1,87 @@ +#ifndef __XDCAM_Handler_hpp__ +#define __XDCAM_Handler_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2007 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. + +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +#include "source/ExpatAdapter.hpp" + +// ================================================================================================= +/// \file XDCAM_Handler.hpp +/// \brief Folder format handler for XDCAM. +/// +/// This header ... +/// +// ================================================================================================= + +extern XMPFileHandler * XDCAM_MetaHandlerCTor ( XMPFiles * parent ); + +extern bool XDCAM_CheckFormat ( XMP_FileFormat format, + const std::string & rootPath, + const std::string & gpName, + const std::string & parentName, + const std::string & leafName, + XMPFiles * parent ); + +static const XMP_OptionBits kXDCAM_HandlerFlags = (kXMPFiles_CanInjectXMP | + kXMPFiles_CanExpand | + kXMPFiles_CanRewrite | + kXMPFiles_PrefersInPlace | + kXMPFiles_CanReconcile | + kXMPFiles_AllowsOnlyXMP | + kXMPFiles_ReturnsRawPacket | + kXMPFiles_HandlerOwnsFile | + kXMPFiles_AllowsSafeUpdate | + kXMPFiles_FolderBasedFormat); + +class XDCAM_MetaHandler : public XMPFileHandler +{ +public: + + bool GetFileModDate ( XMP_DateTime * modDate ); + + void CacheFileData(); + void ProcessXMP(); + + void UpdateFile ( bool doSafeUpdate ); + void WriteTempFile ( XMP_IO* tempRef ); + + XMP_OptionBits GetSerializeOptions() // *** These should be standard for standalone XMP files. + { return (kXMP_UseCompactFormat | kXMP_OmitPacketWrapper); }; + + XDCAM_MetaHandler ( XMPFiles * _parent ); + virtual ~XDCAM_MetaHandler(); + +private: + + XDCAM_MetaHandler() : isFAM(false), expat(0), clipMetadata(0) {}; // Hidden on purpose. + + bool MakeClipFilePath ( std::string * path, XMP_StringPtr suffix, bool checkFile = false ); + bool MakeMediaproPath ( std::string * path, bool checkFile = false ); + void MakeLegacyDigest ( std::string * digestStr ); + void CleanupLegacyXML(); + + bool GetMediaProMetadata ( SXMPMeta * xmpObjPtr, const std::string& clipUMID, bool digestFound ); + + std::string rootPath, clipName, xdcNS, legacyNS; + + bool isFAM; + + ExpatAdapter * expat; + XML_Node * clipMetadata; // ! Don't delete, points into the Expat tree. + +}; // XDCAM_MetaHandler + +// ================================================================================================= + +#endif /* __XDCAM_Handler_hpp__ */ diff --git a/XMPFiles/source/FormatSupport/AIFF/AIFFBehavior.cpp b/XMPFiles/source/FormatSupport/AIFF/AIFFBehavior.cpp new file mode 100644 index 0000000..a97c2b0 --- /dev/null +++ b/XMPFiles/source/FormatSupport/AIFF/AIFFBehavior.cpp @@ -0,0 +1,302 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/AIFF/AIFFBehavior.h" +#include "XMPFiles/source/FormatSupport/IFF/Chunk.h" +#include "source/XMP_LibUtils.hpp" +#include "source/XIO.hpp" + +#include <algorithm> + +using namespace IFF_RIFF; + +// +// Static init +// +const BigEndian& AIFFBehavior::mEndian = BigEndian::getInstance(); + +//----------------------------------------------------------------------------- +// +// AIFFBehavior::getRealSize(...) +// +// Purpose: Validate the passed in size value, identify the valid size if the +// passed in isn't valid and return the valid size. +// Throw an exception if the passed in size isn't valid and there's +// no way to identify a valid size. +// +//----------------------------------------------------------------------------- + +XMP_Uns64 AIFFBehavior::getRealSize( const XMP_Uns64 size, const ChunkIdentifier& id, IChunkContainer& tree, XMP_IO* stream ) +{ + if( (size & 0x80000000) > 0 ) + { + XMP_Throw( "Unknown size value", kXMPErr_BadFileFormat ); + } + + return size; +} + +//----------------------------------------------------------------------------- +// +// AIFFBehavior::isValidTopLevelChunk(...) +// +// Purpose: Return true if the passed identifier is valid for top-level chunks +// of a certain format. +// +//----------------------------------------------------------------------------- + +bool AIFFBehavior::isValidTopLevelChunk( const ChunkIdentifier& id, XMP_Uns32 chunkNo ) +{ + return (chunkNo == 0) && (id.id == kChunk_FORM) && ((id.type == kType_AIFF) || (id.type == kType_AIFC)); +} + +//----------------------------------------------------------------------------- +// +// AIFFBehavior::getMaxChunkSize(...) +// +// Purpose: Return the maximum size of a single chunk, i.e. the maximum size +// of a top-level chunk. +// +//----------------------------------------------------------------------------- + +XMP_Uns64 AIFFBehavior::getMaxChunkSize() const +{ + return 0x80000000LL; // 2 GByte +} + +//----------------------------------------------------------------------------- +// +// AIFFBehavior::fixHierarchy(...) +// +// Purpose: Fix the hierarchy of chunks depending ones based on size changes of +// one or more chunks and second based on format specific rules. +// Throw an exception if the hierarchy can't be fixed. +// +//----------------------------------------------------------------------------- + +void AIFFBehavior::fixHierarchy( IChunkContainer& tree ) +{ + XMP_Validate( tree.numChildren() == 1, "AIFF files should only have one top level chunk (FORM)", kXMPErr_BadFileFormat); + Chunk* formChunk = tree.getChildAt(0); + + XMP_Validate( (formChunk->getType() == kType_AIFF) || (formChunk->getType() == kType_AIFC), "Invalid type for AIFF/AIFC top level chunk (FORM)", kXMPErr_BadFileFormat); + + if( formChunk->hasChanged() ) + { + // + // none of the modified chunks should be smaller than 12Byte + // + for( XMP_Uns32 i=0; i<formChunk->numChildren(); i++ ) + { + Chunk* chunk = formChunk->getChildAt(i); + + if( chunk->hasChanged() && chunk->getSize() != chunk->getOriginalSize() ) + { + XMP_Validate( chunk->getSize() >= Chunk::TYPE_SIZE, "Modified chunk smaller than 12bytes", kXMPErr_InternalFailure ); + } + } + + // + // move new added chunks to temporary container + // + Chunk* tmpContainer = Chunk::createChunk( mEndian ); + this->moveChunks( *formChunk, *tmpContainer, formChunk->numChildren() - mChunksAdded ); + + // + // for all children chunks until the last child of the initial list is reached + // try to arrange the chunks at the current location using exisiting free space + // or FREE chunks around, otherwise move the chunk to the end + // + this->arrangeChunksInPlace( *formChunk, *tmpContainer ); + + // + // for all chunks that were moved to the end try to find a FREE chunk for them + // + this->arrangeChunksInTree( *tmpContainer, *formChunk ); + + // + // append all remaining new added chunks to the end of the tree + // + this->moveChunks( *tmpContainer, *formChunk, 0 ); + delete tmpContainer; + + // + // check for FREE chunks at the end + // + Chunk* endFREE = this->mergeFreeChunks( *formChunk, formChunk->numChildren() - 1 ); + + if( endFREE != NULL ) + { + formChunk->removeChildAt( formChunk->numChildren() - 1 ); + delete endFREE; + } + + // + // Fix the offset values of all chunks. Throw an exception in the case that + // the offset of a non-modified chunk needs to be reset. + // + XMP_Validate( formChunk->getOffset() == 0, "Invalid offset for AIFF/AIFC top level chunk (FORM)", kXMPErr_InternalFailure ); + + this->validateOffsets( tree ); + } +} + +void AIFFBehavior::insertChunk( IChunkContainer& tree, Chunk& chunk ) +{ + XMP_Validate( tree.numChildren() == 1, "AIFF files should only have one top level chunk (FORM)", kXMPErr_BadFileFormat); + Chunk* formChunk = tree.getChildAt(0); + + XMP_Validate( (formChunk->getType() == kType_AIFF) || (formChunk->getType() == kType_AIFC), "Invalid type for AIFF/AIFC top level chunk (FORM)", kXMPErr_BadFileFormat); + + // add new chunk to the end of the AIFF:FORM + formChunk->appendChild(&chunk); + + mChunksAdded++; +} + +bool AIFFBehavior::removeChunk( IChunkContainer& tree, Chunk& chunk ) +{ + XMP_Validate( chunk.getID() != kChunk_FORM, "Can't remove FORM chunk!", kXMPErr_InternalFailure ); + XMP_Validate( chunk.getChunkMode() != CHUNK_UNKNOWN, "Cant' remove UNKNOWN Chunk", kXMPErr_InternalFailure ); + + XMP_Validate( tree.numChildren() == 1, "AIFF files should only have one top level chunk (FORM)", kXMPErr_BadFileFormat); + + Chunk* formChunk = tree.getChildAt(0); + + XMP_Validate( (formChunk->getType() == kType_AIFF) || (formChunk->getType() == kType_AIFC), "Invalid type for AIFF/AIFC top level chunk (FORM)", kXMPErr_BadFileFormat); + + XMP_Uns32 i = std::find( formChunk->firstChild(), formChunk->lastChild(), &chunk ) - formChunk->firstChild(); + + XMP_Validate( i < formChunk->numChildren(), "Invalid chunk in tree", kXMPErr_InternalFailure ); + + // + // adjust new chunks counter + // + if( i > formChunk->numChildren() - mChunksAdded - 1 ) + { + mChunksAdded--; + } + + if( i < formChunk->numChildren()-1 ) + { + // + // fill gap with free chunk + // + Chunk* free = this->createFREE( chunk.getPadSize( true ) ); + formChunk->replaceChildAt( i, free ); + free->setAsNew(); + + // + // merge JUNK chunks + // + this->mergeFreeChunks( *formChunk, i ); + } + else + { + // + // remove chunk from tree + // + formChunk->removeChildAt( i ); + } + + return true; +} + +XMP_Bool AIFFBehavior::isFREEChunk( const Chunk& chunk ) const +{ + XMP_Bool ret = ( chunk.getID() == kChunk_APPL && chunk.getType() == kType_FREE ); + + // + // if the signature is not 'APPL':'FREE' the it could be an annotation chunk + // (ID: 'ANNO') which data area is smaller than 4bytes and the data is zero + // + if( !ret && chunk.getID() == kChunk_ANNO && chunk.getSize() < Chunk::TYPE_SIZE ) + { + ret = chunk.getSize() == 0; + + if( !ret ) + { + const XMP_Uns8* buffer; + chunk.getData( &buffer ); + + XMP_Uns8* data = new XMP_Uns8[static_cast<size_t>( chunk.getSize() )]; + memset( data, 0, static_cast<size_t>( chunk.getSize() ) ); + + ret = ( memcmp( data, buffer, static_cast<size_t>( chunk.getSize() ) ) == 0 ); + + delete[] data; + } + } + + return ret; +} + +Chunk* AIFFBehavior::createFREE( XMP_Uns64 chunkSize ) +{ + XMP_Int64 alloc = chunkSize - Chunk::HEADER_SIZE; + + Chunk* chunk = NULL; + XMP_Uns8* data = NULL; + + if( alloc > 0 ) + { + data = new XMP_Uns8[static_cast<size_t>( alloc )]; + memset( data, 0, static_cast<size_t>( alloc ) ); + } + + if( alloc < Chunk::TYPE_SIZE ) + { + // + // if the required size is smaller than the minimum size of a 'APPL':'FREE' chunk + // then create an annotation chunk 'ANNO' and zero the data + // + if( alloc > 0 ) + { + chunk = Chunk::createUnknownChunk( mEndian, kChunk_ANNO, 0, alloc ); + chunk->setData( data, alloc ); + } + else + { + chunk = Chunk::createHeaderChunk( mEndian, kChunk_ANNO ); + } + } + else + { + // + // create a 'APPL':'FREE' chunk + // + alloc -= Chunk::TYPE_SIZE; + + if( alloc > 0 ) + { + chunk = Chunk::createUnknownChunk( mEndian, kChunk_APPL, kType_FREE, alloc+Chunk::TYPE_SIZE ); + chunk->setData( data, alloc, true ); + } + else + { + chunk = Chunk::createHeaderChunk( mEndian, kChunk_APPL, kType_FREE ); + } + } + + delete[] data; + + // force set dirty flag + chunk->setChanged(); + + return chunk; +} + +XMP_Uns64 AIFFBehavior::getMinFREESize() const +{ + // avoid creation of chunks with size==0 + return static_cast<XMP_Uns64>( Chunk::HEADER_SIZE ) + 2; +} diff --git a/XMPFiles/source/FormatSupport/AIFF/AIFFBehavior.h b/XMPFiles/source/FormatSupport/AIFF/AIFFBehavior.h new file mode 100644 index 0000000..deadac3 --- /dev/null +++ b/XMPFiles/source/FormatSupport/AIFF/AIFFBehavior.h @@ -0,0 +1,152 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _AIFFBEHAVIOR_h_ +#define _AIFFBEHAVIOR_h_ + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" + +#include "XMPFiles/source/FormatSupport/IFF/IChunkBehavior.h" +#include "XMPFiles/source/FormatSupport/IFF/ChunkPath.h" +#include "source/Endian.h" + +namespace IFF_RIFF +{ + +/** + AIFF behavior class. + + Implements the IChunkBehavior interface +*/ + + +class AIFFBehavior : public IChunkBehavior +{ +public: + /** + ctor/dtor + */ + AIFFBehavior() : mChunksAdded(0) {} + ~AIFFBehavior() {} + + /** + Validate the passed in size value, identify the valid size if the passed in isn't valid + and return the valid size. + throw an exception if the passed in size isn't valid and there's no way to identify a + valid size. + + @param size Size value + @param id Identifier of chunk + @param tree Chunk tree + @param stream Stream handle + + @return Valid size value. + */ + XMP_Uns64 getRealSize( const XMP_Uns64 size, const ChunkIdentifier& id, IChunkContainer& tree, XMP_IO* stream ); + + /** + Return the maximum size of a single chunk, i.e. the maximum size of a top-level chunk. + + @return Maximum size + */ + XMP_Uns64 getMaxChunkSize() const; + + /** + Return true if the passed identifier is valid for top-level chunks of a certain format. + + @param id Chunk identifier + @param chunkNo order number of top-level chunk + @return true, if passed id is a valid top-level chunk + */ + bool isValidTopLevelChunk( const ChunkIdentifier& id, XMP_Uns32 chunkNo ); + + /** + Fix the hierarchy of chunks depending ones based on size changes of one or more chunks + and second based on format specific rules. + Throw an exception if the hierarchy can't be fixed. + + @param tree Vector of root chunks. + */ + void fixHierarchy( IChunkContainer& tree ); + + /** + Insert a new chunk into the hierarchy of chunks. The behavior needs to decide the position + of the new chunk and has to do the insertion. + + @param tree Chunk tree + @param chunk New chunk + */ + void insertChunk( IChunkContainer& tree, Chunk& chunk ) ; + + /** + Remove the chunk described by the passed ChunkPath. + + @param tree Chunk tree + @param path Path to the chunk that needs to be removed + + @return true if the chunk was removed and need to be deleted + */ + bool removeChunk( IChunkContainer& tree, Chunk& chunk ) ; + +private: + + + /** + Create a FREE chunk. + If the chunkSize is smaller than the header+type - size then create an annotation chunk. + If the passed size is odd, then add a pad byte. + + @param chunkSize Total size including header + @return New FREE chunk + */ + Chunk* createFREE( XMP_Uns64 chunkSize ); + + /** + Check if the passed chunk is a FREE chunk. + (Could be also a small annotation chunk with zero bytes in its data) + + @param chunk A chunk + + @return true if the passed chunk is a FREE chunk + */ + XMP_Bool isFREEChunk( const Chunk& chunk ) const; + + /** + Retrieve the free space at the passed position in the child list of the parent tree. + If there's a FREE chunk then return it. + + @param outFreeBytes On return it takes the number of free bytes + @param tree Parent tree + @param index Position in the child list of the parent tree + + @return FREE chunk if available + */ + Chunk* getFreeSpace( XMP_Int64& outFreeBytes, const IChunkContainer& tree, XMP_Uns32 index ) const; + + /** + Return the minimum size of a FREE chunk + */ + XMP_Uns64 getMinFREESize( ) const; + +private: + XMP_Uns32 mChunksAdded; + + /** AIFF is always Big Endian */ + static const BigEndian& mEndian; + +}; // IFF_RIFF + +} +#endif diff --git a/XMPFiles/source/FormatSupport/AIFF/AIFFMetadata.cpp b/XMPFiles/source/FormatSupport/AIFF/AIFFMetadata.cpp new file mode 100644 index 0000000..8fadba3 --- /dev/null +++ b/XMPFiles/source/FormatSupport/AIFF/AIFFMetadata.cpp @@ -0,0 +1,46 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/AIFF/AIFFMetadata.h" + +using namespace IFF_RIFF; + +//----------------------------------------------------------------------------- +// +// AIFFMetadata::AIFFMetadata(...) +// +// Purpose: ctor/dtor +// +//----------------------------------------------------------------------------- + +AIFFMetadata::AIFFMetadata() +{ +} + +AIFFMetadata::~AIFFMetadata() +{ +} + +//----------------------------------------------------------------------------- +// +// AIFFMetadata::isEmptyValue(...) +// +// Purpose: Is the value of the passed ValueObject and its id "empty"? +// +//----------------------------------------------------------------------------- + +bool AIFFMetadata::isEmptyValue( XMP_Uns32 id, ValueObject& valueObj ) +{ + TValueObject<std::string>* strObj = dynamic_cast<TValueObject<std::string>*>(&valueObj); + + return ( strObj == NULL || ( strObj != NULL && strObj->getValue().empty() ) ); +} diff --git a/XMPFiles/source/FormatSupport/AIFF/AIFFMetadata.h b/XMPFiles/source/FormatSupport/AIFF/AIFFMetadata.h new file mode 100644 index 0000000..13dbf1c --- /dev/null +++ b/XMPFiles/source/FormatSupport/AIFF/AIFFMetadata.h @@ -0,0 +1,63 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _AIFFMetadata_h_ +#define _AIFFMetadata_h_ + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" + +#include "source/XMP_LibUtils.hpp" + +#include "XMPFiles/source/FormatSupport/IFF/IChunkData.h" +#include "XMPFiles/source/NativeMetadataSupport/IMetadata.h" + + +namespace IFF_RIFF +{ + +/** + * AIFF Metadata model. + * Implements the IMetadata interface + */ +class AIFFMetadata : public IMetadata +{ +public: + enum + { + kName, // std::string + kAuthor, // std::string + kCopyright, // std::string + kAnnotation // std::string + }; + +public: + AIFFMetadata(); + ~AIFFMetadata(); + +protected: + /** + * @see IMetadata::isEmptyValue + */ + virtual bool isEmptyValue( XMP_Uns32 id, ValueObject& valueObj ); + +private: + // Operators hidden on purpose + AIFFMetadata( const AIFFMetadata& ) {}; + AIFFMetadata& operator=( const AIFFMetadata& ) { return *this; }; +}; + +} // namespace + +#endif diff --git a/XMPFiles/source/FormatSupport/AIFF/AIFFReconcile.cpp b/XMPFiles/source/FormatSupport/AIFF/AIFFReconcile.cpp new file mode 100644 index 0000000..af91524 --- /dev/null +++ b/XMPFiles/source/FormatSupport/AIFF/AIFFReconcile.cpp @@ -0,0 +1,63 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/AIFF/AIFFReconcile.h" +#include "XMPFiles/source/FormatSupport/AIFF/AIFFMetadata.h" +#include "XMPFiles/source/NativeMetadataSupport/MetadataSet.h" +#include "source/XMP_LibUtils.hpp" +#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp" +#include "source/XIO.hpp" + + +using namespace IFF_RIFF; + +static const MetadataPropertyInfo kAIFFProperties[] = +{ +// XMP NS XMP Property Name Native Metadata Identifier Native Datatype XMP Datatype Delete Priority ExportPolicy + { kXMP_NS_DC, "title", AIFFMetadata::kName, kNativeType_StrUTF8, kXMPType_Localized, true, false, kExport_Always }, // dc:title <-> FORM:AIFF/NAME + { kXMP_NS_DC, "creator", AIFFMetadata::kAuthor, kNativeType_StrUTF8, kXMPType_Array, true, false, kExport_Always }, // dc:creator <-> FORM:AIFF/AUTH + { kXMP_NS_DC, "rights", AIFFMetadata::kCopyright, kNativeType_StrUTF8, kXMPType_Localized, true, false, kExport_Always }, // dc:rights <-> FORM:AIFF/(c) + { kXMP_NS_DM, "logComment", AIFFMetadata::kAnnotation, kNativeType_StrUTF8, kXMPType_Simple, true, false, kExport_Always }, // xmpDM:logComment <-> FORM:AIFF/ANNO + { NULL } +}; + +XMP_Bool AIFFReconcile::importToXMP( SXMPMeta& outXMP, const MetadataSet& inMetaData ) +{ + XMP_Bool changed = false; + + // the reconciliation is based on the existing outXMP packet + AIFFMetadata *aiffMeta = inMetaData.get<AIFFMetadata>(); + + if (aiffMeta != NULL) + { + changed = IReconcile::importNativeToXMP( outXMP, *aiffMeta, kAIFFProperties, false ); + } + + return changed; +}//reconcile + + +XMP_Bool AIFFReconcile::exportFromXMP( MetadataSet& outMetaData, SXMPMeta& inXMP ) +{ + XMP_Bool changed = false; + + // Get the appropriate metadata container + AIFFMetadata *aiffMeta = outMetaData.get<AIFFMetadata>(); + + // If the metadata container is not available, skip that part of the process + if( aiffMeta != NULL ) + { + changed = IReconcile::exportXMPToNative( *aiffMeta, inXMP, kAIFFProperties ); + }//if AIFF is set + + return changed; +}//dissolve diff --git a/XMPFiles/source/FormatSupport/AIFF/AIFFReconcile.h b/XMPFiles/source/FormatSupport/AIFF/AIFFReconcile.h new file mode 100644 index 0000000..41e5989 --- /dev/null +++ b/XMPFiles/source/FormatSupport/AIFF/AIFFReconcile.h @@ -0,0 +1,40 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _AIFFReconcile_h_ +#define _AIFFReconcile_h_ + +#include "XMPFiles/source/NativeMetadataSupport/IReconcile.h" + +namespace IFF_RIFF +{ + +class AIFFReconcile : public IReconcile +{ +public: + ~AIFFReconcile() {}; + + /** + * @see IReconcile::importToXMP + * Legacy values are always imported. + * If the values are not UTF-8 they will be converted to UTF-8 except in ServerMode + */ + XMP_Bool importToXMP( SXMPMeta& outXMP, const MetadataSet& inMetaData ); + + /** + * @see IReconcile::exportFromXMP + * XMP values are always exported to Legacy as UTF-8 encoded + */ + XMP_Bool exportFromXMP( MetadataSet& outMetaData, SXMPMeta& inXMP ); + +}; + +} + +#endif diff --git a/XMPFiles/source/FormatSupport/ASF_Support.cpp b/XMPFiles/source/FormatSupport/ASF_Support.cpp new file mode 100644 index 0000000..35adce6 --- /dev/null +++ b/XMPFiles/source/FormatSupport/ASF_Support.cpp @@ -0,0 +1,1438 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/ASF_Support.hpp" +#include "source/UnicodeConversions.hpp" +#include "source/XIO.hpp" + +#if XMP_WinBuild + #define snprintf _snprintf + #pragma warning ( disable : 4996 ) // '...' was declared deprecated + #pragma warning ( disable : 4267 ) // *** conversion (from size_t), possible loss of date (many 64 bit related) +#endif + +// ============================================================================================= + +// Platforms other than Win +#if ! XMP_WinBuild +int IsEqualGUID ( const GUID& guid1, const GUID& guid2 ) +{ + return (memcmp ( &guid1, &guid2, sizeof(GUID) ) == 0); +} +#endif + +ASF_Support::ASF_Support() : legacyManager(0), posFileSizeInfo(0) {} + +ASF_Support::ASF_Support ( ASF_LegacyManager* _legacyManager ) : posFileSizeInfo(0) +{ + legacyManager = _legacyManager; +} + +ASF_Support::~ASF_Support() +{ + legacyManager = 0; +} + +// ============================================================================================= + +long ASF_Support::OpenASF ( XMP_IO* fileRef, ObjectState & inOutObjectState ) +{ + XMP_Uns64 pos = 0; + XMP_Uns64 len; + + try { + pos = fileRef->Rewind(); + } catch ( ... ) {} + + if ( pos != 0 ) return 0; + + // read first and following chunks + while ( ReadObject ( fileRef, inOutObjectState, &len, pos) ) {} + + return inOutObjectState.objects.size(); + +} + +// ============================================================================================= + +bool ASF_Support::ReadObject ( XMP_IO* fileRef, ObjectState & inOutObjectState, XMP_Uns64 * objectLength, XMP_Uns64 & inOutPosition ) +{ + + try { + + XMP_Uns64 startPosition = inOutPosition; + XMP_Uns32 bytesRead; + ASF_ObjectBase objectBase; + + bytesRead = fileRef->ReadAll ( &objectBase, kASF_ObjectBaseLen ); + if ( bytesRead != kASF_ObjectBaseLen ) return false; + + *objectLength = GetUns64LE ( &objectBase.size ); + inOutPosition += *objectLength; + + ObjectData newObject; + + newObject.pos = startPosition; + newObject.len = *objectLength; + newObject.guid = objectBase.guid; + + // xmpIsLastObject indicates, that the XMP-object is the last top-level object + // reset here, if any another object is read + inOutObjectState.xmpIsLastObject = false; + + if ( IsEqualGUID ( ASF_Header_Object, newObject.guid ) ) { + + // header object ? + this->ReadHeaderObject ( fileRef, inOutObjectState, newObject ); + + } else if ( IsEqualGUID ( ASF_XMP_Metadata, newObject.guid ) ) { + + // check object for XMP GUID + inOutObjectState.xmpPos = newObject.pos + kASF_ObjectBaseLen; + inOutObjectState.xmpLen = newObject.len - kASF_ObjectBaseLen; + inOutObjectState.xmpIsLastObject = true; + inOutObjectState.xmpObject = newObject; + newObject.xmp = true; + + } + + inOutObjectState.objects.push_back ( newObject ); + + fileRef->Seek ( inOutPosition, kXMP_SeekFromStart ); + + } catch ( ... ) { + + return false; + + } + + return true; + +} + +// ============================================================================================= + +bool ASF_Support::ReadHeaderObject ( XMP_IO* fileRef, ObjectState& inOutObjectState, const ObjectData& newObject ) +{ + if ( ! IsEqualGUID ( ASF_Header_Object, newObject.guid) || (! legacyManager ) ) return false; + + std::string buffer; + + legacyManager->SetPadding(0); + + try { + + // read header-object structure + XMP_Uns64 pos = newObject.pos; + XMP_Uns32 bufferSize = kASF_ObjectBaseLen + 6; + + buffer.clear(); + buffer.reserve ( bufferSize ); + buffer.assign ( bufferSize, ' ' ); + fileRef->Seek ( pos, kXMP_SeekFromStart ); + fileRef->ReadAll ( const_cast<char*>(buffer.data()), bufferSize ); + + XMP_Uns64 read = bufferSize; + pos += bufferSize; + + // read contained header objects + XMP_Uns32 numberOfHeaders = GetUns32LE ( &buffer[24] ); + ASF_ObjectBase objectBase; + + while ( read < newObject.len ) { + + fileRef->Seek ( pos, kXMP_SeekFromStart ); + if ( kASF_ObjectBaseLen != fileRef->Read ( &objectBase, kASF_ObjectBaseLen, true ) ) break; + + fileRef->Seek ( pos, kXMP_SeekFromStart ); + objectBase.size = GetUns64LE ( &objectBase.size ); + + if ( IsEqualGUID ( ASF_File_Properties_Object, objectBase.guid) && (objectBase.size >= 104 ) ) { + + buffer.clear(); + buffer.reserve ( XMP_Uns32( objectBase.size ) ); + buffer.assign ( XMP_Uns32( objectBase.size ), ' ' ); + fileRef->ReadAll ( const_cast<char*>(buffer.data()), XMP_Int32(objectBase.size) ); + + // save position of filesize-information + posFileSizeInfo = (pos + 40); + + // creation date + std::string sub ( buffer.substr ( 48, 8 ) ); + legacyManager->SetField ( ASF_LegacyManager::fieldCreationDate, sub ); + + // broadcast flag set ? + XMP_Uns32 flags = GetUns32LE ( &buffer[88] ); + inOutObjectState.broadcast = (flags & 1); + legacyManager->SetBroadcast ( inOutObjectState.broadcast ); + + legacyManager->SetObjectExists ( ASF_LegacyManager::objectFileProperties ); + + } else if ( IsEqualGUID ( ASF_Content_Description_Object, objectBase.guid) && (objectBase.size >= 34 ) ) { + + buffer.clear(); + buffer.reserve ( XMP_Uns32( objectBase.size ) ); + buffer.assign ( XMP_Uns32( objectBase.size ), ' ' ); + fileRef->ReadAll ( const_cast<char*>(buffer.data()), XMP_Int32(objectBase.size) ); + + XMP_Uns16 titleLen = GetUns16LE ( &buffer[24] ); + XMP_Uns16 authorLen = GetUns16LE ( &buffer[26] ); + XMP_Uns16 copyrightLen = GetUns16LE ( &buffer[28] ); + XMP_Uns16 descriptionLen = GetUns16LE ( &buffer[30] ); + XMP_Uns16 ratingLen = GetUns16LE ( &buffer[32] ); + + XMP_Uns16 fieldPos = 34; + + std::string titleStr = buffer.substr ( fieldPos, titleLen ); + fieldPos += titleLen; + legacyManager->SetField ( ASF_LegacyManager::fieldTitle, titleStr ); + + std::string authorStr = buffer.substr ( fieldPos, authorLen ); + fieldPos += authorLen; + legacyManager->SetField ( ASF_LegacyManager::fieldAuthor, authorStr ); + + std::string copyrightStr = buffer.substr ( fieldPos, copyrightLen ); + fieldPos += copyrightLen; + legacyManager->SetField ( ASF_LegacyManager::fieldCopyright, copyrightStr ); + + std::string descriptionStr = buffer.substr ( fieldPos, descriptionLen ); + fieldPos += descriptionLen; + legacyManager->SetField ( ASF_LegacyManager::fieldDescription, descriptionStr ); + + /* rating is currently not part of reconciliation + std::string ratingStr = buffer.substr ( fieldPos, ratingLen ); + fieldPos += ratingLen; + legacyData.append ( titleStr ); + */ + + legacyManager->SetObjectExists ( ASF_LegacyManager::objectContentDescription ); + + } else if ( IsEqualGUID ( ASF_Content_Branding_Object, objectBase.guid ) ) { + + buffer.clear(); + buffer.reserve ( XMP_Uns32( objectBase.size ) ); + buffer.assign ( XMP_Uns32( objectBase.size ), ' ' ); + fileRef->ReadAll ( const_cast<char*>(buffer.data()), XMP_Int32(objectBase.size) ); + + XMP_Uns32 fieldPos = 28; + + // copyright URL is 3. element with variable size + for ( int i = 1; i <= 3 ; ++i ) { + XMP_Uns32 len = GetUns32LE ( &buffer[fieldPos] ); + if ( i == 3 ) { + std::string copyrightURLStr = buffer.substr ( fieldPos + 4, len ); + legacyManager->SetField ( ASF_LegacyManager::fieldCopyrightURL, copyrightURLStr ); + } + fieldPos += (len + 4); + } + + legacyManager->SetObjectExists ( ASF_LegacyManager::objectContentBranding ); + +#if ! Exclude_LicenseURL_Recon + + } else if ( IsEqualGUID ( ASF_Content_Encryption_Object, objectBase.guid ) ) { + + buffer.clear(); + buffer.reserve ( XMP_Uns32( objectBase.size ) ); + buffer.assign ( XMP_Uns32( objectBase.size ), ' ' ); + fileRef->ReadAll ( const_cast<char*>(buffer.data()), XMP_Int32(objectBase.size) ); + + XMP_Uns32 fieldPos = 24; + + // license URL is 4. element with variable size + for ( int i = 1; i <= 4 ; ++i ) { + XMP_Uns32 len = GetUns32LE ( &buffer[fieldPos] ); + if ( i == 4 ) { + std::string licenseURLStr = buffer.substr ( fieldPos + 4, len ); + legacyManager->SetField ( ASF_LegacyManager::fieldLicenseURL, licenseURLStr ); + } + fieldPos += (len + 4); + } + + legacyManager->SetObjectExists ( objectContentEncryption ); + +#endif + + } else if ( IsEqualGUID ( ASF_Padding_Object, objectBase.guid ) ) { + + legacyManager->SetPadding ( legacyManager->GetPadding() + (objectBase.size - 24) ); + + } else if ( IsEqualGUID ( ASF_Header_Extension_Object, objectBase.guid ) ) { + + this->ReadHeaderExtensionObject ( fileRef, inOutObjectState, pos, objectBase ); + + } + + pos += objectBase.size; + read += objectBase.size; + } + + } catch ( ... ) { + + return false; + + } + + legacyManager->ComputeDigest(); + + return true; +} + +// ============================================================================================= + +bool ASF_Support::WriteHeaderObject ( XMP_IO* sourceRef, XMP_IO* destRef, const ObjectData& object, ASF_LegacyManager& _legacyManager, bool usePadding ) +{ + if ( ! IsEqualGUID ( ASF_Header_Object, object.guid ) ) return false; + + std::string buffer; + XMP_Uns16 valueUns16LE; + XMP_Uns32 valueUns32LE; + XMP_Uns64 valueUns64LE; + + try { + + // read header-object structure + XMP_Uns64 pos = object.pos; + XMP_Uns32 bufferSize = kASF_ObjectBaseLen + 6; + + buffer.clear(); + buffer.reserve ( bufferSize ); + buffer.assign ( bufferSize, ' ' ); + sourceRef->Seek ( pos, kXMP_SeekFromStart ); + sourceRef->ReadAll ( const_cast<char*>(buffer.data()), bufferSize ); + + XMP_Uns64 read = bufferSize; + pos += bufferSize; + + // read contained header objects + XMP_Uns32 numberOfHeaders = GetUns32LE ( &buffer[24] ); + ASF_ObjectBase objectBase; + + // prepare new header in memory + std::string header; + + int changedObjects = _legacyManager.changedObjects(); + int exportedObjects = 0; + int writtenObjects = 0; + + header.append ( buffer.c_str(), bufferSize ); + + while ( read < object.len ) { + + sourceRef->Seek ( pos, kXMP_SeekFromStart ); + if ( kASF_ObjectBaseLen != sourceRef->Read ( &objectBase, kASF_ObjectBaseLen, true ) ) break; + + sourceRef->Seek ( pos, kXMP_SeekFromStart ); + objectBase.size = GetUns64LE ( &objectBase.size ); + + int headerStartPos = header.size(); + + // save position of filesize-information + if ( IsEqualGUID ( ASF_File_Properties_Object, objectBase.guid ) ) { + posFileSizeInfo = (headerStartPos + 40); + } + + // write objects + if ( IsEqualGUID ( ASF_File_Properties_Object, objectBase.guid ) && + (objectBase.size >= 104) && (changedObjects & ASF_LegacyManager::objectFileProperties) ) { + + // copy object and replace creation-date + buffer.reserve ( XMP_Uns32 ( objectBase.size ) ); + buffer.assign ( XMP_Uns32 ( objectBase.size ), ' ' ); + sourceRef->ReadAll ( const_cast<char*>(buffer.data()), XMP_Int32(objectBase.size) ); + header.append ( buffer, 0, XMP_Uns32( objectBase.size ) ); + + if ( ! _legacyManager.GetBroadcast() ) { + buffer = _legacyManager.GetField ( ASF_LegacyManager::fieldCreationDate ); + ReplaceString ( header, buffer, (headerStartPos + 48), 8 ); + } + + exportedObjects |= ASF_LegacyManager::objectFileProperties; + + } else if ( IsEqualGUID ( ASF_Content_Description_Object, objectBase.guid ) && + (objectBase.size >= 34) && (changedObjects & ASF_LegacyManager::objectContentDescription) ) { + + // re-create object with xmp-data + buffer.reserve ( XMP_Uns32( objectBase.size ) ); + buffer.assign ( XMP_Uns32( objectBase.size ), ' ' ); + sourceRef->ReadAll ( const_cast<char*>(buffer.data()), XMP_Int32(objectBase.size) ); + // write header only + header.append ( buffer, 0, XMP_Uns32( kASF_ObjectBaseLen ) ); + + // write length fields + + XMP_Uns16 titleLen = _legacyManager.GetField ( ASF_LegacyManager::fieldTitle).size( ); + valueUns16LE = MakeUns16LE ( titleLen ); + header.append ( (const char*)&valueUns16LE, 2 ); + + XMP_Uns16 authorLen = _legacyManager.GetField ( ASF_LegacyManager::fieldAuthor).size( ); + valueUns16LE = MakeUns16LE ( authorLen ); + header.append ( (const char*)&valueUns16LE, 2 ); + + XMP_Uns16 copyrightLen = _legacyManager.GetField ( ASF_LegacyManager::fieldCopyright).size( ); + valueUns16LE = MakeUns16LE ( copyrightLen ); + header.append ( (const char*)&valueUns16LE, 2 ); + + XMP_Uns16 descriptionLen = _legacyManager.GetField ( ASF_LegacyManager::fieldDescription).size( ); + valueUns16LE = MakeUns16LE ( descriptionLen ); + header.append ( (const char*)&valueUns16LE, 2 ); + + // retrieve existing overall length of preceding fields + XMP_Uns16 precedingLen = 0; + precedingLen += GetUns16LE ( &buffer[24] ); // Title + precedingLen += GetUns16LE ( &buffer[26] ); // Author + precedingLen += GetUns16LE ( &buffer[28] ); // Copyright + precedingLen += GetUns16LE ( &buffer[30] ); // Description + // retrieve existing 'Rating' length + XMP_Uns16 ratingLen = GetUns16LE ( &buffer[32] ); // Rating + valueUns16LE = MakeUns16LE ( ratingLen ); + header.append ( (const char*)&valueUns16LE, 2 ); + + // write field contents + + header.append ( _legacyManager.GetField ( ASF_LegacyManager::fieldTitle ) ); + header.append ( _legacyManager.GetField ( ASF_LegacyManager::fieldAuthor ) ); + header.append ( _legacyManager.GetField ( ASF_LegacyManager::fieldCopyright ) ); + header.append ( _legacyManager.GetField ( ASF_LegacyManager::fieldDescription ) ); + header.append ( buffer, (34 + precedingLen), ratingLen ); + + // update new object size + valueUns64LE = MakeUns64LE ( header.size() - headerStartPos ); + std::string newSize ( (const char*)&valueUns64LE, 8 ); + ReplaceString ( header, newSize, (headerStartPos + 16), 8 ); + + exportedObjects |= ASF_LegacyManager::objectContentDescription; + + } else if ( IsEqualGUID ( ASF_Content_Branding_Object, objectBase.guid ) && + (changedObjects & ASF_LegacyManager::objectContentBranding) ) { + + // re-create object with xmp-data + buffer.reserve ( XMP_Uns32( objectBase.size ) ); + buffer.assign ( XMP_Uns32( objectBase.size ), ' ' ); + sourceRef->ReadAll ( const_cast<char*>(buffer.data()), XMP_Int32(objectBase.size) ); + + // calculate size of fields coming before 'Copyright URL' + XMP_Uns32 length = 28; + length += (GetUns32LE ( &buffer[length] ) + 4); // Banner Image Data + length += (GetUns32LE ( &buffer[length] ) + 4); // Banner Image URL + + // write first part of header + header.append ( buffer, 0, length ); + + // copyright URL + length = _legacyManager.GetField ( ASF_LegacyManager::fieldCopyrightURL).size( ); + valueUns32LE = MakeUns32LE ( length ); + header.append ( (const char*)&valueUns32LE, 4 ); + header.append ( _legacyManager.GetField ( ASF_LegacyManager::fieldCopyrightURL ) ); + + // update new object size + valueUns64LE = MakeUns64LE ( header.size() - headerStartPos ); + std::string newSize ( (const char*)&valueUns64LE, 8 ); + ReplaceString ( header, newSize, (headerStartPos + 16), 8 ); + + exportedObjects |= ASF_LegacyManager::objectContentBranding; + +#if ! Exclude_LicenseURL_Recon + + } else if ( IsEqualGUID ( ASF_Content_Encryption_Object, objectBase.guid ) && + (changedObjects & ASF_LegacyManager::objectContentEncryption) ) { + + // re-create object with xmp-data + buffer.reserve ( XMP_Uns32( objectBase.size ) ); + buffer.assign ( XMP_Uns32( objectBase.size ), ' ' ); + sourceRef->ReadAll ( const_cast<char*>(buffer.data()), XMP_Int32(objectBase.size) ); + + // calculate size of fields coming before 'License URL' + XMP_Uns32 length = 24; + length += (GetUns32LE ( &buffer[length] ) + 4); // Secret Data + length += (GetUns32LE ( &buffer[length] ) + 4); // Protection Type + length += (GetUns32LE ( &buffer[length] ) + 4); // Key ID + + // write first part of header + header.append ( buffer, 0, length ); + + // License URL + length = _legacyManager.GetField ( ASF_LegacyManager::fieldLicenseURL).size( ); + valueUns32LE = MakeUns32LE ( length ); + header.append ( (const char*)&valueUns32LE, 4 ); + header.append ( _legacyManager.GetField ( ASF_LegacyManager::fieldLicenseURL ) ); + + // update new object size + valueUns64LE = MakeUns64LE ( header.size() - headerStartPos ); + std::string newSize ( (const char*)&valueUns64LE, 8 ); + ReplaceString ( header, newSize, (headerStartPos + 16), 8 ); + + exportedObjects |= ASF_LegacyManager::objectContentEncryption; + +#endif + + } else if ( IsEqualGUID ( ASF_Header_Extension_Object, objectBase.guid ) && usePadding ) { + + // re-create object if padding needs to be used + buffer.reserve ( XMP_Uns32( objectBase.size ) ); + buffer.assign ( XMP_Uns32( objectBase.size ), ' ' ); + sourceRef->ReadAll ( const_cast<char*>(buffer.data()), XMP_Int32(objectBase.size) ); + + ASF_Support::WriteHeaderExtensionObject ( buffer, &header, objectBase, 0 ); + + } else if ( IsEqualGUID ( ASF_Padding_Object, objectBase.guid ) && usePadding ) { + + // eliminate padding (will be created as last object) + + } else { + + // simply copy all other objects + buffer.reserve ( XMP_Uns32( objectBase.size ) ); + buffer.assign ( XMP_Uns32( objectBase.size ), ' ' ); + sourceRef->ReadAll ( const_cast<char*>(buffer.data()), XMP_Int32(objectBase.size) ); + + header.append ( buffer, 0, XMP_Uns32( objectBase.size ) ); + + } + + pos += objectBase.size; + read += objectBase.size; + + writtenObjects ++; + + } + + // any objects to create ? + int newObjects = (changedObjects ^ exportedObjects); + + if ( newObjects ) { + + // create new objects with xmp-data + int headerStartPos; + ASF_ObjectBase newObjectBase; + XMP_Uns32 length; + + if ( newObjects & ASF_LegacyManager::objectContentDescription ) { + + headerStartPos = header.size(); + newObjectBase.guid = ASF_Content_Description_Object; + newObjectBase.size = 0; + + // write object header + header.append ( (const char*)&newObjectBase, kASF_ObjectBaseLen ); + + XMP_Uns16 titleLen = _legacyManager.GetField ( ASF_LegacyManager::fieldTitle).size( ); + valueUns16LE = MakeUns16LE ( titleLen ); + header.append ( (const char*)&valueUns16LE, 2 ); + + XMP_Uns16 authorLen = _legacyManager.GetField ( ASF_LegacyManager::fieldAuthor).size( ); + valueUns16LE = MakeUns16LE ( authorLen ); + header.append ( (const char*)&valueUns16LE, 2 ); + + XMP_Uns16 copyrightLen = _legacyManager.GetField ( ASF_LegacyManager::fieldCopyright).size( ); + valueUns16LE = MakeUns16LE ( copyrightLen ); + header.append ( (const char*)&valueUns16LE, 2 ); + + XMP_Uns16 descriptionLen = _legacyManager.GetField ( ASF_LegacyManager::fieldDescription).size( ); + valueUns16LE = MakeUns16LE ( descriptionLen ); + header.append ( (const char*)&valueUns16LE, 2 ); + + XMP_Uns16 ratingLen = 0; + valueUns16LE = MakeUns16LE ( ratingLen ); + header.append ( (const char*)&valueUns16LE, 2 ); + + // write field contents + + header.append ( _legacyManager.GetField ( ASF_LegacyManager::fieldTitle ) ); + header.append ( _legacyManager.GetField ( ASF_LegacyManager::fieldAuthor ) ); + header.append ( _legacyManager.GetField ( ASF_LegacyManager::fieldCopyright ) ); + header.append ( _legacyManager.GetField ( ASF_LegacyManager::fieldDescription ) ); + + // update new object size + valueUns64LE = MakeUns64LE ( header.size() - headerStartPos ); + std::string newSize ( (const char*)&valueUns64LE, 8 ); + ReplaceString ( header, newSize, (headerStartPos + 16), 8 ); + + newObjects &= ~ASF_LegacyManager::objectContentDescription; + + writtenObjects ++; + + } + + if ( newObjects & ASF_LegacyManager::objectContentBranding ) { + + headerStartPos = header.size(); + newObjectBase.guid = ASF_Content_Branding_Object; + newObjectBase.size = 0; + + // write object header + header.append ( (const char*)&newObjectBase, kASF_ObjectBaseLen ); + + // write 'empty' fields + header.append ( 12, '\0' ); + + // copyright URL + length = _legacyManager.GetField ( ASF_LegacyManager::fieldCopyrightURL).size( ); + valueUns32LE = MakeUns32LE ( length ); + header.append ( (const char*)&valueUns32LE, 4 ); + header.append ( _legacyManager.GetField ( ASF_LegacyManager::fieldCopyrightURL ) ); + + // update new object size + valueUns64LE = MakeUns64LE ( header.size() - headerStartPos ); + std::string newSize ( (const char*)&valueUns64LE, 8 ); + ReplaceString ( header, newSize, (headerStartPos + 16), 8 ); + + newObjects &= ~ASF_LegacyManager::objectContentBranding; + + writtenObjects ++; + + } + +#if ! Exclude_LicenseURL_Recon + + if ( newObjects & ASF_LegacyManager::objectContentEncryption ) { + + headerStartPos = header.size(); + newObjectBase.guid = ASF_Content_Encryption_Object; + newObjectBase.size = 0; + + // write object header + header.append ( (const char*)&newObjectBase, kASF_ObjectBaseLen ); + + // write 'empty' fields + header.append ( 12, '\0' ); + + // License URL + length = _legacyManager.GetField ( ASF_LegacyManager::fieldLicenseURL).size( ); + valueUns32LE = MakeUns32LE ( length ); + header.append ( (const char*)&valueUns32LE, 4 ); + header.append ( _legacyManager.GetField ( ASF_LegacyManager::fieldLicenseURL ) ); + + // update new object size + valueUns64LE = MakeUns64LE ( header.size() - headerStartPos ); + std::string newSize ( (const char*)&valueUns64LE, 8 ); + ReplaceString ( header, newSize, (headerStartPos + 16), 8 ); + + newObjects &= ~ASF_LegacyManager::objectContentEncryption; + + writtenObjects ++; + + } + +#endif + + } + + // create padding object ? + if ( usePadding && (header.size ( ) < object.len ) ) { + ASF_Support::CreatePaddingObject ( &header, (object.len - header.size()) ); + writtenObjects ++; + } + + // update new header-object size + valueUns64LE = MakeUns64LE ( header.size() ); + std::string newValue ( (const char*)&valueUns64LE, 8 ); + ReplaceString ( header, newValue, 16, 8 ); + + // update new number of Header objects + valueUns32LE = MakeUns32LE ( writtenObjects ); + newValue = std::string ( (const char*)&valueUns32LE, 4 ); + ReplaceString ( header, newValue, 24, 4 ); + + // if we are operating on the same file (in-place update), place pointer before writing + if ( sourceRef == destRef ) destRef->Seek ( object.pos, kXMP_SeekFromStart ); + + // write header + destRef->Write ( header.c_str(), header.size() ); + + } catch ( ... ) { + + return false; + + } + + return true; + +} + +// ============================================================================================= + +bool ASF_Support::UpdateHeaderObject ( XMP_IO* fileRef, const ObjectData& object, ASF_LegacyManager& _legacyManager ) +{ + return ASF_Support::WriteHeaderObject ( fileRef, fileRef, object, _legacyManager, true ); +} + +// ============================================================================================= + +bool ASF_Support::UpdateFileSize ( XMP_IO* fileRef ) +{ + if ( fileRef == 0 ) return false; + + XMP_Uns64 posCurrent = fileRef->Seek ( 0, kXMP_SeekFromCurrent ); + XMP_Uns64 newSizeLE = MakeUns64LE ( fileRef->Length() ); + + if ( this->posFileSizeInfo != 0 ) { + + fileRef->Seek ( this->posFileSizeInfo, kXMP_SeekFromStart ); + + } else { + + // The position of the file size field is not known, find it. + + ASF_ObjectBase objHeader; + + // Read the Header object at the start of the file. + + fileRef->Rewind(); + fileRef->ReadAll ( &objHeader, kASF_ObjectBaseLen ); + if ( ! IsEqualGUID ( ASF_Header_Object, objHeader.guid ) ) return false; + + XMP_Uns32 childCount; + fileRef->ReadAll ( &childCount, 4 ); + childCount = GetUns32LE ( &childCount ); + + fileRef->Seek ( 2, kXMP_SeekFromCurrent ); // Skip the 2 reserved bytes. + + // Look for the File Properties object in the Header's children. + + for ( ; childCount > 0; --childCount ) { + fileRef->ReadAll ( &objHeader, kASF_ObjectBaseLen ); + if ( IsEqualGUID ( ASF_File_Properties_Object, objHeader.guid ) ) break; + XMP_Uns64 dataLen = GetUns64LE ( &objHeader.size ) - 24; + fileRef->Seek ( dataLen, kXMP_SeekFromCurrent ); // Skip this object's data. + } + if ( childCount == 0 ) return false; + + // Seek to the file size field. + + XMP_Uns64 fpoSize = GetUns64LE ( &objHeader.size ); + if ( fpoSize < (16+8+16+8) ) return false; + fileRef->Seek ( 16, kXMP_SeekFromCurrent ); // Skip to the file size field. + + } + + fileRef->Write ( &newSizeLE, 8 ); // Write the new file size. + + fileRef->Seek ( posCurrent, kXMP_SeekFromStart ); + return true; + +} + +// ============================================================================================= + +bool ASF_Support::ReadHeaderExtensionObject ( XMP_IO* fileRef, ObjectState& inOutObjectState, const XMP_Uns64& _pos, const ASF_ObjectBase& _objectBase ) +{ + if ( ! IsEqualGUID ( ASF_Header_Extension_Object, _objectBase.guid) || (! legacyManager ) ) return false; + + try { + + // read extended header-object structure beginning at the data part (offset = 46) + const XMP_Uns64 offset = 46; + XMP_Uns64 read = 0; + XMP_Uns64 data = (_objectBase.size - offset); + XMP_Uns64 pos = (_pos + offset); + + ASF_ObjectBase objectBase; + + while ( read < data ) { + + fileRef->Seek ( pos, kXMP_SeekFromStart ); + if ( kASF_ObjectBaseLen != fileRef->Read ( &objectBase, kASF_ObjectBaseLen, true ) ) break; + + objectBase.size = GetUns64LE ( &objectBase.size ); + + if ( IsEqualGUID ( ASF_Padding_Object, objectBase.guid ) ) { + legacyManager->SetPadding ( legacyManager->GetPadding() + (objectBase.size - 24) ); + } + + pos += objectBase.size; + read += objectBase.size; + + } + + } catch ( ... ) { + + return false; + + } + + return true; + +} + +// ============================================================================================= + +bool ASF_Support::WriteHeaderExtensionObject ( const std::string& buffer, std::string* header, const ASF_ObjectBase& _objectBase, const int /*reservePadding*/ ) +{ + if ( ! IsEqualGUID ( ASF_Header_Extension_Object, _objectBase.guid ) || (! header) || (buffer.size() < 46) ) return false; + + const XMP_Uns64 offset = 46; + int startPos = header->size(); + + // copy header base + header->append ( buffer, 0, offset ); + + // read extended header-object structure beginning at the data part (offset = 46) + XMP_Uns64 read = 0; + XMP_Uns64 data = (_objectBase.size - offset); + XMP_Uns64 pos = offset; + + ASF_ObjectBase objectBase; + + while ( read < data ) { + + memcpy ( &objectBase, &buffer[int(pos)], kASF_ObjectBaseLen ); + objectBase.size = GetUns64LE ( &objectBase.size ); + + if ( IsEqualGUID ( ASF_Padding_Object, objectBase.guid ) ) { + // eliminate + } else { + // copy other objects + header->append ( buffer, XMP_Uns32(pos), XMP_Uns32(objectBase.size) ); + } + + pos += objectBase.size; + read += objectBase.size; + + } + + // update header extension data size + XMP_Uns32 valueUns32LE = MakeUns32LE ( header->size() - startPos - offset ); + std::string newDataSize ( (const char*)&valueUns32LE, 4 ); + ReplaceString ( *header, newDataSize, (startPos + 42), 4 ); + + // update new object size + XMP_Uns64 valueUns64LE = MakeUns64LE ( header->size() - startPos ); + std::string newObjectSize ( (const char*)&valueUns64LE, 8 ); + ReplaceString ( *header, newObjectSize, (startPos + 16), 8 ); + + return true; + +} + +// ============================================================================================= + +bool ASF_Support::CreatePaddingObject ( std::string* header, const XMP_Uns64 size ) +{ + if ( ( ! header) || (size < 24) ) return false; + + ASF_ObjectBase newObjectBase; + + newObjectBase.guid = ASF_Padding_Object; + newObjectBase.size = MakeUns64LE ( size ); + + // write object header + header->append ( (const char*)&newObjectBase, kASF_ObjectBaseLen ); + + // write 'empty' padding + header->append ( XMP_Uns32 ( size - 24 ), '\0' ); + + return true; + +} + +// ============================================================================================= + +bool ASF_Support::WriteXMPObject ( XMP_IO* fileRef, XMP_Uns32 len, const char* inBuffer ) +{ + bool ret = false; + + ASF_ObjectBase objectBase = { ASF_XMP_Metadata, 0 }; + objectBase.size = MakeUns64LE ( len + kASF_ObjectBaseLen ); + + try { + fileRef->Write ( &objectBase, kASF_ObjectBaseLen ); + fileRef->Write ( inBuffer, len ); + ret = true; + } catch ( ... ) {} + + return ret; + +} + +// ============================================================================================= + +bool ASF_Support::UpdateXMPObject ( XMP_IO* fileRef, const ObjectData& object, XMP_Uns32 len, const char * inBuffer ) +{ + bool ret = false; + + ASF_ObjectBase objectBase = { ASF_XMP_Metadata, 0 }; + objectBase.size = MakeUns64LE ( len + kASF_ObjectBaseLen ); + + try { + fileRef->Seek ( object.pos, kXMP_SeekFromStart ); + fileRef->Write ( &objectBase, kASF_ObjectBaseLen ); + fileRef->Write ( inBuffer, len ); + ret = true; + } catch ( ... ) {} + + return ret; + +} + +// ============================================================================================= + +bool ASF_Support::CopyObject ( XMP_IO* sourceRef, XMP_IO* destRef, const ObjectData& object ) +{ + try { + sourceRef->Seek ( object.pos, kXMP_SeekFromStart ); + XIO::Copy ( sourceRef, destRef, object.len ); + } catch ( ... ) { + return false; + } + + return true; + +} + +// ============================================================================================= + +bool ASF_Support::ReadBuffer ( XMP_IO* fileRef, XMP_Uns64 & pos, XMP_Uns64 len, char * outBuffer ) +{ + try { + + if ( (fileRef == 0) || (outBuffer == 0) ) return false; + + fileRef->Seek ( pos, kXMP_SeekFromStart ); + long bytesRead = fileRef->ReadAll ( outBuffer, XMP_Int32(len) ); + if ( XMP_Uns32 ( bytesRead ) != len ) return false; + + return true; + + } catch ( ... ) {} + + return false; + +} + +// ============================================================================================= + +bool ASF_Support::WriteBuffer ( XMP_IO* fileRef, XMP_Uns64 & pos, XMP_Uns32 len, const char * inBuffer ) +{ + try { + + if ( (fileRef == 0) || (inBuffer == 0) ) return false; + + fileRef->Seek ( pos, kXMP_SeekFromStart ); + fileRef->Write ( inBuffer, len ); + + return true; + + } catch ( ... ) {} + + return false; + +} + +// ================================================================================================= + +std::string ASF_Support::ReplaceString ( std::string& operand, std::string& str, int offset, int count ) +{ + std::basic_string<char>::iterator iterF1, iterL1, iterF2, iterL2; + + iterF1 = operand.begin() + offset; + iterL1 = operand.begin() + offset + count; + iterF2 = str.begin(); + iterL2 = str.begin() + count; + + return operand.replace ( iterF1, iterL1, iterF2, iterL2 ); + +} + +// ================================================================================================= + +ASF_LegacyManager::ASF_LegacyManager() : fields(fieldLast), broadcastSet(false), digestComputed(false), + imported(false), objectsExisting(0), objectsToExport(0), legacyDiff(0), padding(0) +{ + // Nothing more to do. +} + +// ================================================================================================= + +ASF_LegacyManager::~ASF_LegacyManager() +{ + // Nothing to do. +} + +// ================================================================================================= + +bool ASF_LegacyManager::SetField ( fieldType field, const std::string& value ) +{ + if ( field >= fieldLast ) return false; + + unsigned int maxSize = this->GetFieldMaxSize ( field ); + + if (value.size ( ) <= maxSize ) { + fields[field] = value; + } else { + fields[field] = value.substr ( 0, maxSize ); + } + + if ( field == fieldCopyrightURL ) NormalizeStringDisplayASCII ( fields[field] ); + + #if ! Exclude_LicenseURL_Recon + if ( field == fieldLicenseURL ) NormalizeStringDisplayASCII ( fields[field] ); + #endif + + return true; + +} + +// ================================================================================================= + +std::string ASF_LegacyManager::GetField ( fieldType field ) +{ + if ( field >= fieldLast ) return std::string(); + return fields[field]; +} + +// ================================================================================================= + +unsigned int ASF_LegacyManager::GetFieldMaxSize ( fieldType field ) +{ + unsigned int maxSize = 0; + + switch ( field ) { + + case fieldCreationDate : + maxSize = 8; + break; + + case fieldTitle : + case fieldAuthor : + case fieldCopyright : + case fieldDescription : + maxSize = 0xFFFF; + break; + + case fieldCopyrightURL : +#if ! Exclude_LicenseURL_Recon + case fieldLicenseURL : +#endif + maxSize = 0xFFFFFFFF; + break; + + default: + break; + + } + + return maxSize; + +} + +// ================================================================================================= + +void ASF_LegacyManager::SetObjectExists ( objectType object ) +{ + objectsExisting |= object; +} + +// ================================================================================================= + +void ASF_LegacyManager::SetBroadcast ( const bool broadcast ) +{ + broadcastSet = broadcast; +} + +// ================================================================================================= + +bool ASF_LegacyManager::GetBroadcast() +{ + return broadcastSet; +} + +// ================================================================================================= + +void ASF_LegacyManager::ComputeDigest() +{ + MD5_CTX context; + MD5_Digest digest; + char buffer[40]; + + MD5Init ( &context ); + digestStr.clear(); + digestStr.reserve ( 160 ); + + for ( int type=0; type < fieldLast; ++type ) { + + if (fields[type].size ( ) > 0 ) { + snprintf ( buffer, sizeof(buffer), "%d,", type ); + digestStr.append ( buffer ); + MD5Update ( &context, (XMP_Uns8*)fields[type].data(), fields[type].size() ); + } + + } + + if( digestStr.size() > 0 ) digestStr[digestStr.size()-1] = ';'; + + MD5Final ( digest, &context ); + + size_t in, out; + for ( in = 0, out = 0; in < 16; in += 1, out += 2 ) { + XMP_Uns8 byte = digest[in]; + buffer[out] = ReconcileUtils::kHexDigits [ byte >> 4 ]; + buffer[out+1] = ReconcileUtils::kHexDigits [ byte & 0xF ]; + } + buffer[32] = 0; + + digestStr.append ( buffer ); + + digestComputed = true; + +} + +// ================================================================================================= + +bool ASF_LegacyManager::CheckDigest ( const SXMPMeta& xmp ) +{ + bool ret = false; + + if ( ! digestComputed ) this->ComputeDigest(); + + std::string oldDigest; + + if ( xmp.GetProperty ( kXMP_NS_ASF, "NativeDigest", &oldDigest, 0 ) ) { + ret = (digestStr == oldDigest); + } + + return ret; + +} + +// ================================================================================================= + +void ASF_LegacyManager::SetDigest ( SXMPMeta* xmp ) +{ + if ( ! digestComputed ) this->ComputeDigest(); + + xmp->SetProperty ( kXMP_NS_ASF, "NativeDigest", digestStr.c_str() ); + +} + +// ================================================================================================= + +void ASF_LegacyManager::ImportLegacy ( SXMPMeta* xmp ) +{ + std::string utf8; + + if ( ! broadcastSet ) { + ConvertMSDateToISODate ( fields[fieldCreationDate], &utf8 ); + if ( ! utf8.empty() ) xmp->SetProperty ( kXMP_NS_XMP, "CreateDate", utf8.c_str(), kXMP_DeleteExisting ); + } + + FromUTF16 ( (UTF16Unit*)fields[fieldTitle].c_str(), (fields[fieldTitle].size() / 2), &utf8, false ); + if ( ! utf8.empty() ) xmp->SetLocalizedText ( kXMP_NS_DC, "title", "", "x-default", utf8.c_str(), kXMP_DeleteExisting ); + + xmp->DeleteProperty ( kXMP_NS_DC, "creator" ); + FromUTF16 ( (UTF16Unit*)fields[fieldAuthor].c_str(), (fields[fieldAuthor].size() / 2), &utf8, false ); + if ( ! utf8.empty() ) SXMPUtils::SeparateArrayItems ( xmp, kXMP_NS_DC, "creator", + (kXMP_PropArrayIsOrdered | kXMPUtil_AllowCommas), utf8.c_str() ); + + FromUTF16 ( (UTF16Unit*)fields[fieldCopyright].c_str(), (fields[fieldCopyright].size() / 2), &utf8, false ); + if ( ! utf8.empty() ) xmp->SetLocalizedText ( kXMP_NS_DC, "rights", "", "x-default", utf8.c_str(), kXMP_DeleteExisting ); + + FromUTF16 ( (UTF16Unit*)fields[fieldDescription].c_str(), (fields[fieldDescription].size() / 2), &utf8, false ); + if ( ! utf8.empty() ) xmp->SetLocalizedText ( kXMP_NS_DC, "description", "", "x-default", utf8.c_str(), kXMP_DeleteExisting ); + + if ( ! utf8.empty() ) xmp->SetProperty ( kXMP_NS_XMP_Rights, "WebStatement", fields[fieldCopyrightURL].c_str(), kXMP_DeleteExisting ); + +#if ! Exclude_LicenseURL_Recon + if ( ! fields[fieldLicenseURL].empty() ) xmp->SetProperty ( kXMP_NS_XMP_Rights, "Certificate", fields[fieldLicenseURL].c_str(), kXMP_DeleteExisting ); +#endif + + imported = true; + +} + +// ================================================================================================= + +int ASF_LegacyManager::ExportLegacy ( const SXMPMeta& xmp ) +{ + int changed = 0; + objectsToExport = 0; + legacyDiff = 0; + + std::string utf8; + std::string utf16; + XMP_OptionBits flags; + + if ( ! broadcastSet ) { + if ( xmp.GetProperty ( kXMP_NS_XMP, "CreateDate", &utf8, &flags ) ) { + std::string date; + ConvertISODateToMSDate ( utf8, &date ); + if ( fields[fieldCreationDate] != date ) { + legacyDiff += date.size(); + legacyDiff -= fields[fieldCreationDate].size(); + this->SetField ( fieldCreationDate, date ); + objectsToExport |= objectFileProperties; + changed ++; + } + } + } + + if ( xmp.GetLocalizedText ( kXMP_NS_DC, "title", "", "x-default", 0, &utf8, &flags ) ) { + NormalizeStringTrailingNull ( utf8 ); + ToUTF16 ( (const UTF8Unit*)utf8.data(), utf8.size(), &utf16, false ); + if ( fields[fieldTitle] != utf16 ) { + legacyDiff += utf16.size(); + legacyDiff -= fields[fieldTitle].size(); + this->SetField ( fieldTitle, utf16 ); + objectsToExport |= objectContentDescription; + changed ++; + } + } + + utf8.clear(); + SXMPUtils::CatenateArrayItems ( xmp, kXMP_NS_DC, "creator", 0, 0, kXMPUtil_AllowCommas, &utf8 ); + if ( ! utf8.empty() ) { + NormalizeStringTrailingNull ( utf8 ); + ToUTF16 ( (const UTF8Unit*)utf8.data(), utf8.size(), &utf16, false ); + if ( fields[fieldAuthor] != utf16 ) { + legacyDiff += utf16.size(); + legacyDiff -= fields[fieldAuthor].size(); + this->SetField ( fieldAuthor, utf16 ); + objectsToExport |= objectContentDescription; + changed ++; + } + } + + if ( xmp.GetLocalizedText ( kXMP_NS_DC, "rights", "", "x-default", 0, &utf8, &flags ) ) { + NormalizeStringTrailingNull ( utf8 ); + ToUTF16 ( (const UTF8Unit*)utf8.data(), utf8.size(), &utf16, false ); + if ( fields[fieldCopyright] != utf16 ) { + legacyDiff += utf16.size(); + legacyDiff -= fields[fieldCopyright].size(); + this->SetField ( fieldCopyright, utf16 ); + objectsToExport |= objectContentDescription; + changed ++; + } + } + + if ( xmp.GetLocalizedText ( kXMP_NS_DC, "description", "", "x-default", 0, &utf8, &flags ) ) { + NormalizeStringTrailingNull ( utf8 ); + ToUTF16 ( (const UTF8Unit*)utf8.data(), utf8.size(), &utf16, false ); + if ( fields[fieldDescription] != utf16 ) { + legacyDiff += utf16.size(); + legacyDiff -= fields[fieldDescription].size(); + this->SetField ( fieldDescription, utf16 ); + objectsToExport |= objectContentDescription; + changed ++; + } + } + + if ( xmp.GetProperty ( kXMP_NS_XMP_Rights, "WebStatement", &utf8, &flags ) ) { + NormalizeStringTrailingNull ( utf8 ); + if ( fields[fieldCopyrightURL] != utf8 ) { + legacyDiff += utf8.size(); + legacyDiff -= fields[fieldCopyrightURL].size(); + this->SetField ( fieldCopyrightURL, utf8 ); + objectsToExport |= objectContentBranding; + changed ++; + } + } + +#if ! Exclude_LicenseURL_Recon + if ( xmp.GetProperty ( kXMP_NS_XMP_Rights, "Certificate", &utf8, &flags ) ) { + NormalizeStringTrailingNull ( utf8 ); + if ( fields[fieldLicenseURL] != utf8 ) { + legacyDiff += utf8.size(); + legacyDiff -= fields[fieldLicenseURL].size(); + this->SetField ( fieldLicenseURL, utf8 ); + objectsToExport |= objectContentEncryption; + changed ++; + } + } +#endif + + // find objects, that would need to be created on legacy export + int newObjects = (objectsToExport & !objectsExisting); + + // calculate minimum storage for new objects, that might be created on export + if ( newObjects & objectContentDescription ) + legacyDiff += sizeContentDescription; + if ( newObjects & objectContentBranding ) + legacyDiff += sizeContentBranding; + if ( newObjects & objectContentEncryption ) + legacyDiff += sizeContentEncryption; + + ComputeDigest(); + + return changed; + +} + +// ================================================================================================= + +bool ASF_LegacyManager::hasLegacyChanged() +{ + return (objectsToExport != 0); +} + +// ================================================================================================= + +XMP_Int64 ASF_LegacyManager::getLegacyDiff() +{ + return (this->hasLegacyChanged() ? legacyDiff : 0); +} + +// ================================================================================================= + +int ASF_LegacyManager::changedObjects() +{ + return objectsToExport; +} + +// ================================================================================================= + +void ASF_LegacyManager::SetPadding ( XMP_Int64 _padding ) +{ + padding = _padding; +} + +// ================================================================================================= + +XMP_Int64 ASF_LegacyManager::GetPadding() +{ + return padding; +} + +// ================================================================================================= + +std::string ASF_LegacyManager::NormalizeStringDisplayASCII ( std::string& operand ) +{ + std::basic_string<char>::iterator current = operand.begin(); + std::basic_string<char>::iterator end = operand.end();; + + for ( ; (current != end); ++current ) { + char element = *current; + if ( ( (element < 0x21) && (element != 0x00)) || (element > 0x7e) ) { + *current = '?'; + } + } + + return operand; + +} + +// ================================================================================================= + +std::string ASF_LegacyManager::NormalizeStringTrailingNull ( std::string& operand ) +{ + if ( ( operand.size() > 0) && (operand[operand.size() - 1] != '\0') ) { + operand.append ( 1, '\0' ); + } + + return operand; + +} + +// ================================================================================================= + +int ASF_LegacyManager::DaysInMonth ( XMP_Int32 year, XMP_Int32 month ) +{ + + static short daysInMonth[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + // Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec + + int days = daysInMonth [ month ]; + if ( (month == 2) && IsLeapYear ( year ) ) days += 1; + + return days; + +} + +// ================================================================================================= + +bool ASF_LegacyManager::IsLeapYear ( long year ) +{ + + if ( year < 0 ) year = -year + 1; // Fold the negative years, assuming there is a year 0. + + if ( (year % 4) != 0 ) return false; // Not a multiple of 4. + if ( (year % 100) != 0 ) return true; // A multiple of 4 but not a multiple of 100. + if ( (year % 400) == 0 ) return true; // A multiple of 400. + + return false; // A multiple of 100 but not a multiple of 400. + +} + +// ================================================================================================= + +void ASF_LegacyManager::ConvertMSDateToISODate ( std::string& source, std::string* dest ) +{ + + XMP_Int64 creationDate = GetUns64LE ( source.c_str() ); + XMP_Int64 totalSecs = creationDate / (10*1000*1000); + XMP_Int32 nanoSec = ( ( XMP_Int32) (creationDate - (totalSecs * 10*1000*1000)) ) * 100; + + XMP_Int32 days = (XMP_Int32) (totalSecs / 86400); + totalSecs -= ( ( XMP_Int64)days * 86400 ); + + XMP_Int32 hour = (XMP_Int32) (totalSecs / 3600); + totalSecs -= ( ( XMP_Int64)hour * 3600 ); + + XMP_Int32 minute = (XMP_Int32) (totalSecs / 60); + totalSecs -= ( ( XMP_Int64)minute * 60 ); + + XMP_Int32 second = (XMP_Int32)totalSecs; + + // A little more simple code converts this to an XMP date string: + + XMP_DateTime date; + memset ( &date, 0, sizeof ( date ) ); + + date.year = 1601; // The MS date origin. + date.month = 1; + date.day = 1; + + date.day += days; // Add in the delta. + date.hour = hour; + date.minute = minute; + date.second = second; + date.nanoSecond = nanoSec; + + date.hasTimeZone = true; // ! Needed for ConvertToUTCTime to do anything. + SXMPUtils::ConvertToUTCTime ( &date ); // Normalize the date/time. + SXMPUtils::ConvertFromDate ( date, dest ); // Convert to an ISO 8601 string. + +} + +// ================================================================================================= + +void ASF_LegacyManager::ConvertISODateToMSDate ( std::string& source, std::string* dest ) +{ + XMP_DateTime date; + SXMPUtils::ConvertToDate ( source, &date ); + SXMPUtils::ConvertToUTCTime ( &date ); + + XMP_Int64 creationDate; + creationDate = date.nanoSecond / 100; + creationDate += (XMP_Int64 ( date.second) * (10*1000*1000) ); + creationDate += (XMP_Int64 ( date.minute) * 60 * (10*1000*1000) ); + creationDate += (XMP_Int64 ( date.hour) * 3600 * (10*1000*1000) ); + + XMP_Int32 days = (date.day - 1); + + --date.month; + while ( date.month >= 1 ) { + days += DaysInMonth ( date.year, date.month ); + --date.month; + } + + --date.year; + while ( date.year >= 1601 ) { + days += (IsLeapYear ( date.year) ? 366 : 365 ); + --date.year; + } + + creationDate += (XMP_Int64 ( days) * 86400 * (10*1000*1000) ); + + creationDate = GetUns64LE ( &creationDate ); + dest->assign ( (const char*)&creationDate, 8 ); + +} diff --git a/XMPFiles/source/FormatSupport/ASF_Support.hpp b/XMPFiles/source/FormatSupport/ASF_Support.hpp new file mode 100644 index 0000000..9d9060b --- /dev/null +++ b/XMPFiles/source/FormatSupport/ASF_Support.hpp @@ -0,0 +1,226 @@ +#ifndef __ASF_Support_hpp__ +#define __ASF_Support_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp" +#include "source/XIO.hpp" + +// currently exclude LicenseURL from reconciliation +#define Exclude_LicenseURL_Recon 1 +#define EXCLUDE_LICENSEURL_RECON 1 + +// Defines for platforms other than Win +#if ! XMP_WinBuild + + typedef struct _GUID + { + XMP_Uns32 Data1; + XMP_Uns16 Data2; + XMP_Uns16 Data3; + XMP_Uns8 Data4[8]; + } GUID; + + int IsEqualGUID ( const GUID& guid1, const GUID& guid2 ); + + static const GUID GUID_NULL = { 0x0, 0x0, 0x0, { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 } }; + +#endif + +// header object +static const GUID ASF_Header_Object = { MakeUns32LE(0x75b22630), MakeUns16LE(0x668e), MakeUns16LE(0x11cf), { 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c } }; +// contains ... +static const GUID ASF_File_Properties_Object = { MakeUns32LE(0x8cabdca1), MakeUns16LE(0xa947), MakeUns16LE(0x11cf), { 0x8e, 0xe4, 0x00, 0xc0, 0x0c, 0x20, 0x53, 0x65 } }; +static const GUID ASF_Content_Description_Object = { MakeUns32LE(0x75b22633), MakeUns16LE(0x668e), MakeUns16LE(0x11cf), { 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c } }; +static const GUID ASF_Content_Branding_Object = { MakeUns32LE(0x2211b3fa), MakeUns16LE(0xbd23), MakeUns16LE(0x11d2), { 0xb4, 0xb7, 0x00, 0xa0, 0xc9, 0x55, 0xfc, 0x6e } }; +static const GUID ASF_Content_Encryption_Object = { MakeUns32LE(0x2211b3fb), MakeUns16LE(0xbd23), MakeUns16LE(0x11d2), { 0xb4, 0xb7, 0x00, 0xa0, 0xc9, 0x55, 0xfc, 0x6e } }; +// padding +// Remark: regarding to Microsofts spec only the ASF_Header_Object contains a ASF_Padding_Object +// Real world files show, that the ASF_Header_Extension_Object contains a ASF_Padding_Object +static const GUID ASF_Header_Extension_Object = { MakeUns32LE(0x5fbf03b5), MakeUns16LE(0xa92e), MakeUns16LE(0x11cf), { 0x8e, 0xe3, 0x00, 0xc0, 0x0c, 0x20, 0x53, 0x65 } }; +static const GUID ASF_Padding_Object = { MakeUns32LE(0x1806d474), MakeUns16LE(0xcadf), MakeUns16LE(0x4509), { 0xa4, 0xba, 0x9a, 0xab, 0xcb, 0x96, 0xaa, 0xe8 } }; + +// data object +static const GUID ASF_Data_Object = { MakeUns32LE(0x75b22636), MakeUns16LE(0x668e), MakeUns16LE(0x11cf), { 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c } }; + +// XMP object +static const GUID ASF_XMP_Metadata = { MakeUns32LE(0xbe7acfcb), MakeUns16LE(0x97a9), MakeUns16LE(0x42e8), { 0x9c, 0x71, 0x99, 0x94, 0x91, 0xe3, 0xaf, 0xac } }; + +static const int guidLen = sizeof(GUID); + +typedef struct _ASF_ObjectBase +{ + GUID guid; + XMP_Uns64 size; + +} ASF_ObjectBase; + +static const XMP_Uns32 kASF_ObjectBaseLen = (XMP_Uns32) sizeof(ASF_ObjectBase); + +// ================================================================================================= + +class ASF_LegacyManager { +public: + + enum objectType { + objectFileProperties = 1 << 0, + objectContentDescription = 1 << 1, + objectContentBranding = 1 << 2, + objectContentEncryption = 1 << 3 + }; + + enum minObjectSize { + sizeContentDescription = 34, + sizeContentBranding = 40, + sizeContentEncryption = 40 + }; + + enum fieldType { + // File_Properties_Object + fieldCreationDate = 0, + // Content_Description_Object + fieldTitle, + fieldAuthor, + fieldCopyright, + fieldDescription, + // Content_Branding_Object + fieldCopyrightURL, + #if ! Exclude_LicenseURL_Recon + // Content_Encryption_Object + fieldLicenseURL, + #endif + // last + fieldLast + }; + + ASF_LegacyManager(); + virtual ~ASF_LegacyManager(); + + bool SetField ( fieldType field, const std::string& value ); + std::string GetField ( fieldType field ); + unsigned int GetFieldMaxSize ( fieldType field ); + + void SetObjectExists ( objectType object ); + + void SetBroadcast ( const bool broadcast ); + bool GetBroadcast(); + + void ComputeDigest(); + bool CheckDigest ( const SXMPMeta& xmp ); + void SetDigest ( SXMPMeta* xmp ); + + void ImportLegacy ( SXMPMeta* xmp ); + int ExportLegacy ( const SXMPMeta& xmp ); + bool hasLegacyChanged(); + XMP_Int64 getLegacyDiff(); + int changedObjects(); + + void SetPadding ( XMP_Int64 padding ); + XMP_Int64 GetPadding(); + +private: + + typedef std::vector<std::string> TFields; + TFields fields; + bool broadcastSet; + + std::string digestStr; + bool digestComputed; + + bool imported; + int objectsExisting; + int objectsToExport; + XMP_Int64 legacyDiff; + XMP_Int64 padding; + + static std::string NormalizeStringDisplayASCII ( std::string& operand ); + static std::string NormalizeStringTrailingNull ( std::string& operand ); + + static void ConvertMSDateToISODate ( std::string& source, std::string* dest ); + static void ConvertISODateToMSDate ( std::string& source, std::string* dest ); + + static int DaysInMonth ( XMP_Int32 year, XMP_Int32 month ); + static bool IsLeapYear ( long year ); + +}; // class ASF_LegacyManager + +// ================================================================================================= + +class ASF_Support { +public: + + class ObjectData { + public: + ObjectData() : pos(0), len(0), guid(GUID_NULL), xmp(false) {} + virtual ~ObjectData() {} + XMP_Uns64 pos; // file offset of object + XMP_Uns64 len; // length of object data + GUID guid; // object GUID + bool xmp; // object with XMP ? + }; + + typedef std::vector<ObjectData> ObjectVector; + typedef ObjectVector::iterator ObjectIterator; + + class ObjectState { + + public: + ObjectState() : xmpPos(0), xmpLen(0), xmpIsLastObject(false), broadcast(false) {} + virtual ~ObjectState() {} + XMP_Uns64 xmpPos; + XMP_Uns64 xmpLen; + bool xmpIsLastObject; + bool broadcast; + ObjectData xmpObject; + ObjectVector objects; + }; + + ASF_Support(); + ASF_Support ( ASF_LegacyManager* legacyManager ); + virtual ~ASF_Support(); + + long OpenASF ( XMP_IO* fileRef, ObjectState & inOutObjectState ); + + bool ReadObject ( XMP_IO* fileRef, ObjectState & inOutObjectState, XMP_Uns64 * objectLength, XMP_Uns64 & inOutPosition ); + + bool ReadHeaderObject ( XMP_IO* fileRef, ObjectState& inOutObjectState, const ObjectData& newObject ); + bool WriteHeaderObject ( XMP_IO* sourceRef, XMP_IO* destRef, const ObjectData& object, ASF_LegacyManager& legacyManager, bool usePadding ); + bool UpdateHeaderObject ( XMP_IO* fileRef, const ObjectData& object, ASF_LegacyManager& legacyManager ); + + bool UpdateFileSize ( XMP_IO* fileRef ); + + bool ReadHeaderExtensionObject ( XMP_IO* fileRef, ObjectState& inOutObjectState, const XMP_Uns64& pos, const ASF_ObjectBase& objectBase ); + static bool WriteHeaderExtensionObject ( const std::string& buffer, std::string* header, const ASF_ObjectBase& objectBase, const int reservePadding ); + + static bool CreatePaddingObject ( std::string* header, const XMP_Uns64 size ); + + static bool WriteXMPObject ( XMP_IO* fileRef, XMP_Uns32 len, const char* inBuffer ); + static bool UpdateXMPObject ( XMP_IO* fileRef, const ObjectData& object, XMP_Uns32 len, const char * inBuffer ); + static bool CopyObject ( XMP_IO* sourceRef, XMP_IO* destRef, const ObjectData& object ); + + static bool ReadBuffer ( XMP_IO* fileRef, XMP_Uns64 & pos, XMP_Uns64 len, char * outBuffer ); + static bool WriteBuffer ( XMP_IO* fileRef, XMP_Uns64 & pos, XMP_Uns32 len, const char * inBuffer ); + +private: + + ASF_LegacyManager* legacyManager; + XMP_Uns64 posFileSizeInfo; + + static std::string ReplaceString ( std::string& operand, std::string& str, int offset, int count ); + +}; // class ASF_Support + +// ================================================================================================= + +#endif // __ASF_Support_hpp__ diff --git a/XMPFiles/source/FormatSupport/ID3_Support.cpp b/XMPFiles/source/FormatSupport/ID3_Support.cpp new file mode 100644 index 0000000..2bc9a1f --- /dev/null +++ b/XMPFiles/source/FormatSupport/ID3_Support.cpp @@ -0,0 +1,504 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2008 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "XMPFiles/source/FormatSupport/ID3_Support.hpp" +#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp" + +#include "source/UnicodeConversions.hpp" +#include "source/XIO.hpp" + +#include <vector> + +// ================================================================================================= + +#define MIN(a,b) ((a) < (b) ? (a) : (b)) + +ID3GenreMap* kMapID3GenreCodeToName = 0; // Map from a code like "21" or "RX" to the full name. +ID3GenreMap* kMapID3GenreNameToCode = 0; // Map from the full name to a code like "21" or "RX". + +// ================================================================================================= + +bool ID3_Support::InitializeGlobals() +{ + return true; +} + +// ================================================================================================= + +void ID3_Support::TerminateGlobals() +{ + // nothing yet +} + +// ================================================================================================= +// ID3Header +// ================================================================================================= + +bool ID3_Support::ID3Header::read ( XMP_IO* file ) +{ + + XMP_Assert ( sizeof(fields) == kID3_TagHeaderSize ); + file->ReadAll ( this->fields, kID3_TagHeaderSize ); + + if ( ! CheckBytes ( &this->fields[ID3Header::o_id], "ID3", 3 ) ) { + // chuck in default contents: + const static char defaultHeader[kID3_TagHeaderSize] = { 'I', 'D', '3', 3, 0, 0, 0, 0, 0, 0 }; + memcpy ( this->fields, defaultHeader, kID3_TagHeaderSize ); + return false; // no header found (o.k.) thus stick with new, default header constructed above + } + + XMP_Uns8 major = this->fields[o_vMajor]; + XMP_Uns8 minor = this->fields[o_vMinor]; + XMP_Validate ( ((2 <= major) && (major <= 4)), "Invalid ID3 major version", kXMPErr_BadFileFormat ); + + return true; + +} + +// ================================================================================================= + +void ID3_Support::ID3Header::write ( XMP_IO* file, XMP_Int64 tagSize ) +{ + + XMP_Assert ( (kID3_TagHeaderSize <= tagSize) && (tagSize < 256*1024*1024) ); // 256 MB limit due to synching. + + XMP_Uns32 synchSize = int32ToSynch ( (XMP_Uns32)tagSize - kID3_TagHeaderSize ); + PutUns32BE ( synchSize, &this->fields[ID3Header::o_size] ); + file->Write ( this->fields, kID3_TagHeaderSize ); + +} + +// ================================================================================================= +// ID3v2Frame +// ================================================================================================= + +#define frameDefaults id(0), flags(0), content(0), contentSize(0), active(true), changed(false) + +ID3_Support::ID3v2Frame::ID3v2Frame() : frameDefaults +{ + XMP_Assert ( sizeof(fields) == kV23_FrameHeaderSize ); // Only need to do this in one place. + memset ( this->fields, 0, kV23_FrameHeaderSize ); +} + +// ================================================================================================= + +ID3_Support::ID3v2Frame::ID3v2Frame ( XMP_Uns32 id ) : frameDefaults +{ + memset ( this->fields, 0, kV23_FrameHeaderSize ); + this->id = id; + PutUns32BE ( id, &this->fields[o_id] ); +} + +// ================================================================================================= + +void ID3_Support::ID3v2Frame::release() +{ + if ( this->content != 0 ) delete this->content; + this->content = 0; + this->contentSize = 0; +} + +// ================================================================================================= + +void ID3_Support::ID3v2Frame::setFrameValue ( const std::string& rawvalue, bool needDescriptor, + bool utf16, bool isXMPPRIVFrame, bool needEncodingByte ) +{ + + std::string value; + + if ( isXMPPRIVFrame ) { + + XMP_Assert ( (! needDescriptor) && (! utf16) ); + + value.append ( "XMP\0", 4 ); + value.append ( rawvalue ); + value.append ( "\0", 1 ); // final zero byte + + } else { + + if ( needEncodingByte ) { + if ( utf16 ) { + value.append ( "\x1", 1 ); + } else { + value.append ( "\x0", 1 ); + } + } + + if ( needDescriptor ) value.append ( "eng", 3 ); + + if ( utf16 ) { + + if ( needDescriptor ) value.append ( "\xFF\xFE\0\0", 4 ); + + value.append ( "\xFF\xFE", 2 ); + std::string utf16str; + ToUTF16 ( (XMP_Uns8*) rawvalue.c_str(), rawvalue.size(), &utf16str, false ); + value.append ( utf16str ); + value.append ( "\0\0", 2 ); + + } else { + + std::string convertedValue; + ReconcileUtils::UTF8ToLatin1 ( rawvalue.c_str(), rawvalue.size(), &convertedValue ); + + if ( needDescriptor ) value.append ( "\0", 1 ); + value.append ( convertedValue ); + value.append ( "\0", 1 ); + + } + + } + + this->changed = true; + this->release(); + + this->contentSize = (XMP_Int32) value.size(); + XMP_Validate ( (this->contentSize < 20*1024*1024), "XMP Property exceeds 20MB in size", kXMPErr_InternalFailure ); + this->content = new char [ this->contentSize ]; + memcpy ( this->content, value.c_str(), this->contentSize ); + +} // ID3v2Frame::setFrameValue + +// ================================================================================================= + +XMP_Int64 ID3_Support::ID3v2Frame::read ( XMP_IO* file, XMP_Uns8 majorVersion ) +{ + XMP_Assert ( (2 <= majorVersion) && (majorVersion <= 4) ); + + this->release(); // ensures/allows reuse of 'curFrame' + XMP_Int64 start = file->Offset(); + + if ( majorVersion > 2 ) { + file->ReadAll ( this->fields, kV23_FrameHeaderSize ); + } else { + // Read the 6 byte v2.2 header into the 10 byte form. + memset ( this->fields, 0, kV23_FrameHeaderSize ); // Clear all of the bytes. + file->ReadAll ( &this->fields[o_id], 3 ); // Leave the low order byte as zero. + file->ReadAll ( &this->fields[o_size+1], 3 ); // Read big endian UInt24. + } + + this->id = GetUns32BE ( &this->fields[o_id] ); + + if ( this->id == 0 ) { + file->Seek ( start, kXMP_SeekFromStart ); // Zero ID must mean nothing but padding. + return 0; + } + + this->flags = GetUns16BE ( &this->fields[o_flags] ); + XMP_Validate ( (0 == (this->flags & 0xEE)), "invalid lower bits in frame flags", kXMPErr_BadFileFormat ); + + //*** flag handling, spec :429ff aka line 431ff (i.e. Frame should be discarded) + // compression and all of that..., unsynchronisation + this->contentSize = GetUns32BE ( &this->fields[o_size] ); + if ( majorVersion == 4 ) this->contentSize = synchToInt32 ( this->contentSize ); + + XMP_Validate ( (this->contentSize >= 0), "negative frame size", kXMPErr_BadFileFormat ); + XMP_Validate ( (this->contentSize < 20*1024*1024), "single frame exceeds 20MB", kXMPErr_BadFileFormat ); + + this->content = new char [ this->contentSize ]; + + file->ReadAll ( this->content, this->contentSize ); + return file->Offset() - start; + +} // ID3v2Frame::read + +// ================================================================================================= + +void ID3_Support::ID3v2Frame::write ( XMP_IO* file, XMP_Uns8 majorVersion ) +{ + XMP_Assert ( (2 <= majorVersion) && (majorVersion <= 4) ); + + if ( majorVersion < 4 ) { + PutUns32BE ( this->contentSize, &this->fields[o_size] ); + } else { + PutUns32BE ( int32ToSynch ( this->contentSize ), &this->fields[o_size] ); + } + + if ( majorVersion > 2 ) { + file->Write ( this->fields, kV23_FrameHeaderSize ); + } else { + file->Write ( &this->fields[o_id], 3 ); + file->Write ( &this->fields[o_size+1], 3 ); + } + + file->Write ( this->content, this->contentSize ); + +} // ID3v2Frame::write + +// ================================================================================================= + +bool ID3_Support::ID3v2Frame::advancePastCOMMDescriptor ( XMP_Int32& pos ) +{ + + if ( (this->contentSize - pos) <= 3 ) return false; // silent error, no room left behing language tag + if ( ! CheckBytes ( &this->content[pos], "eng", 3 ) ) return false; // not an error, but leave all non-eng tags alone... + + pos += 3; // skip lang tag + if ( pos >= this->contentSize ) return false; // silent error + + while ( pos < this->contentSize ) { + if ( this->content[pos++] == 0x00 ) break; + } + if ( (pos < this->contentSize) && (this->content[pos] == 0x00) ) pos++; + + if ( (pos == 5) && (this->contentSize == 6) && (GetUns16BE(&this->content[4]) == 0x0031) ) { + return false; + } + + if ( pos > 4 ) { + std::string descriptor = std::string ( &this->content[4], pos-1 ); + if ( 0 == descriptor.substr(0,4).compare( "iTun" ) ) { // begins with engiTun ? + return false; // leave alone, then + } + } + + return true; //o.k., descriptor skipped, time for the real thing. + +} // ID3v2Frame::advancePastCOMMDescriptor + +// ================================================================================================= + +bool ID3_Support::ID3v2Frame::getFrameValue ( XMP_Uns8 majorVersion, XMP_Uns32 logicalID, std::string* utf8string ) +{ + + XMP_Assert ( (this->content != 0) && (this->contentSize >= 0) && (this->contentSize < 20*1024*1024) ); + + if ( this->contentSize == 0 ) { + utf8string->erase(); + return true; // ...it is "of interest", even if empty contents. + } + + XMP_Int32 pos = 0; + XMP_Uns8 encByte = 0; + // WCOP does not have an encoding byte, for all others: use [0] as EncByte, advance pos + if ( logicalID != 0x57434F50 ) { + encByte = this->content[0]; + pos++; + } + + // mode specific forks, COMM or USLT + bool commMode = ( (logicalID == 0x434F4D4D) || (logicalID == 0x55534C54) ); + + switch ( encByte ) { + + case 0: //ISO-8859-1, 0-terminated + { + if ( commMode && (! advancePastCOMMDescriptor ( pos )) ) return false; // not a frame of interest! + + char* localPtr = &this->content[pos]; + size_t localLen = this->contentSize - pos; + ReconcileUtils::Latin1ToUTF8 ( localPtr, localLen, utf8string ); + break; + + } + + case 1: // Unicode, v2.4: UTF-16 (undetermined Endianess), with BOM, terminated 0x00 00 + case 2: // UTF-16BE without BOM, terminated 0x00 00 + { + + if ( commMode && (! advancePastCOMMDescriptor ( pos )) ) return false; // not a frame of interest! + + std::string tmp ( this->content, this->contentSize ); + bool bigEndian = true; // assume for now (if no BOM follows) + + if ( GetUns16BE ( &this->content[pos] ) == 0xFEFF ) { + pos += 2; + bigEndian = true; + } else if ( GetUns16BE ( &this->content[pos] ) == 0xFFFE ) { + pos += 2; + bigEndian = false; + } + + FromUTF16 ( (UTF16Unit*)&this->content[pos], ((this->contentSize - pos)) / 2, utf8string, bigEndian ); + break; + + } + + case 3: // UTF-8 unicode, terminated \0 + { + if ( commMode && (! advancePastCOMMDescriptor ( pos )) ) return false; // not a frame of interest! + + if ( (GetUns32BE ( &this->content[pos]) & 0xFFFFFF00 ) == 0xEFBBBF00 ) { + pos += 3; // swallow any BOM, just in case + } + + utf8string->assign ( &this->content[pos], (this->contentSize - pos) ); + break; + } + + default: + XMP_Throw ( "unknown text encoding", kXMPErr_BadFileFormat ); //COULDDO assume latin-1 or utf-8 as best-effort + break; + + } + + return true; + +} // ID3v2Frame::getFrameValue + +// ================================================================================================= +// ID3v1Tag +// ================================================================================================= + +bool ID3_Support::ID3v1Tag::read ( XMP_IO* file, SXMPMeta* meta ) +{ + // returns returns true, if ID3v1 (or v1.1) exists, otherwise false, sets XMP properties en route + + if ( file->Length() <= 128 ) return false; // ensure sufficient room + file->Seek ( -128, kXMP_SeekFromEnd ); + + XMP_Uns32 tagID = XIO::ReadInt32_BE ( file ); + tagID = tagID & 0xFFFFFF00; // wipe 4th byte + if ( tagID != 0x54414700 ) return false; // must be "TAG" + file->Seek ( -1, kXMP_SeekFromCurrent ); //rewind 1 + + XMP_Uns8 buffer[31]; // nothing is bigger here, than 30 bytes (offsets [0]-[29]) + buffer[30] = 0; // wipe last byte + std::string utf8string; + + file->ReadAll ( buffer, 30 ); + std::string title ( (char*) buffer ); //security: guaranteed to 0-terminate after 30 bytes + if ( ! title.empty() ) { + ReconcileUtils::Latin1ToUTF8 ( title.c_str(), title.size(), &utf8string ); + meta->SetLocalizedText ( kXMP_NS_DC, "title", "", "x-default", utf8string.c_str() ); + } + + file->ReadAll ( buffer, 30 ); + std::string artist( (char*) buffer ); + if ( ! artist.empty() ) { + ReconcileUtils::Latin1ToUTF8 ( artist.c_str(), artist.size(), &utf8string ); + meta->SetProperty ( kXMP_NS_DM, "artist", utf8string.c_str() ); + } + + file->ReadAll ( buffer, 30 ); + std::string album( (char*) buffer ); + if ( ! album.empty() ) { + ReconcileUtils::Latin1ToUTF8 ( album.c_str(), album.size(), &utf8string ); + meta->SetProperty ( kXMP_NS_DM, "album", utf8string.c_str() ); + } + + file->ReadAll ( buffer, 4 ); + buffer[4]=0; // ensure 0-term + std::string year( (char*) buffer ); + if ( ! year.empty() ) { // should be moot for a year, but let's be safe: + ReconcileUtils::Latin1ToUTF8 ( year.c_str(), year.size(), &utf8string ); + meta->SetProperty ( kXMP_NS_XMP, "CreateDate", utf8string.c_str() ); + } + + file->ReadAll ( buffer, 30 ); + std::string comment( (char*) buffer ); + if ( ! comment.empty() ) { + ReconcileUtils::Latin1ToUTF8 ( comment.c_str(), comment.size(), &utf8string ); + meta->SetProperty ( kXMP_NS_DM, "logComment", utf8string.c_str() ); + } + + if ( buffer[28] == 0 ) { + XMP_Uns8 trackNo = buffer[29]; + if ( trackNo > 0 ) { + std::string trackStr; + meta->SetProperty_Int ( kXMP_NS_DM, "trackNumber", trackNo ); + } + } + + XMP_Uns8 genreNo = XIO::ReadUns8 ( file ); + if ( genreNo < 127 ) { + meta->SetProperty ( kXMP_NS_DM, "genre", Genres[genreNo] ); + } + + return true; // ID3Tag found + +} // ID3v1Tag::read + +// ================================================================================================= + +void ID3_Support::ID3v1Tag::write ( XMP_IO* file, SXMPMeta* meta ) +{ + + std::string zeros ( 128, '\0' ); + std::string utf8, latin1; + + file->Seek ( -128, kXMP_SeekFromEnd ); + file->Write ( zeros.data(), 128 ); + + file->Seek ( -128, kXMP_SeekFromEnd ); + XIO::WriteUns8 ( file, 'T' ); + XIO::WriteUns8 ( file, 'A' ); + XIO::WriteUns8 ( file, 'G' ); + + if ( meta->GetLocalizedText ( kXMP_NS_DC, "title", "", "x-default", 0, &utf8, 0 ) ) { + file->Seek ( (-128 + 3), kXMP_SeekFromEnd ); + ReconcileUtils::UTF8ToLatin1 ( utf8.c_str(), utf8.size(), &latin1 ); + file->Write ( latin1.c_str(), MIN ( 30, (XMP_Int32)latin1.size() ) ); + } + + if ( meta->GetProperty ( kXMP_NS_DM, "artist", &utf8, 0 ) ) { + file->Seek ( (-128 + 33), kXMP_SeekFromEnd ); + ReconcileUtils::UTF8ToLatin1 ( utf8.c_str(), utf8.size(), &latin1 ); + file->Write ( latin1.c_str(), MIN ( 30, (XMP_Int32)latin1.size() ) ); + } + + if ( meta->GetProperty ( kXMP_NS_DM, "album", &utf8, 0 ) ) { + file->Seek ( (-128 + 63), kXMP_SeekFromEnd ); + ReconcileUtils::UTF8ToLatin1 ( utf8.c_str(), utf8.size(), &latin1 ); + file->Write ( latin1.c_str(), MIN ( 30, (XMP_Int32)latin1.size() ) ); + } + + if ( meta->GetProperty ( kXMP_NS_XMP, "CreateDate", &utf8, 0 ) ) { + XMP_DateTime dateTime; + SXMPUtils::ConvertToDate( utf8, &dateTime ); + if ( dateTime.hasDate ) { + SXMPUtils::ConvertFromInt ( dateTime.year, "", &latin1 ); + file->Seek ( (-128 + 93), kXMP_SeekFromEnd ); + file->Write ( latin1.c_str(), MIN ( 4, (XMP_Int32)latin1.size() ) ); + } + } + + if ( meta->GetProperty ( kXMP_NS_DM, "logComment", &utf8, 0 ) ) { + file->Seek ( (-128 + 97), kXMP_SeekFromEnd ); + ReconcileUtils::UTF8ToLatin1 ( utf8.c_str(), utf8.size(), &latin1 ); + file->Write ( latin1.c_str(), MIN ( 30, (XMP_Int32)latin1.size() ) ); + } + + if ( meta->GetProperty ( kXMP_NS_DM, "genre", &utf8, 0 ) ) { + + XMP_Uns8 genreNo = 0; + + int i; + const char* genreCString = utf8.c_str(); + for ( i = 0; i < 127; ++i ) { + if ( (strlen(genreCString) == strlen(Genres[i])) && //fixing buggy stricmp behaviour on PPC + (stricmp ( genreCString, Genres[i] ) == 0 ) ) { + genreNo = i; // found + break; + } + } + + file->Seek ( (-128 + 127), kXMP_SeekFromEnd ); + XIO::WriteUns8 ( file, genreNo ); + + } + + if ( meta->GetProperty ( kXMP_NS_DM, "trackNumber", &utf8, kXMP_NoOptions ) ) { + + XMP_Uns8 trackNo = 0; + try { + trackNo = (XMP_Uns8) SXMPUtils::ConvertToInt ( utf8.c_str() ); + file->Seek ( (-128 + 125), kXMP_SeekFromEnd ); + XIO::WriteUns8 ( file, 0 ); // ID3v1.1 extension + XIO::WriteUns8 ( file, trackNo ); + } catch ( ... ) { + // forgive, just don't set this one. + } + + } + +} // ID3v1Tag::write diff --git a/XMPFiles/source/FormatSupport/ID3_Support.hpp b/XMPFiles/source/FormatSupport/ID3_Support.hpp new file mode 100644 index 0000000..43b917d --- /dev/null +++ b/XMPFiles/source/FormatSupport/ID3_Support.hpp @@ -0,0 +1,309 @@ +#ifndef __ID3_Support_hpp__ +#define __ID3_Support_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2008 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#if XMP_WinBuild + #define stricmp _stricmp +#else + static int stricmp ( const char * left, const char * right ) // Case insensitive ASCII compare. + { + char chL = *left; // ! Allow for 0 passes in the loop (one string is empty). + char chR = *right; // ! Return -1 for stricmp ( "a", "Z" ). + + for ( ; (*left != 0) && (*right != 0); ++left, ++right ) { + chL = *left; + chR = *right; + if ( chL == chR ) continue; + if ( ('A' <= chL) && (chL <= 'Z') ) chL |= 0x20; + if ( ('A' <= chR) && (chR <= 'Z') ) chR |= 0x20; + if ( chL != chR ) break; + } + + if ( chL == chR ) return 0; + if ( chL < chR ) return -1; + return 1; + } +#endif + +// ================================================================================================= + +namespace ID3_Support { + + // Genres + static char Genres[128][32] = { + "Blues", // 0 + "Classic Rock", // 1 + "Country", // 2 + "Dance", + "Disco", + "Funk", + "Grunge", + "Hip-Hop", + "Jazz", // 8 + "Metal", + "New Age", // 10 + "Oldies", + "Other", // 12 + "Pop", + "R&B", + "Rap", + "Reggae", // 16 + "Rock", // 17 + "Techno", + "Industrial", + "Alternative", + "Ska", + "Death Metal", + "Pranks", + "Soundtrack", // 24 + "Euro-Techno", + "Ambient", + "Trip-Hop", + "Vocal", + "Jazz+Funk", + "Fusion", + "Trance", + "Classical", // 32 + "Instrumental", + "Acid", + "House", + "Game", + "Sound Clip", + "Gospel", + "Noise", + "AlternRock", + "Bass", + "Soul", //42 + "Punk", + "Space", + "Meditative", + "Instrumental Pop", + "Instrumental Rock", + "Ethnic", + "Gothic", + "Darkwave", + "Techno-Industrial", + "Electronic", + "Pop-Folk", + "Eurodance", + "Dream", + "Southern Rock", + "Comedy", + "Cult", + "Gangsta", + "Top 40", + "Christian Rap", + "Pop/Funk", + "Jungle", + "Native American", + "Cabaret", + "New Wave", // 66 + "Psychadelic", + "Rave", + "Showtunes", + "Trailer", + "Lo-Fi", + "Tribal", + "Acid Punk", + "Acid Jazz", + "Polka", + "Retro", + "Musical", + "Rock & Roll", + "Hard Rock", + "Folk", // 80 + "Folk-Rock", + "National Folk", + "Swing", + "Fast Fusion", + "Bebob", + "Latin", + "Revival", + "Celtic", + "Bluegrass", // 89 + "Avantgarde", + "Gothic Rock", + "Progressive Rock", + "Psychedelic Rock", + "Symphonic Rock", + "Slow Rock", + "Big Band", + "Chorus", + "Easy Listening", + "Acoustic", + "Humour", // 100 + "Speech", + "Chanson", + "Opera", + "Chamber Music", + "Sonata", + "Symphony", + "Booty Bass", + "Primus", + "Porn Groove", + "Satire", + "Slow Jam", + "Club", + "Tango", + "Samba", + "Folklore", + "Ballad", + "Power Ballad", + "Rhythmic Soul", + "Freestyle", + "Duet", + "Punk Rock", + "Drum Solo", + "A capella", + "Euro-House", + "Dance Hall", + "Unknown" // 126 + }; + + // ============================================================================================= + + inline XMP_Int32 synchToInt32 ( XMP_Uns32 rawDataBE ) { + XMP_Validate ( (0 == (rawDataBE & 0x80808080)), "input not synchsafe", kXMPErr_InternalFailure ); + XMP_Int32 r = (rawDataBE & 0x0000007F) + ((rawDataBE >> 1) & 0x00003F80) + + ((rawDataBE >> 2) & 0x001FC000) + ((rawDataBE >> 3) & 0x0FE00000); + return r; + } + + inline XMP_Uns32 int32ToSynch ( XMP_Int32 value ) { + XMP_Validate ( (0 <= 0x0FFFFFFF), "value too big", kXMPErr_InternalFailure ); + XMP_Uns32 r = (value & 0x0000007F) + ((value & 0x00003F80) << 1) + + ((value & 0x001FC000) << 2) + ((value & 0x0FE00000) << 3); + return r; + } + + // ============================================================================================= + + bool InitializeGlobals(); + void TerminateGlobals(); + + // ============================================================================================= + + class ID3Header { // Minimal support to read and write the ID3 header. + public: + + const static size_t o_id = 0; + const static size_t o_vMajor = 3; + const static size_t o_vMinor = 4; + const static size_t o_flags = 5; + const static size_t o_size = 6; + + const static size_t kID3_TagHeaderSize = 10; // This is the same in v2.2, v2.3, and v2.4. + char fields [kID3_TagHeaderSize]; + + ~ID3Header() {}; + + // Read the v2 header into the fields buffer and check the version. + bool read ( XMP_IO* file ); + + // Set the size and write the the v2 header from the fields buffer. + void write ( XMP_IO* file, XMP_Int64 tagSize ); + + }; + + // ============================================================================================= + + class ID3v2Frame { + public: + + // Applies to ID3 v2.2, v2.3, and v2.4. The metadata values are mostly the same, v2.2 has + // smaller frame headers and only supports UTF-16 Unicode. + + const static XMP_Uns16 o_id = 0; + const static XMP_Uns16 o_size = 4; // size after unsync, excludes frame header. + const static XMP_Uns16 o_flags = 8; + + const static int kV23_FrameHeaderSize = 10; // The header for v2.3 and v2.4. + const static int kV22_FrameHeaderSize = 6; // The header for v2.2. + char fields [kV23_FrameHeaderSize]; + + XMP_Uns32 id; + XMP_Uns16 flags; + + char* content; + XMP_Int32 contentSize; // size of variable content, right as its stored in o_size + + bool active; //default: true. flag is lowered, if another frame with replaces this one as "last meaningful frame of its kind" + bool changed; //default: false. flag is raised, if setString() is used + + ID3v2Frame(); + ID3v2Frame ( XMP_Uns32 id ); + + ID3v2Frame ( const ID3v2Frame& orig ) { + XMP_Throw ( "ID3v2Frame copy constructor not implemented", kXMPErr_InternalFailure ); + } + + ~ID3v2Frame() { this->release(); } + + void release(); + + void setFrameValue ( const std::string& rawvalue, bool needDescriptor = false, + bool utf16 = false, bool isXMPPRIVFrame = false, bool needEncodingByte = true ); + + XMP_Int64 read ( XMP_IO* file, XMP_Uns8 majorVersion ); + void write ( XMP_IO* file, XMP_Uns8 majorVersion ); + + // two types of COMM frames should be preserved but otherwise ignored + // * a 6-field long header just having + // encoding(1 byte),lang(3 bytes) and 0x00 31 (no descriptor, "1" as content") + // perhaps only used to indicate client language + // * COMM frames whose description begins with engiTun, these are iTunes flags + // + // returns true: job done as expted + // false: do not use this frame, but preserve (i.e. iTunes marker COMM frame) + bool advancePastCOMMDescriptor ( XMP_Int32& pos ); + + // returns the frame content as a proper UTF8 string + // * w/o the initial encoding byte + // * dealing with BOM's + // + // @returns: by value: character string with the value + // as return value: false if frame is "not of intereset" despite a generally + // "interesting" frame ID, these are + // * iTunes-'marker' COMM frame + bool getFrameValue ( XMP_Uns8 majorVersion, XMP_Uns32 logicalID, std::string* utf8string ); + + }; + + // ============================================================================================= + + class ID3v1Tag { // Support for the fixed length v1 tag found at the end of the file. + public: + + const static XMP_Uns16 o_tag = 0; // must be "TAG" + const static XMP_Uns16 o_title = 3; + const static XMP_Uns16 o_artist = 33; + const static XMP_Uns16 o_album = 63; + const static XMP_Uns16 o_year = 93; + const static XMP_Uns16 o_comment = 97; + const static XMP_Uns16 o_zero = 125; // must be zero for trackNo to be valid + const static XMP_Uns16 o_trackNo = 126; // trackNo + const static XMP_Uns16 o_genre = 127; // last byte: index, or 255 + + const static int kV1_TagSize = 128; + + bool read ( XMP_IO* file, SXMPMeta* meta ); + void write ( XMP_IO* file, SXMPMeta* meta ); + + }; + +} + +#endif // __ID3_Support_hpp__ diff --git a/XMPFiles/source/FormatSupport/IFF/Chunk.cpp b/XMPFiles/source/FormatSupport/IFF/Chunk.cpp new file mode 100644 index 0000000..a2de741 --- /dev/null +++ b/XMPFiles/source/FormatSupport/IFF/Chunk.cpp @@ -0,0 +1,1182 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/IFF/Chunk.h" +#include "source/XMP_LibUtils.hpp" +#include "source/XIO.hpp" + +#include <cstdio> +#include <typeinfo> + +using namespace IFF_RIFF; + +//----------------------------------------------------------------------------- +// +// Chunk::createChunk(...) +// +// Purpose: [static] Static factory to create an unknown chunk +// +//----------------------------------------------------------------------------- + +Chunk* Chunk::createChunk( const IEndian& endian ) +{ + return new Chunk( endian ); +} + + +//----------------------------------------------------------------------------- +// +// Chunk::createUnknownChunk(...) +// +// Purpose: [static] Static factory to create an unknown chunk with initial id, +// sizes and offsets. +// +//----------------------------------------------------------------------------- + +Chunk* Chunk::createUnknownChunk( + const IEndian& endian, + const XMP_Uns32 id, + const XMP_Uns32 type, + const XMP_Uns64 size, + const XMP_Uns64 originalOffset, + const XMP_Uns64 offset +) +{ + Chunk *chunk = new Chunk( endian ); + chunk->setID( id ); + chunk->mOriginalOffset = originalOffset; + chunk->mOffset = offset; + + if (type != 0) + { + chunk->setType(type); + } + + // sizes have to be set after type, otherwise the setType sets the size to 4. + chunk->mSize = chunk->mOriginalSize = size; + chunk->mChunkMode = CHUNK_UNKNOWN; + chunk->mDirty = false; + return chunk; +} + +//----------------------------------------------------------------------------- +// +// Chunk::createHeaderChunk(...) +// +// Purpose: [static] Static factory to create a leaf chunk with no data area or +// only the type in the data area +// +//----------------------------------------------------------------------------- + +Chunk* Chunk::createHeaderChunk( const IEndian& endian, const XMP_Uns32 id, const XMP_Uns32 type /*= kType_NONE*/) +{ + Chunk *chunk = new Chunk( endian ); + chunk->setID( id ); + + XMP_Uns64 size = 0; + + if( type != kType_NONE ) + { + chunk->setType( type ); + size += Chunk::TYPE_SIZE; + } + + chunk->mSize = size; + chunk->mOriginalSize = size; + chunk->mChunkMode = CHUNK_LEAF; + chunk->mDirty = false; + + return chunk; +} + + +//----------------------------------------------------------------------------- +// +// Chunk::Chunk(...) +// +// Purpose: ctor/dtor +// +//----------------------------------------------------------------------------- + +Chunk::Chunk( const IEndian& endian ) +: mEndian( endian ) +{ + // initialize private instance variables + mChunkId.id = kChunk_NONE; + mChunkId.type = kType_NONE; + mSize = 0; + mOriginalSize = 0; + mBufferSize = 0; + mData = NULL; + mParent = NULL; + mOriginalOffset = 0; + mOffset = 0; + mDirty = false; + mChunkMode = CHUNK_UNKNOWN; +} + + +Chunk::~Chunk() +{ + for( ChunkIterator iter = mChildren.begin(); iter != mChildren.end(); iter++ ) + { + delete *iter; + } + + // Free allocated data buffer + if( mData != NULL ) + { + delete [] mData; + } +} + + +/************************ IChunk interface implementation ************************/ + +//----------------------------------------------------------------------------- +// +// Chunk::getData(...) +// +// Purpose: access data area of Chunk +// +//----------------------------------------------------------------------------- + +XMP_Uns64 Chunk::getData( const XMP_Uns8** data ) const +{ + if( data == NULL ) + { + XMP_Throw ( "Invalid data pointer.", kXMPErr_BadParam ); + } + + *data = mData; + + return mBufferSize; +} + + +//----------------------------------------------------------------------------- +// +// Chunk::setData(...) +// +// Purpose: Set new data for the chunk. +// Will delete an existing internal buffer and recreate a new one +// and copy the given data into that new buffer. +// +//----------------------------------------------------------------------------- + +void Chunk::setData( const XMP_Uns8* const data, XMP_Uns64 size, XMP_Bool writeType /*=false*/ ) +{ + // chunk nodes cannot contain data + if ( mChunkMode == CHUNK_NODE ) + { + XMP_Throw ( "A chunk node cannot contain data.", kXMPErr_BadParam ); + } + else if ( data == NULL || size == 0 ) + { + XMP_Throw ( "Invalid data pointer.", kXMPErr_BadParam ); + } + + if( mData != NULL ) + { + delete [] mData; + } + + if( writeType ) + { + mBufferSize = size + TYPE_SIZE; + mData = new XMP_Uns8[static_cast<size_t>(mBufferSize)]; // Throws bad_alloc exception in case of being out of memory + setType( mChunkId.type ); + memcpy( &mData[TYPE_SIZE], data, static_cast<size_t>(size) ); + } + else + { + mBufferSize = size; + mData = new XMP_Uns8[static_cast<size_t>(mBufferSize)]; // Throws bad_alloc exception in case of being out of memory + // ! We assume that size IS the actual size of that input buffer, otherwise behavior is undefined + memcpy( mData, data, static_cast<size_t>(size) ); + + // set the type variable + if( mBufferSize >= TYPE_SIZE ) + { + //Chunk type is always BE + //The first four bytes could be the type + mChunkId.type = BigEndian::getInstance().getUns32( mData ); + } + } + + mChunkMode = CHUNK_LEAF; + setChanged(); + adjustSize(); +} + +//----------------------------------------------------------------------------- +// +// Chunk::getUns32(...) +// +// Purpose: The following methods are getter/setter for certain data types. +// They always take care of little-endian/big-endian issues. +// The offset starts at the data area of the Chunk. +// +//----------------------------------------------------------------------------- + +XMP_Uns32 Chunk::getUns32( XMP_Uns64 offset ) const +{ + if( offset + sizeof(XMP_Uns32) > mBufferSize ) + { + XMP_Throw ( "Data access out of bounds", kXMPErr_BadIndex ); + } + return mEndian.getUns32( &mData[offset] ); +} + + +void Chunk::setUns32( XMP_Uns32 value, XMP_Uns64 offset ) +{ + // chunk nodes cannot contain data + if ( mChunkMode == CHUNK_NODE ) + { + XMP_Throw ( "A chunk node cannot contain data.", kXMPErr_BadParam ); + } + + // If the new value exceeds the size of the buffer, recreate the buffer + adjustInternalBuffer( offset + sizeof(XMP_Uns32) ); + // Write the new value + mEndian.putUns32( value, &mData[offset] ); + // Chunk becomes leaf chunk when adding data + mChunkMode = CHUNK_LEAF; + // Flag the chunk as dirty + setChanged(); + // If the buffer is bigger than the Chunk size, adjust the Chunk size + adjustSize(); + +} + + +XMP_Uns64 Chunk::getUns64( XMP_Uns64 offset ) const +{ + if( offset + sizeof(XMP_Uns64) > mBufferSize ) + { + XMP_Throw ( "Data access out of bounds", kXMPErr_BadIndex ); + } + return mEndian.getUns64( &mData[offset] ); +} + + +void Chunk::setUns64( XMP_Uns64 value, XMP_Uns64 offset ) +{ + // chunk nodes cannot contain data + if ( mChunkMode == CHUNK_NODE ) + { + XMP_Throw ( "A chunk node cannot contain data.", kXMPErr_BadParam ); + } + + // If the new value exceeds the size of the buffer, recreate the buffer + adjustInternalBuffer( offset + sizeof(XMP_Uns64) ); + // Write the new value + mEndian.putUns64( value, &mData[offset] ); + // Chunk becomes leaf chunk when adding data + mChunkMode = CHUNK_LEAF; + // Flag the chunk as dirty + setChanged(); + // If the buffer is bigger than the Chunk size, adjust the Chunk size + adjustSize(); +} + + +XMP_Int32 Chunk::getInt32( XMP_Uns64 offset ) const +{ + if( offset + sizeof(XMP_Int32) > mBufferSize ) + { + XMP_Throw ( "Data access out of bounds", kXMPErr_BadIndex ); + } + return mEndian.getUns32( &mData[offset] ); +} + + +void Chunk::setInt32( XMP_Int32 value, XMP_Uns64 offset ) +{ + // chunk nodes cannot contain data + if ( mChunkMode == CHUNK_NODE ) + { + XMP_Throw ( "A chunk node cannot contain data.", kXMPErr_BadParam ); + } + + // If the new value exceeds the size of the buffer, recreate the buffer + adjustInternalBuffer( offset + sizeof(XMP_Int32) ); + // Write the new value + mEndian.putUns32( value, &mData[offset] ); + // Chunk becomes leaf chunk when adding data + mChunkMode = CHUNK_LEAF; + // Flag the chunk as dirty + setChanged(); + // If the buffer is bigger than the Chunk size, adjust the Chunk size + adjustSize(); +} + + +XMP_Int64 Chunk::getInt64( XMP_Uns64 offset ) const +{ + if( offset + sizeof(XMP_Int64) > mBufferSize ) + { + XMP_Throw ( "Data access out of bounds", kXMPErr_BadIndex ); + } + return mEndian.getUns64( &mData[offset] ); +} + + +void Chunk::setInt64( XMP_Int64 value, XMP_Uns64 offset ) +{ + // chunk nodes cannot contain data + if ( mChunkMode == CHUNK_NODE ) + { + XMP_Throw ( "A chunk node cannot contain data.", kXMPErr_BadParam ); + } + + // If the new value exceeds the size of the buffer, recreate the buffer + adjustInternalBuffer( offset + sizeof(XMP_Int64) ); + // Write the new value + mEndian.putUns64( value, &mData[offset] ); + // Chunk becomes leaf chunk when adding data + mChunkMode = CHUNK_LEAF; + // Flag the chunk as dirty + setChanged(); + // If the buffer is bigger than the Chunk size, adjust the Chunk size + adjustSize(); +} + + +std::string Chunk::getString( XMP_Uns64 size /*=0*/, XMP_Uns64 offset /*=0*/ ) const +{ + if( offset + size > mBufferSize ) + { + XMP_Throw ( "Data access out of bounds", kXMPErr_BadIndex ); + } + + XMP_Uns64 requestedSize = size != 0 ? size : mBufferSize - offset; + + std::string str((char *)&mData[offset],static_cast<size_t>(requestedSize)); + + return str; +} + + +void Chunk::setString( std::string value, XMP_Uns64 offset ) +{ + if ( mChunkMode == CHUNK_NODE ) + { + XMP_Throw ( "A chunk node cannot contain data.", kXMPErr_BadParam ); + } + + // If the new value exceeds the size of the buffer, recreate the buffer + adjustInternalBuffer( offset + value.length() ); + // Write the new value + memcpy( &mData[offset], value.data(), value.length() ); + // Chunk becomes leaf chunk when adding data + mChunkMode = CHUNK_LEAF; + // Flag the chunk as dirty + setChanged(); + // If the buffer is bigger than the Chunk size, adjust the Chunk size + adjustSize(); +} + + +/************************ Chunk public methods ************************/ + +//----------------------------------------------------------------------------- +// +// Chunk::setID(...) +// +// Purpose: Sets the chunk id. +// +//----------------------------------------------------------------------------- + +void Chunk::setID( XMP_Uns32 id ) +{ + mChunkId.id = id; + setChanged(); +} + + +//----------------------------------------------------------------------------- +// +// Chunk::setType(...) +// +// Purpose: Sets the chunk type +// +//----------------------------------------------------------------------------- + +void Chunk::setType( XMP_Uns32 type ) +{ + mChunkId.type = type; + + // reserve space for type + // setChanged() and adjustSize() implicitly called + // make sure that no exception is thrown + ChunkMode existing = mChunkMode; + mChunkMode = CHUNK_UNKNOWN; + setUns32(0, 0); + mChunkMode = existing; + + BigEndian::getInstance().putUns32( type, mData ); +} + +//----------------------------------------------------------------------------- +// +// Chunk::getPadSize(...) +// +// Purpose: Returns the original size of the Chunk including a pad byte if +// the size isn't a even number +// +//----------------------------------------------------------------------------- + +XMP_Uns64 Chunk::getOriginalPadSize( bool includeHeader /*= false*/ ) const +{ + XMP_Uns64 ret = this->getOriginalSize( includeHeader ); + + if( ret & 1 ) + { + ret++; + } + + return ret; +} + +//----------------------------------------------------------------------------- +// +// Chunk::getPadSize(...) +// +// Purpose: Returns the current size of the Chunk including a pad byte if the +// size isn't a even number +// +//----------------------------------------------------------------------------- + +XMP_Uns64 Chunk::getPadSize( bool includeHeader /*= false*/ ) const +{ + XMP_Uns64 ret = this->getSize( includeHeader ); + + if( ret & 1 ) + { + ret++; + } + + return ret; +} + +//----------------------------------------------------------------------------- +// +// Chunk::calculateSize(...) +// +// Purpose: Calculate the size of this chunks based on its children sizes. +// If this chunk has no children then no new size will be calculated. +// +//----------------------------------------------------------------------------- + +XMP_Uns64 Chunk::calculateSize( bool setOriginal /*= false*/ ) +{ + XMP_Uns64 size = 0LL; + + // + // calculate only foe nodes + // + if( this->getChunkMode() == CHUNK_NODE ) + { + // + // calculate size of all children + // + for( ChunkIterator iter = mChildren.begin(); iter != mChildren.end(); iter++ ) + { + XMP_Uns64 childSize = (*iter)->getSize(true); + + size += childSize; + + // + // take account of pad byte + // + if( childSize & 1 ) + { + size++; + } + } + + // + // assume that we have a type + // + size += Chunk::TYPE_SIZE; + + // + // set dirty flag only if something has changed + // + if( size != mSize || ( setOriginal && size != mOriginalSize ) ) + { + this->setChanged(); + } + + // + // set new size(s) + // + if( setOriginal ) + { + mOriginalSize = size; + } + + mSize = size; + } + else + size = mSize; + + return size; +} + + +//----------------------------------------------------------------------------- +// +// Chunk::setOffset(...) +// +// Purpose: Adjust the offset that this chunk has within the file +// +//----------------------------------------------------------------------------- + +void Chunk::setOffset (XMP_Uns64 newOffset) // changes during rearranging +{ + XMP_Uns64 oldOffset = mOffset; + mOffset = newOffset; + + if( mOffset != oldOffset ) + { + setChanged(); + } +} + + +//----------------------------------------------------------------------------- +// +// Chunk::resetChanges(...) +// +// Purpose: Resets the dirty status for this chunk and its children to false +// +//----------------------------------------------------------------------------- + +void Chunk::resetChanges() +{ + mDirty = false; + + for( ChunkIterator iter = mChildren.begin(); iter != mChildren.end(); iter++ ) + { + (*iter)->resetChanges(); + } +} //resetChanges + + +//----------------------------------------------------------------------------- +// +// Chunk::setAsNew(...) +// +// Purpose: Sets all necessary member variables to flag this chunk as a new one +// being inserted into the tree +// +//----------------------------------------------------------------------------- + +void Chunk::setAsNew() +{ + mOriginalSize = mSize; + mOriginalOffset = mOffset; +} + +//----------------------------------------------------------------------------- +// +// Chunk::toString(...) +// +// Purpose: Creates a string representation of the chunk (debug method) +// +//----------------------------------------------------------------------------- + +std::string Chunk::toString( std::string tabs, XMP_Bool showOriginal ) +{ + const BigEndian &BE = BigEndian::getInstance(); + char buffer[256]; + XMP_Uns32 id = BE.getUns32(&this->mChunkId.id); + XMP_Uns32 type = BE.getUns32(&this->mChunkId.type); + + XMP_Uns64 size, offset; + + if ( showOriginal ) + { + size = mEndian.getUns64(&this->mOriginalSize); + offset = mEndian.getUns64(&this->mOriginalOffset); + } + else + { + size = mEndian.getUns64(&this->mSize); + offset = mEndian.getUns64(&this->mOffset); + } + + snprintf( buffer, 255, "%.4s -- " + "size: 0x%.8llX, " + "type: %.4s, " + "offset: 0x%.8llX", + (char*)(&id), + size, + (char*)(&type), + offset ); + std::string str(buffer); + + // Dump children + if ( mChildren.size() > 0) + { + tabs.append("\t"); + } + + for( ChunkIterator iter = mChildren.begin(); iter != mChildren.end(); iter++ ) + { + str += "\n"; + str += tabs; + str += (*iter)->toString(tabs , showOriginal); + } + + return str; +} + + +/************************ file access ************************/ + +//----------------------------------------------------------------------------- +// +// Chunk::readChunk(...) +// +// Purpose: Read id, size and offset and create a chunk with mode CHUNK_UNKNOWN. +// The file is expected to be open and is not closed! +// +//----------------------------------------------------------------------------- + +void Chunk::readChunk( XMP_IO* file ) +{ + if( file == NULL ) + { + XMP_Throw( "Chunk::readChunk: Must pass a valid file pointer", kXMPErr_BadParam ); + } + + if( mChunkId.id != kChunk_NONE ) + { + XMP_Throw ( "readChunk must not be called more than once", kXMPErr_InternalFailure ); + } + // error handling is done in the controller + // determine offset in the file + mOriginalOffset = mOffset = file->Offset(); + //ID is always BE + mChunkId.id = XIO::ReadUns32_BE( file ); + // Size can be both + if (typeid(mEndian) == typeid(LittleEndian)) + { + mOriginalSize = mSize = XIO::ReadUns32_LE( file ); + + } + else + { + mOriginalSize = mSize = XIO::ReadUns32_BE( file ); + } + + // For Type do not assume any format as it could be data, read it as bytes + if (mSize >= TYPE_SIZE) + { + mData = new XMP_Uns8[TYPE_SIZE]; + + for ( XMP_Uns32 i = 0; i < TYPE_SIZE ; i++ ) + { + mData[i] = XIO::ReadUns8( file ); + } + //Chunk type is always BE + //The first four bytes could be the type + mChunkId.type = BigEndian::getInstance().getUns32( mData ); + } + + mDirty = false; +}//readChunk + + +//----------------------------------------------------------------------------- +// +// Chunk::cacheChunkData(...) +// +// Purpose: Stores the data in the class (only called if required). +// The file is expected to be open and is not closed! +// +//----------------------------------------------------------------------------- + +void Chunk::cacheChunkData( XMP_IO* file ) +{ + XMP_Enforce( file != NULL ); + + if( mChunkMode != CHUNK_UNKNOWN ) + { + XMP_Throw ( "chunk already has either data or children.", kXMPErr_BadParam ); + } + + // error handling is done in the controller + + // continue only when the chunk contains data + if (mSize != 0) + { + mBufferSize = mSize; + XMP_Uns8* tmp = new XMP_Uns8[XMP_Uns32(mSize)]; + + // Do we have a type? + if (mSize >= TYPE_SIZE) + { + // add type in front of new buffer + for ( XMP_Uns32 i = 0; i < TYPE_SIZE ; i++ ) + { + tmp[i] = mData[i]; + } + // Read rest of data from file + if( mSize != TYPE_SIZE ) + { + // Chunks that are cached are very probably not bigger than 2GB, so cast is safe + file->ReadAll ( &tmp[TYPE_SIZE], static_cast<XMP_Int32>(mSize - TYPE_SIZE) ); + } + } + else + { + // Chunks that are cached are very probably not bigger than 2GB, so cast is safe + file->ReadAll ( tmp, static_cast<XMP_Int32>(mSize) ); + } + // deletes the existing array + delete [] mData; + //assign the new buffer + mData = tmp; + } + + // Remember that this method has been called + mDirty = false; + mChunkMode = CHUNK_LEAF; +} + + +//----------------------------------------------------------------------------- +// +// Chunk::writeChunk(...) +// +// Purpose: Write or updates chunk (new data, new size, new position). +// The file is expected to be open and is not closed! +// +//----------------------------------------------------------------------------- + +void Chunk::writeChunk( XMP_IO* file ) +{ + if( file == NULL ) + { + XMP_Throw( "Chunk::writeChunk: Must pass a valid file pointer", kXMPErr_BadParam ); + } + + if (mChunkMode == CHUNK_UNKNOWN) + { + if (hasChanged()) + { + XMP_Throw ( "A chunk with mode unknown must not be changed & written.", kXMPErr_BadParam ); + } + + // do nothing + } + else if (hasChanged()) + { + // positions the file pointer + file->Seek ( mOffset, kXMP_SeekFromStart ); + + + // ============ This part is identical for CHUNK_LEAF and CHUNK_TYPE ============ + + // writes ID (starting with offset) + XIO::WriteInt32_BE( file, mChunkId.id ); + + // writes size, which is always 32bit + XMP_Uns32 outSize = ( mSize >= 0x00000000FFFFFFFF ? 0xFFFFFFFF : static_cast<XMP_Uns32>( mSize & 0x00000000FFFFFFFF ) ); + + if (typeid(mEndian) == typeid(LittleEndian)) + { + XIO::WriteUns32_LE( file, static_cast<XMP_Uns32>(mSize) ); + } + else + { + XIO::WriteUns32_BE( file, static_cast<XMP_Uns32>(mSize) ); + } + + + // ============ This part is different for CHUNK_LEAF and CHUNK_TYPE ============ + if (mChunkMode == CHUNK_LEAF) + { + // writes buffer (including the optional type at the beginning) + // Cached chunks will very probably not be bigger than 2GB, so cast is safe + file->Write ( mData, static_cast<XMP_Int32>(mSize) ); + if ( mSize % 2 == 1 ) + { + // for odd file sizes, a pad byte is written + XIO::WriteUns8 ( file, 0 ); + } + } + else // mChunkMode == CHUNK_NODE + { + // writes type if defined + if (mChunkId.type != kType_NONE) + { + XIO::WriteInt32_BE( file, mChunkId.type ); + } + + // calls writeChunk on it's children + for( ChunkIterator iter = mChildren.begin(); iter != mChildren.end(); iter++ ) + { + (*iter)->writeChunk( file ); + } + } + } + + // set back dirty state + mDirty = false; +} + + +/************************ children access ************************/ + +//----------------------------------------------------------------------------- +// +// Chunk::numChildren(...) +// +// Purpose: Returns the number children chunks +// +//----------------------------------------------------------------------------- + +XMP_Uns32 Chunk::numChildren() const +{ + return static_cast<XMP_Uns32>( mChildren.size() ); +} + + +//----------------------------------------------------------------------------- +// +// Chunk::getChildAt(...) +// +// Purpose: Returns a child node +// +//----------------------------------------------------------------------------- + +Chunk* Chunk::getChildAt( XMP_Uns32 pos ) const +{ + try + { + return mChildren.at(pos); + } + catch( ... ) + { + XMP_Throw ( "Non-existing child requested.", kXMPErr_BadIndex ); + } +} + + +//----------------------------------------------------------------------------- +// +// Chunk::appendChild(...) +// +// Purpose: Appends a child node at the end of the children list +// +//----------------------------------------------------------------------------- + +void Chunk::appendChild( Chunk* child, XMP_Bool adjustSizes ) +{ + if (mChunkMode == CHUNK_LEAF) + { + XMP_Throw ( "A chunk leaf cannot contain children.", kXMPErr_BadParam ); + } + + try + { + mChildren.push_back( child ); + // make this the parent of the new node + child->mParent = this; + mChunkMode = CHUNK_NODE; + + // set offset of new child + XMP_Uns64 childOffset = 0; + + if( this->numChildren() == 1 ) + { + // first added child + if( this->getID() != kChunk_NONE ) + { + childOffset = this->getOffset() + Chunk::HEADER_SIZE + ( this->getType() == kType_NONE ? 0 : Chunk::TYPE_SIZE ); + } + } + else + { + Chunk* predecessor = this->getChildAt( this->numChildren() - 2 ); + childOffset = predecessor->getOffset() + predecessor->getPadSize( true ); + } + + child->setOffset( childOffset ); + + setChanged(); + + if ( adjustSizes ) + { + // to fix the sizes of this node and parents + adjustSize( child->getSize(true) ); + } + } + catch (...) + { + XMP_Throw ( "Vector error in appendChild", kXMPErr_InternalFailure ); + } +} + + +//----------------------------------------------------------------------------- +// +// Chunk::insertChildAt(...) +// +// Purpose: Inserts a child node at a certain position +// +//----------------------------------------------------------------------------- + +void Chunk::insertChildAt( XMP_Uns32 pos, Chunk* child ) +{ + if (mChunkMode == CHUNK_LEAF) + { + XMP_Throw ( "A chunk leaf cannot contain children.", kXMPErr_BadParam ); + } + + try + { + if (pos <= mChildren.size()) + { + mChildren.insert(mChildren.begin() + pos, child); + // make this the parent of the new node + child->mParent = this; + mChunkMode = CHUNK_NODE; + + // set offset of new child + XMP_Uns64 childOffset = 0; + + if( pos == 0 ) + { + if( this->getID() != kChunk_NONE ) + { + childOffset = this->getOffset() + Chunk::HEADER_SIZE + ( this->getType() == kType_NONE ? 0 : Chunk::TYPE_SIZE ); + } + } + else + { + Chunk* predecessor = this->getChildAt( pos-1 ); + childOffset = predecessor->getOffset() + predecessor->getPadSize( true ); + } + + child->setOffset( childOffset ); + + setChanged(); + + // to fix the sizes of this node and parents + adjustSize( child->getSize(true) ); + } + else + { + XMP_Throw ( "Index not valid.", kXMPErr_BadIndex ); + } + } + catch (...) + { + XMP_Throw ( "Index not valid.", kXMPErr_BadIndex ); + } +} + + +//----------------------------------------------------------------------------- +// +// Chunk::removeChildAt(...) +// +// Purpose: Removes a child node at a given position +// +//----------------------------------------------------------------------------- + +Chunk* Chunk::removeChildAt( XMP_Uns32 pos ) +{ + Chunk* toDelete = NULL; + + try + { + toDelete = mChildren.at(pos); + // to fix the size of this node + XMP_Int64 sizeDeleted = static_cast<XMP_Int64>(toDelete->getSize(true)); + mChildren.erase(mChildren.begin() + pos); + + setChanged(); + + // to fix the sizes of this node and parents + adjustSize(-sizeDeleted); + } + catch (...) + { + XMP_Throw ( "Index not valid.", kXMPErr_BadIndex ); + } + + return toDelete; +} + +//----------------------------------------------------------------------------- +// +// replaceChildAt(...) +// +// Purpose: Remove child at the passed position and insert the new chunk +// +//----------------------------------------------------------------------------- + +Chunk* Chunk::replaceChildAt( XMP_Uns32 pos, Chunk* child ) +{ + Chunk* toDelete = NULL; + + try + { + // + // removed old chunk + // + toDelete = mChildren.at(pos); + mChildren.erase(mChildren.begin() + pos); + + // + // insert new chunk + // + mChildren.insert(mChildren.begin() + pos, child); + // make this the parent of the new node + child->mParent = this; + mChunkMode = CHUNK_NODE; + + // set offset + child->setOffset( toDelete->getOffset() ); + + setChanged(); + + // to fix the sizes of this node and parents + adjustSize( child->getPadSize() - toDelete->getPadSize() ); + } + catch (...) + { + XMP_Throw ( "Index not valid.", kXMPErr_BadIndex ); + } + + return toDelete; +} + +//----------------------------------------------------------------------------- +// +// Chunk::firstChild(...) +// +// Purpose: iterators +// +//----------------------------------------------------------------------------- + +Chunk::ConstChunkIterator Chunk::firstChild() const +{ + return mChildren.begin(); +} + + +Chunk::ConstChunkIterator Chunk::lastChild() const +{ + return mChildren.end(); +} + + +/******************* Private Methods ***************************/ + +//----------------------------------------------------------------------------- +// +// Chunk::setChanged(...) +// +// Purpose: Sets this node and all of its parents up to the tree root dirty +// +//----------------------------------------------------------------------------- + +void Chunk::setChanged() +{ + mDirty = true; + + if (mParent != NULL) + { + mParent->setChanged(); + } +} + + +//----------------------------------------------------------------------------- +// +// Chunk::adjustInternalBuffer(...) +// +// Purpose: Resizes the internal byte buffer to the given size if the new size +// is bigger than the current one. +// If the new size is smaller, the buffer is not adjusted +// +//----------------------------------------------------------------------------- + +void Chunk::adjustInternalBuffer( XMP_Uns64 newSize ) +{ + // only adjust if the new size is bigger than the old one. + // If it is smaller, leave the buffer alone + if( newSize > mBufferSize ) + { + XMP_Uns8 *tmp = new XMP_Uns8[static_cast<size_t>(newSize)]; // Might throw bad_alloc exception + + // Do we have an old buffer? + if( mData != NULL ) + { + // Copy it to the new one and delete the old one + memcpy( tmp, mData, static_cast<size_t>(mBufferSize) ); + + delete [] mData; + } + mData = tmp; + mBufferSize = newSize; + } +}//adjustInternalBuffer + + +//----------------------------------------------------------------------------- +// +// Chunk::adjustSize(...) +// +// Purpose: Adjusts the chunk size and the parents chunk sizes +// +//----------------------------------------------------------------------------- + +void Chunk::adjustSize( XMP_Int64 sizeChange ) +{ + // Calculate leaf sizeChange + if (mChunkMode == CHUNK_LEAF) + { + // Note: The leave nodes size is equal to the buffer size can have odd and even sizes. + XMP_Uns64 sizeInclPad = mSize + (mSize % 2); + sizeChange = mBufferSize - sizeInclPad; + mSize = mBufferSize; + + // if the difference is odd, the corrected even size has be incremented by 1 + sizeChange += abs(sizeChange % 2); + } + else // mChunkMode == CHUNK_NODE/CHUNK_UNKNOWN + { + // if the difference is odd, the corrected even size has be incremented by 1 + // (or decremented by 1 when < 0). + sizeChange += sizeChange % 2; + + // the chunk node gets the corrected (odd->even) size + mSize += sizeChange; + } + + + if (mParent != NULL) + { + // adjusts the parents size with the corrected (odd->even) size difference of this node + mParent->adjustSize(sizeChange); + } +}//adjustSize diff --git a/XMPFiles/source/FormatSupport/IFF/Chunk.h b/XMPFiles/source/FormatSupport/IFF/Chunk.h new file mode 100644 index 0000000..2d170ed --- /dev/null +++ b/XMPFiles/source/FormatSupport/IFF/Chunk.h @@ -0,0 +1,462 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _Chunk_h_ +#define _Chunk_h_ + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" + +#include "source/Endian.h" +#include "XMPFiles/source/FormatSupport/IFF/ChunkPath.h" +#include "XMPFiles/source/FormatSupport/IFF/IChunkData.h" +#include "XMPFiles/source/FormatSupport/IFF/IChunkContainer.h" + +namespace IFF_RIFF +{ + /** + * CHUNK_UNKNOWN = Either new chunk or a chunk that was read, but not cached + * (it is not decided yet whether it becones a node or leaf or is not cached at all) + * CHUNK_NODE = Node chunk that contains children, but no own data (except the optional type) + * CHUNK_LEAF = Leaf chunk that contains data but no children + */ + enum ChunkMode { CHUNK_UNKNOWN = 0, CHUNK_NODE = 1, CHUNK_LEAF = 2 }; + +/** + * Each Chunk of the IFF/RIFF based file formats (e.g. WAVE, AVI, AIFF) are represented by + * instances of the class Chunk. + * A chunk can be a node chunk containing children, a leaf chunk containing data or an "unknown" chunk, + * which means that its content has not cached/loaded yet (or will never be during the file handling); + * see ChunkMode for more details. + * + * Note: A Chunk can either have a chunk OR a list of child chunks, but never both. + * + * This class provides access to the children or the data of a chunk, depending of the type. + * It keeps track of its size. When the size is changed (by adding or removing data/ or children), + * the size is also fixed for the parent hierarchy. + * + * The dirty flag (hasChanged()) that is set for each change of a chunk is also promoted to the parents. + * The chunk stores its original and new offset within the host file, but its *not* automatically correctin the offset; + * this is done by the IChunkBehavior class. + * The Chunk class provides an interface to iterate through the tree structure of Chunks. + * There are methods to insert, remove and move Chunk's in its children tree structure. + * + * The chunk can read itself from a host file (readChunk()), but it does not automatically read its children, + * because they are not necessarily used by the file handler. + * The method writeChunk() recurses through the complete chunk tree and writes the *changed* chunks back to the host file. + * It is important that the offsets have been fixed before. + * + * Table about endianess in the different RIFF file formats: + * + * | ID size type data + * ----------------------------------------- + * AVI | BE LE BE LE + * WAV | BE LE BE LE + * AIFF | BE BE BE BE + */ +class Chunk : public IChunkData, + public IChunkContainer +{ + public: + /** Factory to create an empty chunk */ + static Chunk* createChunk( const IEndian& endian ); + + /** Factory to create an empty chunk */ + static Chunk* createUnknownChunk( + const IEndian& endian, + const XMP_Uns32 id, + const XMP_Uns32 type, + const XMP_Uns64 size, + const XMP_Uns64 originalOffset = 0, + const XMP_Uns64 offset = 0 + ); + + /** Static factory to create a leaf chunk with no data area or only the type in the data area */ + static Chunk* createHeaderChunk( const IEndian& endian, const XMP_Uns32 id, const XMP_Uns32 type = kType_NONE ); + + /** + * dtor + */ + ~Chunk(); + + + //===================== IChunkData interface implementation ================ + + /** + * Get the chunk ID + * + * @return Return the ID, 0 if the chunk does not have an ID. + */ + inline XMP_Uns32 getID() const { return mChunkId.id; } + + /** + * Get the chunk type (if available) + * (the first four data bytes of the chunk could be a chunk type) + * + * @return Return the type, kType_NONE if the chunk does not contain data. + */ + inline XMP_Uns32 getType() const { return mChunkId.type; } + + /** + * Get the chunk identifier [id and type] + * + * @return Return the identifier + */ + inline const ChunkIdentifier& getIdentifier() const { return mChunkId; } + + /** + * Access the data of the chunk. + * + * @param data OUT pointer to the byte array + * @return size of the data block, 0 if no data is available + */ + XMP_Uns64 getData( const XMP_Uns8** data ) const; + + /** + * Set new data for the chunk. + * Will delete an existing internal buffer and recreate a new one + * and copy the given data into that new buffer. + * + * @param data pointer to the data to put into the chunk + * @param size Size of the data block + * @param writeType if true, the type of the chunk (getType()) is written in front of the data block. + */ + void setData( const XMP_Uns8* const data, XMP_Uns64 size, XMP_Bool writeType = false ); + + /** + * Returns the current size of the Chunk. + * + * @param includeHeader if set, the returned size will be the whole chunk size including the header + * @return Returns either the size of the data block of the chunk or size of the whole chunk, including the eight byte header. + */ + XMP_Uns64 getSize( bool includeHeader = false ) const { return includeHeader ? mSize + HEADER_SIZE : mSize; } + + /** + * Returns the current size of the Chunk including a pad byte if the size isn't a even number + * + * @param includeHeader if set, the returned size will be the whole chunk size including the header + * @return Returns either the size of the data block of the chunk or size of the whole chunk, including the eight byte header. + */ + XMP_Uns64 getPadSize( bool includeHeader = false ) const; + /** + * @return Returns the mode of the chunk (see ChunkMode definition). + */ + ChunkMode getChunkMode() const { return mChunkMode; } + + /* The following methods are getter/setter for certain data types. + * They always take care of little-endian/big-endian issues. + * The offset starts at the data area of the Chunk. */ + + XMP_Uns32 getUns32( XMP_Uns64 offset=0 ) const; + void setUns32( XMP_Uns32 value, XMP_Uns64 offset=0 ); + + XMP_Uns64 getUns64( XMP_Uns64 offset=0 ) const; + void setUns64( XMP_Uns64 value, XMP_Uns64 offset=0 ); + + XMP_Int32 getInt32( XMP_Uns64 offset=0 ) const; + void setInt32( XMP_Int32 value, XMP_Uns64 offset=0 ); + + XMP_Int64 getInt64( XMP_Uns64 offset=0 ) const; + void setInt64( XMP_Int64 value, XMP_Uns64 offset=0 ); + + std::string getString( XMP_Uns64 size = 0, XMP_Uns64 offset=0 ) const; + void setString( std::string value, XMP_Uns64 offset=0 ); + + + //===================== IChunk interface implementation ================ + + //FIXME XMP exception if size cast from 64 to 32 looses data + + /** + * Sets the chunk id. + */ + void setID( XMP_Uns32 id ); + + /** + * Sets the chunk type. + */ + void setType( XMP_Uns32 type ); + + /** + * Sets the chunk size. + * NOTE: Should only be used for repairing wrong sizes in files (repair flag). + * Normally Size is changed by changing the data automatically! + */ + inline void setSize( XMP_Uns64 newSize, bool setOriginal = false ) { mDirty = mSize != newSize; mSize = newSize; mOriginalSize = setOriginal ? newSize : mOriginalSize; } + + /** + * Calculate the size of this chunks based on its children sizes. + * If this chunk has no children then no new size will be calculated. + */ + XMP_Uns64 calculateSize( bool setOriginal = false ); + + /** + * @return Returns the offset of the chunk within the stream. + */ + inline XMP_Uns64 getOffset () const { return mOffset; } + + /** + * @return Returns the original offset of the chunk within the stream. + */ + inline XMP_Uns64 getOriginalOffset () const { return mOriginalOffset; } + + /** + * Returns the original size of the Chunk + * + * @param includeHeader if set, the returned original size will be the whole chunk size including the header + * @return Returns the original size of the chunk within the stream (inluding/excluding headerSize). + */ + inline XMP_Uns64 getOriginalSize( bool includeHeader = false ) const { return includeHeader ? mOriginalSize + HEADER_SIZE : mOriginalSize; } + + /** + * Returns the original size of the Chunk including a pad byte if the size isn't a even number + * + * @param includeHeader if set, the returned size will be the whole chunk size including the header + * @return Returns either the size of the data block of the chunk or size of the whole chunk, including the eight byte header. + */ + XMP_Uns64 getOriginalPadSize( bool includeHeader = false ) const; + + /** + * Adjust the offset that this chunk has within the file. + * + * @param newOffset the new offset within the file stream + */ + void setOffset (XMP_Uns64 newOffset); // changes during rearranging + + /** + * Has the Chunk class changes, or has the position within the file been changed? + * If the result is true the chunk has to be written back to the file + * (all parent chunks are also set to dirty in that case). + * + * @return Returns true if the chunk node has been modified. + */ + XMP_Bool hasChanged() const { return mDirty; } + + /** + * Sets this node and all of its parents up to the tree root dirty. + */ + void setChanged(); + + /** + * Resets the dirty status for this chunk and its children to false + */ + void resetChanges(); + + /** + *Sets all necessary member variables to flag this chunk as a new one being inserted into the tree + */ + void setAsNew(); + + /** + * @return Returns the parent chunk (can be NULL if this is the root of the tree). + */ + inline Chunk* getParent() const { return mParent; } + + /** + * Creates a string representation of the chunk (debug method). + */ + std::string toString( std::string tabs = std::string() , XMP_Bool showOriginal = false ); + + + //------------------- + // file access + //------------------- + + /** + * Read id, size and offset and create a chunk with mode CHUNK_UNKNOWN. + * The file is expected to be open and is not closed! + * + * @param file File reference to read the chunk from + */ + void readChunk( XMP_IO* file ); + + /** + * Stores the data in the class (only called if required). + * The file is expected to be open and is not closed! + * + * @param file File reference to cache the chunk data + */ + void cacheChunkData( XMP_IO* file ); + + /** + * Write or updates chunk (new data, new size, new position). + * The file is expected to be open and is not closed! + * + * Behavior for the different chunk types: + * + * CHUNK_UNKNOWN: + * - does not write anything back + * - throws exception if hasChanged == true + * + * CHUNK_LEAF: + * - writes ID (starting with offset) + * - writes size + * - writes buffer (including the optional type at the beginning) + * + * CHUNK_NODE: + * - writes ID (starting with offset) + * - writes size + * - writes type if defined + * - calls writeChunk on it's children + * + * Note: readChunk() and optionally cacheChunkData() has to be called before! + * + * @param file File reference to write the chunk to + */ + void writeChunk( XMP_IO* file ); + + + //------------------- + // children access + //------------------- + + /** + * @return Returns the number children chunks. + */ + XMP_Uns32 numChildren() const; + + /** + * Returns a child node. + * + * @param pos position of the child node to return + * @return Returns the child node at the given position. + */ + Chunk* getChildAt( XMP_Uns32 pos ) const; + + /** + * Appends a child node at the end of the children list. + * + * @param node the new node + * @param adjustSizes adjust size of chunk and parents + * @return Returns the added node. + */ + void appendChild( Chunk* node, XMP_Bool adjustSizes = true ); + + /** + * Inserts a child node at a certain position. + * + * @param pos position in the children list to add the new node + * @param node the new node + * @return Returns the added node. + */ + void insertChildAt( XMP_Uns32 pos, Chunk* node ); + + /** + * Removes a child node at a given position. + * + * @param pos position of the node to delete in the children list + * + * @return The removed chunk + */ + Chunk* removeChildAt( XMP_Uns32 pos ); + + /** + * Remove child at the passed position and insert the new chunk + * + * @param pos Position of chunk that will be replaced + * @param chunk New chunk + * + * @return Replaced chunk + */ + Chunk* replaceChildAt( XMP_Uns32 pos, Chunk* node ); + + //-------------------- + // children iteration + //-------------------- + + typedef std::vector<Chunk*>::iterator ChunkIterator; + typedef std::vector<Chunk*>::const_iterator ConstChunkIterator; + + ConstChunkIterator firstChild() const; + + ConstChunkIterator lastChild() const; + + /** The size of the header (id+size) */ + static const XMP_Uns8 HEADER_SIZE = 8; + /** The size of the type */ + static const XMP_Uns8 TYPE_SIZE = 4; + + + private: + /** stores the chunk header */ + ChunkIdentifier mChunkId; + /** Original size of chunk without the header */ + XMP_Uns64 mOriginalSize; + /** size of chunk without the header */ + XMP_Uns64 mSize; + /** size of the internal buffer */ + XMP_Uns64 mBufferSize; + /** buffer for the chunk data without the header, but including the type (first 4 bytes). */ + XMP_Uns8* mData; + /** Buffer to hold the first 4 bytes that are used for either the type or as data. + * Only used for ReadChunk and CacheChunk */ + ChunkMode mChunkMode; + + /** + * Current position in stream (file). Can only be changed by moving the chunks around + * (by using an IChunkBehavior class). + * Note: Sizes are stored in chunk because it can be changed by the handler (i.e. by changing the data) + */ + XMP_Uns64 mOriginalOffset; + /** + * New position of the chunk in the stream. + * It is initialized to MAXINT64 when there is no new offset. + * If the offset has been changed the dirty flag has to be set. + */ + XMP_Uns64 mOffset; + + /** + * The dirty flag indicates that the chunk (and all parent chunks) has been modified or moved and + * that it therefore needs to be written to file. + */ + XMP_Bool mDirty; // has Chunk data changed? has Chunk position changed? + + /** The parent of this node; only the root node does not have a parent. */ + Chunk* mParent; + + /** Stores the byte order for this node. + * Note: The endianess does not change within one file */ + const IEndian& mEndian; + + /** The list of child nodes. */ + std::vector<Chunk*> mChildren; + + /** + * private ctor, prevents direct invokation. + * + * @param endian Endian util + */ + Chunk( const IEndian& endian ); + + /** + * Resizes the internal byte buffer to the given size if the new size is bigger than the current one. + * If the new size is smaller, the buffer is not adjusted + */ + void adjustInternalBuffer( XMP_Uns64 newSize ); + + /** + * Adjusts the chunk size and the parents chunk sizes. + * - Leaf chunks always have the size of their data, inluding the 4-byte type and excluding the header. + * Leaf chunks can have an ODD size! + * - Node chunks have the added size of all of their children, including the childrens header, but excluding it's own header. + * IMPORTANT: When a leaf child node has an ODD size of data, + * a pad byte is added during the writing process and the parent's size INCLUDES the pad byte. + */ + void adjustSize( XMP_Int64 sizeChange = 0 ); + +}; // Chunk + +} // namespace + +#endif diff --git a/XMPFiles/source/FormatSupport/IFF/ChunkController.cpp b/XMPFiles/source/FormatSupport/IFF/ChunkController.cpp new file mode 100644 index 0000000..786cbd1 --- /dev/null +++ b/XMPFiles/source/FormatSupport/IFF/ChunkController.cpp @@ -0,0 +1,709 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FormatSupport/IFF/ChunkController.h" +#include "XMPFiles/source/FormatSupport/IFF/Chunk.h" + +#include <cstdio> + +using namespace IFF_RIFF; + +//----------------------------------------------------------------------------- +// +// ChunkController::ChunkController(...) +// +// Purpose: ctor/dtor +// +//----------------------------------------------------------------------------- + +ChunkController::ChunkController( IChunkBehavior* chunkBehavior, XMP_Bool bigEndian ) +: mEndian (NULL), + mChunkBehavior (chunkBehavior), + mFileSize (0), + mRoot (NULL), + mTrailingGarbageSize (0), + mTrailingGarbageOffset (0) +{ + if (bigEndian) + { + mEndian = &BigEndian::getInstance(); + } else { + mEndian = &LittleEndian::getInstance(); + } + + // create virtual root chunk + mRoot = Chunk::createChunk(*mEndian); + + // share chunk paths with behavior + mChunkBehavior->setMovablePaths( &mChunkPaths ); +} + +ChunkController::~ChunkController() +{ + delete dynamic_cast<Chunk*>(mRoot); +} + +//----------------------------------------------------------------------------- +// +// ChunkController::addChunkPath(...) +// +// Purpose: Adds the given path to the array of "Chunk's of interest" +// +//----------------------------------------------------------------------------- + +void ChunkController::addChunkPath( const ChunkPath& path ) +{ + mChunkPaths.push_back(path); +} + +//----------------------------------------------------------------------------- +// +// ChunkController::compareChunkPaths(...) +// +// Purpose: The function parses all the sibling chunks. For every chunk it +// either caches the chunk, skips it, or calls the function recusivly +// for the children chunks +// +//----------------------------------------------------------------------------- + +ChunkPath::MatchResult ChunkController::compareChunkPaths(const ChunkPath& currentPath) +{ + ChunkPath::MatchResult result = ChunkPath::kNoMatch; + + for( PathIterator iter = mChunkPaths.begin(); ( result == ChunkPath::kNoMatch ) && ( iter != mChunkPaths.end() ); iter++ ) + { + result = iter->match(currentPath); + } + + return result; +} + +//----------------------------------------------------------------------------- +// +// ChunkController::parseChunks(...) +// +// Purpose: The function Parses all the sibling chunks. For every chunk it +// either caches the chunk, skips it, or calls the function recusivly +// for the children chunks +// +//----------------------------------------------------------------------------- + +void ChunkController::parseChunks( XMP_IO* stream, ChunkPath& currentPath, XMP_OptionBits* options /* = NULL */, Chunk* parent /* = NULL */) +{ + XMP_Uns64 filePos = stream->Offset(); + XMP_Bool isRoot = (parent == mRoot); + XMP_Uns64 parseLimit = mFileSize; + XMP_Uns32 chunkCnt = 0; + + parent = ( parent == NULL ? dynamic_cast<Chunk*>(mRoot) : parent ); + + // + // calculate the parse limit + // + if ( !isRoot ) + { + parseLimit = parent->getOriginalOffset() + parent->getSize( true ); + + if( parseLimit > mFileSize ) + { + parseLimit = mFileSize; + } + } + + while ( filePos < parseLimit ) + { + XMP_Uns64 fileTail = mFileSize - filePos; + + // + // check if there is enough space (at least for id and size) + // + if ( fileTail < Chunk::HEADER_SIZE ) + { + //preserve rest of bytes (fileTail) + mTrailingGarbageOffset = filePos; + mTrailingGarbageSize = fileTail; + break; // stop parsing + } + else + { + bool chunkJump = false; + + // + // create a new Chunk + // + Chunk* chunk = Chunk::createChunk(* mEndian ); + + bool readFailure = false; + // + // read the Chunk (id, size, [type]) without caching the data + // + try + { + chunk->readChunk( stream ); + } + catch( ... ) + { + // remember exception during reading the chunk + readFailure = true; + } + + // + // validate chunk ID for top-level chunks + // + if( isRoot && ! mChunkBehavior->isValidTopLevelChunk( chunk->getIdentifier(), chunkCnt ) ) + { + // notValid: preserve rest of bytes (fileTail) + mTrailingGarbageOffset = filePos; + mTrailingGarbageSize = fileTail; + //delete unused chunk (because these are undefined trailing bytes) + delete chunk; + break; // stop parsing + } + else if ( readFailure ) + { + delete chunk; + XMP_Throw ( "Bad RIFF chunk", kXMPErr_BadFileFormat ); + } + + // + // parenting + // (as early as possible in order to be able to clean up + // the tree correctly in the case of an exception) + // + parent->appendChild(chunk, false); + + // count top-level chunks + if( isRoot ) + { + chunkCnt++; + } + + // + // check size if value exceeds 4GB border + // + if( chunk->getSize() >= 0x00000000FFFFFFFFLL ) + { + // remember file position + XMP_Int64 currentFilePos = stream->Offset(); + + // ask for the "real" size value + XMP_Uns64 realSize = mChunkBehavior->getRealSize( chunk->getSize(), + chunk->getIdentifier(), + *mRoot, + stream ); + + // set new size at chunk + chunk->setSize( realSize, true ); + + // set flag if the file position changed + chunkJump = currentFilePos < stream->Offset(); + } + + // + // Repair if needed + // + if ( filePos + chunk->getSize(true) > mFileSize ) + { + bool isUpdate = ( options != NULL ? XMP_OptionIsSet ( *options, kXMPFiles_OpenForUpdate ) : false ); + bool repairFile = ( options != NULL ? XMP_OptionIsSet ( *options, kXMPFiles_OpenRepairFile ) : false ); + + if ( ( ! isUpdate ) || ( repairFile && isRoot ) ) + { + chunk->setSize( mFileSize-filePos-Chunk::HEADER_SIZE, true ); + } + else + { + XMP_Throw ( "Bad RIFF chunk size", kXMPErr_BadFileFormat ); + } + } + + // extend search path + currentPath.append( chunk->getIdentifier() ); + + // first 4 bytes might be already read by the chunk->readChunk function + XMP_Uns64 offsetOfChunkRead = stream->Offset() - filePos - Chunk::HEADER_SIZE; + + switch ( compareChunkPaths(currentPath) ) + { + case ChunkPath::kFullMatch : + { + chunk->cacheChunkData( stream ); + } + break; + + case ChunkPath::kPartMatch : + { + parseChunks( stream, currentPath, options, chunk); + // recalculate the size based on the sizes of its children + chunk->calculateSize( true ); + } + break; + + case ChunkPath::kNoMatch : + { + // Not a chunk we are interested in, so mark it as not changed + // It will then be ignored by any further logic + chunk->resetChanges(); + + if ( !chunkJump && chunk->getSize() > 0) // if chunk not empty + { + XMP_Validate( stream->Offset() + chunk->getSize() - offsetOfChunkRead <= mFileSize , "ERROR: want's to skip beyond EOF", kXMPErr_InternalFailure); + stream->Seek ( chunk->getSize() - offsetOfChunkRead , kXMP_SeekFromCurrent ); + } + } + break; + } + + // remove last identifier from current path + currentPath.remove(); + + // update current file position + filePos = stream->Offset(); + + // skip pad byte if there is one (if size odd) + if( filePos < mFileSize && + ( ( chunkJump && ( stream->Offset() & 1 ) > 0 ) || + ( !chunkJump && ( chunk->getSize() & 1 ) > 0 ) ) ) + { + stream->Seek ( 1 , kXMP_SeekFromCurrent ); + filePos++; + } + } + } +} + + +//----------------------------------------------------------------------------- +// +// ChunkController::parseFile(...) +// +// Purpose: construct the tree, parse children for list of interesting Chunks +// All requested leaf chunks are cached, the parent chunks are created +// but not cached and the rest is skipped +// +//----------------------------------------------------------------------------- + +void ChunkController::parseFile( XMP_IO* stream, XMP_OptionBits* options /* = NULL */ ) +{ + // store file information in root node + mFileSize = stream ->Length(); + ChunkPath currentPath; + + // Make sure the tree is clean before parsing + cleanupTree(); + + try + { + parseChunks( stream, currentPath, options, dynamic_cast<Chunk*>(mRoot) ); + } + catch( ... ) + { + this->cleanupTree(); + throw; + } +} + + +//----------------------------------------------------------------------------- +// +// ChunkController::writeFile(...) +// +// Purpose: Called by the handler to write back the changes to the file. +// +//----------------------------------------------------------------------------- +void ChunkController::writeFile( XMP_IO* stream ) + +{ + // + // if any of the top-level chunks exceeds their maximum size then skip writing and throw an exception + // + for( XMP_Uns32 i=0; i<mRoot->numChildren(); i++ ) + { + Chunk* toplevel = mRoot->getChildAt(i); + XMP_Validate( toplevel->getSize() < mChunkBehavior->getMaxChunkSize(), "Exceeded maximum chunk size.", kXMPErr_AssertFailure ); + } + + // + // if exception is thrown write chunk is skipped + // + mChunkBehavior->fixHierarchy(*mRoot); + + if (mRoot->numChildren() > 0) + { + // The new file size (without trailing garbage) is the offset of the last top-level chunk + its size. + // NOTE: the padding bytes can be ignored, as the top-level chunk is always a node, not a leaf. + Chunk* lastChild = mRoot->getChildAt(mRoot->numChildren() - 1); + XMP_Uns64 newFileSize = lastChild->getOffset() + lastChild->getSize(true); + + // Move garbage tail after last top-level chunk, + // BEFORE the chunks are written -- in case the file shrinks + if (mTrailingGarbageSize > 0 && newFileSize != mTrailingGarbageOffset) + { + XIO::Move( stream, mTrailingGarbageOffset, stream, newFileSize, mTrailingGarbageSize ); + newFileSize += mTrailingGarbageSize; + } + + // Write changed and new chunks to the file + for( XMP_Uns32 i = 0; i < mRoot->numChildren(); i++ ) + { + Chunk* child = mRoot->getChildAt(i); + child->writeChunk( stream ); + } + + // file has been completely written, + // truncate the file it has been bigger before + if (newFileSize < mFileSize) + { + stream->Truncate ( newFileSize ); + } + } +} + +//----------------------------------------------------------------------------- +// +// ChunkController::getChunk(...) +// +// Purpose: returns a certain Chunk +// +//----------------------------------------------------------------------------- + +IChunkData* ChunkController::getChunk( const ChunkPath& path, XMP_Bool last ) const +{ + IChunkData* ret = NULL; + + if( path.length() > 0 ) + { + ChunkPath current; + ret = this->findChunk( path, current, *(dynamic_cast<Chunk*>(mRoot)), last ); + } + + return ret; +} + + +//----------------------------------------------------------------------------- +// +// ChunkController::findChunk(...) +// +// Purpose: Find a chunk described by path in the hierarchy of chunks starting +// at the passed chunk. +// The position of chunk in the hierarchy is described by the parameter +// currentPath. +// This method is supposed to be recursively. +// +//----------------------------------------------------------------------------- + +Chunk* ChunkController::findChunk( const ChunkPath& path, ChunkPath& currentPath, const Chunk& chunk, XMP_Bool last ) const +{ + Chunk* ret = NULL; + XMP_Uns32 cnt = 0; + + if( path.length() > currentPath.length() ) + { + for( XMP_Uns32 i=0; i<chunk.numChildren() && ret == NULL; i++ ) + { + //if last is true go backwards + last ? cnt=chunk.numChildren()-1-i : cnt=i; + + Chunk* child = NULL; + + try + { + child = chunk.getChildAt(cnt); + } + catch(...) + { + child = NULL; + } + + if( child != NULL ) + { + currentPath.append( child->getIdentifier() ); + + switch( path.match( currentPath ) ) + { + case ChunkPath::kFullMatch: + { + ret = child; + } + break; + + case ChunkPath::kPartMatch: + { + ret = this->findChunk( path, currentPath, *child, last ); + } + break; + + case ChunkPath::kNoMatch: + { + // Nothing to do + } + break; + } + + currentPath.remove(); + } + } + } + + return ret; +} + + +//----------------------------------------------------------------------------- +// +// ChunkController::getChunks(...) +// +// Purpose: Returns all chunks that match completely to the passed path. +// +//----------------------------------------------------------------------------- + +const std::vector<IChunkData*>& ChunkController::getChunks( const ChunkPath& path ) +{ + mSearchResults.clear(); + + if( path.length() > 0 ) + { + ChunkPath current; + this->findChunks( path, current, *(dynamic_cast<Chunk*>(mRoot)) ); + } + + return mSearchResults; +}//getChunks + + +//----------------------------------------------------------------------------- +// +// ChunkController::getTopLevelTypes(...) +// +// Purpose: Return an array containing the types of the top level nodes +// Top level nodes are the ones beneath ROOT +// +//----------------------------------------------------------------------------- + +const std::vector<XMP_Uns32> ChunkController::getTopLevelTypes() +{ + std::vector<XMP_Uns32> typeList; + + for( XMP_Uns32 i = 0; i < mRoot->numChildren(); i++ ) + { + typeList.push_back( mRoot->getChildAt( i )->getType() ); + } + + return typeList; +}// getTopLevelTypes + + +//----------------------------------------------------------------------------- +// +// ChunkController::findChunks(...) +// +// Purpose: Find all chunks described by path in the hierarchy of chunks starting +// at the passed chunk. +// The position of chunks in the hierarchy is described by the parameter +// currentPath. Found chunks that match to the path are stored in the +// member mSearchResults. +// This method is supposed to be recursively. +// +//----------------------------------------------------------------------------- + +void ChunkController::findChunks( const ChunkPath& path, ChunkPath& currentPath, const Chunk& chunk ) +{ + if( path.length() > currentPath.length() ) + { + for( XMP_Uns32 i=0; i<chunk.numChildren(); i++ ) + { + Chunk* child = NULL; + + try + { + child = chunk.getChildAt(i); + } + catch(...) + { + child = NULL; + } + + if( child != NULL ) + { + currentPath.append( child->getIdentifier() ); + + switch( path.match( currentPath ) ) + { + case ChunkPath::kFullMatch: + { + mSearchResults.push_back( child ); + } + break; + + case ChunkPath::kPartMatch: + { + this->findChunks( path, currentPath, *child ); + } + break; + + case ChunkPath::kNoMatch: + { + // Nothing to do + } + break; + } + + currentPath.remove(); + } + } + } +}//findChunks + + +//----------------------------------------------------------------------------- +// +// ChunkController::cleanupTree(...) +// +// Purpose: Cleanup function called from destructor and in case of an exception +// +//----------------------------------------------------------------------------- + +void ChunkController::cleanupTree() +{ + delete dynamic_cast<Chunk*>(mRoot); + mRoot = Chunk::createChunk(*mEndian); +} + + +//----------------------------------------------------------------------------- +// +// ChunkController::dumpTree(...) +// +// Purpose: dumps the tree structure +// +//----------------------------------------------------------------------------- + +std::string ChunkController::dumpTree( ) +{ + std::string ret; + char buffer[256]; + + if ( mRoot != NULL ) + { + ret = mRoot->toString(); + } + + if ( mTrailingGarbageSize != 0 ) + { + snprintf( buffer, 255, "\n Trailing Bytes: %llu", mTrailingGarbageSize ); + + std::string str(buffer); + ret.append(str); + } + return ret; +} + +//----------------------------------------------------------------------------- +// +// ChunkController::createChunk(...) +// +// Purpose: Create a new empty chunk +// +//----------------------------------------------------------------------------- + +IChunkData* ChunkController::createChunk( XMP_Uns32 id, XMP_Uns32 type /*= kType_NONE*/ ) +{ + Chunk* chunk = Chunk::createChunk(* mEndian ); + + chunk->setID( id ); + if( type != kType_NONE ) + { + chunk->setType( type ); + } + + return chunk; +} + +//----------------------------------------------------------------------------- +// +// ChunkController::insertChunk(...) +// +// Purpose: Insert a new chunk. The position of this new chunk within the +// hierarchy is determined internally by the behavior. +// Throws an exception if a chunk cannot be inserted into the tree +// +//----------------------------------------------------------------------------- + +void ChunkController::insertChunk( IChunkData* chunk ) +{ + XMP_Validate( chunk != NULL, "ERROR inserting Chunk. Chunk is NULL.", kXMPErr_InternalFailure ); + Chunk* ch = dynamic_cast<Chunk*>(chunk); + mChunkBehavior->insertChunk( *mRoot, *ch ); + // sets OriginalSize = Size / OriginalOffset = Offset + ch->setAsNew(); + // force set dirty flag + ch->setChanged(); +} + +//----------------------------------------------------------------------------- +// +// ChunkController::removeChunk(...) +// +// Purpose: Delete a chunk or remove/delete it from the tree. +// If the chunk exists within the chunk hierarchy the chunk gets removed +// from the tree and deleted. +// If it is not in the tree, then it is only destroyed. +// +//----------------------------------------------------------------------------- + +void ChunkController::removeChunk( IChunkData* chunk ) +{ + if( chunk != NULL ) + { + Chunk* chk = dynamic_cast<Chunk*>(chunk); + + if( this->isInTree( chk ) ) + { + if( mChunkBehavior->removeChunk( *mRoot, *chk ) ) + { + delete chk; + } + } + else + { + delete chk; + } + } +} + +//----------------------------------------------------------------------------- +// +// ChunkController::isInTree(...) +// +// Purpose: return true if the passed in Chunk is part of the Chunk tree +// +//----------------------------------------------------------------------------- + +bool ChunkController::isInTree( Chunk* chunk ) +{ + bool ret = ( mRoot == chunk ); + + if( !ret && chunk != NULL ) + { + Chunk* parent = chunk->getParent(); + + while( !ret && parent != NULL ) + { + ret = ( mRoot == parent ); + parent = parent->getParent(); + } + } + + return ret; +} diff --git a/XMPFiles/source/FormatSupport/IFF/ChunkController.h b/XMPFiles/source/FormatSupport/IFF/ChunkController.h new file mode 100644 index 0000000..933da36 --- /dev/null +++ b/XMPFiles/source/FormatSupport/IFF/ChunkController.h @@ -0,0 +1,246 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _ChunkController_h_ +#define _ChunkController_h_ + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "source/XMP_LibUtils.hpp" + +#include "XMPFiles/source/FormatSupport/IFF/ChunkPath.h" +#include "XMPFiles/source/FormatSupport/IFF/IChunkBehavior.h" + +class IEndian; + +namespace IFF_RIFF +{ +/** + The class ChunkController is supposed to act as an controller between the IRIFFHandler and the actual chunks (Chunk instances). + It controls the parsing and writing of the passed stream. +*/ + +class IChunkData; +class IChunkContainer; +class Chunk; + +class ChunkController +{ + public: + /** + * Constructor: + * Creates an IEndian based instance for further usage. + * + * @param IChunkBehavior* chunkBehavior : for AVI the IChunkBehavior instance would be an instance of a IChunkBehavior class, that knows + * about the 1,2,4 GB border, padding byte special cases, AVIX stuff and so on. That knowledge would + * be used during writeFile() + * In the case of WAVE it would be an instance of WAVEBehavior that would know how to get the 64bit + * size values for RF64 if required. That knowledge would be used during parseFile() + * @param XMP_Bool bigEndian set True if file chunk data is big endian (e.g. AIFF). + * Must explicitely be set, so that handlers do not accidentaly use the wrong endianess + */ + ChunkController( IChunkBehavior* chunkBehavior, XMP_Bool bigEndian ); + + ~ChunkController(); + + /** + * Adds the given path to the array of "Chunk's of interest", + * + * @param path List of Paths that should be parsed + * example AVI: [ RIFF:AVI/LIST:INFO , RIFF:AVIX/LIST:INFO, RIFF:AVI/LIST:TDAT ] + */ + void addChunkPath( const ChunkPath& path ); + + /** + * construct the tree, parse children for list of interesting Chunks + * All requested leaf chunks are cached, the parent chunks are created but not cached + * and the rest is skipped. + * + * @param stream the open [file] stream with file pointer at the beginning of the file + * + */ + void parseFile( XMP_IO* stream, XMP_OptionBits* options = NULL ); + + /** + * Create a new empty chunk + * + * @param id Chunk identifier + * @param type Chunk type [optional] + * @return New IChunkData with passed id/type + */ + IChunkData* createChunk( XMP_Uns32 id, XMP_Uns32 type = kType_NONE ); + + /** + * Insert a new chunk. The position of this new chunk within the hierarchy + * is determined internally by the behavior. + * Throws an exception if a chunk cannot be inserted into the tree + * + * @param chunk The chunk to insert into the tree + */ + void insertChunk( IChunkData* chunk ); + + /** + * Delete a chunk or remove/delete it from the tree. + * If the chunk exists within the chunk hierarchy the chunk gets removed from the tree and deleted. + * If it is not in the tree, then it is only destroyed. + * + * @param chunk Chunk to remove/delete + */ + void removeChunk( IChunkData* chunk ); + + /** + * Called by the handler to write back the changes to the file. + * 1. fix the file tree (ChunkBehavior#fixHierarchy), + * offsets are corrected, no overlapping chunks; + * if rearranging fails, the file is not touched + * 2. write the changed chunks to the file + * + * @param stream the open [file] stream for writing, the file pointer must be at the beginning + */ + void writeFile( XMP_IO* stream ); + + /** + * Returns the first (or last) Chunk that matches the passed path. + * + * @param path the path of the Chunk to return + * @param last in case of duplicates return the last one + * @return Returns Chunk or NULL + */ + IChunkData* getChunk( const ChunkPath& path, XMP_Bool last = false ) const; + + /** + * Returns all chunks that match completely to the passed path. + * E.g. if FORM:AIFF/LIST is given, it would return all LIST chunks in FORM:AIFF + * + * @param path the path of the Chunk to return + * @return list of found chunks or empty list + */ + const std::vector<IChunkData*>& getChunks( const ChunkPath& path ); + + /** + * returns the number of the bytes after the last valid IFF chunk + */ + inline XMP_Int64 getTrailingGarbageSize() { return mTrailingGarbageSize; }; + + /** + * returns the file size + */ + inline XMP_Int64 getFileSize() { return mFileSize; }; + + /** + * Return an array containing the types of the top level nodes + * Top level nodes are the ones beneath ROOT + */ + const std::vector<XMP_Uns32> getTopLevelTypes(); + + /** + * dumps the tree structure + * + */ + std::string dumpTree( ); + + protected: + /** + * Standard Constructor: + * Hidden on purpose. Must not be used! + * A Controller must have a behavior! + */ + ChunkController() { XMP_Throw("Ctor hidden", kXMPErr_InternalFailure); } + + /** + * The function Parses all the sibling chunks. For every chunk it either caches the chunk, + * skips it, or calls the function recusivly for the children chunks + * + * @param stream the file stream + * @param currentPath the path/id of the Chunk to return + * @param options handler options + * @param parent pointer to the parent chunk + */ + void parseChunks( XMP_IO* stream, ChunkPath& currentPath, XMP_OptionBits* options = NULL, Chunk* parent = NULL ); + + /** + * The function parses all the sibling chunks. For every chunk it either caches the chunk, + * skips it, or calls the function recusivly for the children chunks + * + * @param ChunkPath& currentPath: the path/id of the Chunk to return + */ + ChunkPath::MatchResult compareChunkPaths( const ChunkPath& currentPath ); + + /** + * Find a chunk described by path in the hierarchy of chunks starting at the passed chunk. + * The position of chunk in the hierarchy is described by the parameter currentPath. + * This method is supposed to be recursively. + */ + Chunk* findChunk( const ChunkPath& path, ChunkPath& currentPath, const Chunk& chunk, XMP_Bool last = false ) const; + + /** + * Find all chunks described by path in the hierarchy of chunks starting at the passed chunk. + * The position of chunks in the hierarchy is described by the parameter currentPath. + * Found chunks that match to the path are stored in the member mSearchResults. + * This method is supposed to be recursively. + */ + void findChunks( const ChunkPath& path, ChunkPath& currentPath, const Chunk& chunk ); + + /** + * Cleanup function called from destructor and in case of an exception + */ + void cleanupTree(); + + /** + * return true if the passed in Chunk is part of the Chunk tree + * + * @param chunk the chunk that shall be checked. + */ + bool isInTree( Chunk* chunk ); + + + // Members + + /** + * Endian class. Either BigEndian oder LittleEndian. Based on the file format. + */ + const IEndian* mEndian; + + /** + * Chunk behaviour class. Has file format specific function for getting the size and + * rearranging the chunk tree. + */ + IChunkBehavior* mChunkBehavior; + + /** The list of chunks wich should be cached. */ + std::vector<ChunkPath> mChunkPaths; + + /** Iterator for the list of chunk paths */ + typedef std::vector<ChunkPath>::iterator PathIterator; + + /** The overall filesize after parsing the file stream */ + XMP_Uns64 mFileSize; + + /** The root of the Chunk Tree (top level list) */ + IChunkContainer* mRoot; + + /** Offset of trailing garbage characters */ + XMP_Uns64 mTrailingGarbageOffset; + + /** Size of trailing garbage characters */ + XMP_Uns64 mTrailingGarbageSize; + + /** search results of method getChunks(...) */ + ChunkPath mSearchPath; + + /** Cached search results */ + std::vector<IChunkData*> mSearchResults; +}; // ChunkController + +} // namespace + +#endif diff --git a/XMPFiles/source/FormatSupport/IFF/ChunkPath.cpp b/XMPFiles/source/FormatSupport/IFF/ChunkPath.cpp new file mode 100644 index 0000000..54680f8 --- /dev/null +++ b/XMPFiles/source/FormatSupport/IFF/ChunkPath.cpp @@ -0,0 +1,239 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "XMPFiles/source/FormatSupport/IFF/ChunkPath.h" +#include "source/XMP_LibUtils.hpp" + +#include <vector> + +using namespace IFF_RIFF; + +typedef std::vector<ChunkIdentifier>::size_type ChunkSizeType; + +//----------------------------------------------------------------------------- +// +// ChunkPath::ChunkPath(...) +// +// Purpose: ctor/dtor +// +//----------------------------------------------------------------------------- + +ChunkPath::ChunkPath( const ChunkIdentifier* path /*= NULL*/, XMP_Uns32 size /*=0*/ ) +{ + if( path != NULL ) + { + for( XMP_Uns32 i=0; i<size; i++ ) + { + this->append( path[i] ); + } + } +} + +ChunkPath::ChunkPath( const ChunkPath& path ) +{ + for( XMP_Int32 i=0; i<path.length(); i++ ) + { + this->append( path.identifier(i) ); + } +} + +ChunkPath::ChunkPath( const ChunkIdentifier& identifier ) +{ + this->append( identifier ); +} + +ChunkPath::~ChunkPath() +{ + this->clear(); +} + + +ChunkPath & ChunkPath::operator=( const ChunkPath &rhs ) +{ + for( XMP_Int32 i = 0; i < rhs.length(); i++ ) + { + this->append( rhs.identifier(i) ); + } + + return *this; +} + +//----------------------------------------------------------------------------- +// +// ChunkPath::clear(...) +// +// Purpose: Remove all ChunkIdentifier's from the path +// +//----------------------------------------------------------------------------- + +void ChunkPath::clear() +{ + mPath.clear(); +} + +//----------------------------------------------------------------------------- +// +// ChunkPath::append(...) +// +// Purpose: Append a ChunkIdentifier to the end of the path +// +//----------------------------------------------------------------------------- + +void ChunkPath::append( XMP_Uns32 id, XMP_Uns32 type /*= kType_NONE*/ ) +{ + ChunkIdentifier ci; + + ci.id = id; + ci.type = type; + + mPath.push_back(ci); +} + + +void ChunkPath::append( const ChunkIdentifier& identifier ) +{ + mPath.push_back(identifier); +} + + +void ChunkPath::append( const ChunkIdentifier* path, XMP_Uns32 size ) +{ + if( path != NULL ) + { + for( XMP_Uns32 i=0; i < size; i++ ) + { + this->append( path[i] ); + } + } +} + +//----------------------------------------------------------------------------- +// +// ChunkPath::insert(...) +// +// Purpose: Insert an identifier +// +//----------------------------------------------------------------------------- + +void ChunkPath::insert( const ChunkIdentifier& identifier, XMP_Uns32 pos /*= 0*/ ) +{ + if( pos >= mPath.size() ) + { + this->append( identifier ); + } + else + { + mPath.insert( mPath.begin() + pos, identifier ); + } +} + +//----------------------------------------------------------------------------- +// +// ChunkPath::remove(...) +// +// Purpose: Remove the endmost ChunkIdentifier from the path +// +//----------------------------------------------------------------------------- + +void ChunkPath::remove() +{ + mPath.pop_back(); +} + +//----------------------------------------------------------------------------- +// +// ChunkPath::removeAt(...) +// +// Purpose: Remove the ChunkIdentifier at the passed position in the path +// +//----------------------------------------------------------------------------- + +void ChunkPath::removeAt( XMP_Int32 pos ) +{ + if( ! mPath.empty() && pos >= 0 && (ChunkSizeType)pos < mPath.size() ) + { + mPath.erase( mPath.begin() + pos ); + } + else + { + XMP_Throw( "Index out of range.", kXMPErr_BadIndex ); + } +} + +//----------------------------------------------------------------------------- +// +// ChunkPath::identifier(...) +// +// Purpose: Return ChunkIdentifier at the passed position +// +//----------------------------------------------------------------------------- + +const ChunkIdentifier& ChunkPath::identifier( XMP_Int32 pos ) const +{ + return mPath.at(pos); +} + +//----------------------------------------------------------------------------- +// +// ChunkPath::length(...) +// +// Purpose: Return the number of ChunkIdentifier's in the path +// +//----------------------------------------------------------------------------- + +XMP_Int32 ChunkPath::length() const +{ + return (XMP_Int32)mPath.size(); +} + +//----------------------------------------------------------------------------- +// +// ChunkPath::match(...) +// +// Purpose: Compare the passed ChunkPath with this path. +// +//----------------------------------------------------------------------------- + +ChunkPath::MatchResult ChunkPath::match( const ChunkPath& path ) const +{ + MatchResult ret = kNoMatch; + + if( path.length() > 0 ) + { + XMP_Int32 depth = ( this->length() > path.length() ? path.length() : this->length() ); + XMP_Int32 matchCount = 0; + + for( XMP_Int32 i=0; i<depth; i++ ) + { + const ChunkIdentifier& id1 = this->identifier(i); + const ChunkIdentifier& id2 = path.identifier(i); + + if( id1.id == id2.id ) + { + if( i == this->length() - 1 && id1.type == kType_NONE ) + { + matchCount++; + } + else if( id1.type == id2.type ) + { + matchCount++; + } + } + else + break; + } + + if( matchCount == depth ) + { + ret = ( path.length() >= this->length() ? kFullMatch : kPartMatch ); + } + } + + return ret; +} diff --git a/XMPFiles/source/FormatSupport/IFF/ChunkPath.h b/XMPFiles/source/FormatSupport/IFF/ChunkPath.h new file mode 100644 index 0000000..c0f149b --- /dev/null +++ b/XMPFiles/source/FormatSupport/IFF/ChunkPath.h @@ -0,0 +1,190 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _ChunkPath_h_ +#define _ChunkPath_h_ + +#include "public/include/XMP_Environment.h" // ! This must be the first include. +#include "public/include/XMP_Const.h" + +#include <limits.h> // For UINT_MAX. +#include <vector> + +namespace IFF_RIFF +{ + +/** + A ChunkPath describes one certain chunk in the hierarchy of chunks + of the IFF/RIFF file format. + Each chunks gets identified by a structure of the type ChunkIdentifier. + Which consists of the 4byte ID of the chunk and, if applicable, the 4byte + type of the chunk. +*/ + +// IFF/RIFF ids +enum { + // invalid ID + kChunk_NONE = UINT_MAX, + + // format chunks + kChunk_RIFF = 0x52494646, + kChunk_RF64 = 0x52463634, + kChunk_FORM = 0x464F524D, + kChunk_JUNK = 0x4A554E4B, + kChunk_JUNQ = 0x4A554E51, + + + // other container chunks + kChunk_LIST = 0x4C495354, + + // other relevant chunks + kChunk_XMP = 0x5F504D58, // "_PMX" + + kChunk_data = 0x64617461, + + //should occur only in AVI + kChunk_Cr8r = 0x43723872, + kChunk_PrmL = 0x50726D4C, + + //should occur only in WAV + kChunk_DISP = 0x44495350, + kChunk_bext = 0x62657874, + kChunk_cart = 0x63617274, + kChunk_ds64 = 0x64733634, + + // AIFF + kChunk_APPL = 0x4150504C, + kChunk_NAME = 0x4E414D45, + kChunk_AUTH = 0x41555448, + kChunk_CPR = 0x28632920, + kChunk_ANNO = 0x414E4E4F +}; + +// IFF/RIFF types +enum { + kType_AVI_ = 0x41564920, + kType_AVIX = 0x41564958, + kType_WAVE = 0x57415645, + kType_AIFF = 0x41494646, + kType_AIFC = 0x41494643, + kType_INFO = 0x494E464F, + kType_Tdat = 0x54646174, + + // AIFF + kType_XMP = 0x584D5020, + kType_FREE = 0x46524545, + + kType_NONE = UINT_MAX +}; + + +struct ChunkIdentifier +{ + XMP_Uns32 id; + XMP_Uns32 type; +}; + +/** +* calculates the size of a ChunkIdentifier array. +* Has to be a macro as the sizeof operator does nto work for pointer function parameters +*/ +#define SizeOfCIArray(ciArray) ( sizeof(ciArray) / sizeof(ChunkIdentifier) ) + + +class ChunkPath +{ +public: + /** + ctor/dtor + */ + ChunkPath( const ChunkIdentifier* path = NULL, XMP_Uns32 size = 0 ); + ChunkPath( const ChunkPath& path ); + ChunkPath( const ChunkIdentifier& identifier ); + ~ChunkPath(); + + ChunkPath & operator=( const ChunkPath &rhs ); + + /** + Append a ChunkIdentifier to the end of the path + + @param id 4byte id of chunk + @param type 4byte type of chunk + */ + void append( XMP_Uns32 id, XMP_Uns32 type = kType_NONE ); + void append( const ChunkIdentifier& identifier ); + + /** + Append a whole path + + @param path Array of ChunkIdentifiert objects + @param size number of elements in the given array + */ + void append( const ChunkIdentifier* path = NULL, XMP_Uns32 size = 0 ); + + /** + Insert an identifier + + @param identifier id and type + @param pos position within the path + */ + void insert( const ChunkIdentifier& identifier, XMP_Uns32 pos = 0 ); + + /** + Remove the endmost ChunkIdentifier from the path + */ + void remove(); + /** + Remove the ChunkIdentifier at the passed position in the path. + Throw exception if the position is out of range. + + @param pos Position of ChunkIdentifier in the path + */ + void removeAt( XMP_Int32 pos ); + + /** + Return ChunkIdentifier at the passed position + + @param pos Position of ChunkIdentifier in the path + @return ChunkIdentifier at passed position (throw exception if + the position is out of range) + */ + const ChunkIdentifier& identifier( XMP_Int32 pos ) const; + + /** + Return the number of ChunkIdentifier's in the path + */ + XMP_Int32 length() const; + + /** + Remove all ChunkIdentifier's from the path + */ + void clear(); + + /** + Compare the passed ChunkPath with this path. + + @param path Path to compare with this path + @return Match result + */ + enum MatchResult + { + kNoMatch = 0, + kPartMatch = 1, + kFullMatch = 2 + }; + + MatchResult match( const ChunkPath& path ) const; + +private: + std::vector<ChunkIdentifier> mPath; +}; + +} + +#endif diff --git a/XMPFiles/source/FormatSupport/IFF/IChunkBehavior.cpp b/XMPFiles/source/FormatSupport/IFF/IChunkBehavior.cpp new file mode 100644 index 0000000..44159c2 --- /dev/null +++ b/XMPFiles/source/FormatSupport/IFF/IChunkBehavior.cpp @@ -0,0 +1,595 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/IFF/IChunkBehavior.h" +#include "XMPFiles/source/FormatSupport/IFF/Chunk.h" + +#include <algorithm> + +using namespace IFF_RIFF; + +//----------------------------------------------------------------------------- +// +// getIndex(...) +// +// Purpose: [static] Calculate index of chunk in tree +// +//----------------------------------------------------------------------------- + +static XMP_Uns32 getIndex( const IChunkContainer& tree, const Chunk& chunk ) +{ + const Chunk& parent = dynamic_cast<const Chunk&>( tree ); + + return std::find( parent.firstChild(), parent.lastChild(), &chunk ) - parent.firstChild(); +} + +//----------------------------------------------------------------------------- +// +// IChunkBehavior::arrangeChunksInPlace(...) +// +// Purpose: Try to arrange all chunks of the source tree at their current location. +// In a loop the method takes each chunk of the srcTree. +// * If a chunk is a FREE chunk then it is removed. +// * If a chunk is a known movable chunk then adjust its offset so that there's +// no gap to its previous chunk. If the chunk offset was adjusted and/or the +// chunk grew/shrank in its size then remember the offset difference for +// further processing +// * If the chunk is neither movable nor a FREE chunk and there is a offset +// difference then fill possible gaps with FREE chunk or move chunks to +// the destTree. +// +//----------------------------------------------------------------------------- + +void IChunkBehavior::arrangeChunksInPlace( IChunkContainer& srcTree, IChunkContainer& destTree ) +{ + XMP_Validate( &srcTree != &destTree, "Source and destination tree mustn't be the same", kXMPErr_InternalFailure ); + + XMP_Int64 offsetAdjust = 0; + + for( XMP_Int32 index=0; index<static_cast<XMP_Int32>( srcTree.numChildren() ); index++ ) + { + Chunk* chunk = srcTree.getChildAt( index ); + + // + // Is chunk one that might be moved in the tree + // (and so it's a chunk that might be modified) + // + if( this->isMovable( *chunk ) ) + { + // + // Are there FREE chunks above this chunk? + // Then remove it. + // + Chunk* freeChunk = NULL; + + if( index > 0 ) + { + // find FREE chunk and merge possible multiple FREE chunks to one chunk + freeChunk = this->mergeFreeChunks( srcTree, index-1 ); + } + + if( freeChunk != NULL ) + { + // update running index + index = ::getIndex( srcTree, *chunk ); + + // subtract size of FREE chunk from offset adjust value + offsetAdjust -= static_cast<XMP_Int64>( freeChunk->getPadSize(true) ); + + // remove FREE chunk from the tree + srcTree.removeChildAt( index-1 ); + delete freeChunk; + + // update running index because one chunk was removed from the tree + index--; + } + + // + // offset needs to be adjusted + // + if( offsetAdjust != 0 ) + { + chunk->setOffset( chunk->getOffset() + offsetAdjust ); + } + + // + // update adjust value if the size of the chunk has changed + // (and so the offsets of following chunks needs to be adjusted) + // + offsetAdjust += chunk->getPadSize() - chunk->getOriginalPadSize(); + } + else if( this->isFREEChunk( *chunk ) && offsetAdjust != 0 ) + { + // + // chunk is a FREE chunk, just remove it + // + + // merge FREE chunks + chunk = this->mergeFreeChunks( srcTree, index ); + + // update running index (in case multiple FREE chunk were merged) + index = ::getIndex( srcTree, *chunk ); + + // update adjust value about the total size of the FREE chunk + offsetAdjust -= static_cast<XMP_Int64>( chunk->getPadSize(true) ); + + // remove FREE chunk from tree + srcTree.removeChildAt( index ); + delete chunk; + + // update running index + index--; + } + else if( offsetAdjust != 0 ) + { + // + // the current chunk can't be moved, + // so we can't adjust the offset of this chunk + // + XMP_Uns64 gap = 0; + + if( offsetAdjust > 0 ) + { + // + // One or more foregoing chunks grew in their seize and so + // the offset of following chunks needs to be adjusted. + // But since the current chunk can't be moved one or more previous + // chunks are now overlapping over the current chunk. + // + // So now one or more of the previous chunks needs to be removed + // (moved to the destTree) so that the offset value of the current + // chunk can stay where it is. + // A possible gap will be filled with a FREE chunk. + // + + Chunk* preChunk = NULL; + + // + // count Chunks that needs to be moved + // + XMP_Validate( index-1 >= 0, "There shouldn't be an offset adjust value for the first chunk", kXMPErr_InternalFailure ); + + XMP_Int32 preIndex = index; + XMP_Uns64 preSize = 0; + + do + { + preIndex--; + preChunk = srcTree.getChildAt( preIndex ); + + XMP_Validate( this->isMovable( *preChunk ) || this->isFREEChunk( *preChunk ), "Movable or FREE chunk expected", kXMPErr_InternalFailure ); + + preSize += preChunk->getPadSize( true ); + + } while( static_cast<XMP_Int64>( preSize ) < offsetAdjust && preIndex > 0 ); + + // + // move chunks + // + for( XMP_Uns32 rem=preIndex; rem<static_cast<XMP_Uns32>( index ); rem++ ) + { + // always fetch chunk at the first index of the range because + // these chunks are removed from the tree + preChunk = srcTree.removeChildAt( preIndex ); + + if( this->isFREEChunk( *preChunk ) ) + { + delete preChunk; + } + else + { + destTree.appendChild( preChunk, false ); + } + } + + // update current index + index = ::getIndex( srcTree, *chunk ); + + // + // calculate size of gap + // + XMP_Uns64 curOffset = chunk->getOffset(); + XMP_Uns64 preOffset = Chunk::HEADER_SIZE + Chunk::TYPE_SIZE; + + if( index > 0 ) + { + preChunk = srcTree.getChildAt( index-1 ); + preOffset = preChunk->getOffset() + preChunk->getPadSize( true ); + } + + gap = curOffset - preOffset; + } + else if( offsetAdjust < 0 ) + { + // + // There is a gap between the previous chunk and the current one. + // Fill the gap with a FREE chunk. + // + gap = offsetAdjust * (-1); + } + + // + // if there is a gap we need to fill it with a FREE chunk + // + if( gap > 0 ) + { + // + // The gap must be at least as big as the minimum size of FREE chunks. + // If that's not the case we need to move more chunks to expand + // the gap. + // + while( gap < this->getMinFREESize() ) + { + XMP_Validate( index > 0, "Not enough space to insert FREE chunk", kXMPErr_Unimplemented ); + + Chunk* preChunk = srcTree.removeChildAt( index-1 ); + gap += preChunk->getPadSize(true); + destTree.appendChild( preChunk, false ); + + // update running index + index--; + } + + // + // Fill the gap with a FREE chunk. + // + Chunk* freeChunk = this->createFREE( gap ); + srcTree.insertChildAt( index, freeChunk ); + freeChunk->setAsNew(); + + // update running index + index++; + } + + // reset adjust value + offsetAdjust = 0; + } + } +} + +//----------------------------------------------------------------------------- +// +// IChunkBehavior::arrangeChunksInTree(...) +// +// Purpose: This method proceeds the list of Chunks of the source tree in the +// passed range and looks for FREE chunks in the destination tree to +// move the source chunks to. +// Source tree and destination tree could be one and the same but it's +// not required. If both trees are the same then it's not allowed to +// cross source and destination ranges. +// +//----------------------------------------------------------------------------- + +void IChunkBehavior::arrangeChunksInTree( IChunkContainer& srcTree, IChunkContainer& destTree ) +{ + XMP_Validate( &srcTree != &destTree, "Source and destination tree mustn't be the same", kXMPErr_InternalFailure ); + + if( srcTree.numChildren() > 0 ) + { + // + // for all chunks that were moved to the end try to find a FREE chunk for them + // + for( XMP_Int32 index=srcTree.numChildren()-1; index>=0; index-- ) + { + Chunk* chunk = srcTree.getChildAt(index); + + // + // find a FREE chunk where the chunk would fit in + // + XMP_Int32 freeIndex = this->findFREEChunk( destTree, chunk->getSize(true) ); + + if( freeIndex >= 0 ) + { + Chunk* freeChunk = destTree.getChildAt( freeIndex ); + + // remove chunk from source tree + srcTree.removeChildAt( index ); + + // insert chunk at new location + destTree.insertChildAt( freeIndex, chunk ); + + // remove the FREE chunk + destTree.removeChildAt( freeIndex+1 ); + + // + // if the size of the FREE chunk is larger than the size of the chunk then fill + // the gap with a new FREE chunk (the method findFREEChunk takes care that the + // remaining space is large enough for a new FREE chunk, but findFREEChunk also + // takes account of possible pad bytes in its calculations! Therefore following + // calculations have to take account of a possible pad byte as well!) + // + if( freeChunk->getPadSize( true ) > chunk->getPadSize( true ) ) + { + Chunk* remainFreeChunk = this->createFREE( freeChunk->getPadSize( true ) - chunk->getPadSize( true ) ); + destTree.insertChildAt( freeIndex+1, remainFreeChunk ); + remainFreeChunk->setAsNew(); + } + + delete freeChunk; + } + } + } +} + +//----------------------------------------------------------------------------- +// +// IChunkBehavior::validateOffsets(...) +// +// Purpose: Fix recursively the offset values of all modified chunks. +// At the same time the method checks the offset value of all not +// modified chunks and throws an exception if there is any discrepance +// with the calculated offset. +// +//----------------------------------------------------------------------------- + +void IChunkBehavior::validateOffsets( IChunkContainer& tree, XMP_Uns64 startOffset /*= 0*/ ) +{ + XMP_Uns64 offset = startOffset; + + // + // for all children of the tree + // + for( XMP_Uns32 i=0; i<tree.numChildren(); i++ ) + { + Chunk* chunk = tree.getChildAt(i); + + // the offset of a not modified chunk should match the calculated offset + XMP_Validate( chunk->getOffset() == offset, "Invalid offset", kXMPErr_InternalFailure ); + + if( !this->isMovable( *chunk ) ) + { + XMP_Validate( chunk->getOffset() == chunk->getOriginalOffset(), "Invalid offset non-modified chunk", kXMPErr_InternalFailure ); + } + + // go through children + if( chunk->getChunkMode() == CHUNK_NODE ) + { + this->validateOffsets( *chunk, offset + Chunk::HEADER_SIZE + Chunk::TYPE_SIZE ); + } + + // calculate next offset + offset += chunk->getPadSize(true); + } +} + +//----------------------------------------------------------------------------- +// +// IChunkBehavior::getFreeSpace(...) +// +// Purpose: Retrieve the free space at the passed position in the child list of +// the parent tree. If there's a FREE chunk then return it. +// +//----------------------------------------------------------------------------- + +Chunk* IChunkBehavior::getFreeSpace( XMP_Int64& outFreeBytes, const IChunkContainer& tree, XMP_Uns32 index ) const +{ + // validate index + XMP_Validate( index < tree.numChildren(), "Invalid index", kXMPErr_InternalFailure ); + + Chunk* ret = NULL; + + Chunk* chunk = tree.getChildAt( index ); + + if( this->isFREEChunk( *chunk ) ) + { + // + // chunk is a FREE chunk + // + outFreeBytes = chunk->getSize( true ); + ret = chunk; + } + else if( chunk->getChunkMode() != CHUNK_UNKNOWN && chunk->hasChanged() ) + { + // + // chunk is NOT a FREE chunk but the size of this chunk has changed + // + outFreeBytes = chunk->getOriginalSize() - chunk->getSize(); + } + + return ret; +} + +//----------------------------------------------------------------------------- +// +// IChunkBehavior::mergeFreeChunks(...) +// +// Purpose: Try to merge existing FREE chunks at the passed position in the +// child list of the passed parent tree. The algorithm looks at the +// position, before the position and after the position. +// +//----------------------------------------------------------------------------- + +Chunk* IChunkBehavior::mergeFreeChunks( IChunkContainer& tree, XMP_Uns32 index ) +{ + // validate index + XMP_Validate( index < tree.numChildren(), "Invalid index", kXMPErr_InternalFailure ); + + Chunk* ret = NULL; + + Chunk* chunk = tree.getChildAt( index ); + + // + // is chunk a FREE chunk + // + if( this->isFREEChunk( *chunk ) ) + { + XMP_Uns32 indexStart = index; + XMP_Uns32 indexEnd = index; + + XMP_Uns64 size = chunk->getPadSize( true ); + + // + // find FREE chunks before start chunk + // + if( index > 0 ) + { + XMP_Int32 i = XMP_Int32( index-1 ); + Chunk* c = NULL; + + do + { + c = tree.getChildAt(i); + + if( this->isFREEChunk( *c ) ) + { + size += c->getPadSize( true ); + indexStart = XMP_Uns32(i); + i--; + } + else + { + c = NULL; + } + + } while( i >= 0 && c != NULL ); + } + + // + // find FREE chunks after start chunk + // + if( index+1 < tree.numChildren() ) + { + XMP_Uns32 i = index+1; + Chunk* c = NULL; + + do + { + c = tree.getChildAt(i); + + if( this->isFREEChunk( *c ) ) + { + size += c->getPadSize( true ); + indexEnd = i; + i++; + } + else + { + c = NULL; + } + + } while( i < tree.numChildren() && c != NULL ); + } + + if( indexStart < indexEnd ) + { + // + // more than one FREE chunks, so merge them + // + for( XMP_Uns32 i=indexStart; i<=indexEnd; i++ ) + { + Chunk* f = tree.getChildAt( indexStart ); + tree.removeChildAt( indexStart ); + delete f; + } + + ret = this->createFREE( size ); + tree.insertChildAt( indexStart, ret ); + ret->setAsNew(); + } + else + { + // + // one single FREE chunk + // + ret = chunk; + } + } + + return ret; +} + +//----------------------------------------------------------------------------- +// +// IChunkBehavior::findFREEChunk(...) +// +// Purpose: Find a FREE chunk with the passed total size (including header). +// If the FREE chunk is found then take care of the fact that is has +// to be that large (or larger) then the minimum size of a FREE chunk. +// The method takes also into account that the passed size probably +// not includes a pad byte +// +//----------------------------------------------------------------------------- + +XMP_Int32 IChunkBehavior::findFREEChunk( const IChunkContainer& tree, XMP_Uns64 requiredSize /*including header*/ ) +{ + XMP_Int32 ret = -1; + + for( XMP_Uns32 i=0; i<tree.numChildren(); i++ ) + { + Chunk* chunk = tree.getChildAt(i); + + XMP_Uns64 requiredSizePad = requiredSize + ( requiredSize % 2 ); // required size including pad byte + + if( this->isFREEChunk( *chunk ) && + ( chunk->getPadSize( true ) == requiredSizePad || + chunk->getPadSize( true ) >= requiredSizePad + getMinFREESize() ) ) + { + ret = i; + break; + } + } + + return ret; +} + +//----------------------------------------------------------------------------- +// +// IChunkBehavior::moveChunks(...) +// +// Purpose: Move a range of chunks from one container to another. +// +//----------------------------------------------------------------------------- + +void IChunkBehavior::moveChunks( IChunkContainer& srcTree, IChunkContainer& destTree, XMP_Uns32 start ) +{ + XMP_Validate( &srcTree != &destTree, "Source tree and destination tree shouldn't be the same", kXMPErr_InternalFailure ); + + XMP_Uns32 end = srcTree.numChildren(); + + for( XMP_Uns32 index=start; index<end; index++ ) + { + Chunk* chunk = srcTree.removeChildAt( start ); + destTree.appendChild( chunk, true ); + } +} + +//----------------------------------------------------------------------------- +// +// IChunkBehavior::isMovable(...) +// +// Purpose: May we move a chunk of passed id/type +// +//----------------------------------------------------------------------------- + +bool IChunkBehavior::isMovable( const Chunk& chunk ) const +{ + bool ret = false; + + if( !this->isFREEChunk( chunk ) && mMovablePaths != NULL ) + { + ChunkPath path( chunk.getIdentifier() ); + Chunk* parent = chunk.getParent(); + + while( parent != NULL && parent->getID() != kChunk_NONE ) + { + path.insert( parent->getIdentifier() ); + parent = parent->getParent(); + } + + for( std::vector<ChunkPath>::iterator iter=mMovablePaths->begin(); iter!=mMovablePaths->end() && !ret; iter++ ) + { + ret = ( iter->match( path ) == ChunkPath::kFullMatch ); + } + } + + return ret; +} diff --git a/XMPFiles/source/FormatSupport/IFF/IChunkBehavior.h b/XMPFiles/source/FormatSupport/IFF/IChunkBehavior.h new file mode 100644 index 0000000..418afb3 --- /dev/null +++ b/XMPFiles/source/FormatSupport/IFF/IChunkBehavior.h @@ -0,0 +1,239 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _IChunkBehavior_h_ +#define _IChunkBehavior_h_ + +#include "public/include/XMP_Environment.h" // ! This must be the first include. +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" +#include <vector> + +namespace IFF_RIFF +{ + +/** + The IChunkBehavior is an interface that provides access to algorithm + for the read and write process of IFF/RIFF formated streams. + A file format specific instance based on this interface gets injected into + the class ChunkController and offers format specific algorithm wherever + the processing of a certain file format differs from the general specification + of RIFF/IFF. + That is e.g. the RF64 format where it is possible that the size value of the + top level chunk doesn't represent the real size. Or AVI, where are special rules + if the size of a chunk exceed the 4GB border. +*/ + +class IChunkContainer; +class Chunk; +struct ChunkIdentifier; +class ChunkPath; + +class IChunkBehavior +{ +public: + IChunkBehavior() : mMovablePaths(NULL) {} + virtual ~IChunkBehavior() {}; + + /** + * Set list of chunk paths of chunks that might be moved within the hierarchy + */ + inline void setMovablePaths( std::vector<ChunkPath>* paths ) { mMovablePaths = paths; } + + /** + Validate the passed in size value, identify the valid size if the passed in isn't valid + and return the valid size. + throw an exception if the passed in size isn't valid and there's no way to identify a + valid size. + + @param size Size value + @param id Identifier of chunk + @param tree Chunk tree + @param stream Stream handle + + @return Valid size value. + */ + virtual XMP_Uns64 getRealSize( const XMP_Uns64 size, const ChunkIdentifier& id, IChunkContainer& tree, XMP_IO* stream ) = 0; + + /** + Return the maximum size of a single chunk, i.e. the maximum size of a top-level chunk. + + @return Maximum size + */ + virtual XMP_Uns64 getMaxChunkSize() const = 0; + + /** + Return true if the passed identifier is valid for top-level chunks of a certain format. + + @param id Chunk identifier + @param chunkNo order number of top-level chunk + @return true, if passed id is a valid top-level chunk + */ + virtual bool isValidTopLevelChunk( const ChunkIdentifier& id, XMP_Uns32 chunkNo ) = 0; + + /** + Fix the hierarchy of chunks depending ones based on size changes of one or more chunks + and second based on format specific rules. + Throw an exception if the hierarchy can't be fixed. + + @param tree Vector of root chunks. + */ + virtual void fixHierarchy( IChunkContainer& tree ) = 0; + + /** + Insert a new chunk into the hierarchy of chunks. The behavior needs to decide the position + of the new chunk and has to do the insertion. + + @param tree Chunk tree + @param chunk New chunk + */ + virtual void insertChunk( IChunkContainer& tree, Chunk& chunk ) = 0; + + /** + Remove the chunk described by the passed ChunkPath. + + @param tree Chunk tree + @param path Path to the chunk that needs to be removed + + @return true if the chunk was removed and need to be deleted + */ + virtual bool removeChunk( IChunkContainer& tree, Chunk& chunk ) = 0; + +protected: + /** + Create a FREE chunk. + If the chunkSize is smaller than the header+type - size then create an annotation chunk. + If the passed size is odd, then add a pad byte. + + @param chunkSize Total size including header + @return New FREE chunk + */ + virtual Chunk* createFREE( XMP_Uns64 chunkSize ) = 0; + + /** + Check if the passed chunk is a FREE chunk. + (Could be also a small annotation chunk with zero bytes in its data) + + @param chunk A chunk + + @return true if the passed chunk is a FREE chunk + */ + virtual XMP_Bool isFREEChunk( const Chunk& chunk ) const = 0; + + /** + Return the minimum size of a FREE chunk + */ + virtual XMP_Uns64 getMinFREESize( ) const + = 0; +protected: + /************************************************************************/ + /* END of Interface. The following are helper functions for all derived */ + /* Behavior Classes */ + /************************************************************************/ + + /** + Find a FREE chunk with the passed total size (including header). If the FREE chunk is found then + take care of the fact that is has to be that large (or larger) then the minimum size of a FREE chunk. + The method takes also into account that the passed size probably not includes a pad byte + + @param tree Parent tree + @param requiredSize Required total size (including header) + + @return Index of found FREE chunk + */ + XMP_Int32 findFREEChunk( const IChunkContainer& tree, XMP_Uns64 requiredSize ); + + /** + May we move a chunk of passed id/type + + @param identifier id and type of chunk + @return true if such a chunk might be moved within the tree + */ + bool isMovable( const Chunk& chunk ) const; + + /** + Validate recursively the offset values of all chunks. + Throws an exception if there is any discrepancy with the calculated offset. + + @param tree (Sub-)tree of chunks + @param startOffset First offset in the (sub-)tree + */ + void validateOffsets( IChunkContainer& tree, XMP_Uns64 startOffset = 0 ); + + /** + Retrieve the free space at the passed position in the child list of the parent tree. + If there's a FREE chunk then return it. + + @param outFreeBytes On return it takes the number of free bytes + @param tree Parent tree + @param index Position in the child list of the parent tree + + @return FREE chunk if available + */ + Chunk* getFreeSpace( XMP_Int64& outFreeBytes, const IChunkContainer& tree, XMP_Uns32 index ) const ; + + /** + Try to arrange all chunks of the source tree at their current location. + The method looks for FREE chunk around or for size changes of the chunks around and try that space. + If a chunk can't be arrange at its location it is moved to the end of the destination tree. + + @param srcTree Tree that consists of the chunks that needs to be arranged + @param destTree Tree where chunks are added to if they can't be arranged + + @return Index of last proceeded chunk + */ + void arrangeChunksInPlace( IChunkContainer& srcTree, IChunkContainer& destTree ); + + /** + This method proceeds the list of Chunks of the source tree in the passed range and looks for FREE chunks + in the destination tree to move the source chunks to. + Source tree and destination tree could be one and the same but it's not required. If both trees are the + same then it's not allowed to cross source and destination ranges. + + @param srcTree Tree that consists of the chunks that needs to be arranged + @param destTree Tree where the method looks for FREE chunks + @param srcStart Start index within the source tree + @param srcEnd End index within the source tree (if booth, srcStart and srcEnd are zero then the complete list + of the source tree is proceeded) + @param destStart Start index within the destination tree + @param destEnd End index within the destination tree (if booth, destStart and destEnd are zero then the complete list + of the destination tree is proceeded) + */ + void arrangeChunksInTree( IChunkContainer& srcTree, IChunkContainer& destTree ); + + /** + Try to merge existing FREE chunks at the passed position in the child list + of the passed parent tree. + The algorithm looks at the position, before the position and after the position. + + @param tree Parent tree + @param index Position in the child list of the parent tree + + @return FREE chunk if available at the passed position (in case of a merge + the merged FREE chunk) + */ + Chunk* mergeFreeChunks( IChunkContainer& tree, XMP_Uns32 index ); + + /** + Move a range of chunks from one container to another starting at the start index up to the + end of the srcTree. + + @param srcTree Source container + @param destTree Destination container + @param start Start index of source container + */ + void moveChunks( IChunkContainer& srcTree, IChunkContainer& destTree, XMP_Uns32 start ); + +private: + std::vector<ChunkPath>* mMovablePaths; +}; + +} // IChunkBehavior + +#endif diff --git a/XMPFiles/source/FormatSupport/IFF/IChunkContainer.h b/XMPFiles/source/FormatSupport/IFF/IChunkContainer.h new file mode 100644 index 0000000..04ad425 --- /dev/null +++ b/XMPFiles/source/FormatSupport/IFF/IChunkContainer.h @@ -0,0 +1,87 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _IChunkContainer_h_ +#define _IChunkContainer_h_ + +#include "public/include/XMP_Environment.h" // ! This must be the first include. + +namespace IFF_RIFF +{ + +/** + The interface IChunkContainer defines the access to child chunks of + an existing chunk. +*/ + +class Chunk; + +class IChunkContainer +{ +public: + virtual ~IChunkContainer() {}; + + /** + * @return Returns the number children chunks. + */ + virtual XMP_Uns32 numChildren() const = 0; + + /** + * Returns a child node. + * + * @param pos position of the child node to return + * @return Returns the child node at the given position. + */ + virtual Chunk* getChildAt( XMP_Uns32 pos ) const = 0; + + /** + * Appends a child node at the end of the children list. + * + * @param node the new node + * @param adjustSizes adjust size&offset of chunk and parents + * @return Returns the added node. + */ + virtual void appendChild( Chunk* node, XMP_Bool adjustSizes = true ) = 0; + + /** + * Inserts a child node at a certain position. + * + * @param pos position in the children list to add the new node + * @param node the new node + * @return Returns the added node. + */ + virtual void insertChildAt( XMP_Uns32 pos, Chunk* node ) = 0; + + /** + * Removes a child node at a given position. + * + * @param pos position of the node to delete in the children list + * + * @return The removed chunk + */ + virtual Chunk* removeChildAt( XMP_Uns32 pos ) = 0; + + /** + * Remove child at the passed position and insert the new chunk + * + * @param pos Position of chunk that will be replaced + * @param chunk New chunk + * + * @return Replaced chunk + */ + virtual Chunk* replaceChildAt( XMP_Uns32 pos, Chunk* node ) = 0; + + /** creates a string representation of the chunk and its children. + */ + virtual std::string toString( std::string tab = std::string() , XMP_Bool showOriginal = false ) = 0; +}; + +} + +#endif diff --git a/XMPFiles/source/FormatSupport/IFF/IChunkData.h b/XMPFiles/source/FormatSupport/IFF/IChunkData.h new file mode 100644 index 0000000..62674e6 --- /dev/null +++ b/XMPFiles/source/FormatSupport/IFF/IChunkData.h @@ -0,0 +1,108 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _IChunkData_h_ +#define _IChunkData_h_ + +#include "public/include/XMP_Environment.h" // ! This must be the first include. +#include "public/include/XMP_Const.h" +#include "source/Endian.h" +#include "XMPFiles/source/FormatSupport/IFF/ChunkPath.h" +#include <string> + +namespace IFF_RIFF +{ + +/** + * This interface allow access to only the data part of a chunk. + */ +class IChunkData +{ + public: + + virtual ~IChunkData() {}; + + /** + * Get the chunk ID + * + * @return Return the ID, 0 if the chunk does not have an ID. + */ + virtual XMP_Uns32 getID() const = 0; + + /** + * Get the chunk type (if available) + * (the first four data bytes of the chunk could be a chunk type) + * + * @return Return the type, kType_NONE if the chunk does not contain data. + */ + virtual XMP_Uns32 getType() const = 0; + + /** + * Get the chunk identifier [id and type] + * + * @return Return the identifier + */ + virtual const ChunkIdentifier& getIdentifier() const = 0; + + /** + * Access the data of the chunk. + * + * @param data OUT pointer to the byte array + * @return size of the data block, 0 if no data is available + */ + virtual XMP_Uns64 getData( const XMP_Uns8** data ) const = 0; + + /** + * Set new data for the chunk. + * Will delete an existing internal buffer and recreate a new one + * and copy the given data into that new buffer. + * + * @param data pointer to the data to put into the chunk + * @param size Size of the data block + */ + virtual void setData( const XMP_Uns8* const data, XMP_Uns64 size, XMP_Bool writeType = false ) = 0; + + /** + * Returns the current size of the Chunk without pad byte. + * + * @param includeHeader if set, the returned size will be the whole chunk size including the header + * @return either size of the data block of the chunk or size of the whole chunk + */ + virtual XMP_Uns64 getSize( bool includeHeader = false ) const = 0; + + + /* The following methods are getter/setter for certain data types. + They always take care of little-endian/big-endian issues. + The offset starts at the data area of the Chunk. */ + + virtual XMP_Uns32 getUns32( XMP_Uns64 offset=0 ) const = 0; + virtual void setUns32( XMP_Uns32 value, XMP_Uns64 offset=0 ) = 0; + + virtual XMP_Uns64 getUns64( XMP_Uns64 offset=0 ) const = 0; + virtual void setUns64( XMP_Uns64 value, XMP_Uns64 offset=0 ) = 0; + + virtual XMP_Int32 getInt32( XMP_Uns64 offset=0 ) const = 0; + virtual void setInt32( XMP_Int32 value, XMP_Uns64 offset=0 ) = 0; + + virtual XMP_Int64 getInt64( XMP_Uns64 offset=0 ) const = 0; + virtual void setInt64( XMP_Int64 value, XMP_Uns64 offset=0 ) = 0; + + virtual std::string getString( XMP_Uns64 size = 0, XMP_Uns64 offset=0 ) const = 0; + virtual void setString( std::string value, XMP_Uns64 offset=0 ) = 0; + + /** + * Creates a string representation of the chunk and its children. + */ + virtual std::string toString( std::string tabs = std::string() , XMP_Bool showOriginal = false ) = 0; + +}; // IChunkData + +} // namespace + +#endif diff --git a/XMPFiles/source/FormatSupport/IPTC_Support.cpp b/XMPFiles/source/FormatSupport/IPTC_Support.cpp new file mode 100644 index 0000000..e8fda45 --- /dev/null +++ b/XMPFiles/source/FormatSupport/IPTC_Support.cpp @@ -0,0 +1,758 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/IPTC_Support.hpp" +#include "source/EndianUtils.hpp" +#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp" +#include "source/XIO.hpp" + +// ================================================================================================= +/// \file IPTC_Support.cpp +/// \brief XMPFiles support for IPTC (IIM) DataSets. +/// +// ================================================================================================= + +enum { kUTF8_IncomingMode = 0, kUTF8_LosslessMode = 1, kUTF8_AlwaysMode = 2 }; +#ifndef kUTF8_Mode + #define kUTF8_Mode kUTF8_AlwaysMode +#endif + +const DataSetCharacteristics kKnownDataSets[] = + { { kIPTC_ObjectType, kIPTC_UnmappedText, 67, "", "" }, // Not mapped to XMP. + { kIPTC_IntellectualGenre, kIPTC_MapSpecial, 68, kXMP_NS_IPTCCore, "IntellectualGenre" }, // Only the name part is in the XMP. + { kIPTC_Title, kIPTC_MapLangAlt, 64, kXMP_NS_DC, "title" }, + { kIPTC_EditStatus, kIPTC_UnmappedText, 64, "", "" }, // Not mapped to XMP. + { kIPTC_EditorialUpdate, kIPTC_UnmappedText, 2, "", "" }, // Not mapped to XMP. + { kIPTC_Urgency, kIPTC_MapSimple, 1, kXMP_NS_Photoshop, "Urgency" }, + { kIPTC_SubjectCode, kIPTC_MapSpecial, 236, kXMP_NS_IPTCCore, "SubjectCode" }, // Only the reference number is in the XMP. + { kIPTC_Category, kIPTC_MapSimple, 3, kXMP_NS_Photoshop, "Category" }, + { kIPTC_SuppCategory, kIPTC_MapArray, 32, kXMP_NS_Photoshop, "SupplementalCategories" }, + { kIPTC_FixtureIdentifier, kIPTC_UnmappedText, 32, "", "" }, // Not mapped to XMP. + { kIPTC_Keyword, kIPTC_MapArray, 64, kXMP_NS_DC, "subject" }, + { kIPTC_ContentLocCode, kIPTC_UnmappedText, 3, "", "" }, // Not mapped to XMP. + { kIPTC_ContentLocName, kIPTC_UnmappedText, 64, "", "" }, // Not mapped to XMP. + { kIPTC_ReleaseDate, kIPTC_UnmappedText, 8, "", "" }, // Not mapped to XMP. + { kIPTC_ReleaseTime, kIPTC_UnmappedText, 11, "", "" }, // Not mapped to XMP. + { kIPTC_ExpDate, kIPTC_UnmappedText, 8, "", "" }, // Not mapped to XMP. + { kIPTC_ExpTime, kIPTC_UnmappedText, 11, "", "" }, // Not mapped to XMP. + { kIPTC_Instructions, kIPTC_MapSimple, 256, kXMP_NS_Photoshop, "Instructions" }, + { kIPTC_ActionAdvised, kIPTC_UnmappedText, 2, "", "" }, // Not mapped to XMP. + { kIPTC_RefService, kIPTC_UnmappedText, 10, "", "" }, // Not mapped to XMP. ! Interleave 2:45, 2:47, 2:50! + { kIPTC_RefDate, kIPTC_UnmappedText, 8, "", "" }, // Not mapped to XMP. ! Interleave 2:45, 2:47, 2:50! + { kIPTC_RefNumber, kIPTC_UnmappedText, 8, "", "" }, // Not mapped to XMP. ! Interleave 2:45, 2:47, 2:50! + { kIPTC_DateCreated, kIPTC_MapSpecial, 8, kXMP_NS_Photoshop, "DateCreated" }, // ! Reformatted date. Combined with 2:60, TimeCreated. + { kIPTC_TimeCreated, kIPTC_UnmappedText, 11, "", "" }, // ! Combined with 2:55, DateCreated. + { kIPTC_DigitalCreateDate, kIPTC_Map3Way, 8, "", "" }, // ! 3 way Exif-IPTC-XMP date/time set. Combined with 2:63, DigitalCreateTime. + { kIPTC_DigitalCreateTime, kIPTC_UnmappedText, 11, "", "" }, // ! Combined with 2:62, DigitalCreateDate. + { kIPTC_OriginProgram, kIPTC_UnmappedText, 32, "", "" }, // Not mapped to XMP. + { kIPTC_ProgramVersion, kIPTC_UnmappedText, 10, "", "" }, // Not mapped to XMP. + { kIPTC_ObjectCycle, kIPTC_UnmappedText, 1, "", "" }, // Not mapped to XMP. + { kIPTC_Creator, kIPTC_Map3Way, 32, "", "" }, // ! In the 3 way Exif-IPTC-XMP set. + { kIPTC_CreatorJobtitle, kIPTC_MapSimple, 32, kXMP_NS_Photoshop, "AuthorsPosition" }, + { kIPTC_City, kIPTC_MapSimple, 32, kXMP_NS_Photoshop, "City" }, + { kIPTC_Location, kIPTC_MapSimple, 32, kXMP_NS_IPTCCore, "Location" }, + { kIPTC_State, kIPTC_MapSimple, 32, kXMP_NS_Photoshop, "State" }, + { kIPTC_CountryCode, kIPTC_MapSimple, 3, kXMP_NS_IPTCCore, "CountryCode" }, + { kIPTC_Country, kIPTC_MapSimple, 64, kXMP_NS_Photoshop, "Country" }, + { kIPTC_JobID, kIPTC_MapSimple, 32, kXMP_NS_Photoshop, "TransmissionReference" }, + { kIPTC_Headline, kIPTC_MapSimple, 256, kXMP_NS_Photoshop, "Headline" }, + { kIPTC_Provider, kIPTC_MapSimple, 32, kXMP_NS_Photoshop, "Credit" }, + { kIPTC_Source, kIPTC_MapSimple, 32, kXMP_NS_Photoshop, "Source" }, + { kIPTC_CopyrightNotice, kIPTC_Map3Way, 128, "", "" }, // ! In the 3 way Exif-IPTC-XMP set. + { kIPTC_Contact, kIPTC_UnmappedText, 128, "", "" }, // Not mapped to XMP. + { kIPTC_Description, kIPTC_Map3Way, 2000, "", "" }, // ! In the 3 way Exif-IPTC-XMP set. + { kIPTC_DescriptionWriter, kIPTC_MapSimple, 32, kXMP_NS_Photoshop, "CaptionWriter" }, + { kIPTC_RasterizedCaption, kIPTC_UnmappedBin, 7360, "", "" }, // Not mapped to XMP. ! Binary data! + { kIPTC_ImageType, kIPTC_UnmappedText, 2, "", "" }, // Not mapped to XMP. + { kIPTC_ImageOrientation, kIPTC_UnmappedText, 1, "", "" }, // Not mapped to XMP. + { kIPTC_LanguageID, kIPTC_UnmappedText, 3, "", "" }, // Not mapped to XMP. + { kIPTC_AudioType, kIPTC_UnmappedText, 2, "", "" }, // Not mapped to XMP. + { kIPTC_AudioSampleRate, kIPTC_UnmappedText, 6, "", "" }, // Not mapped to XMP. + { kIPTC_AudioSampleRes, kIPTC_UnmappedText, 2, "", "" }, // Not mapped to XMP. + { kIPTC_AudioDuration, kIPTC_UnmappedText, 6, "", "" }, // Not mapped to XMP. + { kIPTC_AudioOutcue, kIPTC_UnmappedText, 64, "", "" }, // Not mapped to XMP. + { kIPTC_PreviewFormat, kIPTC_UnmappedBin, 2, "", "" }, // Not mapped to XMP. ! Binary data! + { kIPTC_PreviewFormatVers, kIPTC_UnmappedBin, 2, "", "" }, // Not mapped to XMP. ! Binary data! + { kIPTC_PreviewData, kIPTC_UnmappedBin, 256000, "", "" }, // Not mapped to XMP. ! Binary data! + { 255, kIPTC_MapSpecial, 0, 0, 0 } }; // ! Must be last as a sentinel. + +// A combination of the IPTC "Subject Reference System Guidelines" and IIMv4.1 Appendix G. +const IntellectualGenreMapping kIntellectualGenreMappings[] = +{ { "001", "Current" }, + { "002", "Analysis" }, + { "003", "Archive material" }, + { "004", "Background" }, + { "005", "Feature" }, + { "006", "Forecast" }, + { "007", "History" }, + { "008", "Obituary" }, + { "009", "Opinion" }, + { "010", "Polls and surveys" }, + { "010", "Polls & Surveys" }, + { "011", "Profile" }, + { "012", "Results listings and statistics" }, + { "012", "Results Listings & Tables" }, + { "013", "Side bar and supporting information" }, + { "013", "Side bar & Supporting information" }, + { "014", "Summary" }, + { "015", "Transcript and verbatim" }, + { "015", "Transcript & Verbatim" }, + { "016", "Interview" }, + { "017", "From the scene" }, + { "017", "From the Scene" }, + { "018", "Retrospective" }, + { "019", "Synopsis" }, + { "019", "Statistics" }, + { "020", "Update" }, + { "021", "Wrapup" }, + { "021", "Wrap-up" }, + { "022", "Press release" }, + { "022", "Press Release" }, + { "023", "Quote" }, + { "024", "Press-digest" }, + { "025", "Review" }, + { "026", "Curtain raiser" }, + { "027", "Actuality" }, + { "028", "Question and answer" }, + { "029", "Music" }, + { "030", "Response to a question" }, + { "031", "Raw sound" }, + { "032", "Scener" }, + { "033", "Text only" }, + { "034", "Voicer" }, + { "035", "Fixture" }, + { 0, 0 } }; // ! Must be last as a sentinel. + +// ================================================================================================= +// FindKnownDataSet +// ================ + +static const DataSetCharacteristics* FindKnownDataSet ( XMP_Uns8 dsNum ) +{ + size_t i = 0; + + while ( kKnownDataSets[i].dsNum < dsNum ) ++i; // The list is short enough for a linear search. + + if ( kKnownDataSets[i].dsNum != dsNum ) return 0; + return &kKnownDataSets[i]; + +} // FindKnownDataSet + +// ================================================================================================= +// IPTC_Manager::ParseMemoryDataSets +// ================================= +// +// Parse the IIM block. All datasets are put into the map, although we only really care about 1:90 +// and the known 2:xx ones. This approach is tolerant of ill-formed IIM where the datasets are not +// sorted by ascending record number. + +void IPTC_Manager::ParseMemoryDataSets ( const void* data, XMP_Uns32 length, bool copyData /* = true */ ) +{ + // Get rid of any existing data. + + DataSetMap::iterator dsPos = this->dataSets.begin(); + DataSetMap::iterator dsEnd = this->dataSets.end(); + + for ( ; dsPos != dsEnd; ++dsPos ) this->DisposeLooseValue ( dsPos->second ); + + this->dataSets.clear(); + + if ( this->ownedContent ) free ( this->iptcContent ); + this->ownedContent = false; // Set to true later if the content is copied. + this->iptcContent = 0; + this->iptcLength = 0; + + this->changed = false; + + if ( length == 0 ) return; + if ( (data == 0) || (*((XMP_Uns8*)data) != 0x1C) ) XMP_Throw ( "Not valid IPTC, no leading 0x1C", kXMPErr_BadIPTC ); + + // Allocate space for the full in-memory data and copy it. + + if ( length > 10*1024*1024 ) XMP_Throw ( "Outrageous length for memory-based IPTC", kXMPErr_BadIPTC ); + this->iptcLength = length; + + if ( ! copyData ) { + this->iptcContent = (XMP_Uns8*)data; + } else { + this->iptcContent = (XMP_Uns8*) malloc(length); + if ( this->iptcContent == 0 ) XMP_Throw ( "Out of memory", kXMPErr_NoMemory ); + memcpy ( this->iptcContent, data, length ); // AUDIT: Safe, malloc'ed length bytes above. + this->ownedContent = true; + } + + // Build the map of the DataSets. The records should be in ascending order, but we tolerate out + // of order IIM produced by some unknown apps. The DataSets in a record can be in any order. + // There are no record markers, just DataSets, so the ordering is really just clumping of + // DataSets by record. A normal DataSet has a 5 byte header followed by the value. An extended + // DataSet has a special length in the header, a variable sized value length, and the value. + // + // Normal DataSet + // 0 uint8 0x1C + // 1 uint8 record number + // 2 uint8 DataSet number + // 3 uint16 big endian value size, 0..32767, larger means extended DataSet + // + // In an extended DataSet the extended length size is the low 15 bits of the standard size. The + // extended length follows as a big endian unsigned number. The IPTC does not specify, but we + // require the extended length size to be in the range 1..4. It should only be 3 or 4, we allow + // the degenerate cases. + + XMP_Uns8* iptcPtr = this->iptcContent; + XMP_Uns8* iptcEnd = iptcPtr + length; + XMP_Uns8* iptcLimit = iptcEnd - kMinDataSetSize; + XMP_Uns32 dsLen; // ! The large form can have values up to 4GB in length. + + this->utf8Encoding = false; + + for ( ; iptcPtr <= iptcLimit; iptcPtr += dsLen ) { + + // iptcLimit - last possible DataSet, 5 bytes before IIM block end + + // iptcPtr - working pointer to the current byte of interest + // dsPtr - pointer to the current DataSet's header + // dsLen - value length, does not include extended size bytes + + XMP_Uns8* dsPtr = iptcPtr; + XMP_Uns8 oneC = *iptcPtr; + XMP_Uns8 recNum = *(iptcPtr+1); + XMP_Uns8 dsNum = *(iptcPtr+2); + + if ( oneC != 0x1C ) break; // No more DataSets. + + dsLen = GetUns16BE ( iptcPtr+3 ); // ! Compute dsLen before any "continue", needed for loop increment! + iptcPtr += 5; // Advance to the data (or extended length). + + if ( (dsLen & 0x8000) != 0 ) { + XMP_Assert ( dsLen <= 0xFFFF ); + XMP_Uns32 lenLen = dsLen & 0x7FFF; + if ( (lenLen == 0) || (lenLen > 4) ) break; // Bad DataSet, can't find the next so quit. + if ( iptcPtr > (iptcEnd - lenLen) ) break; // Bad final DataSet. Throw instead? + dsLen = 0; + for ( XMP_Uns16 i = 0; i < lenLen; ++i, ++iptcPtr ) { + dsLen = (dsLen << 8) + *iptcPtr; + } + } + + if ( iptcPtr > (iptcEnd - dsLen) ) break; // Bad final DataSet. Throw instead? + + // Make a special check for 1:90 denoting UTF-8 text. + if ( (recNum == 1) && (dsNum == 90) ) { + if ( (dsLen == 3) && (memcmp ( iptcPtr, "\x1B\x25\x47", 3 ) == 0) ) this->utf8Encoding = true; + } + + XMP_Uns16 mapID = recNum*1000 + dsNum; + DataSetInfo dsInfo ( recNum, dsNum, dsLen, iptcPtr ); + DataSetMap::iterator dsPos = this->dataSets.find ( mapID ); + + bool repeatable = false; + + const DataSetCharacteristics* knownDS = FindKnownDataSet ( dsNum ); + + if ( (knownDS == 0) || (knownDS->mapForm == kIPTC_MapArray) ) { + repeatable = true; // Allow repeats for unknown DataSets. + } else if ( (dsNum == kIPTC_Creator) || (dsNum == kIPTC_SubjectCode) ) { + repeatable = true; + } + + if ( repeatable || (dsPos == this->dataSets.end()) ) { + DataSetMap::value_type mapValue ( mapID, dsInfo ); + (void) this->dataSets.insert ( this->dataSets.upper_bound ( mapID ), mapValue ); + } else { + this->DisposeLooseValue ( dsPos->second ); + dsPos->second = dsInfo; // Keep the last copy of illegal repeats. + } + + } + +} // IPTC_Manager::ParseMemoryDataSets + +// ================================================================================================= +// IPTC_Manager::GetDataSet +// ======================== + +size_t IPTC_Manager::GetDataSet ( XMP_Uns8 dsNum, DataSetInfo* info, size_t which /* = 0 */ ) const +{ + + XMP_Uns16 mapID = 2000 + dsNum; // ! Only deal with 2:xx datasets. + DataSetMap::const_iterator dsPos = this->dataSets.lower_bound ( mapID ); + if ( (dsPos == this->dataSets.end()) || (dsPos->second.recNum != 2) || (dsNum != dsPos->second.dsNum) ) return 0; + + size_t dsCount = this->dataSets.count ( mapID ); + if ( which >= dsCount ) return 0; // Valid range for which is 0 .. count-1. + + if ( info != 0 ) { + for ( size_t i = 0; i < which; ++i ) ++dsPos; // Can't do "dsPos += which", no iter+int operator. + *info = dsPos->second; + } + + return dsCount; + +} // IPTC_Manager::GetDataSet + +// ================================================================================================= +// IPTC_Manager::GetDataSet_UTF8 +// ============================= + +size_t IPTC_Manager::GetDataSet_UTF8 ( XMP_Uns8 dsNum, std::string * utf8Str, size_t which /* = 0 */ ) const +{ + if ( utf8Str != 0 ) utf8Str->erase(); + + DataSetInfo dsInfo; + size_t dsCount = GetDataSet ( dsNum, &dsInfo, which ); + if ( dsCount == 0 ) return 0; + + if ( utf8Str != 0 ) { + if ( this->utf8Encoding ) { + utf8Str->assign ( (char*)dsInfo.dataPtr, dsInfo.dataLen ); + } else if ( ! ignoreLocalText ) { + ReconcileUtils::LocalToUTF8 ( dsInfo.dataPtr, dsInfo.dataLen, utf8Str ); + } else if ( ReconcileUtils::IsASCII ( dsInfo.dataPtr, dsInfo.dataLen ) ) { + utf8Str->assign ( (char*)dsInfo.dataPtr, dsInfo.dataLen ); + } + } + + return dsCount; + +} // IPTC_Manager::GetDataSet_UTF8 + +// ================================================================================================= +// IPTC_Manager::DisposeLooseValue +// =============================== +// +// Dispose of loose values from SetDataSet calls after the last UpdateMemoryDataSets. + +// ! Don't try to make the DataSetInfo struct be self-cleaning. It is a primary public type, returned +// ! from GetDataSet. Making it self-cleaning would get into nasty assignment and pointer ownership +// ! issues, far worse than doing this explicit cleanup. + +void IPTC_Manager::DisposeLooseValue ( DataSetInfo & dsInfo ) +{ + if ( dsInfo.dataLen == 0 ) return; + + XMP_Uns8* dataBegin = this->iptcContent; + XMP_Uns8* dataEnd = dataBegin + this->iptcLength; + + if ( ((XMP_Uns8*)dsInfo.dataPtr < dataBegin) || ((XMP_Uns8*)dsInfo.dataPtr >= dataEnd) ) { + free ( (void*) dsInfo.dataPtr ); + dsInfo.dataPtr = 0; + } + +} // IPTC_Manager::DisposeLooseValue + +// ================================================================================================= +// IPTC_Manager::AppendDataSet +// +// ! Calling instance must make sure that dsPtr is large enough to hold data from dsInfo ! +// =========================== + +XMP_Uns8* IPTC_Manager::AppendDataSet ( XMP_Uns8* dsPtr, const DataSetInfo & dsInfo ) { + + dsPtr[0] = 0x1C; + dsPtr[1] = dsInfo.recNum; + dsPtr[2] = dsInfo.dsNum; + dsPtr += 3; + + XMP_Uns32 dsLen = dsInfo.dataLen; + if ( dsLen <= 0x7FFF ) { + PutUns16BE ( (XMP_Uns16)dsLen, dsPtr ); + dsPtr += 2; + } else { + PutUns16BE ( 0x8004, dsPtr ); + PutUns32BE ( dsLen, dsPtr+2 ); + dsPtr += 6; + } + // AUDIT: Calling instance must make sure that dsPtr is large enough. + // Currently this function is only called from UpdateMemoryDataSets where the size of dsPtr + // is calculated including all data sets in the list, so the dsInfo data should always fit. + memcpy ( dsPtr, dsInfo.dataPtr, dsLen ); + dsPtr += dsLen; + + return dsPtr; + +} // IPTC_Manager::AppendDataSet + +// ================================================================================================= +// ================================================================================================= + +// ================================================================================================= +// IPTC_Writer::~IPTC_Writer +// ========================= +// +// Dispose of loose values from SetDataSet calls after the last UpdateMemoryDataSets. + +IPTC_Writer::~IPTC_Writer() +{ + DataSetMap::iterator dsPos = this->dataSets.begin(); + DataSetMap::iterator dsEnd = this->dataSets.end(); + + for ( ; dsPos != dsEnd; ++dsPos ) this->DisposeLooseValue ( dsPos->second ); + +} // IPTC_Writer::~IPTC_Writer + +// ================================================================================================= +// IPTC_Writer::SetDataSet_UTF8 +// ============================ + +void IPTC_Writer::SetDataSet_UTF8 ( XMP_Uns8 dsNum, const void* utf8Ptr, XMP_Uns32 utf8Len, long which /* = -1 */ ) +{ + const DataSetCharacteristics* knownDS = FindKnownDataSet ( dsNum ); + if ( knownDS == 0 ) XMP_Throw ( "Can only set known IPTC DataSets", kXMPErr_InternalFailure ); + + // Decide which character encoding to use and get a temporary pointer to the value. + + XMP_Uns8 * tempPtr; + XMP_Uns32 dataLen; + std::string localStr; + + if ( kUTF8_Mode == kUTF8_AlwaysMode ) { + + // Always use UTF-8. + if ( ! this->utf8Encoding ) this->ConvertToUTF8(); + tempPtr = (XMP_Uns8*) utf8Ptr; + dataLen = utf8Len; + + } else if ( kUTF8_Mode == kUTF8_IncomingMode ) { + + // Only use UTF-8 if that was what the parsed block used. + if ( this->utf8Encoding ) { + tempPtr = (XMP_Uns8*) utf8Ptr; + dataLen = utf8Len; + } else if ( ! ignoreLocalText ) { + ReconcileUtils::UTF8ToLocal ( utf8Ptr, utf8Len, &localStr ); + tempPtr = (XMP_Uns8*) localStr.data(); + dataLen = (XMP_Uns32) localStr.size(); + } else { + if ( ! ReconcileUtils::IsASCII ( utf8Ptr, utf8Len ) ) return; + tempPtr = (XMP_Uns8*) utf8Ptr; + dataLen = utf8Len; + } + + } else if ( kUTF8_Mode == kUTF8_LosslessMode ) { + + // Convert to UTF-8 if needed to prevent round trip loss. + if ( this->utf8Encoding ) { + tempPtr = (XMP_Uns8*) utf8Ptr; + dataLen = utf8Len; + } else if ( ! ignoreLocalText ) { + std::string rtStr; + ReconcileUtils::UTF8ToLocal ( utf8Ptr, utf8Len, &localStr ); + ReconcileUtils::LocalToUTF8 ( localStr.data(), localStr.size(), &rtStr ); + if ( (rtStr.size() == utf8Len) && (memcmp ( rtStr.data(), utf8Ptr, utf8Len ) == 0) ) { + tempPtr = (XMP_Uns8*) localStr.data(); // No loss, keep local encoding. + dataLen = (XMP_Uns32) localStr.size(); + } else { + this->ConvertToUTF8(); // Had loss, change everything to UTF-8. + XMP_Assert ( this->utf8Encoding ); + tempPtr = (XMP_Uns8*) utf8Ptr; + dataLen = utf8Len; + } + } else { + if ( ! ReconcileUtils::IsASCII ( utf8Ptr, utf8Len ) ) return; + tempPtr = (XMP_Uns8*) utf8Ptr; + dataLen = utf8Len; + } + + } + + // Set the value for this DataSet, making a non-transient copy of the value. Respect UTF-8 character + // boundaries when truncating. This is easy to check. If the first truncated byte has 10 in the + // high order 2 bits then we are in the middle of a UTF-8 multi-byte character. + // Back up to just before a byte with 11 in the high order 2 bits. + + if ( dataLen > knownDS->maxLen ) { + dataLen = (XMP_Uns32)knownDS->maxLen; + if ( this->utf8Encoding && ((tempPtr[dataLen] >> 6) == 2) ) { + for ( ; (dataLen > 0) && ((tempPtr[dataLen] >> 6) != 3); --dataLen ) {} + } + } + + XMP_Uns16 mapID = 2000 + dsNum; // ! Only deal with 2:xx datasets. + DataSetMap::iterator dsPos = this->dataSets.find ( mapID ); + long currCount = (long) this->dataSets.count ( mapID ); + + bool repeatable = false; + + if ( knownDS->mapForm == kIPTC_MapArray ) { + repeatable = true; + } else if ( (dsNum == kIPTC_Creator) || (dsNum == kIPTC_SubjectCode) ) { + repeatable = true; + } + + if ( ! repeatable ) { + + if ( which > 0 ) XMP_Throw ( "Non-repeatable IPTC DataSet", kXMPErr_BadParam ); + + } else { + + if ( which < 0 ) which = currCount; // The default is to append. + + if ( which > currCount ) { + XMP_Throw ( "Invalid index for IPTC DataSet", kXMPErr_BadParam ); + } else if ( which == currCount ) { + dsPos = this->dataSets.end(); // To make later checks do the right thing. + } else { + dsPos = this->dataSets.lower_bound ( mapID ); + for ( ; which > 0; --which ) ++dsPos; + } + + } + + if ( dsPos != this->dataSets.end() ) { + if ( (dsPos->second.dataLen == dataLen) && (memcmp ( dsPos->second.dataPtr, tempPtr, dataLen ) == 0) ) { + return; // ! New value matches the old, don't update. + } + } + + XMP_Uns8 * dataPtr = (XMP_Uns8*) malloc ( dataLen ); + if ( dataPtr == 0 ) XMP_Throw ( "Out of memory", kXMPErr_NoMemory ); + memcpy ( dataPtr, tempPtr, dataLen ); // AUDIT: Safe, malloc'ed dataLen bytes above. + + DataSetInfo dsInfo ( 2, dsNum, dataLen, dataPtr ); + + if ( dsPos != this->dataSets.end() ) { + this->DisposeLooseValue ( dsPos->second ); + dsPos->second = dsInfo; + } else { + DataSetMap::value_type mapValue ( mapID, dsInfo ); + (void) this->dataSets.insert ( this->dataSets.upper_bound ( mapID ), mapValue ); + } + + this->changed = true; + +} // IPTC_Writer::SetDataSet_UTF8 + +// ================================================================================================= +// IPTC_Writer::DeleteDataSet +// ========================== + +void IPTC_Writer::DeleteDataSet ( XMP_Uns8 dsNum, long which /* = -1 */ ) +{ + XMP_Uns16 mapID = 2000 + dsNum; // ! Only deal with 2:xx datasets. + DataSetMap::iterator dsBegin = this->dataSets.lower_bound ( mapID ); // Set for which == -1. + DataSetMap::iterator dsEnd = this->dataSets.upper_bound ( mapID ); + + if ( dsBegin == dsEnd ) return; // Nothing to delete. + + if ( which >= 0 ) { + long currCount = (long) this->dataSets.count ( mapID ); + if ( which >= currCount ) return; // Nothing to delete. + for ( ; which > 0; --which ) ++dsBegin; + dsEnd = dsBegin; ++dsEnd; // ! Can't do "dsEnd = dsBegin+1"! + } + + for ( DataSetMap::iterator dsPos = dsBegin; dsPos != dsEnd; ++dsPos ) { + this->DisposeLooseValue ( dsPos->second ); + } + + this->dataSets.erase ( dsBegin, dsEnd ); + this->changed = true; + +} // IPTC_Writer::DeleteDataSet + +// ================================================================================================= +// IPTC_Writer::UpdateMemoryDataSets +// ================================= +// +// Reconstruct the entire IIM block. This does not include any final alignment padding, that is an +// artifact of some specific wrappers such as Photoshop image resources. + +void IPTC_Writer::UpdateMemoryDataSets() +{ + if ( ! this->changed ) return; + + DataSetMap::iterator dsPos; + DataSetMap::iterator dsEnd = this->dataSets.end(); + + if ( kUTF8_Mode == kUTF8_LosslessMode ) { + if ( this->utf8Encoding ) { + if ( ! this->CheckRoundTripLoss() ) this->ConvertToLocal(); + } else { + if ( this->CheckRoundTripLoss() ) this->ConvertToUTF8(); + } + } + + // Compute the length of the new IIM block. All DataSets other than 1:90 and 2:xx are preserved + // as-is. If local text is used then 1:90 is omitted, if UTF-8 text is used then 1:90 is written + // to say so. The map key of (record*1000 + dataset) provides the desired overall order. + + XMP_Uns32 newLength = (5+2); // We always write 2:00 for the IIM version. + if ( this->utf8Encoding ) newLength += (5+3); // For 1:90, if written. + + for ( dsPos = this->dataSets.begin(); dsPos != dsEnd; ++dsPos ) { // Accumulate the other sizes. + const XMP_Uns16 mapID = dsPos->first; + if ( (mapID == 1090) || (mapID == 2000) ) continue; // Already dealt with 1:90 and 2:00. + XMP_Uns32 dsLen = dsPos->second.dataLen; + newLength += (5 + dsLen); + if ( dsLen > 0x7FFF ) newLength += 4; // We always use a 4 byte extended length for big values. + } + + // Allocate the new IIM block. + + XMP_Uns8* newContent = (XMP_Uns8*) malloc ( newLength ); + if ( newContent == 0 ) XMP_Throw ( "Out of memory", kXMPErr_NoMemory ); + + XMP_Uns8* dsPtr = newContent; + + // Write the record 0 DataSets. There should not be any, but let's be safe. + + for ( dsPos = this->dataSets.begin(); dsPos != dsEnd; ++dsPos ) { + const DataSetInfo & currDS = dsPos->second; + if ( currDS.recNum > 0 ) break; + dsPtr = AppendDataSet ( dsPtr, currDS ); + } + + // Write 1:90 then any other record 1 DataSets. + + if ( this->utf8Encoding ) { // Write 1:90 only if text is UTF-8. + memcpy ( dsPtr, "\x1C\x01\x5A\x00\x03\x1B\x25\x47", (5+3) ); // AUDIT: Within range of allocation. + dsPtr += (5+3); + } + + for ( ; dsPos != dsEnd; ++dsPos ) { + const DataSetInfo & currDS = dsPos->second; + if ( currDS.recNum > 1 ) break; + XMP_Assert ( currDS.recNum == 1 ); + if ( currDS.dsNum == 90 ) continue; + dsPtr = AppendDataSet ( dsPtr, currDS ); + } + + // Write 2:00 then all of the other DataSets from all records. + + if ( this->utf8Encoding ) { + // Start with 2:00 for version 4. + memcpy ( dsPtr, "\x1C\x02\x00\x00\x02\x00\x04", (5+2) ); // AUDIT: Within range of allocation. + dsPtr += (5+2); + } else { + // Start with 2:00 for version 2. + // *** We should probably write version 4 all the time. This is a late CS3 change, don't want + // *** to risk breaking other apps that might be strict about version checking. + memcpy ( dsPtr, "\x1C\x02\x00\x00\x02\x00\x02", (5+2) ); // AUDIT: Within range of allocation. + dsPtr += (5+2); + } + + for ( ; dsPos != dsEnd; ++dsPos ) { + const DataSetInfo & currDS = dsPos->second; + XMP_Assert ( currDS.recNum > 1 ); + if ( dsPos->first == 2000 ) continue; // Check both the record number and DataSet number. + dsPtr = AppendDataSet ( dsPtr, currDS ); + } + + XMP_Assert ( dsPtr == (newContent + newLength) ); + + // Parse the new block, it is the best way to reset internal info and rebuild the map. + + this->ParseMemoryDataSets ( newContent, newLength, false ); // Don't make another copy of the content. + XMP_Assert ( this->iptcLength == newLength ); + this->ownedContent = (newLength > 0); // We really do own the new content, if not empty. + +} // IPTC_Writer::UpdateMemoryDataSets + +// ================================================================================================= +// IPTC_Writer::ConvertToUTF8 +// ========================== +// +// Convert the values of existing text DataSets to UTF-8. For now we only accept text DataSets. + +void IPTC_Writer::ConvertToUTF8() +{ + XMP_Assert ( ! this->utf8Encoding ); + std::string utf8Str; + + DataSetMap::iterator dsPos = this->dataSets.begin(); + DataSetMap::iterator dsEnd = this->dataSets.end(); + + for ( ; dsPos != dsEnd; ++dsPos ) { + + DataSetInfo & dsInfo = dsPos->second; + + ReconcileUtils::LocalToUTF8 ( dsInfo.dataPtr, dsInfo.dataLen, &utf8Str ); + this->DisposeLooseValue ( dsInfo ); + + dsInfo.dataLen = (XMP_Uns32)utf8Str.size(); + dsInfo.dataPtr = (XMP_Uns8*) malloc ( dsInfo.dataLen ); + if ( dsInfo.dataPtr == 0 ) XMP_Throw ( "Out of memory", kXMPErr_NoMemory ); + memcpy ( dsInfo.dataPtr, utf8Str.data(), dsInfo.dataLen ); // AUDIT: Safe, malloc'ed dataLen bytes above. + + } + + this->utf8Encoding = true; + +} // IPTC_Writer::ConvertToUTF8 + +// ================================================================================================= +// IPTC_Writer::ConvertToLocal +// =========================== +// +// Convert the values of existing text DataSets to local. For now we only accept text DataSets. + +void IPTC_Writer::ConvertToLocal() +{ + XMP_Assert ( this->utf8Encoding ); + std::string localStr; + + DataSetMap::iterator dsPos = this->dataSets.begin(); + DataSetMap::iterator dsEnd = this->dataSets.end(); + + for ( ; dsPos != dsEnd; ++dsPos ) { + + DataSetInfo & dsInfo = dsPos->second; + + ReconcileUtils::UTF8ToLocal ( dsInfo.dataPtr, dsInfo.dataLen, &localStr ); + this->DisposeLooseValue ( dsInfo ); + + dsInfo.dataLen = (XMP_Uns32)localStr.size(); + dsInfo.dataPtr = (XMP_Uns8*) malloc ( dsInfo.dataLen ); + if ( dsInfo.dataPtr == 0 ) XMP_Throw ( "Out of memory", kXMPErr_NoMemory ); + memcpy ( dsInfo.dataPtr, localStr.data(), dsInfo.dataLen ); // AUDIT: Safe, malloc'ed dataLen bytes above. + + } + + this->utf8Encoding = false; + +} // IPTC_Writer::ConvertToLocal + +// ================================================================================================= +// IPTC_Writer::CheckRoundTripLoss +// =============================== +// +// See if we still need UTF-8 because of round-trip loss. Returns true if there is loss. + +bool IPTC_Writer::CheckRoundTripLoss() +{ + XMP_Assert ( this->utf8Encoding ); + std::string localStr, rtStr; + + DataSetMap::iterator dsPos = this->dataSets.begin(); + DataSetMap::iterator dsEnd = this->dataSets.end(); + + for ( ; dsPos != dsEnd; ++dsPos ) { + + DataSetInfo & dsInfo = dsPos->second; + + XMP_StringPtr utf8Ptr = (XMP_StringPtr) dsInfo.dataPtr; + XMP_StringLen utf8Len = dsInfo.dataLen; + + ReconcileUtils::UTF8ToLocal ( utf8Ptr, utf8Len, &localStr ); + ReconcileUtils::LocalToUTF8 ( localStr.data(), localStr.size(), &rtStr ); + + if ( (rtStr.size() != utf8Len) || (memcmp ( rtStr.data(), utf8Ptr, utf8Len ) != 0) ) { + return true; // Had round-trip loss, keep UTF-8. + } + + } + + return false; // No loss. + +} // IPTC_Writer::CheckRoundTripLoss + +// ================================================================================================= diff --git a/XMPFiles/source/FormatSupport/IPTC_Support.hpp b/XMPFiles/source/FormatSupport/IPTC_Support.hpp new file mode 100644 index 0000000..4c2d2bc --- /dev/null +++ b/XMPFiles/source/FormatSupport/IPTC_Support.hpp @@ -0,0 +1,305 @@ +#ifndef __IPTC_Support_hpp__ +#define __IPTC_Support_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. + +#include <map> + +#include "public/include/XMP_Const.h" +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/EndianUtils.hpp" + +// ================================================================================================= +/// \file IPTC_Support.hpp +/// \brief XMPFiles support for IPTC (IIM) DataSets. +/// +/// This header provides IPTC (IIM) DataSet support specific to the needs of XMPFiles. This is not +/// intended for general purpose IPTC processing. There is a small tree of derived classes, 1 +/// virtual base class and 2 concrete leaf classes: +/// \code +/// IPTC_Manager - The root virtual base class. +/// IPTC_Reader - A derived concrete leaf class for memory-based read-only access. +/// IPTC_Writer - A derived concrete leaf class for memory-based read-write access. +/// \endcode +/// +/// \c IPTC_Manager declares all of the public methods except for specialized constructors in the +/// leaf classes. The read-only classes throw an XMP_Error exception for output methods like +/// \c SetDataSet. They return appropriate values for "safe" methods, \c IsChanged will return false +/// for example. +/// +/// The IPTC DataSet organization differs from TIFF tags and Photoshop image resources in allowing +/// muultiple occurrences for some IDs. The C++ STL multimap is a natural data structure for IPTC. +/// +/// Support is only provided for DataSet 1:90 to decide if local or UTF-8 text encoding is used, and +/// the following text valued DataSets: 2:05, 2:10, 2:15, 2:20, 2:25, 2:40, 2:55, 2:80, 2:85, 2:90, +/// 2:95, 2:101, 2:103, 2:105, 2:110, 2:115, 2:116, 2:120, and 2:122. DataSet 2:00 is ignored when +/// reading but always written. +/// +/// \note Unlike the TIFF_Manager and PSIR_Manager class trees, IPTC_Manager only provides in-memory +/// implementations. The total size of IPTC data is small enough to make this reasonable. +/// +/// \note These classes are for use only when directly compiled and linked. They should not be +/// packaged in a DLL by themselves. They do not provide any form of C++ ABI protection. +// ================================================================================================= + + +// ================================================================================================= +// ================================================================================================= + +enum { // List of recognized 2:* IIM DataSets. The names are from IIMv4 and IPTC4XMP. + kIPTC_ObjectType = 3, + kIPTC_IntellectualGenre = 4, + kIPTC_Title = 5, + kIPTC_EditStatus = 7, + kIPTC_EditorialUpdate = 8, + kIPTC_Urgency = 10, + kIPTC_SubjectCode = 12, + kIPTC_Category = 15, + kIPTC_SuppCategory = 20, + kIPTC_FixtureIdentifier = 22, + kIPTC_Keyword = 25, + kIPTC_ContentLocCode = 26, + kIPTC_ContentLocName = 27, + kIPTC_ReleaseDate = 30, + kIPTC_ReleaseTime = 35, + kIPTC_ExpDate = 37, + kIPTC_ExpTime = 38, + kIPTC_Instructions = 40, + kIPTC_ActionAdvised = 42, + kIPTC_RefService = 45, + kIPTC_RefDate = 47, + kIPTC_RefNumber = 50, + kIPTC_DateCreated = 55, + kIPTC_TimeCreated = 60, + kIPTC_DigitalCreateDate = 62, + kIPTC_DigitalCreateTime = 63, + kIPTC_OriginProgram = 65, + kIPTC_ProgramVersion = 70, + kIPTC_ObjectCycle = 75, + kIPTC_Creator = 80, + kIPTC_CreatorJobtitle = 85, + kIPTC_City = 90, + kIPTC_Location = 92, + kIPTC_State = 95, + kIPTC_CountryCode = 100, + kIPTC_Country = 101, + kIPTC_JobID = 103, + kIPTC_Headline = 105, + kIPTC_Provider = 110, + kIPTC_Source = 115, + kIPTC_CopyrightNotice = 116, + kIPTC_Contact = 118, + kIPTC_Description = 120, + kIPTC_DescriptionWriter = 122, + kIPTC_RasterizedCaption = 125, + kIPTC_ImageType = 130, + kIPTC_ImageOrientation = 131, + kIPTC_LanguageID = 135, + kIPTC_AudioType = 150, + kIPTC_AudioSampleRate = 151, + kIPTC_AudioSampleRes = 152, + kIPTC_AudioDuration = 153, + kIPTC_AudioOutcue = 154, + kIPTC_PreviewFormat = 200, + kIPTC_PreviewFormatVers = 201, + kIPTC_PreviewData = 202 +}; + +enum { // Forms of mapping legacy IPTC to XMP. Order is significant, see PhotoDataUtils::Import2WayIPTC! + kIPTC_MapSimple, // The XMP is simple, the last DataSet occurrence is kept. + kIPTC_MapLangAlt, // The XMP is a LangAlt x-default item, the last DataSet occurrence is kept. + kIPTC_MapArray, // The XMP is an unordered array, all DataSets are kept. + kIPTC_MapSpecial, // The mapping requires DataSet specific code. + kIPTC_Map3Way, // Has a 3 way mapping between Exif, IPTC, and XMP. + kIPTC_UnmappedText, // A text DataSet that is not mapped to XMP. + kIPTC_UnmappedBin // A binary DataSet that is not mapped to XMP. +}; + +struct DataSetCharacteristics { + XMP_Uns8 dsNum; + XMP_Uns8 mapForm; + size_t maxLen; + XMP_StringPtr xmpNS; + XMP_StringPtr xmpProp; +}; + +extern const DataSetCharacteristics kKnownDataSets[]; + +struct IntellectualGenreMapping { + XMP_StringPtr refNum; // The reference number as a 3 digit string. + XMP_StringPtr name; // The intellectual genre name. +}; + +extern const IntellectualGenreMapping kIntellectualGenreMappings[]; + +// ================================================================================================= +// IPTC_Manager +// ============ + +class IPTC_Manager { +public: + + // --------------------------------------------------------------------------------------------- + // Types and constants. + + struct DataSetInfo { + XMP_Uns8 recNum, dsNum; + XMP_Uns32 dataLen; + XMP_Uns8 * dataPtr; // ! The data is read-only. Raw data pointer, beware of character encoding. + DataSetInfo() : recNum(0), dsNum(0), dataLen(0), dataPtr(0) {}; + DataSetInfo ( XMP_Uns8 _recNum, XMP_Uns8 _dsNum, XMP_Uns32 _dataLen, XMP_Uns8 * _dataPtr ) + : recNum(_recNum), dsNum(_dsNum), dataLen(_dataLen), dataPtr(_dataPtr) {}; + }; + + // --------------------------------------------------------------------------------------------- + // Parse a binary IPTC (IIM) block. + + void ParseMemoryDataSets ( const void* data, XMP_Uns32 length, bool copyData = true ); + + // --------------------------------------------------------------------------------------------- + // Get the information about a 2:xx DataSet. Returns the number of occurrences. The "which" + // parameter selects the occurrence, they are numbered from 0 to count-1. Returns 0 if which is + // too large. + + size_t GetDataSet ( XMP_Uns8 dsNum, DataSetInfo* info, size_t which = 0 ) const; + + // --------------------------------------------------------------------------------------------- + // Get the value of a text 2:xx DataSet as UTF-8. The returned pointer must be treated as + // read-only. Calls GetDataSet then does a local to UTF-8 conversion if necessary. + + size_t GetDataSet_UTF8 ( XMP_Uns8 dsNum, std::string * utf8Str, size_t which = 0 ) const; + + // --------------------------------------------------------------------------------------------- + // Set the value of a text 2:xx DataSet from a UTF-8 string. Does a UTF-8 to local conversion if + // necessary. If the encoding mode is currently local and this value has round-trip loss, then + // the encoding mode will be changed to UTF-8 and all existing values will be converted. + // Modifies an existing occurrence if "which" is within range. Adds an occurrence if which + // equals the current count, or which is -1 and repeats are allowed. Throws an exception if + // which is too large. The dataPtr provides the raw data, text must be in the right encoding. + + virtual void SetDataSet_UTF8 ( XMP_Uns8 dsNum, const void* utf8Ptr, XMP_Uns32 utf8Len, long which = -1 ) = 0; + + // --------------------------------------------------------------------------------------------- + // Delete an existing 2:xx DataSet. Deletes all occurrences if which is -1. + + virtual void DeleteDataSet ( XMP_Uns8 dsNum, long which = -1 ) = 0; + + // --------------------------------------------------------------------------------------------- + // Determine if any 2:xx DataSets are changed. + + virtual bool IsChanged() = 0; + + // --------------------------------------------------------------------------------------------- + // Determine if UTF-8 or local text encoding is being used. + + bool UsingUTF8() const { return this->utf8Encoding; }; + + // -------------------------------------------------- + // Update all DataSets to reflect the changed values. + + virtual void UpdateMemoryDataSets() = 0; + + // --------------------------------------------------------------------------------------------- + // Get the location and size of the full IPTC block. The client must call UpdateMemoryDataSets + // first if appropriate. The returned dataPtr must be treated as read only. It exists until the + // IPTC_Manager destructor is called. + + XMP_Uns32 GetBlockInfo ( void** dataPtr ) const + { if ( dataPtr != 0 ) *dataPtr = this->iptcContent; return this->iptcLength; }; + + // --------------------------------------------------------------------------------------------- + + virtual ~IPTC_Manager() { if ( this->ownedContent ) free ( this->iptcContent ); }; + +protected: + + enum { kMinDataSetSize = 5 }; // 1+1+1+2 + + typedef std::multimap<XMP_Uns16,DataSetInfo> DataSetMap; + DataSetMap dataSets; // ! All datasets are in the map, key is (record*1000 + dataset). + + XMP_Uns8* iptcContent; + XMP_Uns32 iptcLength; + + bool changed; + bool ownedContent; // True if IPTC_Manager destructor needs to release the content block. + bool utf8Encoding; // True if text values are encoded as UTF-8. + + IPTC_Manager() : iptcContent(0), iptcLength(0), + changed(false), ownedContent(false), utf8Encoding(false) {}; + + void DisposeLooseValue ( DataSetInfo & dsInfo ); + XMP_Uns8* AppendDataSet ( XMP_Uns8* dsPtr, const DataSetInfo & dsInfo ); + +}; // IPTC_Manager + + +// ================================================================================================= +// ================================================================================================= + + +// ================================================================================================= +// IPTC_Reader +// =========== + +class IPTC_Reader : public IPTC_Manager { +public: + + IPTC_Reader() {}; + + void SetDataSet_UTF8 ( XMP_Uns8 dsNum, const void* utf8Ptr, XMP_Uns32 utf8Len, long which = -1 ) { NotAppropriate(); }; + + void DeleteDataSet ( XMP_Uns8 dsNum, long which = -1 ) { NotAppropriate(); }; + + bool IsChanged() { return false; }; + + void UpdateMemoryDataSets() { NotAppropriate(); }; + + virtual ~IPTC_Reader() {}; + +private: + + static inline void NotAppropriate() { XMP_Throw ( "Not appropriate for IPTC_Reader", kXMPErr_InternalFailure ); }; + +}; // IPTC_Reader + +// ================================================================================================= +// IPTC_Writer +// =========== + +class IPTC_Writer : public IPTC_Manager { +public: + + void SetDataSet_UTF8 ( XMP_Uns8 dsNum, const void* utf8Ptr, XMP_Uns32 utf8Len, long which = -1 ); + + void DeleteDataSet ( XMP_Uns8 dsNum, long which = -1 ); + + bool IsChanged() { return changed; }; + + void UpdateMemoryDataSets (); + + IPTC_Writer() {}; + + virtual ~IPTC_Writer(); + +private: + + void ConvertToUTF8(); + void ConvertToLocal(); + + bool CheckRoundTripLoss(); + +}; // IPTC_Writer + +// ================================================================================================= + +#endif // __IPTC_Support_hpp__ diff --git a/XMPFiles/source/FormatSupport/ISOBaseMedia_Support.cpp b/XMPFiles/source/FormatSupport/ISOBaseMedia_Support.cpp new file mode 100644 index 0000000..670b8fb --- /dev/null +++ b/XMPFiles/source/FormatSupport/ISOBaseMedia_Support.cpp @@ -0,0 +1,153 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2007 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/ISOBaseMedia_Support.hpp" +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XIO.hpp" + +// ================================================================================================= +/// \file ISOBaseMedia_Support.cpp +/// \brief Manager for parsing and serializing ISO Base Media files ( MPEG-4 and JPEG-2000). +/// +// ================================================================================================= + +namespace ISOMedia { + +static BoxInfo voidInfo; + +// ================================================================================================= +// GetBoxInfo - from memory +// ======================== + +const XMP_Uns8 * GetBoxInfo ( const XMP_Uns8 * boxPtr, const XMP_Uns8 * boxLimit, + BoxInfo * info, bool throwErrors /* = false */ ) +{ + XMP_Uns32 u32Size; + + if ( info == 0 ) info = &voidInfo; + info->boxType = info->headerSize = 0; + info->contentSize = 0; + + if ( boxPtr >= boxLimit ) XMP_Throw ( "Bad offset to GetBoxInfo", kXMPErr_InternalFailure ); + + if ( (boxLimit - boxPtr) < 8 ) { // Is there enough space for a standard box header? + if ( throwErrors ) XMP_Throw ( "No space for ISO box header", kXMPErr_BadFileFormat ); + info->headerSize = (XMP_Uns32) (boxLimit - boxPtr); + return boxLimit; + } + + u32Size = GetUns32BE ( boxPtr ); + info->boxType = GetUns32BE ( boxPtr+4 ); + + if ( u32Size >= 8 ) { + info->headerSize = 8; // Normal explicit size case. + info->contentSize = u32Size - 8; + } else if ( u32Size == 0 ) { + info->headerSize = 8; // The box goes to EoF - treat it as "to limit". + info->contentSize = (boxLimit - boxPtr) - 8; + } else if ( u32Size != 1 ) { + if ( throwErrors ) XMP_Throw ( "Bad ISO box size, 2..7", kXMPErr_BadFileFormat ); + info->headerSize = 8; // Bad total size in the range of 2..7, treat as 8. + info->contentSize = 0; + } else { + if ( (boxLimit - boxPtr) < 16 ) { // Is there enough space for an extended box header? + if ( throwErrors ) XMP_Throw ( "No space for ISO extended header", kXMPErr_BadFileFormat ); + info->headerSize = (XMP_Uns32) (boxLimit - boxPtr); + return boxLimit; + } + XMP_Uns64 u64Size = GetUns64BE ( boxPtr+8 ); + if ( u64Size < 16 ) { + if ( throwErrors ) XMP_Throw ( "Bad ISO extended box size, < 16", kXMPErr_BadFileFormat ); + u64Size = 16; // Treat bad total size as 16. + } + info->headerSize = 16; + info->contentSize = u64Size - 16; + } + + XMP_Assert ( (XMP_Uns64)(boxLimit - boxPtr) >= (XMP_Uns64)info->headerSize ); + if ( info->contentSize > (XMP_Uns64)((boxLimit - boxPtr) - info->headerSize) ) { + if ( throwErrors ) XMP_Throw ( "Bad ISO box content size", kXMPErr_BadFileFormat ); + info->contentSize = ((boxLimit - boxPtr) - info->headerSize); // Trim a bad content size to the limit. + } + + return (boxPtr + info->headerSize + info->contentSize); + +} // GetBoxInfo + +// ================================================================================================= +// GetBoxInfo - from a file +// ======================== + +XMP_Uns64 GetBoxInfo ( XMP_IO* fileRef, const XMP_Uns64 boxOffset, const XMP_Uns64 boxLimit, + BoxInfo * info, bool doSeek /* = true */, bool throwErrors /* = false */ ) +{ + XMP_Uns8 buffer [8]; + XMP_Uns32 u32Size; + + if ( info == 0 ) info = &voidInfo; + info->boxType = info->headerSize = 0; + info->contentSize = 0; + + if ( boxOffset >= boxLimit ) XMP_Throw ( "Bad offset to GetBoxInfo", kXMPErr_InternalFailure ); + + if ( (boxLimit - boxOffset) < 8 ) { // Is there enough space for a standard box header? + if ( throwErrors ) XMP_Throw ( "No space for ISO box header", kXMPErr_BadFileFormat ); + info->headerSize = (XMP_Uns32) (boxLimit - boxOffset); + return boxLimit; + } + + if ( doSeek ) fileRef->Seek ( boxOffset, kXMP_SeekFromStart ); + (void) fileRef->ReadAll ( buffer, 8 ); + + u32Size = GetUns32BE ( &buffer[0] ); + info->boxType = GetUns32BE ( &buffer[4] ); + + if ( u32Size >= 8 ) { + info->headerSize = 8; // Normal explicit size case. + info->contentSize = u32Size - 8; + } else if ( u32Size == 0 ) { + info->headerSize = 8; // The box goes to EoF. + info->contentSize = fileRef->Length() - (boxOffset + 8); + } else if ( u32Size != 1 ) { + if ( throwErrors ) XMP_Throw ( "Bad ISO box size, 2..7", kXMPErr_BadFileFormat ); + info->headerSize = 8; // Bad total size in the range of 2..7, treat as 8. + info->contentSize = 0; + } else { + if ( (boxLimit - boxOffset) < 16 ) { // Is there enough space for an extended box header? + if ( throwErrors ) XMP_Throw ( "No space for ISO extended header", kXMPErr_BadFileFormat ); + info->headerSize = (XMP_Uns32) (boxLimit - boxOffset); + return boxLimit; + } + (void) fileRef->ReadAll ( buffer, 8 ); + XMP_Uns64 u64Size = GetUns64BE ( &buffer[0] ); + if ( u64Size < 16 ) { + if ( throwErrors ) XMP_Throw ( "Bad ISO extended box size, < 16", kXMPErr_BadFileFormat ); + u64Size = 16; // Treat bad total size as 16. + } + info->headerSize = 16; + info->contentSize = u64Size - 16; + } + + XMP_Assert ( (boxLimit - boxOffset) >= info->headerSize ); + if ( info->contentSize > (boxLimit - boxOffset - info->headerSize) ) { + if ( throwErrors ) XMP_Throw ( "Bad ISO box content size", kXMPErr_BadFileFormat ); + info->contentSize = (boxLimit - boxOffset - info->headerSize); // Trim a bad content size to the limit. + } + + return (boxOffset + info->headerSize + info->contentSize); + +} // GetBoxInfo + +} // namespace ISO_Media + + +// ================================================================================================= diff --git a/XMPFiles/source/FormatSupport/ISOBaseMedia_Support.hpp b/XMPFiles/source/FormatSupport/ISOBaseMedia_Support.hpp new file mode 100644 index 0000000..115a0e8 --- /dev/null +++ b/XMPFiles/source/FormatSupport/ISOBaseMedia_Support.hpp @@ -0,0 +1,104 @@ +#ifndef __ISOBaseMedia_Support_hpp__ +#define __ISOBaseMedia_Support_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2007 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +// ================================================================================================= +/// \file ISOBaseMedia_Support.hpp +/// \brief XMPFiles support for the ISO Base Media File Format. +/// +/// \note These classes are for use only when directly compiled and linked. They should not be +/// packaged in a DLL by themselves. They do not provide any form of C++ ABI protection. +// ================================================================================================= + +namespace ISOMedia { + + enum { + k_ftyp = 0x66747970UL, // File header Box, no version/flags. + + k_mp41 = 0x6D703431UL, // Compatible brand codes + k_mp42 = 0x6D703432UL, + k_f4v = 0x66347620UL, + k_qt = 0x71742020UL, + + k_moov = 0x6D6F6F76UL, // Container Box, no version/flags. + k_mvhd = 0x6D766864UL, // Data FullBox, has version/flags. + k_hdlr = 0x68646C72UL, + k_udta = 0x75647461UL, // Container Box, no version/flags. + k_cprt = 0x63707274UL, // Data FullBox, has version/flags. + k_uuid = 0x75756964UL, // Data Box, no version/flags. + k_free = 0x66726565UL, // Free space Box, no version/flags. + k_mdat = 0x6D646174UL, // Media data Box, no version/flags. + + k_trak = 0x7472616BUL, // Types for the QuickTime timecode track. + k_tkhd = 0x746B6864UL, + k_edts = 0x65647473UL, + k_elst = 0x656C7374UL, + k_mdia = 0x6D646961UL, + k_mdhd = 0x6D646864UL, + k_tmcd = 0x746D6364UL, + k_mhlr = 0x6D686C72UL, + k_minf = 0x6D696E66UL, + k_stbl = 0x7374626CUL, + k_stsd = 0x73747364UL, + k_stsc = 0x73747363UL, + k_stco = 0x7374636FUL, + k_co64 = 0x636F3634UL, + + k_meta = 0x6D657461UL, // Types for the iTunes metadata boxes. + k_ilst = 0x696C7374UL, + k_mdir = 0x6D646972UL, + k_mean = 0x6D65616EUL, + k_name = 0x6E616D65UL, + k_data = 0x64617461UL, + k_hyphens = 0x2D2D2D2DUL, + + k_skip = 0x736B6970UL, // Additional classic QuickTime top level boxes. + k_wide = 0x77696465UL, + k_pnot = 0x706E6F74UL, + + k_XMP_ = 0x584D505FUL // The QuickTime variant XMP box. + }; + + static XMP_Uns32 k_xmpUUID [4] = { MakeUns32BE ( 0xBE7ACFCBUL ), + MakeUns32BE ( 0x97A942E8UL ), + MakeUns32BE ( 0x9C719994UL ), + MakeUns32BE ( 0x91E3AFACUL ) }; + + struct BoxInfo { + XMP_Uns32 boxType; // In memory as native endian! + XMP_Uns32 headerSize; // Normally 8 or 16, less than 8 if available space is too small. + XMP_Uns64 contentSize; // Always the real size, never 0 for "to EoF". + BoxInfo() : boxType(0), headerSize(0), contentSize(0) {}; + }; + + // Get basic info about a box in memory, returning a pointer to the following box. + const XMP_Uns8 * GetBoxInfo ( const XMP_Uns8 * boxPtr, const XMP_Uns8 * boxLimit, + BoxInfo * info, bool throwErrors = false ); + + // Get basic info about a box in a file, returning the offset of the following box. The I/O + // pointer is left at the start of the box's content. Returns the offset of the following box. + XMP_Uns64 GetBoxInfo ( XMP_IO* fileRef, const XMP_Uns64 boxOffset, const XMP_Uns64 boxLimit, + BoxInfo * info, bool doSeek = true, bool throwErrors = false ); + +// XMP_Uns32 CountChildBoxes ( XMP_IO* fileRef, const XMP_Uns64 childOffset, const XMP_Uns64 childLimit ); + +} // namespace ISO_Media + +// ================================================================================================= + +#endif // __ISOBaseMedia_Support_hpp__ diff --git a/XMPFiles/source/FormatSupport/MOOV_Support.cpp b/XMPFiles/source/FormatSupport/MOOV_Support.cpp new file mode 100644 index 0000000..b9b4996 --- /dev/null +++ b/XMPFiles/source/FormatSupport/MOOV_Support.cpp @@ -0,0 +1,554 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2009 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "XMPFiles/source/FormatSupport/MOOV_Support.hpp" + +#include "XMPFiles/source/FormatSupport/ISOBaseMedia_Support.hpp" + +#include <string.h> + +// ================================================================================================= +/// \file MOOV_Support.cpp +/// \brief XMPFiles support for the 'moov' box in MPEG-4 and QuickTime files. +// ================================================================================================= + +// ================================================================================================= +// ================================================================================================= +// MOOV_Manager - The parsing and reading routines are all commmon +// ================================================================================================= +// ================================================================================================= + +#ifndef TraceParseMoovTree + #define TraceParseMoovTree 0 +#endif + +#ifndef TraceUpdateMoovTree + #define TraceUpdateMoovTree 0 +#endif + +// ================================================================================================= +// MOOV_Manager::PickContentPtr +// ============================ + +XMP_Uns8 * MOOV_Manager::PickContentPtr ( const BoxNode & node ) const +{ + if ( node.contentSize == 0 ) { + return 0; + } else if ( node.changed ) { + return (XMP_Uns8*) &node.changedContent[0]; + } else { + return (XMP_Uns8*) &this->fullSubtree[0] + node.offset + node.headerSize; + } +} // MOOV_Manager::PickContentPtr + +// ================================================================================================= +// MOOV_Manager::FillBoxInfo +// ========================= + +void MOOV_Manager::FillBoxInfo ( const BoxNode & node, BoxInfo * info ) const +{ + if ( info == 0 ) return; + + info->boxType = node.boxType; + info->childCount = (XMP_Uns32)node.children.size(); + info->contentSize = node.contentSize; + info->content = PickContentPtr ( node ); + +} // MOOV_Manager::FillBoxInfo + +// ================================================================================================= +// MOOV_Manager::GetBoxInfo +// ========================= + +void MOOV_Manager::GetBoxInfo ( const BoxRef ref, BoxInfo * info ) const +{ + + this->FillBoxInfo ( *((BoxNode*)ref), info ); + +} // MOOV_Manager::GetBoxInfo + +// ================================================================================================= +// MOOV_Manager::GetBox +// ==================== +// +// Find a box given the type path. Pick the first child of each type. + +MOOV_Manager::BoxRef MOOV_Manager::GetBox ( const char * boxPath, BoxInfo * info ) const +{ + size_t pathLen = strlen(boxPath); + XMP_Assert ( (pathLen >= 4) && XMP_LitNMatch ( boxPath, "moov", 4 ) ); + if ( info != 0 ) memset ( info, 0, sizeof(BoxInfo) ); + + const char * pathPtr = boxPath + 5; // Skip the "moov/" portion. + const char * pathEnd = boxPath + pathLen; + + BoxRef currRef = &this->moovNode; + + while ( pathPtr < pathEnd ) { + + XMP_Assert ( (pathEnd - pathPtr) >= 4 ); + XMP_Uns32 boxType = GetUns32BE ( pathPtr ); + pathPtr += 5; // ! Don't care that the last step goes 1 too far. + + currRef = this->GetTypeChild ( currRef, boxType, 0 ); + if ( currRef == 0 ) return 0; + + } + + this->FillBoxInfo ( *((BoxNode*)currRef), info ); + return currRef; + +} // MOOV_Manager::GetBox + +// ================================================================================================= +// MOOV_Manager::GetNthChild +// ========================= + +MOOV_Manager::BoxRef MOOV_Manager::GetNthChild ( BoxRef parentRef, size_t childIndex, BoxInfo * info ) const +{ + XMP_Assert ( parentRef != 0 ); + const BoxNode & parent = *((BoxNode*)parentRef); + if ( info != 0 ) memset ( info, 0, sizeof(BoxInfo) ); + + if ( childIndex >= parent.children.size() ) return 0; + + const BoxNode & currNode = parent.children[childIndex]; + + this->FillBoxInfo ( currNode, info ); + return &currNode; + +} // MOOV_Manager::GetNthChild + +// ================================================================================================= +// MOOV_Manager::GetTypeChild +// ========================== + +MOOV_Manager::BoxRef MOOV_Manager::GetTypeChild ( BoxRef parentRef, XMP_Uns32 childType, BoxInfo * info ) const +{ + XMP_Assert ( parentRef != 0 ); + const BoxNode & parent = *((BoxNode*)parentRef); + if ( info != 0 ) memset ( info, 0, sizeof(BoxInfo) ); + if ( parent.children.empty() ) return 0; + + size_t i = 0, limit = parent.children.size(); + for ( ; i < limit; ++i ) { + const BoxNode & currNode = parent.children[i]; + if ( currNode.boxType == childType ) { + this->FillBoxInfo ( currNode, info ); + return &currNode; + } + } + + return 0; + +} // MOOV_Manager::GetTypeChild + +// ================================================================================================= +// MOOV_Manager::GetParsedOffset +// ============================= + +XMP_Uns32 MOOV_Manager::GetParsedOffset ( BoxRef ref ) const +{ + XMP_Assert ( ref != 0 ); + const BoxNode & node = *((BoxNode*)ref); + + if ( node.changed ) return 0; + return node.offset; + +} // MOOV_Manager::GetParsedOffset + +// ================================================================================================= +// MOOV_Manager::GetHeaderSize +// =========================== + +XMP_Uns32 MOOV_Manager::GetHeaderSize ( BoxRef ref ) const +{ + XMP_Assert ( ref != 0 ); + const BoxNode & node = *((BoxNode*)ref); + + if ( node.changed ) return 0; + return node.headerSize; + +} // MOOV_Manager::GetHeaderSize + +// ================================================================================================= +// MOOV_Manager::ParseMemoryTree +// ============================= +// +// Parse the fullSubtree data, building the BoxNode tree for the stuff that we care about. Tolerate +// errors like content ending too soon, make a best effoert to parse what we can. + +void MOOV_Manager::ParseMemoryTree ( XMP_Uns8 fileMode ) +{ + this->fileMode = fileMode; + + this->moovNode.offset = this->moovNode.boxType = 0; + this->moovNode.headerSize = this->moovNode.contentSize = 0; + this->moovNode.children.clear(); + this->moovNode.changedContent.clear(); + this->moovNode.changed = false; + + if ( this->fullSubtree.empty() ) return; + + ISOMedia::BoxInfo moovInfo; + const XMP_Uns8 * moovOrigin = &this->fullSubtree[0]; + const XMP_Uns8 * moovLimit = moovOrigin + this->fullSubtree.size(); + + (void) ISOMedia::GetBoxInfo ( moovOrigin, moovLimit, &moovInfo ); + XMP_Enforce ( moovInfo.boxType == ISOMedia::k_moov ); + + XMP_Uns64 fullMoovSize = moovInfo.headerSize + moovInfo.contentSize; + if ( fullMoovSize > moovBoxSizeLimit ) { // From here on we know 32-bit offsets are safe. + XMP_Throw ( "Oversize 'moov' box", kXMPErr_EnforceFailure ); + } + + this->moovNode.boxType = ISOMedia::k_moov; + this->moovNode.headerSize = moovInfo.headerSize; + this->moovNode.contentSize = (XMP_Uns32)moovInfo.contentSize; + + bool ignoreMetaBoxes = (fileMode == kFileIsTraditionalQT); // ! Don't want, these don't follow ISO spec. + #if TraceParseMoovTree + fprintf ( stderr, "Parsing 'moov' subtree, moovNode @ 0x%X, ignoreMetaBoxes = %d\n", + &this->moovNode, ignoreMetaBoxes ); + #endif + this->ParseNestedBoxes ( &this->moovNode, "moov", ignoreMetaBoxes ); + +} // MOOV_Manager::ParseMemoryTree + +// ================================================================================================= +// MOOV_Manager::ParseNestedBoxes +// ============================== +// +// Add the current level of child boxes to the parent node, recurse as appropriate. + +void MOOV_Manager::ParseNestedBoxes ( BoxNode * parentNode, const std::string & parentPath, bool ignoreMetaBoxes ) +{ + ISOMedia::BoxInfo isoInfo; + const XMP_Uns8 * moovOrigin = &this->fullSubtree[0]; + const XMP_Uns8 * childOrigin = moovOrigin + parentNode->offset + parentNode->headerSize; + const XMP_Uns8 * childLimit = childOrigin + parentNode->contentSize; + const XMP_Uns8 * nextChild; + + parentNode->contentSize = 0; // Exclude nested box size. + if ( parentNode->boxType == ISOMedia::k_meta ) { // ! The 'meta' box is a FullBox. + parentNode->contentSize = 4; + childOrigin += 4; + } + + for ( const XMP_Uns8 * currChild = childOrigin; currChild < childLimit; currChild = nextChild ) { + + nextChild = ISOMedia::GetBoxInfo ( currChild, childLimit, &isoInfo ); + if ( (isoInfo.boxType == 0) && + (isoInfo.headerSize < 8) && + (isoInfo.contentSize == 0) ) continue; // Skip trailing padding that QT sometimes writes. + + XMP_Uns32 childOffset = (XMP_Uns32) (currChild - moovOrigin); + parentNode->children.push_back ( BoxNode ( childOffset, isoInfo.boxType, isoInfo.headerSize, (XMP_Uns32)isoInfo.contentSize ) ); + BoxNode * newChild = &parentNode->children.back(); + + #if TraceParseMoovTree + size_t depth = (parentPath.size()+1) / 5; + for ( size_t i = 0; i < depth; ++i ) fprintf ( stderr, " " ); + XMP_Uns32 be32 = MakeUns32BE ( newChild->boxType ); + XMP_Uns32 addr32 = (XMP_Uns32) this->PickContentPtr ( *newChild ); + fprintf ( stderr, " Parsed %s/%.4s, offset 0x%X, size %d, content @ 0x%X, BoxNode @ 0x%X\n", + parentPath.c_str(), &be32, newChild->offset, newChild->contentSize, addr32, newChild ); + #endif + + const char * pathSuffix = 0; // Set to non-zero for boxes of interest. + char buffer[6]; buffer[0] = 0; + + switch ( isoInfo.boxType ) { // Want these boxes regardless of parent. + case ISOMedia::k_udta : pathSuffix = "/udta"; break; + case ISOMedia::k_meta : pathSuffix = "/meta"; break; + case ISOMedia::k_ilst : pathSuffix = "/ilst"; break; + case ISOMedia::k_trak : pathSuffix = "/trak"; break; + case ISOMedia::k_edts : pathSuffix = "/edts"; break; + case ISOMedia::k_mdia : pathSuffix = "/mdia"; break; + case ISOMedia::k_minf : pathSuffix = "/minf"; break; + case ISOMedia::k_stbl : pathSuffix = "/stbl"; break; + } + if ( pathSuffix != 0 ) { + this->ParseNestedBoxes ( newChild, (parentPath + pathSuffix), ignoreMetaBoxes ); + } + + } + +} // MOOV_Manager::ParseNestedBoxes + +// ================================================================================================= +// MOOV_Manager::NoteChange +// ======================== + +void MOOV_Manager::NoteChange() +{ + + this->moovNode.changed = true; + +} // MOOV_Manager::NoteChange + +// ================================================================================================= +// MOOV_Manager::SetBox +// ==================== +// +// Save the new data, set this box's changed flag, and set the top changed flag. + +void MOOV_Manager::SetBox ( BoxRef theBox, const void* dataPtr, XMP_Uns32 size ) +{ + XMP_Enforce ( size < moovBoxSizeLimit ); + BoxNode * node = (BoxNode*)theBox; + + if ( node->contentSize == size ) { + + XMP_Uns8 * oldContent = PickContentPtr ( *node ); + if ( memcmp ( oldContent, dataPtr, size ) == 0 ) return; // No change. + memcpy ( oldContent, dataPtr, size ); // Update the old content in-place + this->moovNode.changed = true; + + #if TraceUpdateMoovTree + XMP_Uns32 be32 = MakeUns32BE ( node->boxType ); + fprintf ( stderr, "Updated '%.4s', parse offset 0x%X, same size\n", &be32, node->offset ); + #endif + + } else { + + node->changedContent.assign ( size, 0 ); // Fill with 0's first to get the storage. + memcpy ( &node->changedContent[0], dataPtr, size ); + node->contentSize = size; + node->changed = true; + this->moovNode.changed = true; + + #if TraceUpdateMoovTree + XMP_Uns32 be32 = MakeUns32BE ( node->boxType ); + XMP_Uns32 addr32 = (XMP_Uns32) this->PickContentPtr ( *node ); + fprintf ( stderr, "Updated '%.4s', parse offset 0x%X, new size %d, new content @ 0x%X\n", + &be32, node->offset, node->contentSize, addr32 ); + #endif + + } + +} // MOOV_Manager::SetBox + +// ================================================================================================= +// MOOV_Manager::SetBox +// ==================== +// +// Like above, but create the path to the box if necessary. + +void MOOV_Manager::SetBox ( const char * boxPath, const void* dataPtr, XMP_Uns32 size ) +{ + XMP_Enforce ( size < moovBoxSizeLimit ); + + size_t pathLen = strlen(boxPath); + XMP_Assert ( (pathLen >= 4) && XMP_LitNMatch ( boxPath, "moov", 4 ) ); + + const char * pathPtr = boxPath + 5; // Skip the "moov/" portion. + const char * pathEnd = boxPath + pathLen; + + BoxRef parentRef = 0; + BoxRef currRef = &this->moovNode; + + while ( pathPtr < pathEnd ) { + + XMP_Assert ( (pathEnd - pathPtr) >= 4 ); + XMP_Uns32 boxType = GetUns32BE ( pathPtr ); + pathPtr += 5; // ! Don't care that the last step goes 1 too far. + + parentRef = currRef; + currRef = this->GetTypeChild ( parentRef, boxType, 0 ); + if ( currRef == 0 ) currRef = this->AddChildBox ( parentRef, boxType, 0, 0 ); + + } + + this->SetBox ( currRef, dataPtr, size ); + +} // MOOV_Manager::SetBox + +// ================================================================================================= +// MOOV_Manager::AddChildBox +// ========================= + +MOOV_Manager::BoxRef MOOV_Manager::AddChildBox ( BoxRef parentRef, XMP_Uns32 childType, const void* dataPtr, XMP_Uns32 size ) +{ + BoxNode * parent = (BoxNode*)parentRef; + XMP_Assert ( parent != 0 ); + + parent->children.push_back ( BoxNode ( 0, childType, 0, 0 ) ); + BoxNode * newNode = &parent->children.back(); + this->SetBox ( newNode, dataPtr, size ); + + return newNode; + +} // MOOV_Manager::AddChildBox + +// ================================================================================================= +// MOOV_Manager::DeleteNthChild +// ============================ + +bool MOOV_Manager::DeleteNthChild ( BoxRef parentRef, size_t childIndex ) +{ + BoxNode * parent = (BoxNode*)parentRef; + + if ( childIndex >= parent->children.size() ) return false; + + parent->children.erase ( parent->children.begin() + childIndex ); + return true; + +} // MOOV_Manager::DeleteNthChild + +// ================================================================================================= +// MOOV_Manager::DeleteTypeChild +// ============================= + +bool MOOV_Manager::DeleteTypeChild ( BoxRef parentRef, XMP_Uns32 childType ) +{ + BoxNode * parent = (BoxNode*)parentRef; + + BoxListPos child = parent->children.begin(); + BoxListPos limit = parent->children.end(); + + for ( ; child != limit; ++child ) { + if ( child->boxType == childType ) { + parent->children.erase ( child ); + this->moovNode.changed = true; + return true; + } + } + + return false; + +} // MOOV_Manager::DeleteTypeChild + +// ================================================================================================= +// MOOV_Manager::NewSubtreeSize +// ============================ +// +// Determine the new (changed) size of a subtree. Ignore 'free' and 'wide' boxes. + +XMP_Uns32 MOOV_Manager::NewSubtreeSize ( const BoxNode & node, const std::string & parentPath ) +{ + XMP_Uns32 subtreeSize = 8 + node.contentSize; // All boxes will have 8 byte headers. + + if ( (node.boxType == ISOMedia::k_free) || (node.boxType == ISOMedia::k_wide) ) { + } + + for ( size_t i = 0, limit = node.children.size(); i < limit; ++i ) { + + char suffix[6]; + suffix[0] = '/'; + PutUns32BE ( node.boxType, &suffix[1] ); + suffix[5] = 0; + std::string nodePath = parentPath + suffix; + + subtreeSize += this->NewSubtreeSize ( node.children[i], nodePath ); + XMP_Enforce ( subtreeSize < moovBoxSizeLimit ); + + } + + return subtreeSize; + +} // MOOV_Manager::NewSubtreeSize + +// ================================================================================================= +// MOOV_Manager::AppendNewSubtree +// ============================== +// +// Append this node's header, content, and children. Because the 'meta' box is a FullBox with nested +// boxes, there can be both content and children. Ignore 'free' and 'wide' boxes. + +#define IncrNewPtr(count) { newPtr += count; XMP_Enforce ( newPtr <= newEnd ); } + +#if TraceUpdateMoovTree + static XMP_Uns8 * newOrigin; +#endif + +XMP_Uns8 * MOOV_Manager::AppendNewSubtree ( const BoxNode & node, const std::string & parentPath, + XMP_Uns8 * newPtr, XMP_Uns8 * newEnd ) +{ + if ( (node.boxType == ISOMedia::k_free) || (node.boxType == ISOMedia::k_wide) ) { + } + + XMP_Assert ( (node.boxType != ISOMedia::k_meta) ? (node.children.empty() || (node.contentSize == 0)) : + (node.children.empty() || (node.contentSize == 4)) ); + + XMP_Enforce ( (XMP_Uns32)(newEnd - newPtr) >= (8 + node.contentSize) ); + + #if TraceUpdateMoovTree + XMP_Uns32 be32 = MakeUns32BE ( node.boxType ); + XMP_Uns32 newOffset = (XMP_Uns32) (newPtr - newOrigin); + XMP_Uns32 addr32 = (XMP_Uns32) this->PickContentPtr ( node ); + fprintf ( stderr, " Appending %s/%.4s @ 0x%X, size %d, content @ 0x%X\n", + parentPath.c_str(), &be32, newOffset, node.contentSize, addr32 ); + #endif + + // Leave the size as 0 for now, append the type and content. + + XMP_Uns8 * boxOrigin = newPtr; // Save origin to fill in the final size. + PutUns32BE ( node.boxType, (newPtr + 4) ); + IncrNewPtr ( 8 ); + + if ( node.contentSize != 0 ) { + const XMP_Uns8 * content = PickContentPtr( node ); + memcpy ( newPtr, content, node.contentSize ); + IncrNewPtr ( node.contentSize ); + } + + // Append the nested boxes. + + if ( ! node.children.empty() ) { + + char suffix[6]; + suffix[0] = '/'; + PutUns32BE ( node.boxType, &suffix[1] ); + suffix[5] = 0; + std::string nodePath = parentPath + suffix; + + for ( size_t i = 0, limit = node.children.size(); i < limit; ++i ) { + newPtr = this->AppendNewSubtree ( node.children[i], nodePath, newPtr, newEnd ); + } + + } + + // Fill in the final size. + + PutUns32BE ( (XMP_Uns32)(newPtr - boxOrigin), boxOrigin ); + + return newPtr; + +} // MOOV_Manager::AppendNewSubtree + +// ================================================================================================= +// MOOV_Manager::UpdateMemoryTree +// ============================== + +void MOOV_Manager::UpdateMemoryTree() +{ + if ( ! this->IsChanged() ) return; + + XMP_Uns32 newSize = this->NewSubtreeSize ( this->moovNode, "" ); + XMP_Enforce ( newSize < moovBoxSizeLimit ); + + RawDataBlock newData; + newData.assign ( newSize, 0 ); // Prefill with zeroes, can't append multiple items to a vector. + + XMP_Uns8 * newPtr = &newData[0]; + XMP_Uns8 * newEnd = newPtr + newSize; + + #if TraceUpdateMoovTree + fprintf ( stderr, "Starting MOOV_Manager::UpdateMemoryTree\n" ); + newOrigin = newPtr; + #endif + + XMP_Uns8 * trueEnd = this->AppendNewSubtree ( this->moovNode, "", newPtr, newEnd ); + XMP_Enforce ( trueEnd == newEnd ); + + this->fullSubtree.swap ( newData ); + this->ParseMemoryTree ( this->fileMode ); + +} // MOOV_Manager::UpdateMemoryTree diff --git a/XMPFiles/source/FormatSupport/MOOV_Support.hpp b/XMPFiles/source/FormatSupport/MOOV_Support.hpp new file mode 100644 index 0000000..814a8bb --- /dev/null +++ b/XMPFiles/source/FormatSupport/MOOV_Support.hpp @@ -0,0 +1,217 @@ +#ifndef __MOOV_Support_hpp__ +#define __MOOV_Support_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2009 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. +#include "public/include/XMP_Const.h" + +#include "source/EndianUtils.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +#include <vector> + +#define moovBoxSizeLimit 100*1024*1024 + +// ================================================================================================= +// MOOV_Manager +// ============ + +class MOOV_Manager { +public: + + // --------------------------------------------------------------------------------------------- + // Types and constants + + enum { // Values for fileMode. + kFileIsNormalISO = 0, // A "normal" MPEG-4 file, no 'qt ' compatible brand. + kFileIsModernQT = 1, // Has an 'ftyp' box and 'qt ' compatible brand. + kFileIsTraditionalQT = 2 // Old QuickTime, no 'ftyp' box. + }; + + typedef const void * BoxRef; // Valid until a sibling or higher box is added or deleted. + + struct BoxInfo { + XMP_Uns32 boxType; // In memory as native endian, compares work with ISOMedia::k_* constants. + XMP_Uns32 childCount; // ! A 'meta' box has both content (version/flags) and children! + XMP_Uns32 contentSize; // Does not include the size of nested boxes. + const XMP_Uns8 * content; // Null if contentSize is zero. + BoxInfo() : boxType(0), childCount(0), contentSize(0), content(0) {}; + }; + + // --------------------------------------------------------------------------------------------- + // GetBox - Pick a box given a '/' separated list of box types. Picks the 1st of each type. + // GetBoxInfo - Get the info if we already have the ref. + // GetNthChild - Pick the overall n-th child of the parent, zero based. + // GetTypeChild - Pick the first child of the given type. + // GetParsedOffset - Get the box's offset in the parsed tree, 0 if changed since parsing. + // GetHeaderSize - Get the box's header size in the parsed tree, 0 if changed since parsing. + + BoxRef GetBox ( const char * boxPath, BoxInfo * info ) const; + + void GetBoxInfo ( const BoxRef ref, BoxInfo * info ) const; + + BoxRef GetNthChild ( BoxRef parentRef, size_t childIndex, BoxInfo * info ) const; + BoxRef GetTypeChild ( BoxRef parentRef, XMP_Uns32 childType, BoxInfo * info ) const; + + XMP_Uns32 GetParsedOffset ( BoxRef ref ) const; + XMP_Uns32 GetHeaderSize ( BoxRef ref ) const; + + // --------------------------------------------------------------------------------------------- + // NoteChange - Note overall change, value was directly replaced. + // SetBox(ref) - Replace the content with a copy of the given data. + // SetBox(path) - Like above, but creating path to the box if necessary. + // AddChildBox - Add a child of the given type, using a copy of the given data (may be null) + + void NoteChange(); + + void SetBox ( BoxRef theBox, const void* dataPtr, XMP_Uns32 size ); + void SetBox ( const char * boxPath, const void* dataPtr, XMP_Uns32 size ); + + BoxRef AddChildBox ( BoxRef parentRef, XMP_Uns32 childType, const void * dataPtr, XMP_Uns32 size ); + + // --------------------------------------------------------------------------------------------- + // DeleteNthChild - Delete the overall n-th child, return true if there was one. + // DeleteTypeChild - Delete the first child of the given type, return true if there was one. + + bool DeleteNthChild ( BoxRef parentRef, size_t childIndex ); + bool DeleteTypeChild ( BoxRef parentRef, XMP_Uns32 childType ); + + // --------------------------------------------------------------------------------------------- + + bool IsChanged() const { return this->moovNode.changed; }; + + // --------------------------------------------------------------------------------------------- + // The client is expected to fill in fullSubtree before calling ParseMemoryTree, and directly + // use fullSubtree after calling UpdateMemoryTree. + // + // IMPORTANT: We only support cases where the 'moov' subtree is significantly less than 4 GB, in + // particular with a threshold of probably 100 MB. This has 2 big impacts: we can safely use + // 32-bit offsets and sizes, and comfortably assume everything will fit in available heap space. + + RawDataBlock fullSubtree; // The entire 'moov' box, straight from the file or from UpdateMemoryTree. + + void ParseMemoryTree ( XMP_Uns8 fileMode ); + void UpdateMemoryTree(); + + // --------------------------------------------------------------------------------------------- + + #pragma pack (1) // ! These must match the file layout! + + struct Content_mvhd_0 { + XMP_Uns32 vFlags; // 0 + XMP_Uns32 creationTime; // 4 + XMP_Uns32 modificationTime; // 8 + XMP_Uns32 timescale; // 12 + XMP_Uns32 duration; // 16 + XMP_Int32 rate; // 20 + XMP_Int16 volume; // 24 + XMP_Uns16 pad_1; // 26 + XMP_Uns32 pad_2, pad_3; // 28 + XMP_Int32 matrix [9]; // 36 + XMP_Uns32 preDef [6]; // 72 + XMP_Uns32 nextTrackID; // 96 + }; // 100 + + struct Content_mvhd_1 { + XMP_Uns32 vFlags; // 0 + XMP_Uns64 creationTime; // 4 + XMP_Uns64 modificationTime; // 12 + XMP_Uns32 timescale; // 20 + XMP_Uns64 duration; // 24 + XMP_Int32 rate; // 32 + XMP_Int16 volume; // 36 + XMP_Uns16 pad_1; // 38 + XMP_Uns32 pad_2, pad_3; // 40 + XMP_Int32 matrix [9]; // 48 + XMP_Uns32 preDef [6]; // 84 + XMP_Uns32 nextTrackID; // 108 + }; // 112 + + struct Content_hdlr { // An 'hdlr' box as defined by ISO 14496-12. Maps OK to the QuickTime box. + XMP_Uns32 versionFlags; // 0 + XMP_Uns32 preDef; // 4 + XMP_Uns32 handlerType; // 8 + XMP_Uns32 reserved [3]; // 12 + // Plus optional component name string, null terminated UTF-8. + }; // 24 + + struct Content_stsd_entry { + XMP_Uns32 entrySize; // 0 + XMP_Uns32 format; // 4 + XMP_Uns8 reserved_1 [6]; // 8 + XMP_Uns16 dataRefIndex; // 14 + XMP_Uns32 reserved_2; // 16 + XMP_Uns32 flags; // 20 + XMP_Uns32 timeScale; // 24 + XMP_Uns32 frameDuration; // 28 + XMP_Uns8 frameCount; // 32 + XMP_Uns8 reserved_3; // 33 + // Plus optional trailing ISO boxes. + }; // 34 + + struct Content_stsc_entry { + XMP_Uns32 firstChunkNumber; // 0 + XMP_Uns32 samplesPerChunk; // 4 + XMP_Uns32 sampleDescrID; // 8 + }; // 12 + + // --------------------------------------------------------------------------------------------- + + MOOV_Manager() : fileMode(0) + { + XMP_Assert ( sizeof ( Content_mvhd_0 ) == 100 ); // Make sure the structs really are packed. + XMP_Assert ( sizeof ( Content_mvhd_1 ) == 112 ); + XMP_Assert ( sizeof ( Content_hdlr ) == 24 ); + XMP_Assert ( sizeof ( Content_stsd_entry ) == 34 ); + XMP_Assert ( sizeof ( Content_stsc_entry ) == 12 ); + }; + + virtual ~MOOV_Manager() {}; + +private: + + struct BoxNode; + typedef std::vector<BoxNode> BoxList; + typedef BoxList::iterator BoxListPos; + + struct BoxNode { + // ! Don't have a parent link, it will get destroyed by vector growth! + + XMP_Uns32 offset; // The offset in the fullSubtree, 0 if not in the parse. + XMP_Uns32 boxType; + XMP_Uns32 headerSize; // The actual header size in the fullSubtree, 0 if not in the parse. + XMP_Uns32 contentSize; // The current content size, does not include nested boxes. + BoxList children; + RawDataBlock changedContent; // Might be empty even if changed is true. + bool changed; // If true, the content is in changedContent, else in fullSubtree. + + BoxNode() : offset(0), boxType(0), headerSize(0), contentSize(0), changed(false) {}; + BoxNode ( XMP_Uns32 _offset, XMP_Uns32 _boxType, XMP_Uns32 _headerSize, XMP_Uns32 _contentSize ) + : offset(_offset), boxType(_boxType), headerSize(_headerSize), contentSize(_contentSize), changed(false) {}; + + }; + + XMP_Uns8 fileMode; + BoxNode moovNode; + + void ParseNestedBoxes ( BoxNode * parentNode, const std::string & parentPath, bool ignoreMetaBoxes ); + + XMP_Uns8 * PickContentPtr ( const BoxNode & node ) const; + void FillBoxInfo ( const BoxNode & node, BoxInfo * info ) const; + + XMP_Uns32 NewSubtreeSize ( const BoxNode & node, const std::string & parentPath ); + XMP_Uns8 * AppendNewSubtree ( const BoxNode & node, const std::string & parentPath, + XMP_Uns8 * newPtr, XMP_Uns8 * newEnd ); + +}; // MOOV_Manager + +#endif // __MOOV_Support_hpp__ diff --git a/XMPFiles/source/FormatSupport/MacScriptExtracts.h b/XMPFiles/source/FormatSupport/MacScriptExtracts.h new file mode 100644 index 0000000..9856183 --- /dev/null +++ b/XMPFiles/source/FormatSupport/MacScriptExtracts.h @@ -0,0 +1,244 @@ +#ifndef __MacScriptExtracts__ +#define __MacScriptExtracts__ + +// Extracts of script (smXyz) and language (langXyz) enums from Apple's old Script.h. +// These are used to support "traditional" QuickTime metadata processing. + +/* + Script codes: + These specify a Mac OS encoding that is related to a FOND ID range. + Some of the encodings have several variants (e.g. for different localized systems) + which all share the same script code. + Not all of these script codes are currently supported by Apple software. + Notes: + - Script code 0 (smRoman) is also used (instead of smGreek) for the Greek encoding + in the Greek localized system. + - Script code 28 (smEthiopic) is also used for the Inuit encoding in the Inuktitut + system. +*/ +enum { + smRoman = 0, + smJapanese = 1, + smTradChinese = 2, /* Traditional Chinese*/ + smKorean = 3, + smArabic = 4, + smHebrew = 5, + smGreek = 6, + smCyrillic = 7, + smRSymbol = 8, /* Right-left symbol*/ + smDevanagari = 9, + smGurmukhi = 10, + smGujarati = 11, + smOriya = 12, + smBengali = 13, + smTamil = 14, + smTelugu = 15, + smKannada = 16, /* Kannada/Kanarese*/ + smMalayalam = 17, + smSinhalese = 18, + smBurmese = 19, + smKhmer = 20, /* Khmer/Cambodian*/ + smThai = 21, + smLao = 22, + smGeorgian = 23, + smArmenian = 24, + smSimpChinese = 25, /* Simplified Chinese*/ + smTibetan = 26, + smMongolian = 27, + smEthiopic = 28, + smGeez = 28, /* Synonym for smEthiopic*/ + smCentralEuroRoman = 29, /* For Czech, Slovak, Polish, Hungarian, Baltic langs*/ + smVietnamese = 30, + smExtArabic = 31, /* extended Arabic*/ + smUninterp = 32 /* uninterpreted symbols, e.g. palette symbols*/ +}; + +/* Extended script code for full Unicode input*/ +enum { + smUnicodeScript = 0x7E +}; + +/* Obsolete script code names (kept for backward compatibility):*/ +enum { + smChinese = 2, /* (Use smTradChinese or smSimpChinese)*/ + smRussian = 7, /* Use smCyrillic*/ + /* smMaldivian = 25: deleted, no code for Maldivian*/ + smLaotian = 22, /* Use smLao */ + smAmharic = 28, /* Use smEthiopic or smGeez*/ + smSlavic = 29, /* Use smCentralEuroRoman*/ + smEastEurRoman = 29, /* Use smCentralEuroRoman*/ + smSindhi = 31, /* Use smExtArabic*/ + smKlingon = 32 +}; + +/* + Language codes: + These specify a language implemented using a particular Mac OS encoding. + Not all of these language codes are currently supported by Apple software. +*/ +enum { + langEnglish = 0, /* smRoman script*/ + langFrench = 1, /* smRoman script*/ + langGerman = 2, /* smRoman script*/ + langItalian = 3, /* smRoman script*/ + langDutch = 4, /* smRoman script*/ + langSwedish = 5, /* smRoman script*/ + langSpanish = 6, /* smRoman script*/ + langDanish = 7, /* smRoman script*/ + langPortuguese = 8, /* smRoman script*/ + langNorwegian = 9, /* (Bokmal) smRoman script*/ + langHebrew = 10, /* smHebrew script*/ + langJapanese = 11, /* smJapanese script*/ + langArabic = 12, /* smArabic script*/ + langFinnish = 13, /* smRoman script*/ + langGreek = 14, /* Greek script (monotonic) using smRoman script code*/ + langIcelandic = 15, /* modified smRoman/Icelandic script*/ + langMaltese = 16, /* Roman script*/ + langTurkish = 17, /* modified smRoman/Turkish script*/ + langCroatian = 18, /* modified smRoman/Croatian script*/ + langTradChinese = 19, /* Chinese (Mandarin) in traditional characters*/ + langUrdu = 20, /* smArabic script*/ + langHindi = 21, /* smDevanagari script*/ + langThai = 22, /* smThai script*/ + langKorean = 23 /* smKorean script*/ +}; + +enum { + langLithuanian = 24, /* smCentralEuroRoman script*/ + langPolish = 25, /* smCentralEuroRoman script*/ + langHungarian = 26, /* smCentralEuroRoman script*/ + langEstonian = 27, /* smCentralEuroRoman script*/ + langLatvian = 28, /* smCentralEuroRoman script*/ + langSami = 29, /* language of the Sami people of N. Scandinavia */ + langFaroese = 30, /* modified smRoman/Icelandic script */ + langFarsi = 31, /* modified smArabic/Farsi script*/ + langPersian = 31, /* Synonym for langFarsi*/ + langRussian = 32, /* smCyrillic script*/ + langSimpChinese = 33, /* Chinese (Mandarin) in simplified characters*/ + langFlemish = 34, /* smRoman script*/ + langIrishGaelic = 35, /* smRoman or modified smRoman/Celtic script (without dot above) */ + langAlbanian = 36, /* smRoman script*/ + langRomanian = 37, /* modified smRoman/Romanian script*/ + langCzech = 38, /* smCentralEuroRoman script*/ + langSlovak = 39, /* smCentralEuroRoman script*/ + langSlovenian = 40, /* modified smRoman/Croatian script*/ + langYiddish = 41, /* smHebrew script*/ + langSerbian = 42, /* smCyrillic script*/ + langMacedonian = 43, /* smCyrillic script*/ + langBulgarian = 44, /* smCyrillic script*/ + langUkrainian = 45, /* modified smCyrillic/Ukrainian script*/ + langByelorussian = 46, /* smCyrillic script*/ + langBelorussian = 46 /* Synonym for langByelorussian */ +}; + +enum { + langUzbek = 47, /* Cyrillic script*/ + langKazakh = 48, /* Cyrillic script*/ + langAzerbaijani = 49, /* Azerbaijani in Cyrillic script*/ + langAzerbaijanAr = 50, /* Azerbaijani in Arabic script*/ + langArmenian = 51, /* smArmenian script*/ + langGeorgian = 52, /* smGeorgian script*/ + langMoldavian = 53, /* smCyrillic script*/ + langKirghiz = 54, /* Cyrillic script*/ + langTajiki = 55, /* Cyrillic script*/ + langTurkmen = 56, /* Cyrillic script*/ + langMongolian = 57, /* Mongolian in smMongolian script*/ + langMongolianCyr = 58, /* Mongolian in Cyrillic script*/ + langPashto = 59, /* Arabic script*/ + langKurdish = 60, /* smArabic script*/ + langKashmiri = 61, /* Arabic script*/ + langSindhi = 62, /* Arabic script*/ + langTibetan = 63, /* smTibetan script*/ + langNepali = 64, /* smDevanagari script*/ + langSanskrit = 65, /* smDevanagari script*/ + langMarathi = 66, /* smDevanagari script*/ + langBengali = 67, /* smBengali script*/ + langAssamese = 68, /* smBengali script*/ + langGujarati = 69, /* smGujarati script*/ + langPunjabi = 70 /* smGurmukhi script*/ +}; + +enum { + langOriya = 71, /* smOriya script*/ + langMalayalam = 72, /* smMalayalam script*/ + langKannada = 73, /* smKannada script*/ + langTamil = 74, /* smTamil script*/ + langTelugu = 75, /* smTelugu script*/ + langSinhalese = 76, /* smSinhalese script*/ + langBurmese = 77, /* smBurmese script*/ + langKhmer = 78, /* smKhmer script*/ + langLao = 79, /* smLao script*/ + langVietnamese = 80, /* smVietnamese script*/ + langIndonesian = 81, /* smRoman script*/ + langTagalog = 82, /* Roman script*/ + langMalayRoman = 83, /* Malay in smRoman script*/ + langMalayArabic = 84, /* Malay in Arabic script*/ + langAmharic = 85, /* smEthiopic script*/ + langTigrinya = 86, /* smEthiopic script*/ + langOromo = 87, /* smEthiopic script*/ + langSomali = 88, /* smRoman script*/ + langSwahili = 89, /* smRoman script*/ + langKinyarwanda = 90, /* smRoman script*/ + langRuanda = 90, /* synonym for langKinyarwanda*/ + langRundi = 91, /* smRoman script*/ + langNyanja = 92, /* smRoman script*/ + langChewa = 92, /* synonym for langNyanja*/ + langMalagasy = 93, /* smRoman script*/ + langEsperanto = 94 /* Roman script*/ +}; + +enum { + langWelsh = 128, /* modified smRoman/Celtic script*/ + langBasque = 129, /* smRoman script*/ + langCatalan = 130, /* smRoman script*/ + langLatin = 131, /* smRoman script*/ + langQuechua = 132, /* smRoman script*/ + langGuarani = 133, /* smRoman script*/ + langAymara = 134, /* smRoman script*/ + langTatar = 135, /* Cyrillic script*/ + langUighur = 136, /* Arabic script*/ + langDzongkha = 137, /* (lang of Bhutan) smTibetan script*/ + langJavaneseRom = 138, /* Javanese in smRoman script*/ + langSundaneseRom = 139, /* Sundanese in smRoman script*/ + langGalician = 140, /* smRoman script*/ + langAfrikaans = 141 /* smRoman script */ +}; + +enum { + langBreton = 142, /* smRoman or modified smRoman/Celtic script */ + langInuktitut = 143, /* Inuit script using smEthiopic script code */ + langScottishGaelic = 144, /* smRoman or modified smRoman/Celtic script */ + langManxGaelic = 145, /* smRoman or modified smRoman/Celtic script */ + langIrishGaelicScript = 146, /* modified smRoman/Gaelic script (using dot above) */ + langTongan = 147, /* smRoman script */ + langGreekAncient = 148, /* Classical Greek, polytonic orthography */ + langGreenlandic = 149, /* smRoman script */ + langAzerbaijanRoman = 150, /* Azerbaijani in Roman script */ + langNynorsk = 151 /* Norwegian Nyorsk in smRoman*/ +}; + +enum { + langUnspecified = 32767 /* Special code for use in resources (such as 'itlm') */ +}; + +/* + Obsolete language code names (kept for backward compatibility): + Misspelled, ambiguous, misleading, considered pejorative, archaic, etc. +*/ +enum { + langPortugese = 8, /* Use langPortuguese*/ + langMalta = 16, /* Use langMaltese*/ + langYugoslavian = 18, /* (use langCroatian, langSerbian, etc.)*/ + langChinese = 19, /* (use langTradChinese or langSimpChinese)*/ + langLettish = 28, /* Use langLatvian */ + langLapponian = 29, /* Use langSami*/ + langLappish = 29, /* Use langSami*/ + langSaamisk = 29, /* Use langSami */ + langFaeroese = 30, /* Use langFaroese */ + langIrish = 35, /* Use langIrishGaelic */ + langGalla = 87, /* Use langOromo */ + langAfricaans = 141, /* Use langAfrikaans */ + langGreekPoly = 148 /* Use langGreekAncient*/ +}; + +#endif /* __MacScriptExtracts__ */ diff --git a/XMPFiles/source/FormatSupport/PNG_Support.cpp b/XMPFiles/source/FormatSupport/PNG_Support.cpp new file mode 100644 index 0000000..c58fc87 --- /dev/null +++ b/XMPFiles/source/FormatSupport/PNG_Support.cpp @@ -0,0 +1,341 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2008 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/PNG_Support.hpp" + +#include "source/XIO.hpp" + +#include <string.h> + +typedef std::basic_string<unsigned char> filebuffer; + +namespace CRC +{ + /* Table of CRCs of all 8-bit messages. */ + static unsigned long crc_table[256]; + + /* Flag: has the table been computed? Initially false. */ + static int crc_table_computed = 0; + + /* Make the table for a fast CRC. */ + static void make_crc_table(void) + { + unsigned long c; + int n, k; + + for (n = 0; n < 256; n++) + { + c = (unsigned long) n; + for (k = 0; k < 8; k++) + { + if (c & 1) + { + c = 0xedb88320L ^ (c >> 1); + } + else + { + c = c >> 1; + } + } + crc_table[n] = c; + } + crc_table_computed = 1; + } + + /* Update a running CRC with the bytes buf[0..len-1]--the CRC + should be initialized to all 1's, and the transmitted value + is the 1's complement of the final running CRC (see the + crc() routine below). */ + + static unsigned long update_crc(unsigned long crc, unsigned char *buf, int len) + { + unsigned long c = crc; + int n; + + if (!crc_table_computed) + { + make_crc_table(); + } + + for (n = 0; n < len; n++) + { + c = crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8); + } + + return c; + } + + /* Return the CRC of the bytes buf[0..len-1]. */ + static unsigned long crc(unsigned char *buf, int len) + { + return update_crc(0xffffffffL, buf, len) ^ 0xffffffffL; + } +} // namespace CRC + +namespace PNG_Support +{ + enum chunkType { + // Critical chunks - (shall appear in this order, except PLTE is optional) + IHDR = 'IHDR', + PLTE = 'PLTE', + IDAT = 'IDAT', + IEND = 'IEND', + // Ancillary chunks - (need not appear in this order) + cHRM = 'cHRM', + gAMA = 'gAMA', + iCCP = 'iCCP', + sBIT = 'sBIT', + sRGB = 'sRGB', + bKGD = 'bKGD', + hIST = 'hIST', + tRNS = 'tRNS', + pHYs = 'pHYs', + sPLT = 'sPLT', + tIME = 'tIME', + iTXt = 'iTXt', + tEXt = 'tEXt', + zTXt = 'zTXt' + + }; + + // ============================================================================================= + + long OpenPNG ( XMP_IO* fileRef, ChunkState & inOutChunkState ) + { + XMP_Uns64 pos = 0; + long name; + XMP_Uns32 len; + + pos = fileRef->Seek ( 8, kXMP_SeekFromStart ); + if (pos != 8) return 0; + + // read first and following chunks + while ( ReadChunk ( fileRef, inOutChunkState, &name, &len, pos) ) {} + + return (long)inOutChunkState.chunks.size(); + + } + + // ============================================================================================= + + bool ReadChunk ( XMP_IO* fileRef, ChunkState & inOutChunkState, long * chunkType, XMP_Uns32 * chunkLength, XMP_Uns64 & inOutPosition ) + { + try + { + XMP_Uns64 startPosition = inOutPosition; + long bytesRead; + char buffer[4]; + + bytesRead = fileRef->Read ( buffer, 4 ); + if ( bytesRead != 4 ) return false; + inOutPosition += 4; + *chunkLength = GetUns32BE(buffer); + + bytesRead = fileRef->Read ( buffer, 4 ); + if ( bytesRead != 4 ) return false; + inOutPosition += 4; + *chunkType = GetUns32BE(buffer); + + inOutPosition += *chunkLength; + + bytesRead = fileRef->Read ( buffer, 4 ); + if ( bytesRead != 4 ) return false; + inOutPosition += 4; + long crc = GetUns32BE(buffer); + + ChunkData newChunk; + + newChunk.pos = startPosition; + newChunk.len = *chunkLength; + newChunk.type = *chunkType; + + // check for XMP in iTXt-chunk + if (newChunk.type == iTXt) + { + CheckiTXtChunkHeader(fileRef, inOutChunkState, newChunk); + } + + inOutChunkState.chunks.push_back ( newChunk ); + + fileRef->Seek ( inOutPosition, kXMP_SeekFromStart ); + + } catch ( ... ) { + + return false; + + } + + return true; + + } + + // ============================================================================================= + + bool WriteXMPChunk ( XMP_IO* fileRef, XMP_Uns32 len, const char* inBuffer ) + { + bool ret = false; + unsigned long datalen = (4 + ITXT_HEADER_LEN + len); + unsigned char* buffer = new unsigned char[datalen]; + + try + { + size_t pos = 0; + memcpy(&buffer[pos], ITXT_CHUNK_TYPE, 4); + pos += 4; + memcpy(&buffer[pos], ITXT_HEADER_DATA, ITXT_HEADER_LEN); + pos += ITXT_HEADER_LEN; + memcpy(&buffer[pos], inBuffer, len); + + unsigned long crc_value = MakeUns32BE( CalculateCRC( buffer, datalen )); + datalen -= 4; + unsigned long len_value = MakeUns32BE( datalen ); + datalen += 4; + + fileRef->Write ( &len_value, 4 ); + fileRef->Write ( buffer, datalen ); + fileRef->Write ( &crc_value, 4 ); + + ret = true; + } + catch ( ... ) {} + + delete [] buffer; + + return ret; + } + + // ============================================================================================= + + bool CopyChunk ( XMP_IO* sourceRef, XMP_IO* destRef, ChunkData& chunk ) + { + try + { + sourceRef->Seek ( chunk.pos, kXMP_SeekFromStart ); + XIO::Copy (sourceRef, destRef, (chunk.len + 12)); + + } catch ( ... ) { + + return false; + + } + + return true; + } + + // ============================================================================================= + + unsigned long UpdateChunkCRC( XMP_IO* fileRef, ChunkData& inOutChunkData ) + { + unsigned long ret = 0; + unsigned long datalen = (inOutChunkData.len + 4); + unsigned char* buffer = new unsigned char[datalen]; + + try + { + fileRef->Seek ( (inOutChunkData.pos + 4), kXMP_SeekFromStart ); + + size_t pos = 0; + long bytesRead = fileRef->Read ( &buffer[pos], (inOutChunkData.len + 4) ); + + unsigned long crc = CalculateCRC( buffer, (inOutChunkData.len + 4) ); + unsigned long crc_value = MakeUns32BE( crc ); + + fileRef->Seek ( (inOutChunkData.pos + 4 + 4 + inOutChunkData.len), kXMP_SeekFromStart ); + fileRef->Write ( &crc_value, 4 ); + + ret = crc; + } + catch ( ... ) {} + + delete [] buffer; + + return ret; + } + + // ============================================================================================= + + bool CheckIHDRChunkHeader ( ChunkData& inOutChunkData ) + { + return (inOutChunkData.type == IHDR); + } + + // ============================================================================================= + + unsigned long CheckiTXtChunkHeader ( XMP_IO* fileRef, ChunkState& inOutChunkState, ChunkData& inOutChunkData ) + { + try + { + fileRef->Seek ( (inOutChunkData.pos + 8), kXMP_SeekFromStart ); + + char buffer[ITXT_HEADER_LEN]; + long bytesRead = fileRef->Read ( buffer, ITXT_HEADER_LEN ); + + if (bytesRead == ITXT_HEADER_LEN) + { + if (memcmp(buffer, ITXT_HEADER_DATA, ITXT_HEADER_LEN) == 0) + { + // return length of XMP + if (inOutChunkData.len > ITXT_HEADER_LEN) + { + inOutChunkState.xmpPos = inOutChunkData.pos + 8 + ITXT_HEADER_LEN; + inOutChunkState.xmpLen = inOutChunkData.len - ITXT_HEADER_LEN; + inOutChunkState.xmpChunk = inOutChunkData; + inOutChunkData.xmp = true; + + return inOutChunkState.xmpLen; + } + } + } + } + catch ( ... ) {} + + return 0; + } + + bool ReadBuffer ( XMP_IO* fileRef, XMP_Uns64 & pos, XMP_Uns32 len, char * outBuffer ) + { + try + { + if ( (fileRef == 0) || (outBuffer == 0) ) return false; + + fileRef->Seek ( pos, kXMP_SeekFromStart ); + long bytesRead = fileRef->Read ( outBuffer, len ); + if ( XMP_Uns32(bytesRead) != len ) return false; + + return true; + } + catch ( ... ) {} + + return false; + } + + bool WriteBuffer ( XMP_IO* fileRef, XMP_Uns64 & pos, XMP_Uns32 len, const char * inBuffer ) + { + try + { + if ( (fileRef == 0) || (inBuffer == 0) ) return false; + + fileRef->Seek ( pos, kXMP_SeekFromStart ); + fileRef->Write ( inBuffer, len ); + + return true; + } + catch ( ... ) {} + + return false; + } + + unsigned long CalculateCRC( unsigned char* inBuffer, XMP_Uns32 len ) + { + return CRC::update_crc(0xffffffffL, inBuffer, len) ^ 0xffffffffL; + } + +} // namespace PNG_Support diff --git a/XMPFiles/source/FormatSupport/PNG_Support.hpp b/XMPFiles/source/FormatSupport/PNG_Support.hpp new file mode 100644 index 0000000..6b0afbf --- /dev/null +++ b/XMPFiles/source/FormatSupport/PNG_Support.hpp @@ -0,0 +1,78 @@ +#ifndef __PNG_Support_hpp__ +#define __PNG_Support_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2007 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" + +#define PNG_SIGNATURE_LEN 8 +#define PNG_SIGNATURE_DATA "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A" + +#define ITXT_CHUNK_TYPE "iTXt" + +#define ITXT_HEADER_LEN 22 +#define ITXT_HEADER_DATA "XML:com.adobe.xmp\0\0\0\0\0" + +namespace PNG_Support +{ + class ChunkData + { + public: + ChunkData() : pos(0), len(0), type(0), xmp(false) {} + virtual ~ChunkData() {} + + // | length | type | data | crc(type+data) | + // | 4 | 4 | val(length) | 4 | + // + XMP_Uns64 pos; // file offset of chunk + XMP_Uns32 len; // length of chunk data + long type; // name/type of chunk + bool xmp; // iTXt-chunk with XMP ? + }; + + typedef std::vector<ChunkData> ChunkVector; + typedef ChunkVector::iterator ChunkIterator; + + class ChunkState + { + public: + ChunkState() : xmpPos(0), xmpLen(0) {} + virtual ~ChunkState() {} + + XMP_Uns64 xmpPos; + XMP_Uns32 xmpLen; + ChunkData xmpChunk; + ChunkVector chunks; /* vector of chunks */ + }; + + long OpenPNG ( XMP_IO* fileRef, ChunkState& inOutChunkState ); + + bool ReadChunk ( XMP_IO* fileRef, ChunkState& inOutChunkState, long* chunkType, XMP_Uns32* chunkLength, XMP_Uns64& inOutPosition ); + bool WriteXMPChunk ( XMP_IO* fileRef, XMP_Uns32 len, const char* inBuffer ); + bool CopyChunk ( XMP_IO* sourceRef, XMP_IO* destRef, ChunkData& chunk ); + unsigned long UpdateChunkCRC( XMP_IO* fileRef, ChunkData& inOutChunkData ); + + bool CheckIHDRChunkHeader ( ChunkData& inOutChunkData ); + unsigned long CheckiTXtChunkHeader ( XMP_IO* fileRef, ChunkState& inOutChunkState, ChunkData& inOutChunkData ); + + bool ReadBuffer ( XMP_IO* fileRef, XMP_Uns64& pos, XMP_Uns32 len, char* outBuffer ); + bool WriteBuffer ( XMP_IO* fileRef, XMP_Uns64& pos, XMP_Uns32 len, const char* inBuffer ); + + unsigned long CalculateCRC( unsigned char* inBuffer, XMP_Uns32 len ); + +} // namespace PNG_Support + +#endif // __PNG_Support_hpp__ diff --git a/XMPFiles/source/FormatSupport/PSIR_FileWriter.cpp b/XMPFiles/source/FormatSupport/PSIR_FileWriter.cpp new file mode 100644 index 0000000..aecfec1 --- /dev/null +++ b/XMPFiles/source/FormatSupport/PSIR_FileWriter.cpp @@ -0,0 +1,602 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/PSIR_Support.hpp" +#include "source/EndianUtils.hpp" +#include "source/XIO.hpp" + +#include <string.h> + +// ================================================================================================= +/// \file PSIR_FileWriter.cpp +/// \brief Implementation of the file-based or read-write form of PSIR_Manager. +// ================================================================================================= + +// ================================================================================================= +// IsMetadataImgRsrc +// ================= + +static inline bool IsMetadataImgRsrc ( XMP_Uns16 id ) +{ + if ( id == 0 ) return false; + + int i; + for ( i = 0; id < kPSIR_MetadataIDs[i]; ++i ) {} + if ( id == kPSIR_MetadataIDs[i] ) return true; + return false; + +} // IsMetadataImgRsrc + +// ================================================================================================= +// PSIR_FileWriter::DeleteExistingInfo +// =================================== +// +// Delete all existing info about image resources. + +void PSIR_FileWriter::DeleteExistingInfo() +{ + XMP_Assert ( ! (this->memParsed && this->fileParsed) ); + + if ( this->memParsed ) { + if ( this->ownedContent ) free ( this->memContent ); + } else if ( this->fileParsed ) { + InternalRsrcMap::iterator irPos = this->imgRsrcs.begin(); + InternalRsrcMap::iterator irEnd = this->imgRsrcs.end(); + for ( ; irPos != irEnd; ++irPos ) irPos->second.changed = true; // Fool the InternalRsrcInfo destructor. + } + + this->imgRsrcs.clear(); + + this->memContent = 0; + this->memLength = 0; + + this->changed = false; + this->legacyDeleted = false; + this->memParsed = false; + this->fileParsed = false; + this->ownedContent = false; + +} // PSIR_FileWriter::DeleteExistingInfo + +// ================================================================================================= +// PSIR_FileWriter::~PSIR_FileWriter +// ================================= + +PSIR_FileWriter::~PSIR_FileWriter() +{ + XMP_Assert ( ! (this->memParsed && this->fileParsed) ); + + if ( this->ownedContent ) { + XMP_Assert ( this->memContent != 0 ); + free ( this->memContent ); + } + +} // PSIR_FileWriter::~PSIR_FileWriter + +// ================================================================================================= +// PSIR_FileWriter::GetImgRsrc +// =========================== + +bool PSIR_FileWriter::GetImgRsrc ( XMP_Uns16 id, ImgRsrcInfo* info ) const +{ + InternalRsrcMap::const_iterator rsrcPos = this->imgRsrcs.find ( id ); + if ( rsrcPos == this->imgRsrcs.end() ) return false; + + const InternalRsrcInfo & rsrcInfo = rsrcPos->second; + + if ( info != 0 ) { + info->id = rsrcInfo.id; + info->dataLen = rsrcInfo.dataLen; + info->dataPtr = rsrcInfo.dataPtr; + info->origOffset = rsrcInfo.origOffset; + } + + return true; + +} // PSIR_FileWriter::GetImgRsrc + +// ================================================================================================= +// PSIR_FileWriter::SetImgRsrc +// =========================== + +void PSIR_FileWriter::SetImgRsrc ( XMP_Uns16 id, const void* clientPtr, XMP_Uns32 length ) +{ + InternalRsrcInfo* rsrcPtr = 0; + InternalRsrcMap::iterator rsrcPos = this->imgRsrcs.find ( id ); + + if ( rsrcPos == this->imgRsrcs.end() ) { + + // This resource is not yet in the map, create the map entry. + InternalRsrcMap::value_type mapValue ( id, InternalRsrcInfo ( id, length, this->fileParsed ) ); + rsrcPos = this->imgRsrcs.insert ( rsrcPos, mapValue ); + rsrcPtr = &rsrcPos->second; + + } else { + + rsrcPtr = &rsrcPos->second; + + // The resource already exists, make sure the value is actually changing. + if ( (length == rsrcPtr->dataLen) && + (memcmp ( rsrcPtr->dataPtr, clientPtr, length ) == 0) ) { + return; + } + + rsrcPtr->FreeData(); // Release any existing data allocation. + rsrcPtr->dataLen = length; // And this might be changing. + + } + + rsrcPtr->changed = true; + rsrcPtr->dataPtr = malloc ( length ); + if ( rsrcPtr->dataPtr == 0 ) XMP_Throw ( "Out of memory", kXMPErr_NoMemory ); + memcpy ( rsrcPtr->dataPtr, clientPtr, length ); // AUDIT: Safe, malloc'ed length bytes above. + + this->changed = true; + +} // PSIR_FileWriter::SetImgRsrc + +// ================================================================================================= +// PSIR_FileWriter::DeleteImgRsrc +// ============================== + +void PSIR_FileWriter::DeleteImgRsrc ( XMP_Uns16 id ) +{ + InternalRsrcMap::iterator rsrcPos = this->imgRsrcs.find ( id ); + if ( rsrcPos == this->imgRsrcs.end() ) return; // Nothing to delete. + + this->imgRsrcs.erase ( id ); + this->changed = true; + if ( id != kPSIR_XMP ) this->legacyDeleted = true; + +} // PSIR_FileWriter::DeleteImgRsrc + +// ================================================================================================= +// PSIR_FileWriter::IsLegacyChanged +// ================================ + +bool PSIR_FileWriter::IsLegacyChanged() +{ + + if ( ! this->changed ) return false; + if ( this->legacyDeleted ) return true; + + InternalRsrcMap::iterator irPos = this->imgRsrcs.begin(); + InternalRsrcMap::iterator irEnd = this->imgRsrcs.end(); + + for ( ; irPos != irEnd; ++irPos ) { + const InternalRsrcInfo & rsrcInfo = irPos->second; + if ( rsrcInfo.changed && (rsrcInfo.id != kPSIR_XMP) ) return true; + } + + return false; // Can get here if the XMP is the only thing changed. + +} // PSIR_FileWriter::IsLegacyChanged + +// ================================================================================================= +// PSIR_FileWriter::ParseMemoryResources +// ===================================== + +void PSIR_FileWriter::ParseMemoryResources ( const void* data, XMP_Uns32 length, bool copyData /* = true */ ) +{ + this->DeleteExistingInfo(); + this->memParsed = true; + if ( length == 0 ) return; + + // Allocate space for the full in-memory data and copy it. + + if ( ! copyData ) { + this->memContent = (XMP_Uns8*) data; + XMP_Assert ( ! this->ownedContent ); + } else { + if ( length > 100*1024*1024 ) XMP_Throw ( "Outrageous length for memory-based PSIR", kXMPErr_BadPSIR ); + this->memContent = (XMP_Uns8*) malloc ( length ); + if ( this->memContent == 0 ) XMP_Throw ( "Out of memory", kXMPErr_NoMemory ); + memcpy ( this->memContent, data, length ); // AUDIT: Safe, malloc'ed length bytes above. + this->ownedContent = true; + } + this->memLength = length; + + // Capture the info for all of the resources. We're using a map keyed by ID, so only one + // resource of each ID is recognized. Redundant resources are not legit, but have been seen in + // the field. In particular, one case has been seen of a duplicate IIM block with one empty. + // In general we keep the first seen copy to be compatible with Photoshop. A later non-empty + // copy will be taken though if the current one is empty. + + // ! Don't use map[id] to lookup, that creates a default entry if none exists! + + XMP_Uns8* psirPtr = this->memContent; + XMP_Uns8* psirEnd = psirPtr + length; + XMP_Uns8* psirLimit = psirEnd - kMinImgRsrcSize; + + while ( psirPtr <= psirLimit ) { + + XMP_Uns8* origin = psirPtr; // The beginning of this resource. + XMP_Uns32 type = GetUns32BE(psirPtr); + XMP_Uns16 id = GetUns16BE(psirPtr+4); + psirPtr += 6; // Advance to the resource name. + + XMP_Uns8* namePtr = psirPtr; + XMP_Uns16 nameLen = namePtr[0]; // ! The length for the Pascal string, w/ room for "+2". + psirPtr += ((nameLen + 2) & 0xFFFE); // ! Round up to an even offset. Yes, +2! + + if ( psirPtr > psirEnd-4 ) break; // Bad image resource. Throw instead? + + XMP_Uns32 dataLen = GetUns32BE(psirPtr); + psirPtr += 4; // Advance to the resource data. + + XMP_Uns32 dataOffset = (XMP_Uns32) ( psirPtr - this->memContent ); + XMP_Uns8* nextRsrc = psirPtr + ((dataLen + 1) & 0xFFFFFFFEUL); // ! Round up to an even offset. + + if ( (dataLen > length) || (psirPtr > psirEnd-dataLen) ) break; // Bad image resource. Throw instead? + + if ( type != k8BIM ) { + + XMP_Uns32 rsrcOffset = XMP_Uns32( origin - this->memContent ); + XMP_Uns32 rsrcLength = XMP_Uns32( nextRsrc - origin ); // Includes trailing pad. + XMP_Assert ( (rsrcLength & 1) == 0 ); + this->otherRsrcs.push_back ( OtherRsrcInfo ( rsrcOffset, rsrcLength ) ); + + } else { + + InternalRsrcInfo newInfo ( id, dataLen, kIsMemoryBased ); + newInfo.dataPtr = psirPtr; + newInfo.origOffset = dataOffset; + if ( nameLen != 0 ) newInfo.rsrcName = namePtr; + + InternalRsrcMap::iterator rsrcPos = this->imgRsrcs.find ( id ); + if ( rsrcPos == this->imgRsrcs.end() ) { + this->imgRsrcs.insert ( rsrcPos, InternalRsrcMap::value_type ( id, newInfo ) ); + } else if ( (rsrcPos->second.dataLen == 0) && (newInfo.dataLen != 0) ) { + rsrcPos->second = newInfo; + } + + } + + psirPtr = nextRsrc; + + } + +} // PSIR_FileWriter::ParseMemoryResources + +// ================================================================================================= +// PSIR_FileWriter::ParseFileResources +// =================================== + +void PSIR_FileWriter::ParseFileResources ( XMP_IO* fileRef, XMP_Uns32 length ) +{ + bool ok; + + this->DeleteExistingInfo(); + this->fileParsed = true; + if ( length == 0 ) return; + + // Parse the image resource block. We're using a map keyed by ID, so only one resource of each + // ID is recognized. Redundant resources are not legit, but have been seen in the field. In + // particular, one case has been seen of a duplicate IIM block with one empty. In general we + // keep the first seen copy to be compatible with Photoshop. A later non-empty copy will be + // taken though if the current one is empty. + + // ! Don't use map[id] to lookup, that creates a default entry if none exists! + + IOBuffer ioBuf; + ioBuf.filePos = fileRef->Offset(); + + XMP_Int64 psirOrigin = ioBuf.filePos; // Need this to determine the resource data offsets. + XMP_Int64 fileEnd = ioBuf.filePos + length; + + std::string rsrcPName; + + while ( (ioBuf.filePos + (ioBuf.ptr - ioBuf.data)) < fileEnd ) { + + ok = CheckFileSpace ( fileRef, &ioBuf, 12 ); // The minimal image resource takes 12 bytes. + if ( ! ok ) break; // Bad image resource. Throw instead? + + XMP_Int64 thisRsrcPos = ioBuf.filePos + (ioBuf.ptr - ioBuf.data); + + XMP_Uns32 type = GetUns32BE(ioBuf.ptr); + XMP_Uns16 id = GetUns16BE(ioBuf.ptr+4); + ioBuf.ptr += 6; // Advance to the resource name. + + XMP_Uns16 nameLen = ioBuf.ptr[0]; // ! The length for the Pascal string. + XMP_Uns16 paddedLen = (nameLen + 2) & 0xFFFE; // ! Round up to an even total. Yes, +2! + ok = CheckFileSpace ( fileRef, &ioBuf, paddedLen+4 ); // Get the name text and the data length. + if ( ! ok ) break; // Bad image resource. Throw instead? + + if ( nameLen > 0 ) rsrcPName.assign ( (char*)(ioBuf.ptr), paddedLen ); // ! Include the length byte and pad. + + ioBuf.ptr += paddedLen; // Move to the data length. + XMP_Uns32 dataLen = GetUns32BE(ioBuf.ptr); + XMP_Uns32 dataTotal = ((dataLen + 1) & 0xFFFFFFFEUL); // Round up to an even total. + ioBuf.ptr += 4; // Advance to the resource data. + + XMP_Int64 thisDataPos = ioBuf.filePos + (ioBuf.ptr - ioBuf.data); + XMP_Int64 nextRsrcPos = thisDataPos + dataTotal; + + if ( type != k8BIM ) { + XMP_Uns32 fullRsrcLen = (XMP_Uns32) (nextRsrcPos - thisRsrcPos); + this->otherRsrcs.push_back ( OtherRsrcInfo ( (XMP_Uns32)thisRsrcPos, fullRsrcLen ) ); + MoveToOffset ( fileRef, nextRsrcPos, &ioBuf ); + continue; + } + + InternalRsrcInfo newInfo ( id, dataLen, kIsFileBased ); + InternalRsrcMap::iterator rsrcPos = this->imgRsrcs.find ( id ); + if ( rsrcPos == this->imgRsrcs.end() ) { + rsrcPos = this->imgRsrcs.insert ( rsrcPos, InternalRsrcMap::value_type ( id, newInfo ) ); + } else if ( (rsrcPos->second.dataLen == 0) && (newInfo.dataLen != 0) ) { + rsrcPos->second = newInfo; + } else { + MoveToOffset ( fileRef, nextRsrcPos, &ioBuf ); + continue; + } + InternalRsrcInfo* rsrcPtr = &rsrcPos->second; + + rsrcPtr->origOffset = (XMP_Uns32)thisDataPos; + + if ( nameLen > 0 ) { + rsrcPtr->rsrcName = (XMP_Uns8*) malloc ( paddedLen ); + if ( rsrcPtr->rsrcName == 0 ) XMP_Throw ( "Out of memory", kXMPErr_NoMemory ); + memcpy ( (void*)rsrcPtr->rsrcName, rsrcPName.c_str(), paddedLen ); // AUDIT: Safe, allocated enough bytes above. + } + + if ( ! IsMetadataImgRsrc ( id ) ) { + MoveToOffset ( fileRef, nextRsrcPos, &ioBuf ); + continue; + } + + rsrcPtr->dataPtr = malloc ( dataLen ); // ! Allocate after the IsMetadataImgRsrc check. + if ( rsrcPtr->dataPtr == 0 ) XMP_Throw ( "Out of memory", kXMPErr_NoMemory ); + + if ( dataTotal <= kIOBufferSize ) { + // The image resource data fits within the I/O buffer. + ok = CheckFileSpace ( fileRef, &ioBuf, dataTotal ); + if ( ! ok ) break; // Bad image resource. Throw instead? + memcpy ( (void*)rsrcPtr->dataPtr, ioBuf.ptr, dataLen ); // AUDIT: Safe, malloc'ed dataLen bytes above. + ioBuf.ptr += dataTotal; // ! Add the rounded length. + } else { + // The image resource data is bigger than the I/O buffer. + fileRef->Seek ( (ioBuf.filePos + (ioBuf.ptr - ioBuf.data)), kXMP_SeekFromStart ); + fileRef->ReadAll ( (void*)rsrcPtr->dataPtr, dataLen ); + FillBuffer ( fileRef, nextRsrcPos, &ioBuf ); + } + + } + + #if 0 + { + printf ( "\nPSIR_FileWriter::ParseFileResources, count = %d\n", this->imgRsrcs.size() ); + InternalRsrcMap::iterator irPos = this->imgRsrcs.begin(); + InternalRsrcMap::iterator irEnd = this->imgRsrcs.end(); + for ( ; irPos != irEnd; ++irPos ) { + InternalRsrcInfo& thisRsrc = irPos->second; + printf ( " #%d, dataLen %d, origOffset %d (0x%X)%s\n", + thisRsrc.id, thisRsrc.dataLen, thisRsrc.origOffset, thisRsrc.origOffset, + (thisRsrc.changed ? ", changed" : "") ); + } + } + #endif + +} // PSIR_FileWriter::ParseFileResources + +// ================================================================================================= +// PSIR_FileWriter::UpdateMemoryResources +// ====================================== + +XMP_Uns32 PSIR_FileWriter::UpdateMemoryResources ( void** dataPtr ) +{ + if ( this->fileParsed ) XMP_Throw ( "Not memory based", kXMPErr_EnforceFailure ); + + // Compute the size and allocate the new image resource block. + + XMP_Uns32 newLength = 0; + + InternalRsrcMap::iterator irPos = this->imgRsrcs.begin(); + InternalRsrcMap::iterator irEnd = this->imgRsrcs.end(); + + for ( ; irPos != irEnd; ++irPos ) { // Add in the lengths for the 8BIM resources. + const InternalRsrcInfo & rsrcInfo = irPos->second; + newLength += 10; + newLength += ((rsrcInfo.dataLen + 1) & 0xFFFFFFFEUL); + if ( rsrcInfo.rsrcName == 0 ) { + newLength += 2; + } else { + XMP_Uns32 nameLen = rsrcInfo.rsrcName[0]; + newLength += ((nameLen + 2) & 0xFFFFFFFEUL); // ! Yes, +2 for the length and rounding. + } + } + + for ( size_t i = 0; i < this->otherRsrcs.size(); ++i ) { // Add in the non-8BIM resources. + newLength += this->otherRsrcs[i].rsrcLength; + } + + XMP_Uns8* newContent = (XMP_Uns8*) malloc ( newLength ); + if ( newContent == 0 ) XMP_Throw ( "Out of memory", kXMPErr_NoMemory ); + + // Fill in the new image resource block. + + XMP_Uns8* rsrcPtr = newContent; + + for ( irPos = this->imgRsrcs.begin(); irPos != irEnd; ++irPos ) { // Do the 8BIM resources. + + const InternalRsrcInfo & rsrcInfo = irPos->second; + + PutUns32BE ( k8BIM, rsrcPtr ); + rsrcPtr += 4; + PutUns16BE ( rsrcInfo.id, rsrcPtr ); + rsrcPtr += 2; + + if ( rsrcInfo.rsrcName == 0 ) { + PutUns16BE ( 0, rsrcPtr ); + rsrcPtr += 2; + } else { + XMP_Uns32 nameLen = rsrcInfo.rsrcName[0]; + XMP_Assert ( nameLen > 0 ); + if ( (nameLen+1) > (newLength - (rsrcPtr - newContent)) ) { + XMP_Throw ( "Buffer overrun", kXMPErr_InternalFailure ); + } + memcpy ( rsrcPtr, rsrcInfo.rsrcName, nameLen+1 ); // AUDIT: Protected by the above check. + rsrcPtr += nameLen+1; + if ( (nameLen & 1) == 0 ) { + *rsrcPtr = 0; // Round to an even total. + ++rsrcPtr; + } + } + + PutUns32BE ( rsrcInfo.dataLen, rsrcPtr ); + rsrcPtr += 4; + if ( rsrcInfo.dataLen > (newLength - (rsrcPtr - newContent)) ) { + XMP_Throw ( "Buffer overrun", kXMPErr_InternalFailure ); + } + memcpy ( rsrcPtr, rsrcInfo.dataPtr, rsrcInfo.dataLen ); // AUDIT: Protected by the above check. + rsrcPtr += rsrcInfo.dataLen; + if ( (rsrcInfo.dataLen & 1) != 0 ) { // Pad to an even length if necessary. + *rsrcPtr = 0; + ++rsrcPtr; + } + + } + + for ( size_t i = 0; i < this->otherRsrcs.size(); ++i ) { // Do the non-8BIM resources. + XMP_Uns8* srcPtr = this->memContent + this->otherRsrcs[i].rsrcOffset; + XMP_Uns32 srcLen = this->otherRsrcs[i].rsrcLength; + if ( srcLen > (newLength - (rsrcPtr - newContent)) ) { + XMP_Throw ( "Buffer overrun", kXMPErr_InternalFailure ); + } + memcpy ( rsrcPtr, srcPtr, srcLen ); // AUDIT: Protected by the above check. + rsrcPtr += srcLen; // No need to pad, included in the original resource length. + } + + XMP_Assert ( rsrcPtr == (newContent + newLength) ); + + // Parse the rebuilt image resource block. This is the easiest way to reconstruct the map. + + this->ParseMemoryResources ( newContent, newLength, false ); + this->ownedContent = (newLength > 0); // ! We really do own the new content, if not empty. + + if ( dataPtr != 0 ) *dataPtr = newContent; + return newLength; + +} // PSIR_FileWriter::UpdateMemoryResources + +// ================================================================================================= +// PSIR_FileWriter::UpdateFileResources +// ==================================== + +XMP_Uns32 PSIR_FileWriter::UpdateFileResources ( XMP_IO* sourceRef, XMP_IO* destRef, + void * _ioBuf, XMP_AbortProc abortProc, void * abortArg ) +{ + IgnoreParam(_ioBuf); + const XMP_Uns32 zero32 = 0; + + const bool checkAbort = (abortProc != 0); + + struct RsrcHeader { + XMP_Uns32 type; + XMP_Uns16 id; + }; + XMP_Assert ( (offsetof(RsrcHeader,type) == 0) && (offsetof(RsrcHeader,id) == 4) ); + + if ( this->memParsed ) XMP_Throw ( "Not file based", kXMPErr_EnforceFailure ); + + XMP_Int64 destLenOffset = destRef->Offset(); + XMP_Uns32 destLength = 0; + + destRef->Write ( &destLength, 4 ); // Write a placeholder for the new PSIR section length. + + #if 0 + { + printf ( "\nPSIR_FileWriter::UpdateFileResources, count = %d\n", this->imgRsrcs.size() ); + InternalRsrcMap::iterator irPos = this->imgRsrcs.begin(); + InternalRsrcMap::iterator irEnd = this->imgRsrcs.end(); + for ( ; irPos != irEnd; ++irPos ) { + InternalRsrcInfo& thisRsrc = irPos->second; + printf ( " #%d, dataLen %d, origOffset %d (0x%X)%s\n", + thisRsrc.id, thisRsrc.dataLen, thisRsrc.origOffset, thisRsrc.origOffset, + (thisRsrc.changed ? ", changed" : "") ); + } + } + #endif + + // First write all of the '8BIM' resources from the map. Use the internal data if present, else + // copy the data from the file. + + RsrcHeader outHeader; + outHeader.type = MakeUns32BE ( k8BIM ); + + InternalRsrcMap::iterator rsrcPos = this->imgRsrcs.begin(); + InternalRsrcMap::iterator rsrcEnd = this->imgRsrcs.end(); + + // printf ( "\nPSIR_FileWriter::UpdateFileResources - 8BIM resources\n" ); + for ( ; rsrcPos != rsrcEnd; ++rsrcPos ) { + + InternalRsrcInfo& currRsrc = rsrcPos->second; + + outHeader.id = MakeUns16BE ( currRsrc.id ); + destRef->Write ( &outHeader, 6 ); + destLength += 6; + + if ( currRsrc.rsrcName == 0 ) { + destRef->Write ( &zero32, 2 ); + destLength += 2; + } else { + XMP_Uns16 nameLen = currRsrc.rsrcName[0]; // ! Include room for +1. + XMP_Assert ( nameLen > 0 ); + XMP_Uns16 paddedLen = (nameLen + 2) & 0xFFFE; // ! Round up to an even total. Yes, +2! + destRef->Write ( currRsrc.rsrcName, paddedLen ); + destLength += paddedLen; + } + + XMP_Uns32 dataLen = MakeUns32BE ( currRsrc.dataLen ); + destRef->Write ( &dataLen, 4 ); + // printf ( " #%d, offset %d (0x%X), dataLen %d\n", currRsrc.id, destLength, destLength, currRsrc.dataLen ); + + if ( currRsrc.dataPtr != 0 ) { + destRef->Write ( currRsrc.dataPtr, currRsrc.dataLen ); + } else { + sourceRef->Seek ( currRsrc.origOffset, kXMP_SeekFromStart ); + XIO::Copy ( sourceRef, destRef, currRsrc.dataLen ); + } + + destLength += 4 + currRsrc.dataLen; + + if ( (currRsrc.dataLen & 1) != 0 ) { + destRef->Write ( &zero32, 1 ); // ! Pad the data to an even length. + ++destLength; + } + + } + + // Now write all of the non-8BIM resources. Copy the entire resource chunk from the source file. + + // printf ( "\nPSIR_FileWriter::UpdateFileResources - other resources\n" ); + for ( size_t i = 0; i < this->otherRsrcs.size(); ++i ) { + // printf ( " offset %d (0x%X), length %d", + // this->otherRsrcs[i].rsrcOffset, this->otherRsrcs[i].rsrcOffset, this->otherRsrcs[i].rsrcLength ); + sourceRef->Seek ( this->otherRsrcs[i].rsrcOffset, kXMP_SeekFromStart ); + XIO::Copy ( sourceRef, destRef, this->otherRsrcs[i].rsrcLength ); + destLength += this->otherRsrcs[i].rsrcLength; // Alignment padding is already included. + } + + // Write the final PSIR section length, seek back to the end of the file, return the length. + + // printf ( "\nPSIR_FileWriter::UpdateFileResources - final length %d (0x%X)\n", destLength, destLength ); + destRef->Seek ( destLenOffset, kXMP_SeekFromStart ); + XMP_Uns32 outLen = MakeUns32BE ( destLength ); + destRef->Write ( &outLen, 4 ); + destRef->Seek ( 0, kXMP_SeekFromEnd ); + + // *** Not rebuilding the internal map - turns out we never want it, why pay for the I/O. + // *** Should probably add an option for all of these cases, memory and file based. + + return destLength; + +} // PSIR_FileWriter::UpdateFileResources diff --git a/XMPFiles/source/FormatSupport/PSIR_MemoryReader.cpp b/XMPFiles/source/FormatSupport/PSIR_MemoryReader.cpp new file mode 100644 index 0000000..7432554 --- /dev/null +++ b/XMPFiles/source/FormatSupport/PSIR_MemoryReader.cpp @@ -0,0 +1,112 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/PSIR_Support.hpp" +#include "source/EndianUtils.hpp" +#include "source/XIO.hpp" + +#include <string.h> + +// ================================================================================================= +/// \file PSIR_MemoryReader.cpp +/// \brief Implementation of the memory-based read-only form of PSIR_Manager. +// ================================================================================================= + +// ================================================================================================= +// PSIR_MemoryReader::GetImgRsrc +// ============================= + +bool PSIR_MemoryReader::GetImgRsrc ( XMP_Uns16 id, ImgRsrcInfo* info ) const +{ + ImgRsrcMap::const_iterator rsrcPos = this->imgRsrcs.find ( id ); + if ( rsrcPos == this->imgRsrcs.end() ) return false; + + if ( info != 0 ) *info = rsrcPos->second; + return true; + +} // PSIR_MemoryReader::GetImgRsrc + +// ================================================================================================= +// PSIR_MemoryReader::ParseMemoryResources +// ======================================= + +void PSIR_MemoryReader::ParseMemoryResources ( const void* data, XMP_Uns32 length, bool copyData /* = true */ ) +{ + // Get rid of any existing image resources. + + if ( this->ownedContent ) free ( this->psirContent ); + this->ownedContent = false; + this->psirContent = 0; + this->psirLength = 0; + this->imgRsrcs.clear(); + + if ( length == 0 ) return; + + // Allocate space for the full in-memory data and copy it. + + if ( ! copyData ) { + this->psirContent = (XMP_Uns8*) data; + XMP_Assert ( ! this->ownedContent ); + } else { + if ( length > 100*1024*1024 ) XMP_Throw ( "Outrageous length for memory-based PSIR", kXMPErr_BadPSIR ); + this->psirContent = (XMP_Uns8*) malloc(length); + if ( this->psirContent == 0 ) XMP_Throw ( "Out of memory", kXMPErr_NoMemory ); + memcpy ( this->psirContent, data, length ); // AUDIT: Safe, malloc'ed length bytes above. + this->ownedContent = true; + } + + this->psirLength = length; + + // Capture the info for all of the resources. We're using a map keyed by ID, so only one + // resource of each ID is recognized. Redundant resources are not legit, but have been seen in + // the field. In particular, one case has been seen of a duplicate IIM block with one empty. + // In general we keep the first seen copy to be compatible with Photoshop. A later non-empty + // copy will be taken though if the current one is empty. + + // ! Don't use map[id] to lookup, that creates a default entry if none exists! + + XMP_Uns8* psirPtr = this->psirContent; + XMP_Uns8* psirEnd = psirPtr + length; + XMP_Uns8* psirLimit = psirEnd - kMinImgRsrcSize; + + while ( psirPtr <= psirLimit ) { + + XMP_Uns32 type = GetUns32BE(psirPtr); + XMP_Uns16 id = GetUns16BE(psirPtr+4); + psirPtr += 6; // Advance to the resource name. + + XMP_Uns16 nameLen = psirPtr[0]; // ! The length for the Pascal string, w/ room for "+2". + psirPtr += ((nameLen + 2) & 0xFFFE); // ! Round up to an even offset. Yes, +2! + + if ( psirPtr > psirEnd-4 ) break; // Bad image resource. Throw instead? + + XMP_Uns32 dataLen = GetUns32BE(psirPtr); + psirPtr += 4; // Advance to the resource data. + XMP_Uns32 psirOffset = (XMP_Uns32) (psirPtr - this->psirContent); + + if ( (dataLen > length) || (psirPtr > psirEnd-dataLen) ) break; // Bad image resource. Throw instead? + + if ( type == k8BIM ) { // For read-only usage we ignore everything other than '8BIM' resources. + ImgRsrcInfo newInfo ( id, dataLen, psirPtr, psirOffset ); + ImgRsrcMap::iterator rsrcPos = this->imgRsrcs.find ( id ); + if ( rsrcPos == this->imgRsrcs.end() ) { + this->imgRsrcs.insert ( rsrcPos, ImgRsrcMap::value_type ( id, newInfo ) ); + } else if ( (rsrcPos->second.dataLen == 0) && (newInfo.dataLen != 0) ) { + rsrcPos->second = newInfo; + } + } + + psirPtr += ((dataLen + 1) & 0xFFFFFFFEUL); // ! Round up to an even offset. + + } + +} // PSIR_MemoryReader::ParseMemoryResources diff --git a/XMPFiles/source/FormatSupport/PSIR_Support.hpp b/XMPFiles/source/FormatSupport/PSIR_Support.hpp new file mode 100644 index 0000000..b0ec13a --- /dev/null +++ b/XMPFiles/source/FormatSupport/PSIR_Support.hpp @@ -0,0 +1,328 @@ +#ifndef __PSIR_Support_hpp__ +#define __PSIR_Support_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" + +#include "source/EndianUtils.hpp" + +#include <map> + +// ================================================================================================= +/// \file PSIR_Support.hpp +/// \brief XMPFiles support for Photoshop image resources. +/// +/// This header provides Photoshop image resource (PSIR) support specific to the needs of XMPFiles. +/// This is not intended for general purpose PSIR processing. PSIR_Manager is an abstract base +/// class with 2 concrete derived classes, PSIR_MemoryReader and PSIR_FileWriter. +/// +/// PSIR_MemoryReader provides read-only support for PSIR streams that are small enough to be kept +/// entirely in memory. This allows optimizations to reduce heap usage and processing code. It is +/// sufficient for browsing access to the image resources (mainly the IPTC) in JPEG files. Think of +/// PSIR_MemoryReader as "memory-based AND read-only". +/// +/// PSIR_FileWriter is for cases where updates are needed or the PSIR stream is too large to be kept +/// entirely in memory. Think of PSIR_FileWriter as "file-based OR read-write". +/// +/// The needs of XMPFiles are well defined metadata access. Only a few image resources are handled. +/// This is the case for all of the derived classes, even though the memory based ones happen to +/// have all of the image resources in memory. Being "handled" means being in the image resource +/// map used by GetImgRsrc. The handled image resources are: +/// \li 1028 - IPTC +/// \li 1034 - Copyrighted flag +/// \li 1035 - Copyright information URL +/// \li 1058 - Exif metadata +/// \li 1060 - XMP +/// \li 1061 - IPTC digest +/// +/// \note These classes are for use only when directly compiled and linked. They should not be +/// packaged in a DLL by themselves. They do not provide any form of C++ ABI protection. +// ================================================================================================= + + +// These aren't inside PSIR_Manager because the static array can't be initialized there. + +enum { + k8BIM = 0x3842494DUL, // The 4 ASCII characters '8BIM'. + kMinImgRsrcSize = 4+2+2+4 // The minimum size for an image resource. +}; + +enum { + kPSIR_IPTC = 1028, + kPSIR_CopyrightFlag = 1034, + kPSIR_CopyrightURL = 1035, + kPSIR_Exif = 1058, + kPSIR_XMP = 1060, + kPSIR_IPTCDigest = 1061 +}; + +enum { kPSIR_MetadataCount = 6 }; +static const XMP_Uns16 kPSIR_MetadataIDs[] = // ! Must be in descending order with 0 sentinel. + { kPSIR_IPTCDigest, kPSIR_XMP, kPSIR_Exif, kPSIR_CopyrightURL, kPSIR_CopyrightFlag, kPSIR_IPTC, 0 }; + +// ================================================================================================= +// ================================================================================================= + +// NOTE: Although Photoshop image resources have a type and ID, for metadatya we only care about +// those of type "8BIM". Resources of other types are preserved in files, but can't be individually +// accessed through the PSIR_Manager API. + +// ================================================================================================= +// PSIR_Manager +// ============ + +class PSIR_Manager { // The abstract base class. +public: + + // --------------------------------------------------------------------------------------------- + // Types and constants + + struct ImgRsrcInfo { + XMP_Uns16 id; + XMP_Uns32 dataLen; + const void* dataPtr; // ! The data is read-only! + XMP_Uns32 origOffset; // The offset (at parse time) of the resource data. + ImgRsrcInfo() : id(0), dataLen(0), dataPtr(0), origOffset(0) {}; + ImgRsrcInfo ( XMP_Uns16 _id, XMP_Uns32 _dataLen, void* _dataPtr, XMP_Uns32 _origOffset ) + : id(_id), dataLen(_dataLen), dataPtr(_dataPtr), origOffset(_origOffset) {}; + }; + + // The origOffset is the absolute file offset for file parses, the memory block offset for + // memory parses. It is the offset of the resource data portion, not the overall resource. + + // --------------------------------------------------------------------------------------------- + // Get the information about a "handled" image resource. Returns false if the image resource is + // not handled, even if it was present in the parsed input. + + virtual bool GetImgRsrc ( XMP_Uns16 id, ImgRsrcInfo* info ) const = 0; + + // --------------------------------------------------------------------------------------------- + // Set the value for an image resource. It can be any resource, even one not originally handled. + + virtual void SetImgRsrc ( XMP_Uns16 id, const void* dataPtr, XMP_Uns32 length ) = 0; + + // --------------------------------------------------------------------------------------------- + // Delete an image resource. Does nothing if the image resource does not exist. + + virtual void DeleteImgRsrc ( XMP_Uns16 id ) = 0; + + // --------------------------------------------------------------------------------------------- + // Determine if the image resources are changed. + + virtual bool IsChanged() = 0; + virtual bool IsLegacyChanged() = 0; + + // --------------------------------------------------------------------------------------------- + + virtual void ParseMemoryResources ( const void* data, XMP_Uns32 length, bool copyData = true ) = 0; + virtual void ParseFileResources ( XMP_IO* fileRef, XMP_Uns32 length ) = 0; + + // --------------------------------------------------------------------------------------------- + // Update the image resources to reflect the changed values. Both \c UpdateMemoryResources and + // \c UpdateFileResources return the new size of the image resource block. The dataPtr returned + // by \c UpdateMemoryResources must be treated as read only. It exists until the PSIR_Manager + // destructor is called. UpdateMemoryResources can be used on a read-only instance to get the + // raw data block info. + + virtual XMP_Uns32 UpdateMemoryResources ( void** dataPtr ) = 0; + virtual XMP_Uns32 UpdateFileResources ( XMP_IO* sourceRef, XMP_IO* destRef, + void * _ioBuf, XMP_AbortProc abortProc, void * abortArg ) = 0; + // *** Temporary hack above, _ioBuf is IOBuffer* but don't want to include XIO.hpp. + + // --------------------------------------------------------------------------------------------- + + virtual ~PSIR_Manager() {}; + +protected: + + PSIR_Manager() {}; + +}; // PSIR_Manager + + +// ================================================================================================= +// ================================================================================================= + + +// ================================================================================================= +// PSIR_MemoryReader +// ================= + +class PSIR_MemoryReader : public PSIR_Manager { // The leaf class for memory-based read-only access. +public: + + bool GetImgRsrc ( XMP_Uns16 id, ImgRsrcInfo* info ) const; + + void SetImgRsrc ( XMP_Uns16 id, const void* dataPtr, XMP_Uns32 length ) { NotAppropriate(); }; + + void DeleteImgRsrc ( XMP_Uns16 id ) { NotAppropriate(); }; + + bool IsChanged() { return false; }; + bool IsLegacyChanged() { return false; }; + + void ParseMemoryResources ( const void* data, XMP_Uns32 length, bool copyData = true ); + void ParseFileResources ( XMP_IO* file, XMP_Uns32 length ) { NotAppropriate(); }; + + XMP_Uns32 UpdateMemoryResources ( void** dataPtr ) { if ( dataPtr != 0 ) *dataPtr = psirContent; return psirLength; }; + XMP_Uns32 UpdateFileResources ( XMP_IO* sourceRef, XMP_IO* destRef, + void * _ioBuf, XMP_AbortProc abortProc, void * abortArg ) { NotAppropriate(); return 0; }; + // *** Temporary hack above, _ioBuf is IOBuffer* but don't want to include XIO.hpp. + + PSIR_MemoryReader() : ownedContent(false), psirLength(0), psirContent(0) {}; + + virtual ~PSIR_MemoryReader() { if ( this->ownedContent ) free ( this->psirContent ); }; + +private: + + // Memory usage notes: PSIR_MemoryReader is for memory-based read-only usage (both apply). There + // is no need to ever allocate separate blocks of memory, everything is used directly from the + // PSIR stream. + + bool ownedContent; + + XMP_Uns32 psirLength; + XMP_Uns8* psirContent; + + typedef std::map<XMP_Uns16,ImgRsrcInfo> ImgRsrcMap; + + ImgRsrcMap imgRsrcs; + + static inline void NotAppropriate() { XMP_Throw ( "Not appropriate for PSIR_Reader", kXMPErr_InternalFailure ); }; + +}; // PSIR_MemoryReader + + +// ================================================================================================= +// ================================================================================================= + + +// ================================================================================================= +// PSIR_FileWriter +// =============== + +class PSIR_FileWriter : public PSIR_Manager { // The leaf class for file-based read-write access. +public: + + bool GetImgRsrc ( XMP_Uns16 id, ImgRsrcInfo* info ) const; + + void SetImgRsrc ( XMP_Uns16 id, const void* dataPtr, XMP_Uns32 length ); + + void DeleteImgRsrc ( XMP_Uns16 id ); + + bool IsChanged() { return this->changed; }; + + bool IsLegacyChanged(); + + void ParseMemoryResources ( const void* data, XMP_Uns32 length, bool copyData = true ); + void ParseFileResources ( XMP_IO* file, XMP_Uns32 length ); + + XMP_Uns32 UpdateMemoryResources ( void** dataPtr ); + XMP_Uns32 UpdateFileResources ( XMP_IO* sourceRef, XMP_IO* destRef, + void * _ioBuf, XMP_AbortProc abortProc, void * abortArg ); + // *** Temporary hack above, _ioBuf is IOBuffer* but don't want to include XIO.hpp. + + PSIR_FileWriter() : changed(false), legacyDeleted(false), memParsed(false), fileParsed(false), + ownedContent(false), memLength(0), memContent(0) {}; + + virtual ~PSIR_FileWriter(); + + // Memory usage notes: PSIR_FileWriter is for file-based OR read/write usage. For memory-based + // streams the dataPtr and rsrcName are initially into the stream. The dataPtr becomes a + // separate allocation when SetImgRsrc is called, the rsrcName stays into the original stream. + // For file-based streams the dataPtr and rsrcName are always a separate allocation. Again, + // the dataPtr changes when SetImgRsrc is called, the rsrcName stays unchanged. + + // ! The working data values are always big endian, no matter where stored. It is the client's + // ! responsibility to flip them as necessary. + + static const bool kIsFileBased = true; // For use in the InternalRsrcInfo constructor. + static const bool kIsMemoryBased = false; + + struct InternalRsrcInfo { + public: + + bool changed; + bool fileBased; + XMP_Uns16 id; + XMP_Uns32 dataLen; + void* dataPtr; // ! Null if the value is not captured! + XMP_Uns32 origOffset; // The offset (at parse time) of the resource data. + XMP_Uns8* rsrcName; // ! A Pascal string, leading length byte, no nul terminator! + + inline void FreeData() { + if ( this->fileBased || this->changed ) { + if ( this->dataPtr != 0 ) { free ( this->dataPtr ); this->dataPtr = 0; } + } + } + + inline void FreeName() { + if ( this->fileBased && (this->rsrcName != 0) ) { + free ( this->rsrcName ); + this->rsrcName = 0; + } + } + + InternalRsrcInfo ( XMP_Uns16 _id, XMP_Uns32 _dataLen, bool _fileBased ) + : changed(false), fileBased(_fileBased), id(_id), dataLen(_dataLen), dataPtr(0), + origOffset(0), rsrcName(0) {}; + ~InternalRsrcInfo() { this->FreeData(); this->FreeName(); }; + + void operator= ( const InternalRsrcInfo & in ) + { + // ! Gag! Transfer ownership of the dataPtr and rsrcName! + this->FreeData(); + memcpy ( this, &in, sizeof(*this) ); // AUDIT: Use of sizeof(InternalRsrcInfo) is safe. + *((void**)&in.dataPtr) = 0; // The pointer is now owned by "this". + *((void**)&in.rsrcName) = 0; // The pointer is now owned by "this". + }; + + private: + + InternalRsrcInfo() // Hidden on purpose, fileBased must be properly set. + : changed(false), fileBased(false), id(0), dataLen(0), dataPtr(0), origOffset(0), rsrcName(0) {}; + + }; + + // The origOffset is the absolute file offset for file parses, the memory block offset for + // memory parses. It is the offset of the resource data portion, not the overall resource. + +private: + + bool changed, legacyDeleted; + bool memParsed, fileParsed; + bool ownedContent; + + XMP_Uns32 memLength; + XMP_Uns8* memContent; + + typedef std::map<XMP_Uns16,InternalRsrcInfo> InternalRsrcMap; + InternalRsrcMap imgRsrcs; + + struct OtherRsrcInfo { // For the resources of types other than "8BIM". + XMP_Uns32 rsrcOffset; // The offset of the resource origin, the type field. + XMP_Uns32 rsrcLength; // The full length of the resource, offset to the next resource. + OtherRsrcInfo() : rsrcOffset(0), rsrcLength(0) {}; + OtherRsrcInfo ( XMP_Uns32 _rsrcOffset, XMP_Uns32 _rsrcLength ) + : rsrcOffset(_rsrcOffset), rsrcLength(_rsrcLength) {}; + }; + std::vector<OtherRsrcInfo> otherRsrcs; + + void DeleteExistingInfo(); + +}; // PSIR_FileWriter + +#endif // __PSIR_Support_hpp__ diff --git a/XMPFiles/source/FormatSupport/QuickTime_Support.cpp b/XMPFiles/source/FormatSupport/QuickTime_Support.cpp new file mode 100644 index 0000000..5a33ab4 --- /dev/null +++ b/XMPFiles/source/FormatSupport/QuickTime_Support.cpp @@ -0,0 +1,1150 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2009 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#if XMP_MacBuild + #include <CoreServices/CoreServices.h> +#else + #include "XMPFiles/source/FormatSupport/MacScriptExtracts.h" +#endif + +#include "XMPFiles/source/FormatSupport/QuickTime_Support.hpp" + +#include "source/UnicodeConversions.hpp" +#include "source/UnicodeInlines.incl_cpp" +#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp" +#include "source/XIO.hpp" + +// ================================================================================================= + +static const char * kMacRomanUTF8 [128] = { // UTF-8 mappings for MacRoman 80..FF. + "\xC3\x84", "\xC3\x85", "\xC3\x87", "\xC3\x89", "\xC3\x91", "\xC3\x96", "\xC3\x9C", "\xC3\xA1", + "\xC3\xA0", "\xC3\xA2", "\xC3\xA4", "\xC3\xA3", "\xC3\xA5", "\xC3\xA7", "\xC3\xA9", "\xC3\xA8", + "\xC3\xAA", "\xC3\xAB", "\xC3\xAD", "\xC3\xAC", "\xC3\xAE", "\xC3\xAF", "\xC3\xB1", "\xC3\xB3", + "\xC3\xB2", "\xC3\xB4", "\xC3\xB6", "\xC3\xB5", "\xC3\xBA", "\xC3\xB9", "\xC3\xBB", "\xC3\xBC", + "\xE2\x80\xA0", "\xC2\xB0", "\xC2\xA2", "\xC2\xA3", "\xC2\xA7", "\xE2\x80\xA2", "\xC2\xB6", "\xC3\x9F", + "\xC2\xAE", "\xC2\xA9", "\xE2\x84\xA2", "\xC2\xB4", "\xC2\xA8", "\xE2\x89\xA0", "\xC3\x86", "\xC3\x98", + "\xE2\x88\x9E", "\xC2\xB1", "\xE2\x89\xA4", "\xE2\x89\xA5", "\xC2\xA5", "\xC2\xB5", "\xE2\x88\x82", "\xE2\x88\x91", + "\xE2\x88\x8F", "\xCF\x80", "\xE2\x88\xAB", "\xC2\xAA", "\xC2\xBA", "\xCE\xA9", "\xC3\xA6", "\xC3\xB8", + "\xC2\xBF", "\xC2\xA1", "\xC2\xAC", "\xE2\x88\x9A", "\xC6\x92", "\xE2\x89\x88", "\xE2\x88\x86", "\xC2\xAB", + "\xC2\xBB", "\xE2\x80\xA6", "\xC2\xA0", "\xC3\x80", "\xC3\x83", "\xC3\x95", "\xC5\x92", "\xC5\x93", + "\xE2\x80\x93", "\xE2\x80\x94", "\xE2\x80\x9C", "\xE2\x80\x9D", "\xE2\x80\x98", "\xE2\x80\x99", "\xC3\xB7", "\xE2\x97\x8A", + "\xC3\xBF", "\xC5\xB8", "\xE2\x81\x84", "\xE2\x82\xAC", "\xE2\x80\xB9", "\xE2\x80\xBA", "\xEF\xAC\x81", "\xEF\xAC\x82", + "\xE2\x80\xA1", "\xC2\xB7", "\xE2\x80\x9A", "\xE2\x80\x9E", "\xE2\x80\xB0", "\xC3\x82", "\xC3\x8A", "\xC3\x81", + "\xC3\x8B", "\xC3\x88", "\xC3\x8D", "\xC3\x8E", "\xC3\x8F", "\xC3\x8C", "\xC3\x93", "\xC3\x94", + "\xEF\xA3\xBF", "\xC3\x92", "\xC3\x9A", "\xC3\x9B", "\xC3\x99", "\xC4\xB1", "\xCB\x86", "\xCB\x9C", + "\xC2\xAF", "\xCB\x98", "\xCB\x99", "\xCB\x9A", "\xC2\xB8", "\xCB\x9D", "\xCB\x9B", "\xCB\x87" +}; + +static const XMP_Uns32 kMacRomanCP [128] = { // Unicode codepoints for MacRoman 80..FF. + 0x00C4, 0x00C5, 0x00C7, 0x00C9, 0x00D1, 0x00D6, 0x00DC, 0x00E1, + 0x00E0, 0x00E2, 0x00E4, 0x00E3, 0x00E5, 0x00E7, 0x00E9, 0x00E8, + 0x00EA, 0x00EB, 0x00ED, 0x00EC, 0x00EE, 0x00EF, 0x00F1, 0x00F3, + 0x00F2, 0x00F4, 0x00F6, 0x00F5, 0x00FA, 0x00F9, 0x00FB, 0x00FC, + 0x2020, 0x00B0, 0x00A2, 0x00A3, 0x00A7, 0x2022, 0x00B6, 0x00DF, + 0x00AE, 0x00A9, 0x2122, 0x00B4, 0x00A8, 0x2260, 0x00C6, 0x00D8, + 0x221E, 0x00B1, 0x2264, 0x2265, 0x00A5, 0x00B5, 0x2202, 0x2211, + 0x220F, 0x03C0, 0x222B, 0x00AA, 0x00BA, 0x03A9, 0x00E6, 0x00F8, + 0x00BF, 0x00A1, 0x00AC, 0x221A, 0x0192, 0x2248, 0x2206, 0x00AB, + 0x00BB, 0x2026, 0x00A0, 0x00C0, 0x00C3, 0x00D5, 0x0152, 0x0153, + 0x2013, 0x2014, 0x201C, 0x201D, 0x2018, 0x2019, 0x00F7, 0x25CA, + 0x00FF, 0x0178, 0x2044, 0x20AC, 0x2039, 0x203A, 0xFB01, 0xFB02, + 0x2021, 0x00B7, 0x201A, 0x201E, 0x2030, 0x00C2, 0x00CA, 0x00C1, + 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF, 0x00CC, 0x00D3, 0x00D4, + 0xF8FF, 0x00D2, 0x00DA, 0x00DB, 0x00D9, 0x0131, 0x02C6, 0x02DC, // ! U+F8FF is private use solid Apple icon. + 0x00AF, 0x02D8, 0x02D9, 0x02DA, 0x00B8, 0x02DD, 0x02DB, 0x02C7 +}; + +// ------------------------------------------------------------------------------------------------- + +static const XMP_Uns16 kMacLangToScript_0_94 [95] = { + + /* langEnglish (0) */ smRoman, + /* langFrench (1) */ smRoman, + /* langGerman (2) */ smRoman, + /* langItalian (3) */ smRoman, + /* langDutch (4) */ smRoman, + /* langSwedish (5) */ smRoman, + /* langSpanish (6) */ smRoman, + /* langDanish (7) */ smRoman, + /* langPortuguese (8) */ smRoman, + /* langNorwegian (9) */ smRoman, + + /* langHebrew (10) */ smHebrew, + /* langJapanese (11) */ smJapanese, + /* langArabic (12) */ smArabic, + /* langFinnish (13) */ smRoman, + /* langGreek (14) */ smRoman, + /* langIcelandic (15) */ smRoman, + /* langMaltese (16) */ smRoman, + /* langTurkish (17) */ smRoman, + /* langCroatian (18) */ smRoman, + /* langTradChinese (19) */ smTradChinese, + + /* langUrdu (20) */ smArabic, + /* langHindi (21) */ smDevanagari, + /* langThai (22) */ smThai, + /* langKorean (23) */ smKorean, + /* langLithuanian (24) */ smCentralEuroRoman, + /* langPolish (25) */ smCentralEuroRoman, + /* langHungarian (26) */ smCentralEuroRoman, + /* langEstonian (27) */ smCentralEuroRoman, + /* langLatvian (28) */ smCentralEuroRoman, + /* langSami (29) */ kNoMacScript, // ! Not known, missing from Apple comments. + + /* langFaroese (30) */ smRoman, + /* langFarsi (31) */ smArabic, + /* langRussian (32) */ smCyrillic, + /* langSimpChinese (33) */ smSimpChinese, + /* langFlemish (34) */ smRoman, + /* langIrishGaelic (35) */ smRoman, + /* langAlbanian (36) */ smRoman, + /* langRomanian (37) */ smRoman, + /* langCzech (38) */ smCentralEuroRoman, + /* langSlovak (39) */ smCentralEuroRoman, + + /* langSlovenian (40) */ smRoman, + /* langYiddish (41) */ smHebrew, + /* langSerbian (42) */ smCyrillic, + /* langMacedonian (43) */ smCyrillic, + /* langBulgarian (44) */ smCyrillic, + /* langUkrainian (45) */ smCyrillic, + /* langBelorussian (46) */ smCyrillic, + /* langUzbek (47) */ smCyrillic, + /* langKazakh (48) */ smCyrillic, + /* langAzerbaijani (49) */ smCyrillic, + + /* langAzerbaijanAr (50) */ smArabic, + /* langArmenian (51) */ smArmenian, + /* langGeorgian (52) */ smGeorgian, + /* langMoldavian (53) */ smCyrillic, + /* langKirghiz (54) */ smCyrillic, + /* langTajiki (55) */ smCyrillic, + /* langTurkmen (56) */ smCyrillic, + /* langMongolian (57) */ smMongolian, + /* langMongolianCyr (58) */ smCyrillic, + /* langPashto (59) */ smArabic, + + /* langKurdish (60) */ smArabic, + /* langKashmiri (61) */ smArabic, + /* langSindhi (62) */ smArabic, + /* langTibetan (63) */ smTibetan, + /* langNepali (64) */ smDevanagari, + /* langSanskrit (65) */ smDevanagari, + /* langMarathi (66) */ smDevanagari, + /* langBengali (67) */ smBengali, + /* langAssamese (68) */ smBengali, + /* langGujarati (69) */ smGujarati, + + /* langPunjabi (70) */ smGurmukhi, + /* langOriya (71) */ smOriya, + /* langMalayalam (72) */ smMalayalam, + /* langKannada (73) */ smKannada, + /* langTamil (74) */ smTamil, + /* langTelugu (75) */ smTelugu, + /* langSinhalese (76) */ smSinhalese, + /* langBurmese (77) */ smBurmese, + /* langKhmer (78) */ smKhmer, + /* langLao (79) */ smLao, + + /* langVietnamese (80) */ smVietnamese, + /* langIndonesian (81) */ smRoman, + /* langTagalog (82) */ smRoman, + /* langMalayRoman (83) */ smRoman, + /* langMalayArabic (84) */ smArabic, + /* langAmharic (85) */ smEthiopic, + /* langTigrinya (86) */ smEthiopic, + /* langOromo (87) */ smEthiopic, + /* langSomali (88) */ smRoman, + /* langSwahili (89) */ smRoman, + + /* langKinyarwanda (90) */ smRoman, + /* langRundi (91) */ smRoman, + /* langNyanja (92) */ smRoman, + /* langMalagasy (93) */ smRoman, + /* langEsperanto (94) */ smRoman + +}; // kMacLangToScript_0_94 + +static const XMP_Uns16 kMacLangToScript_128_151 [24] = { + + /* langWelsh (128) */ smRoman, + /* langBasque (129) */ smRoman, + + /* langCatalan (130) */ smRoman, + /* langLatin (131) */ smRoman, + /* langQuechua (132) */ smRoman, + /* langGuarani (133) */ smRoman, + /* langAymara (134) */ smRoman, + /* langTatar (135) */ smCyrillic, + /* langUighur (136) */ smArabic, + /* langDzongkha (137) */ smTibetan, + /* langJavaneseRom (138) */ smRoman, + /* langSundaneseRom (139) */ smRoman, + + /* langGalician (140) */ smRoman, + /* langAfrikaans (141) */ smRoman, + /* langBreton (142) */ smRoman, + /* langInuktitut (143) */ smEthiopic, + /* langScottishGaelic (144) */ smRoman, + /* langManxGaelic (145) */ smRoman, + /* langIrishGaelicScript (146) */ smRoman, + /* langTongan (147) */ smRoman, + /* langGreekAncient (148) */ smGreek, + /* langGreenlandic (149) */ smRoman, + + /* langAzerbaijanRoman (150) */ smRoman, + /* langNynorsk (151) */ smRoman + +}; // kMacLangToScript_128_151 + +// ------------------------------------------------------------------------------------------------- + +static const char * kMacToXMPLang_0_94 [95] = { + + /* langEnglish (0) */ "en", + /* langFrench (1) */ "fr", + /* langGerman (2) */ "de", + /* langItalian (3) */ "it", + /* langDutch (4) */ "nl", + /* langSwedish (5) */ "sv", + /* langSpanish (6) */ "es", + /* langDanish (7) */ "da", + /* langPortuguese (8) */ "pt", + /* langNorwegian (9) */ "no", + + /* langHebrew (10) */ "he", + /* langJapanese (11) */ "ja", + /* langArabic (12) */ "ar", + /* langFinnish (13) */ "fi", + /* langGreek (14) */ "el", + /* langIcelandic (15) */ "is", + /* langMaltese (16) */ "mt", + /* langTurkish (17) */ "tr", + /* langCroatian (18) */ "hr", + /* langTradChinese (19) */ "zh", + + /* langUrdu (20) */ "ur", + /* langHindi (21) */ "hi", + /* langThai (22) */ "th", + /* langKorean (23) */ "ko", + /* langLithuanian (24) */ "lt", + /* langPolish (25) */ "pl", + /* langHungarian (26) */ "hu", + /* langEstonian (27) */ "et", + /* langLatvian (28) */ "lv", + /* langSami (29) */ "se", + + /* langFaroese (30) */ "fo", + /* langFarsi (31) */ "fa", + /* langRussian (32) */ "ru", + /* langSimpChinese (33) */ "zh", + /* langFlemish (34) */ "nl", + /* langIrishGaelic (35) */ "ga", + /* langAlbanian (36) */ "sq", + /* langRomanian (37) */ "ro", + /* langCzech (38) */ "cs", + /* langSlovak (39) */ "sk", + + /* langSlovenian (40) */ "sl", + /* langYiddish (41) */ "yi", + /* langSerbian (42) */ "sr", + /* langMacedonian (43) */ "mk", + /* langBulgarian (44) */ "bg", + /* langUkrainian (45) */ "uk", + /* langBelorussian (46) */ "be", + /* langUzbek (47) */ "uz", + /* langKazakh (48) */ "kk", + /* langAzerbaijani (49) */ "az", + + /* langAzerbaijanAr (50) */ "az", + /* langArmenian (51) */ "hy", + /* langGeorgian (52) */ "ka", + /* langMoldavian (53) */ "ro", + /* langKirghiz (54) */ "ky", + /* langTajiki (55) */ "tg", + /* langTurkmen (56) */ "tk", + /* langMongolian (57) */ "mn", + /* langMongolianCyr (58) */ "mn", + /* langPashto (59) */ "ps", + + /* langKurdish (60) */ "ku", + /* langKashmiri (61) */ "ks", + /* langSindhi (62) */ "sd", + /* langTibetan (63) */ "bo", + /* langNepali (64) */ "ne", + /* langSanskrit (65) */ "sa", + /* langMarathi (66) */ "mr", + /* langBengali (67) */ "bn", + /* langAssamese (68) */ "as", + /* langGujarati (69) */ "gu", + + /* langPunjabi (70) */ "pa", + /* langOriya (71) */ "or", + /* langMalayalam (72) */ "ml", + /* langKannada (73) */ "kn", + /* langTamil (74) */ "ta", + /* langTelugu (75) */ "te", + /* langSinhalese (76) */ "si", + /* langBurmese (77) */ "my", + /* langKhmer (78) */ "km", + /* langLao (79) */ "lo", + + /* langVietnamese (80) */ "vi", + /* langIndonesian (81) */ "id", + /* langTagalog (82) */ "tl", + /* langMalayRoman (83) */ "ms", + /* langMalayArabic (84) */ "ms", + /* langAmharic (85) */ "am", + /* langTigrinya (86) */ "ti", + /* langOromo (87) */ "om", + /* langSomali (88) */ "so", + /* langSwahili (89) */ "sw", + + /* langKinyarwanda (90) */ "rw", + /* langRundi (91) */ "rn", + /* langNyanja (92) */ "ny", + /* langMalagasy (93) */ "mg", + /* langEsperanto (94) */ "eo" + +}; // kMacToXMPLang_0_94 + +static const char * kMacToXMPLang_128_151 [24] = { + + /* langWelsh (128) */ "cy", + /* langBasque (129) */ "eu", + + /* langCatalan (130) */ "ca", + /* langLatin (131) */ "la", + /* langQuechua (132) */ "qu", + /* langGuarani (133) */ "gn", + /* langAymara (134) */ "ay", + /* langTatar (135) */ "tt", + /* langUighur (136) */ "ug", + /* langDzongkha (137) */ "dz", + /* langJavaneseRom (138) */ "jv", + /* langSundaneseRom (139) */ "su", + + /* langGalician (140) */ "gl", + /* langAfrikaans (141) */ "af", + /* langBreton (142) */ "br", + /* langInuktitut (143) */ "iu", + /* langScottishGaelic (144) */ "gd", + /* langManxGaelic (145) */ "gv", + /* langIrishGaelicScript (146) */ "ga", + /* langTongan (147) */ "to", + /* langGreekAncient (148) */ "", // ! Has no ISO 639-1 2 letter code. + /* langGreenlandic (149) */ "kl", + + /* langAzerbaijanRoman (150) */ "az", + /* langNynorsk (151) */ "nn" + +}; // kMacToXMPLang_128_151 + +// ------------------------------------------------------------------------------------------------- + +#if XMP_WinBuild + +static UINT kMacScriptToWinCP[34] = { + /* smRoman (0) */ 10000, // There don't seem to be symbolic constants. + /* smJapanese (1) */ 10001, // From http://msdn.microsoft.com/en-us/library/dd317756(VS.85).aspx + /* smTradChinese (2) */ 10002, + /* smKorean (3) */ 10003, + /* smArabic (4) */ 10004, + /* smHebrew (5) */ 10005, + /* smGreek (6) */ 10006, + /* smCyrillic (7) */ 10007, + /* smRSymbol (8) */ 0, + /* smDevanagari (9) */ 0, + /* smGurmukhi (10) */ 0, + /* smGujarati (11) */ 0, + /* smOriya (12) */ 0, + /* smBengali (13) */ 0, + /* smTamil (14) */ 0, + /* smTelugu (15) */ 0, + /* smKannada (16) */ 0, + /* smMalayalam (17) */ 0, + /* smSinhalese (18) */ 0, + /* smBurmese (19) */ 0, + /* smKhmer (20) */ 0, + /* smThai (21) */ 10021, + /* smLao (22) */ 0, + /* smGeorgian (23) */ 0, + /* smArmenian (24) */ 0, + /* smSimpChinese (25) */ 10008, + /* smTibetan (26) */ 0, + /* smMongolian (27) */ 0, + /* smEthiopic (28) */ 0, + /* smGeez (28) */ 0, + /* smCentralEuroRoman (29) */ 10029, + /* smVietnamese (30) */ 0, + /* smExtArabic (31) */ 0, + /* smUninterp (32) */ 0 +}; // kMacScriptToWinCP + +static UINT kMacToWinCP_0_94 [95] = { + + /* langEnglish (0) */ 0, + /* langFrench (1) */ 0, + /* langGerman (2) */ 0, + /* langItalian (3) */ 0, + /* langDutch (4) */ 0, + /* langSwedish (5) */ 0, + /* langSpanish (6) */ 0, + /* langDanish (7) */ 0, + /* langPortuguese (8) */ 0, + /* langNorwegian (9) */ 0, + + /* langHebrew (10) */ 10005, + /* langJapanese (11) */ 10001, + /* langArabic (12) */ 10004, + /* langFinnish (13) */ 0, + /* langGreek (14) */ 10006, + /* langIcelandic (15) */ 10079, + /* langMaltese (16) */ 0, + /* langTurkish (17) */ 10081, + /* langCroatian (18) */ 10082, + /* langTradChinese (19) */ 10002, + + /* langUrdu (20) */ 0, + /* langHindi (21) */ 0, + /* langThai (22) */ 10021, + /* langKorean (23) */ 10003, + /* langLithuanian (24) */ 0, + /* langPolish (25) */ 0, + /* langHungarian (26) */ 0, + /* langEstonian (27) */ 0, + /* langLatvian (28) */ 0, + /* langSami (29) */ 0, + + /* langFaroese (30) */ 0, + /* langFarsi (31) */ 0, + /* langRussian (32) */ 0, + /* langSimpChinese (33) */ 10008, + /* langFlemish (34) */ 0, + /* langIrishGaelic (35) */ 0, + /* langAlbanian (36) */ 0, + /* langRomanian (37) */ 10010, + /* langCzech (38) */ 0, + /* langSlovak (39) */ 0, + + /* langSlovenian (40) */ 0, + /* langYiddish (41) */ 0, + /* langSerbian (42) */ 0, + /* langMacedonian (43) */ 0, + /* langBulgarian (44) */ 0, + /* langUkrainian (45) */ 10017, + /* langBelorussian (46) */ 0, + /* langUzbek (47) */ 0, + /* langKazakh (48) */ 0, + /* langAzerbaijani (49) */ 0, + + /* langAzerbaijanAr (50) */ 0, + /* langArmenian (51) */ 0, + /* langGeorgian (52) */ 0, + /* langMoldavian (53) */ 0, + /* langKirghiz (54) */ 0, + /* langTajiki (55) */ 0, + /* langTurkmen (56) */ 0, + /* langMongolian (57) */ 0, + /* langMongolianCyr (58) */ 0, + /* langPashto (59) */ 0, + + /* langKurdish (60) */ 0, + /* langKashmiri (61) */ 0, + /* langSindhi (62) */ 0, + /* langTibetan (63) */ 0, + /* langNepali (64) */ 0, + /* langSanskrit (65) */ 0, + /* langMarathi (66) */ 0, + /* langBengali (67) */ 0, + /* langAssamese (68) */ 0, + /* langGujarati (69) */ 0, + + /* langPunjabi (70) */ 0, + /* langOriya (71) */ 0, + /* langMalayalam (72) */ 0, + /* langKannada (73) */ 0, + /* langTamil (74) */ 0, + /* langTelugu (75) */ 0, + /* langSinhalese (76) */ 0, + /* langBurmese (77) */ 0, + /* langKhmer (78) */ 0, + /* langLao (79) */ 0, + + /* langVietnamese (80) */ 0, + /* langIndonesian (81) */ 0, + /* langTagalog (82) */ 0, + /* langMalayRoman (83) */ 0, + /* langMalayArabic (84) */ 0, + /* langAmharic (85) */ 0, + /* langTigrinya (86) */ 0, + /* langOromo (87) */ 0, + /* langSomali (88) */ 0, + /* langSwahili (89) */ 0, + + /* langKinyarwanda (90) */ 0, + /* langRundi (91) */ 0, + /* langNyanja (92) */ 0, + /* langMalagasy (93) */ 0, + /* langEsperanto (94) */ 0 + +}; // kMacToWinCP_0_94 + +#endif + +// ================================================================================================= +// GetMacScript +// ============ + +static XMP_Uns16 GetMacScript ( XMP_Uns16 macLang ) +{ + XMP_Uns16 macScript = kNoMacScript; + + if ( macLang <= 94 ) { + macScript = kMacLangToScript_0_94[macLang]; + } else if ( (128 <= macLang) && (macLang <= 151) ) { + macScript = kMacLangToScript_0_94[macLang-128]; + } + + return macScript; + +} // GetMacScript + +// ================================================================================================= +// GetWinCP +// ======== + +#if XMP_WinBuild + +static UINT GetWinCP ( XMP_Uns16 macLang ) +{ + UINT winCP = 0; + + if ( macLang <= 94 ) winCP = kMacToWinCP_0_94[macLang]; + + if ( winCP == 0 ) { + XMP_Uns16 macScript = GetMacScript ( macLang ); + if ( macScript != kNoMacScript ) winCP = kMacScriptToWinCP[macScript]; + } + + return winCP; + +} // GetWinCP + +#endif + +// ================================================================================================= +// GetXMPLang +// ========== + +static XMP_StringPtr GetXMPLang ( XMP_Uns16 macLang ) +{ + XMP_StringPtr xmpLang = ""; + + if ( macLang <= 94 ) { + xmpLang = kMacToXMPLang_0_94[macLang]; + } else if ( (128 <= macLang) && (macLang <= 151) ) { + xmpLang = kMacToXMPLang_128_151[macLang-128]; + } + + return xmpLang; + +} // GetXMPLang + +// ================================================================================================= +// GetMacLang +// ========== + +static XMP_Uns16 GetMacLang ( std::string * xmpLang ) +{ + if ( *xmpLang == "" ) return kNoMacLang; + + size_t hyphenPos = xmpLang->find ( '-' ); // Make sure the XMP language is "generic". + if ( hyphenPos != std::string::npos ) xmpLang->erase ( hyphenPos ); + + for ( XMP_Uns16 i = 0; i <= 94; ++i ) { // Using std::map would be faster. + if ( *xmpLang == kMacToXMPLang_0_94[i] ) return i; + } + + for ( XMP_Uns16 i = 128; i <= 151; ++i ) { // Using std::map would be faster. + if ( *xmpLang == kMacToXMPLang_128_151[i-128] ) return i; + } + + return kNoMacLang; + +} // GetMacLang + +// ================================================================================================= +// MacRomanToUTF8 +// ============== + +static void MacRomanToUTF8 ( const std::string & macRoman, std::string * utf8 ) +{ + utf8->erase(); + + for ( XMP_Uns8* chPtr = (XMP_Uns8*)macRoman.c_str(); *chPtr != 0; ++chPtr ) { // ! Don't trust that char is unsigned. + if ( *chPtr < 0x80 ) { + (*utf8) += (char)*chPtr; + } else { + (*utf8) += kMacRomanUTF8[(*chPtr)-0x80]; + } + } + +} // MacRomanToUTF8 + +// ================================================================================================= +// UTF8ToMacRoman +// ============== + +static void UTF8ToMacRoman ( const std::string & utf8, std::string * macRoman ) +{ + macRoman->erase(); + bool inNonMRSpan = false; + + for ( const XMP_Uns8 * chPtr = (XMP_Uns8*)utf8.c_str(); *chPtr != 0; ++chPtr ) { // ! Don't trust that char is unsigned. + if ( *chPtr < 0x80 ) { + (*macRoman) += (char)*chPtr; + inNonMRSpan = false; + } else { + XMP_Uns32 cp = GetCodePoint ( &chPtr ); + --chPtr; // Make room for the loop increment. + XMP_Uns8 mr; + for ( mr = 0; (mr < 0x80) && (cp != kMacRomanCP[mr]); ++mr ) {}; // Using std::map would be faster. + if ( mr < 0x80 ) { + (*macRoman) += (char)(mr+0x80); + inNonMRSpan = false; + } else if ( ! inNonMRSpan ) { + (*macRoman) += '?'; + inNonMRSpan = true; + } + } + } + +} // UTF8ToMacRoman + +// ================================================================================================= +// IsMacLangKnown +// ============== + +static inline bool IsMacLangKnown ( XMP_Uns16 macLang ) +{ + XMP_Uns16 macScript = GetMacScript ( macLang ); + if ( macScript == kNoMacScript ) return false; + + #if XMP_UNIXBuild + if ( macScript != smRoman ) return false; + #elif XMP_WinBuild + if ( GetWinCP(macLang) == 0 ) return false; + #endif + + return true; + +} // IsMacLangKnown + +// ================================================================================================= +// ConvertToMacLang +// ================ + +bool ConvertToMacLang ( const std::string & utf8Value, XMP_Uns16 macLang, std::string * macValue ) +{ + macValue->erase(); + if ( macLang == kNoMacLang ) macLang = 0; // *** Zero is English, ought to use the "active" OS lang. + if ( ! IsMacLangKnown ( macLang ) ) return false; + + #if XMP_MacBuild + XMP_Uns16 macScript = GetMacScript ( macLang ); + ReconcileUtils::UTF8ToMacEncoding ( macScript, macLang, (XMP_Uns8*)utf8Value.c_str(), utf8Value.size(), macValue ); + #elif XMP_UNIXBuild + UTF8ToMacRoman ( utf8Value, macValue ); + #elif XMP_WinBuild + UINT winCP = GetWinCP ( macLang ); + ReconcileUtils::UTF8ToWinEncoding ( winCP, (XMP_Uns8*)utf8Value.c_str(), utf8Value.size(), macValue ); + #endif + + return true; + +} // ConvertToMacLang + +// ================================================================================================= +// ConvertFromMacLang +// ================== + +bool ConvertFromMacLang ( const std::string & macValue, XMP_Uns16 macLang, std::string * utf8Value ) +{ + utf8Value->erase(); + if ( ! IsMacLangKnown ( macLang ) ) return false; + + #if XMP_MacBuild + XMP_Uns16 macScript = GetMacScript ( macLang ); + ReconcileUtils::MacEncodingToUTF8 ( macScript, macLang, (XMP_Uns8*)macValue.c_str(), macValue.size(), utf8Value ); + #elif XMP_UNIXBuild + MacRomanToUTF8 ( macValue, utf8Value ); + #elif XMP_WinBuild + UINT winCP = GetWinCP ( macLang ); + ReconcileUtils::WinEncodingToUTF8 ( winCP, (XMP_Uns8*)macValue.c_str(), macValue.size(), utf8Value ); + #endif + + return true; + +} // ConvertFromMacLang + +// ================================================================================================= +// ================================================================================================= +// TradQT_Manager +// ================================================================================================= +// ================================================================================================= + +// ================================================================================================= +// TradQT_Manager::ParseCachedBoxes +// ================================ +// +// Parse the cached '©...' children of the 'moov'/'udta' box. The contents of each cached box are +// a sequence of "mini boxes" analogous to XMP AltText arrays. Each mini box has a 16-bit size, +// 16-bit language code, and text. The size is only the text size. The language codes are Macintosh +// Script Manager langXyz codes. The text encoding is implicit in the language, see comments in +// Apple's Script.h header. + +bool TradQT_Manager::ParseCachedBoxes ( const MOOV_Manager & moovMgr ) +{ + MOOV_Manager::BoxInfo udtaInfo; + MOOV_Manager::BoxRef udtaRef = moovMgr.GetBox ( "moov/udta", &udtaInfo ); + if ( udtaRef == 0 ) return false; + + for ( XMP_Uns32 i = 0; i < udtaInfo.childCount; ++i ) { + + MOOV_Manager::BoxInfo currInfo; + MOOV_Manager::BoxRef currRef = moovMgr.GetNthChild ( udtaRef, i, &currInfo ); + if ( currRef == 0 ) break; // Sanity check, should not happen. + if ( (currInfo.boxType >> 24) != 0xA9 ) continue; + if ( currInfo.contentSize < 2+2+1 ) continue; // Want enough for a non-empty value. + + InfoMapPos newInfo = this->parsedBoxes.insert ( this->parsedBoxes.end(), + InfoMap::value_type ( currInfo.boxType, ParsedBoxInfo ( currInfo.boxType ) ) ); + std::vector<ValueInfo> * newValues = &newInfo->second.values; + + XMP_Uns8 * boxPtr = (XMP_Uns8*) currInfo.content; + XMP_Uns8 * boxEnd = boxPtr + currInfo.contentSize; + XMP_Uns16 miniLen, macLang; + + for ( ; boxPtr < boxEnd-4; boxPtr += miniLen ) { + + miniLen = 4 + GetUns16BE ( boxPtr ); // ! Include header in local miniLen. + macLang = GetUns16BE ( boxPtr+2); + if ( (miniLen <= 4) || (miniLen > (boxEnd - boxPtr)) ) continue; // Ignore bad or empty values. + + XMP_StringPtr valuePtr = (char*)(boxPtr+4); + size_t valueLen = miniLen - 4; + + newValues->push_back ( ValueInfo() ); + ValueInfo * newValue = &newValues->back(); + + // Only set the XMP language if the Mac script is known, i.e. the value can be converted. + + newValue->macLang = macLang; + if ( IsMacLangKnown ( macLang ) ) newValue->xmpLang = GetXMPLang ( macLang ); + newValue->macValue.assign ( valuePtr, valueLen ); + + } + + } + + return (! this->parsedBoxes.empty()); + +} // TradQT_Manager::ParseCachedBoxes + +// ================================================================================================= +// TradQT_Manager::ImportSimpleXMP +// =============================== +// +// Update a simple XMP property if the QT value looks newer. + +bool TradQT_Manager::ImportSimpleXMP ( XMP_Uns32 id, SXMPMeta * xmp, XMP_StringPtr ns, XMP_StringPtr prop ) const +{ + + try { + + InfoMapCPos infoPos = this->parsedBoxes.find ( id ); + if ( infoPos == this->parsedBoxes.end() ) return false; + if ( infoPos->second.values.empty() ) return false; + + std::string xmpValue, tempValue; + XMP_OptionBits flags; + bool xmpExists = xmp->GetProperty ( ns, prop, &xmpValue, &flags ); + if ( xmpExists && (! XMP_PropIsSimple ( flags )) ) { + XMP_Throw ( "TradQT_Manager::ImportSimpleXMP - XMP property must be simple", kXMPErr_BadParam ); + } + + bool convertOK; + const ValueInfo & qtItem = infoPos->second.values[0]; // ! Use the first QT entry. + + if ( xmpExists ) { + convertOK = ConvertToMacLang ( xmpValue, qtItem.macLang, &tempValue ); + if ( ! convertOK ) return false; // throw? + if ( tempValue == qtItem.macValue ) return false; // QT value matches back converted XMP value. + } + + convertOK = ConvertFromMacLang ( qtItem.macValue, qtItem.macLang, &tempValue ); + if ( ! convertOK ) return false; // throw? + xmp->SetProperty ( ns, prop, tempValue.c_str() ); + return true; + + } catch ( ... ) { + + return false; // Don't let one failure abort other imports. + + } + +} // TradQT_Manager::ImportSimpleXMP + +// ================================================================================================= +// TradQT_Manager::ImportLangItem +// ============================== +// +// Update a specific XMP AltText item if the QuickTime value looks newer. + +bool TradQT_Manager::ImportLangItem ( const ValueInfo & qtItem, SXMPMeta * xmp, + XMP_StringPtr ns, XMP_StringPtr langArray ) const +{ + + try { + + XMP_StringPtr genericLang, specificLang; + if ( qtItem.xmpLang[0] != 0 ) { + genericLang = qtItem.xmpLang; + specificLang = qtItem.xmpLang; + } else { + genericLang = ""; + specificLang = "x-default"; + } + + bool convertOK; + std::string xmpValue, tempValue, actualLang; + bool xmpExists = xmp->GetLocalizedText ( ns, langArray, genericLang, specificLang, &actualLang, &xmpValue, 0 ); + if ( xmpExists ) { + convertOK = ConvertToMacLang ( xmpValue, qtItem.macLang, &tempValue ); + if ( ! convertOK ) return false; // throw? + if ( tempValue == qtItem.macValue ) return true; // QT value matches back converted XMP value. + specificLang = actualLang.c_str(); + } + + convertOK = ConvertFromMacLang ( qtItem.macValue, qtItem.macLang, &tempValue ); + if ( ! convertOK ) return false; // throw? + xmp->SetLocalizedText ( ns, langArray, "", specificLang, tempValue.c_str() ); + return true; + + } catch ( ... ) { + + return false; // Don't let one failure abort other imports. + + } + +} // TradQT_Manager::ImportLangItem + +// ================================================================================================= +// TradQT_Manager::ImportLangAltXMP +// ================================ +// +// Update items in the XMP array if the QT value looks newer. + +bool TradQT_Manager::ImportLangAltXMP ( XMP_Uns32 id, SXMPMeta * xmp, XMP_StringPtr ns, XMP_StringPtr langArray ) const +{ + + try { + + InfoMapCPos infoPos = this->parsedBoxes.find ( id ); + if ( infoPos == this->parsedBoxes.end() ) return false; + if ( infoPos->second.values.empty() ) return false; // Quit now if there are no values. + + XMP_OptionBits flags; + bool xmpExists = xmp->GetProperty ( ns, langArray, 0, &flags ); + if ( ! xmpExists ) { + xmp->SetProperty ( ns, langArray, 0, kXMP_PropArrayIsAltText ); + } else if ( ! XMP_ArrayIsAltText ( flags ) ) { + XMP_Throw ( "TradQT_Manager::ImportLangAltXMP - XMP array must be AltText", kXMPErr_BadParam ); + } + + // Process all of the QT values, looking up the appropriate XMP language for each. + + bool haveMappings = false; + const ValueVector & qtValues = infoPos->second.values; + + for ( size_t i = 0, limit = qtValues.size(); i < limit; ++i ) { + const ValueInfo & qtItem = qtValues[i]; + if ( *qtItem.xmpLang == 0 ) continue; // Only do known mappings in the loop. + haveMappings |= this->ImportLangItem ( qtItem, xmp, ns, langArray ); + } + + if ( ! haveMappings ) { + // If nothing mapped, process the first QT item to XMP's "x-default". + haveMappings = this->ImportLangItem ( qtValues[0], xmp, ns, langArray ); // ! No xmpLang implies "x-default". + } + + return haveMappings; + + } catch ( ... ) { + + return false; // Don't let one failure abort other imports. + + } + +} // TradQT_Manager::ImportLangAltXMP + +// ================================================================================================= +// TradQT_Manager::ExportSimpleXMP +// =============================== +// +// Export a simple XMP value to the first existing QuickTime item. Delete all of the QT values if the +// XMP value is empty or the XMP does not exist. + +// ! We don't create new QuickTime items since we don't know the language. + +void TradQT_Manager::ExportSimpleXMP ( XMP_Uns32 id, const SXMPMeta & xmp, XMP_StringPtr ns, XMP_StringPtr prop, + bool createWithZeroLang /* = false */ ) +{ + std::string xmpValue, macValue; + + InfoMapPos infoPos = this->parsedBoxes.find ( id ); + bool qtFound = (infoPos != this->parsedBoxes.end()) && (! infoPos->second.values.empty()); + + bool xmpFound = xmp.GetProperty ( ns, prop, &xmpValue, 0 ); + if ( (! xmpFound) || (xmpValue.empty()) ) { + if ( qtFound ) { + this->parsedBoxes.erase ( infoPos ); + this->changed = true; + } + return; + } + + XMP_Assert ( xmpFound ); + if ( ! qtFound ) { + if ( ! createWithZeroLang ) return; + infoPos = this->parsedBoxes.insert ( this->parsedBoxes.end(), + InfoMap::value_type ( id, ParsedBoxInfo ( id ) ) ); + ValueVector * newValues = &infoPos->second.values; + newValues->push_back ( ValueInfo() ); + ValueInfo * newValue = &newValues->back(); + newValue->macLang = 0; // Happens to be langEnglish. + newValue->xmpLang = kMacToXMPLang_0_94[0]; + this->changed = infoPos->second.changed = true; + } + + ValueInfo * qtItem = &infoPos->second.values[0]; // ! Use the first QT entry. + if ( ! IsMacLangKnown ( qtItem->macLang ) ) return; + + bool convertOK = ConvertToMacLang ( xmpValue, qtItem->macLang, &macValue ); + if ( convertOK && (macValue != qtItem->macValue) ) { + qtItem->macValue = macValue; + this->changed = infoPos->second.changed = true; + } + +} // TradQT_Manager::ExportSimpleXMP + +// ================================================================================================= +// TradQT_Manager::ExportLangAltXMP +// ================================ +// +// Export XMP LangAlt array items to QuickTime, where the language and encoding mappings are known. +// If there are no known language and encoding mappings, map the XMP default item to the first +// existing QuickTime item. + +void TradQT_Manager::ExportLangAltXMP ( XMP_Uns32 id, const SXMPMeta & xmp, XMP_StringPtr ns, XMP_StringPtr langArray ) +{ + bool haveMappings = false; + std::string xmpPath, xmpValue, xmpLang, macValue; + + InfoMapPos infoPos = this->parsedBoxes.find ( id ); + if ( infoPos == this->parsedBoxes.end() ) { + infoPos = this->parsedBoxes.insert ( this->parsedBoxes.end(), + InfoMap::value_type ( id, ParsedBoxInfo ( id ) ) ); + } + + ValueVector * qtValues = &infoPos->second.values; + XMP_Index xmpCount = xmp.CountArrayItems ( ns, langArray ); + bool convertOK; + + if ( xmpCount == 0 ) { + // Delete the "mappable" QuickTime items if there are no XMP values. Leave the others alone. + for ( int i = (int)qtValues->size()-1; i > 0; --i ) { // ! Need a signed index. + if ( (*qtValues)[i].xmpLang[0] != 0 ) { + qtValues->erase ( qtValues->begin() + i ); + this->changed = infoPos->second.changed = true; + } + } + return; + } + + // Go through the XMP and look for a related macLang QuickTime item to update or create. + + for ( XMP_Index xmpIndex = 1; xmpIndex <= xmpCount; ++xmpIndex ) { // ! XMP index starts at 1! + + SXMPUtils::ComposeArrayItemPath ( ns, langArray, xmpIndex, &xmpPath ); + xmp.GetProperty ( ns, xmpPath.c_str(), &xmpValue, 0 ); + xmp.GetQualifier ( ns, xmpPath.c_str(), kXMP_NS_XML, "lang", &xmpLang, 0 ); + if ( xmpLang == "x-default" ) continue; + + XMP_Uns16 macLang = GetMacLang ( &xmpLang ); + if ( macLang == kNoMacLang ) continue; + + size_t qtIndex, qtLimit; + for ( qtIndex = 0, qtLimit = qtValues->size(); qtIndex < qtLimit; ++qtIndex ) { + if ( (*qtValues)[qtIndex].macLang == macLang ) break; + } + + if ( qtIndex == qtLimit ) { + // No existing QuickTime item, try to create one. + if ( ! IsMacLangKnown ( macLang ) ) continue; + qtValues->push_back ( ValueInfo() ); + qtIndex = qtValues->size() - 1; + ValueInfo * newItem = &((*qtValues)[qtIndex]); + newItem->macLang = macLang; + newItem->xmpLang = GetXMPLang ( macLang ); // ! Use the 2 character root language. + } + + ValueInfo * qtItem = &((*qtValues)[qtIndex]); + qtItem->marked = true; // Mark it whether updated or not, don't delete it in the next pass. + + convertOK = ConvertToMacLang ( xmpValue, qtItem->macLang, &macValue ); + if ( convertOK && (macValue != qtItem->macValue) ) { + qtItem->macValue.swap ( macValue ); // No need to make a copy. + haveMappings = true; + } + + } + this->changed |= haveMappings; + infoPos->second.changed |= haveMappings; + + // Go through the QuickTime items that are unmarked and delete those that have an xmpLang + // and known macScript. Clear all marks. + + for ( int i = (int)qtValues->size()-1; i > 0; --i ) { // ! Need a signed index. + ValueInfo * qtItem = &((*qtValues)[i]); + if ( qtItem->marked ) { + qtItem->marked = false; + } else if ( (qtItem->xmpLang[0] != 0) && IsMacLangKnown ( qtItem->macLang ) ) { + qtValues->erase ( qtValues->begin() + i ); + this->changed = infoPos->second.changed = true; + } + } + + // If there were no mappings, export the XMP default item to the first QT item. + + if ( (! haveMappings) && (! qtValues->empty()) ) { + + bool ok = xmp.GetLocalizedText ( ns, langArray, "", "x-default", 0, &xmpValue, 0 ); + if ( ! ok ) return; + + ValueInfo * qtItem = &((*qtValues)[0]); + if ( ! IsMacLangKnown ( qtItem->macLang ) ) return; + + convertOK = ConvertToMacLang ( xmpValue, qtItem->macLang, &macValue ); + if ( convertOK && (macValue != qtItem->macValue) ) { + qtItem->macValue.swap ( macValue ); // No need to make a copy. + this->changed = infoPos->second.changed = true; + } + + } + +} // TradQT_Manager::ExportLangAltXMP + +// ================================================================================================= +// TradQT_Manager::UpdateChangedBoxes +// ================================== + +void TradQT_Manager::UpdateChangedBoxes ( MOOV_Manager * moovMgr ) +{ + MOOV_Manager::BoxInfo udtaInfo; + MOOV_Manager::BoxRef udtaRef = moovMgr->GetBox ( "moov/udta", &udtaInfo ); + XMP_Assert ( (udtaRef != 0) || (udtaInfo.childCount == 0) ); + + if ( udtaRef != 0 ) { // Might not have been a moov/udta box in the parse. + + // First go through the moov/udta/©... children and delete those that are not in the map. + + for ( XMP_Uns32 ordinal = udtaInfo.childCount; ordinal > 0; --ordinal ) { // ! Go backwards because of deletions. + + MOOV_Manager::BoxInfo currInfo; + MOOV_Manager::BoxRef currRef = moovMgr->GetNthChild ( udtaRef, (ordinal-1), &currInfo ); + if ( currRef == 0 ) break; // Sanity check, should not happen. + if ( (currInfo.boxType >> 24) != 0xA9 ) continue; + if ( currInfo.contentSize < 2+2+1 ) continue; // These were skipped by ParseCachedBoxes. + + InfoMapPos infoPos = this->parsedBoxes.find ( currInfo.boxType ); + if ( infoPos == this->parsedBoxes.end() ) moovMgr->DeleteNthChild ( udtaRef, (ordinal-1) ); + + } + + } + + // Now go through the changed items in the map and update them in the moov/udta subtree. + + InfoMapCPos infoPos = this->parsedBoxes.begin(); + InfoMapCPos infoEnd = this->parsedBoxes.end(); + + for ( ; infoPos != infoEnd; ++infoPos ) { + + ParsedBoxInfo * qtItem = (ParsedBoxInfo*) &infoPos->second; + if ( ! qtItem->changed ) continue; + qtItem->changed = false; + + XMP_Uns32 qtTotalSize = 0; // Total size of the QT values, ignoring empty values. + for ( size_t i = 0, limit = qtItem->values.size(); i < limit; ++i ) { + if ( ! qtItem->values[i].macValue.empty() ) { + if ( qtItem->values[i].macValue.size() > 0xFFFF ) qtItem->values[i].macValue.erase ( 0xFFFF ); + qtTotalSize += (XMP_Uns32)(2+2 + qtItem->values[i].macValue.size()); + } + } + + if ( udtaRef == 0 ) { // Might not have been a moov/udta box in the parse. + moovMgr->SetBox ( "moov/udta", 0, 0 ); + udtaRef = moovMgr->GetBox ( "moov/udta", &udtaInfo ); + XMP_Assert ( udtaRef != 0 ); + } + + if ( qtTotalSize == 0 ) { + + moovMgr->DeleteTypeChild ( udtaRef, qtItem->id ); + + } else { + + // Compose the complete box content. + + RawDataBlock fullValue; + fullValue.assign ( qtTotalSize, 0 ); + XMP_Uns8 * valuePtr = &fullValue[0]; + + for ( size_t i = 0, limit = qtItem->values.size(); i < limit; ++i ) { + XMP_Assert ( qtItem->values[i].macValue.size() <= 0xFFFF ); + XMP_Uns16 textSize = (XMP_Uns16)qtItem->values[i].macValue.size(); + if ( textSize == 0 ) continue; + PutUns16BE ( textSize, valuePtr ); valuePtr += 2; + PutUns16BE ( qtItem->values[i].macLang, valuePtr ); valuePtr += 2; + memcpy ( valuePtr, qtItem->values[i].macValue.c_str(), textSize ); valuePtr += textSize; + } + + // Look for an existing box to update, else add a new one. + + MOOV_Manager::BoxInfo itemInfo; + MOOV_Manager::BoxRef itemRef = moovMgr->GetTypeChild ( udtaRef, qtItem->id, &itemInfo ); + + if ( itemRef != 0 ) { + moovMgr->SetBox ( itemRef, &fullValue[0], qtTotalSize ); + } else { + moovMgr->AddChildBox ( udtaRef, qtItem->id, &fullValue[0], qtTotalSize ); + } + + } + + } + +} // TradQT_Manager::UpdateChangedBoxes + +// ================================================================================================= diff --git a/XMPFiles/source/FormatSupport/QuickTime_Support.hpp b/XMPFiles/source/FormatSupport/QuickTime_Support.hpp new file mode 100644 index 0000000..691e746 --- /dev/null +++ b/XMPFiles/source/FormatSupport/QuickTime_Support.hpp @@ -0,0 +1,105 @@ +#ifndef __QuickTime_Support_hpp__ +#define __QuickTime_Support_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2009 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include <string> +#include <vector> +#include <map> + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "XMPFiles/source/FormatSupport/ISOBaseMedia_Support.hpp" +#include "XMPFiles/source/FormatSupport/MOOV_Support.hpp" + +// ================================================================================================= +// ================================================================================================= + +// ================================================================================================= +// TradQT_Manager +// ============== + +// Support for selected traditional QuickTime metadata items. The supported items are the children +// of the 'moov'/'udta' box whose type begins with 0xA9, a MacRoman copyright symbol. Each of these +// is a box whose contents are a sequence of "mini boxes" analogous to XMP AltText arrays. Each mini +// box has a 16-bit size, 16-bit language code, and text. The language code values are the old +// Macintosh Script Manager langXyz codes, the text encoding is implicit, see Mac Script.h. + +enum { // List of recognized items from the QuickTime 'moov'/'udta' box. + // These items are defined by Adobe. + kQTilst_Reel = 0xA952454CUL, // '©REL' + kQTilst_Timecode = 0xA954494DUL, // '©TIM' + kQTilst_TimeScale = 0xA9545343UL, // '©TSC' + kQTilst_TimeSize = 0xA954535AUL // '©TSZ' +}; + +enum { + kNoMacLang = 0xFFFF, + kNoMacScript = 0xFFFF +}; + +extern bool ConvertToMacLang ( const std::string & utf8Value, XMP_Uns16 macLang, std::string * macValue ); +extern bool ConvertFromMacLang ( const std::string & macValue, XMP_Uns16 macLang, std::string * utf8Value ); + +class TradQT_Manager { +public: + + TradQT_Manager() : changed(false) {}; + + bool ParseCachedBoxes ( const MOOV_Manager & moovMgr ); + + bool ImportSimpleXMP ( XMP_Uns32 id, SXMPMeta * xmp, XMP_StringPtr ns, XMP_StringPtr prop ) const; + bool ImportLangAltXMP ( XMP_Uns32 id, SXMPMeta * xmp, XMP_StringPtr ns, XMP_StringPtr langArray ) const; + + void ExportSimpleXMP ( XMP_Uns32 id, const SXMPMeta & xmp, XMP_StringPtr ns, XMP_StringPtr prop, + bool createWithZeroLang = false ); + void ExportLangAltXMP ( XMP_Uns32 id, const SXMPMeta & xmp, XMP_StringPtr ns, XMP_StringPtr langArray ); + + bool IsChanged() const { return this->changed; }; + + void UpdateChangedBoxes ( MOOV_Manager * moovMgr ); + +private: + + struct ValueInfo { + bool marked; + XMP_Uns16 macLang; + XMP_StringPtr xmpLang; // ! Only set if macLang is known, i.e. the value can be converted. + std::string macValue; + ValueInfo() : marked(false), macLang(kNoMacLang), xmpLang("") {}; + }; + typedef std::vector<ValueInfo> ValueVector; + typedef ValueVector::iterator ValueInfoPos; + typedef ValueVector::const_iterator ValueInfoCPos; + + struct ParsedBoxInfo { + XMP_Uns32 id; + ValueVector values; + bool changed; + ParsedBoxInfo() : id(0), changed(false) {}; + ParsedBoxInfo ( XMP_Uns32 _id ) : id(_id), changed(false) {}; + }; + + typedef std::map < XMP_Uns32, ParsedBoxInfo > InfoMap; // Metadata item kind and content info. + typedef InfoMap::iterator InfoMapPos; + typedef InfoMap::const_iterator InfoMapCPos; + + InfoMap parsedBoxes; + bool changed; + + bool ImportLangItem ( const ValueInfo & qtItem, SXMPMeta * xmp, XMP_StringPtr ns, XMP_StringPtr langArray ) const; + +}; // TradQT_Manager + +#endif // __QuickTime_Support_hpp__ diff --git a/XMPFiles/source/FormatSupport/RIFF.cpp b/XMPFiles/source/FormatSupport/RIFF.cpp new file mode 100644 index 0000000..dcf55f2 --- /dev/null +++ b/XMPFiles/source/FormatSupport/RIFF.cpp @@ -0,0 +1,882 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2009 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +// must have access to handler class fields... +#include "XMPFiles/source/FormatSupport/RIFF.hpp" +#include "XMPFiles/source/FormatSupport/RIFF_Support.hpp" +#include "XMPFiles/source/FileHandlers/RIFF_Handler.hpp" + +#include <utility> + +using namespace RIFF; + +namespace RIFF { + +// GENERAL STATIC FUNCTIONS //////////////////////////////////////// + +Chunk* getChunk ( ContainerChunk* parent, RIFF_MetaHandler* handler ) +{ + XMP_IO* file = handler->parent->ioRef; + XMP_Uns8 level = handler->level; + XMP_Uns32 peek = XIO::PeekUns32_LE ( file ); + + if ( level == 0 ) + { + XMP_Validate( peek == kChunk_RIFF, "expected RIFF chunk not found", kXMPErr_BadFileFormat ); + XMP_Enforce( parent == NULL ); + } + else + { + XMP_Validate( peek != kChunk_RIFF, "unexpected RIFF chunk below top-level", kXMPErr_BadFileFormat ); + XMP_Enforce( parent != NULL ); + } + + switch( peek ) + { + case kChunk_RIFF: + return new ContainerChunk( parent, handler ); + case kChunk_LIST: + { + if ( level != 1 ) break; // only care on this level + + // look further (beyond 4+4 = beyond id+size) to check on relevance + file->Seek ( 8, kXMP_SeekFromCurrent ); + XMP_Uns32 containerType = XIO::PeekUns32_LE ( file ); + file->Seek ( -8, kXMP_SeekFromCurrent ); + + bool isRelevantList = ( containerType== kType_INFO || containerType == kType_Tdat ); + if ( !isRelevantList ) break; + + return new ContainerChunk( parent, handler ); + } + case kChunk_XMP: + if ( level != 1 ) break; // ignore on inappropriate levels (might be compound metadata?) + return new XMPChunk( parent, handler ); + case kChunk_DISP: + { + if ( level != 1 ) break; // only care on this level + // peek even further to see if type is 0x001 and size is reasonable + file ->Seek ( 4, kXMP_SeekFromCurrent ); // jump DISP + XMP_Uns32 dispSize = XIO::ReadUns32_LE( file ); + XMP_Uns32 dispType = XIO::ReadUns32_LE( file ); + file ->Seek ( -12, kXMP_SeekFromCurrent ); // rewind, be in front of chunkID again + + // only take as a relevant disp if both criteria met, + // otherwise treat as generic chunk! + if ( (dispType == 0x0001) && ( dispSize < 256 * 1024 ) ) + { + ValueChunk* r = new ValueChunk( parent, handler ); + handler->dispChunk = r; + return r; + } + break; // treat as irrelevant (non-0x1) DISP chunks as generic chunk + } + case kChunk_bext: + { + if ( level != 1 ) break; // only care on this level + // store for now in a value chunk + ValueChunk* r = new ValueChunk( parent, handler ); + handler->bextChunk = r; + return r; + } + case kChunk_PrmL: + { + if ( level != 1 ) break; // only care on this level + ValueChunk* r = new ValueChunk( parent, handler ); + handler->prmlChunk = r; + return r; + } + case kChunk_Cr8r: + { + if ( level != 1 ) break; // only care on this level + ValueChunk* r = new ValueChunk( parent, handler ); + handler->cr8rChunk = r; + return r; + } + case kChunk_JUNQ: + case kChunk_JUNK: + { + JunkChunk* r = new JunkChunk( parent, handler ); + return r; + } + } + // this "default:" section must be ouside switch bracket, to be + // reachable by all those break statements above: + + + // digest 'valuable' container chunks: LIST:INFO, LIST:Tdat + bool insideRelevantList = ( level==2 && parent->id == kChunk_LIST + && ( parent->containerType== kType_INFO || parent->containerType == kType_Tdat )); + + if ( insideRelevantList ) + { + ValueChunk* r = new ValueChunk( parent, handler ); + return r; + } + + // general chunk of no interest, treat as unknown blob + return new Chunk( parent, handler, true, chunk_GENERAL ); +} + +// BASE CLASS CHUNK /////////////////////////////////////////////// +// ad hoc creation +Chunk::Chunk( ContainerChunk* parent, ChunkType c, XMP_Uns32 id ) +{ + this->chunkType = c; // base class assumption + this->parent = parent; + this->id = id; + this->oldSize = 0; + this->newSize = 8; + this->oldPos = 0; // inevitable for ad-hoc + this->needSizeFix = false; + + // good parenting for latter destruction + if ( this->parent != NULL ) + { + this->parent->children.push_back( this ); + if( this->chunkType == chunk_VALUE ) + this->parent->childmap.insert( std::make_pair( this->id, (ValueChunk*) this ) ); + } +} + +// parsing creation +Chunk::Chunk( ContainerChunk* parent, RIFF_MetaHandler* handler, bool skip, ChunkType c ) +{ + chunkType = c; // base class assumption + this->parent = parent; + this->oldSize = 0; + this->hasChange = false; // [2414649] valid assumption at creation time + + XMP_IO* file = handler->parent->ioRef; + + this->oldPos = file->Offset(); + this->id = XIO::ReadUns32_LE( file ); + this->oldSize = XIO::ReadUns32_LE( file ) + 8; + + // Make sure the size is within expected bounds. + XMP_Int64 chunkEnd = this->oldPos + this->oldSize; + XMP_Int64 chunkLimit = handler->oldFileSize; + if ( parent != 0 ) chunkLimit = parent->oldPos + parent->oldSize; + if ( chunkEnd > chunkLimit ) { + bool isUpdate = XMP_OptionIsSet ( handler->parent->openFlags, kXMPFiles_OpenForUpdate ); + bool repairFile = XMP_OptionIsSet ( handler->parent->openFlags, kXMPFiles_OpenRepairFile ); + if ( (! isUpdate) || (repairFile && (parent == 0)) ) { + this->oldSize = chunkLimit - this->oldPos; + } else { + XMP_Throw ( "Bad RIFF chunk size", kXMPErr_BadFileFormat ); + } + } + + this->newSize = this->oldSize; + this->needSizeFix = false; + + if ( skip ) file->Seek ( (this->oldSize - 8), kXMP_SeekFromCurrent ); + + // "good parenting", essential for latter destruction. + if ( this->parent != NULL ) + { + this->parent->children.push_back( this ); + if( this->chunkType == chunk_VALUE ) + this->parent->childmap.insert( std::make_pair( this->id, (ValueChunk*) this ) ); + } +} + +void Chunk::changesAndSize( RIFF_MetaHandler* handler ) +{ + // only unknown chunks should reach this method, + // all others must reach overloads, hence little to do here: + hasChange = false; // unknown chunk ==> no change, naturally + this->newSize = this->oldSize; +} + +std::string Chunk::toString(XMP_Uns8 level ) +{ + char buffer[256]; + snprintf( buffer, 255, "%.4s -- " + "oldSize: 0x%.8llX, " + "newSize: 0x%.8llX, " + "oldPos: 0x%.8llX\n", + (char*)(&this->id), this->oldSize, this->newSize, this->oldPos ); + return std::string(buffer); +} + +void Chunk::write( RIFF_MetaHandler* handler, XMP_IO* file , bool isMainChunk ) +{ + throw new XMP_Error(kXMPErr_InternalFailure, "Chunk::write never to be called for unknown chunks."); +} + +Chunk::~Chunk() +{ + //nothing +} + +// CONTAINER CHUNK ///////////////////////////////////////////////// +// a) creation +// [2376832] expectedSize - minimum padding "parking size" to use, if not available append to end +ContainerChunk::ContainerChunk( ContainerChunk* parent, XMP_Uns32 id, XMP_Uns32 containerType ) : Chunk( NULL /* !! */, chunk_CONTAINER, id ) +{ + // accept no unparented ConatinerChunks + XMP_Enforce( parent != NULL ); + + this->containerType = containerType; + this->newSize = 12; + this->parent = parent; + + chunkVect* siblings = &parent->children; + + // add at end. ( oldSize==0 will flag optimization later in the process) + siblings->push_back( this ); +} + +// b) parsing +ContainerChunk::ContainerChunk( ContainerChunk* parent, RIFF_MetaHandler* handler ) : Chunk( parent, handler, false, chunk_CONTAINER ) +{ + bool repairMode = ( 0 != ( handler->parent->openFlags & kXMPFiles_OpenRepairFile )); + + try + { + XMP_IO* file = handler->parent->ioRef; + XMP_Uns8 level = handler->level; + + // get type of container chunk + this->containerType = XIO::ReadUns32_LE( file ); + + // ensure legality of top-level chunks + if ( level == 0 && handler->riffChunks.size() > 0 ) + { + XMP_Validate( handler->parent->format == kXMP_AVIFile, "only AVI may have multiple top-level chunks", kXMPErr_BadFileFormat ); + XMP_Validate( this->containerType == kType_AVIX, "all chunks beyond main chunk must be type AVIX", kXMPErr_BadFileFormat ); + } + + // has *relevant* subChunks? (there might be e.g. non-INFO LIST chunks we don't care about) + bool hasSubChunks = ( ( this->id == kChunk_RIFF ) || + ( this->id == kChunk_LIST && this->containerType == kType_INFO ) || + ( this->id == kChunk_LIST && this->containerType == kType_Tdat ) + ); + XMP_Int64 endOfChunk = this->oldPos + this->oldSize; + + // this statement catches beyond-EoF-offsets on any level + // exception: level 0, tolerate if in repairMode + if ( (level == 0) && repairMode && (endOfChunk > handler->oldFileSize) ) + { + endOfChunk = handler->oldFileSize; // assign actual file size + this->oldSize = endOfChunk - this->oldPos; //reversely calculate correct oldSize + } + + XMP_Validate( endOfChunk <= handler->oldFileSize, "offset beyond EoF", kXMPErr_BadFileFormat ); + + Chunk* curChild = 0; + if ( hasSubChunks ) + { + handler->level++; + while ( file->Offset() < endOfChunk ) + { + curChild = RIFF::getChunk( this, handler ); + + // digest pad byte - no value validation (0), since some 3rd party files have non-0-padding. + if ( file->Offset() % 2 == 1 ) + { + // [1521093] tolerate missing pad byte at very end of file: + XMP_Uns8 pad; + file->Read ( &pad, 1 ); // Read the pad, tolerate being at EOF. + + } + + // within relevant LISTs, relentlesly delete junk chunks (create a single one + // at end as part of updateAndChanges() + if ( (containerType== kType_INFO || containerType == kType_Tdat) + && ( curChild->chunkType == chunk_JUNK ) ) + { + this->children.pop_back(); + delete curChild; + } // for other chunks: join neighouring Junk chunks into one + else if ( (curChild->chunkType == chunk_JUNK) && ( this->children.size() >= 2 ) ) + { + // nb: if there are e.g 2 chunks, then last one is at(1), prev one at(0) ==> '-2' + Chunk* prevChunk = this->children.at( this->children.size() - 2 ); + if ( prevChunk->chunkType == chunk_JUNK ) + { + // stack up size to prior chunk + prevChunk->oldSize += curChild->oldSize; + prevChunk->newSize += curChild->newSize; + XMP_Enforce( prevChunk->oldSize == prevChunk->newSize ); + // destroy current chunk + this->children.pop_back(); + delete curChild; + } + } + } + handler->level--; + XMP_Validate( file->Offset() == endOfChunk, "subchunks exceed outer chunk size", kXMPErr_BadFileFormat ); + + // pointers for later legacy processing + if ( level==1 && this->id==kChunk_LIST && this->containerType == kType_INFO ) + handler->listInfoChunk = this; + if ( level==1 && this->id==kChunk_LIST && this->containerType == kType_Tdat ) + handler->listTdatChunk = this; + } + else // skip non-interest container chunk + { + file->Seek ( (this->oldSize - 8 - 4), kXMP_SeekFromCurrent ); + } // if - else + + } // try + catch (XMP_Error& e) { + this->release(); // free resources + if ( this->parent != 0) + this->parent->children.pop_back(); // hereby taken care of, so removing myself... + + throw e; // re-throw + } +} + +void ContainerChunk::changesAndSize( RIFF_MetaHandler* handler ) +{ + + // Walk the container subtree adjusting the children that have size changes. The only containers + // are RIFF and LIST chunks, they are treated differently. + // + // LISTs get recomposed as a whole. Existing JUNK children of a LIST are removed, existing real + // children are left in order with their new size, new children have already been appended. The + // LIST as a whole gets a new size that is the sum of the final children. + // + // Special rules apply to various children of a RIFF container. FIrst, adjacent JUNK children + // are combined, this simplifies maximal reuse. The children are recursively adjusted in order + // to get their final size. + // + // Try to determine the final placement of each RIFF child using general rules: + // - if the size is unchanged: leave at current location + // - if the chunk is at the end of the last RIFF chunk and grows: leave at current location + // - if there is enough following JUNK: add part of the JUNK, adjust remaining JUNK size + // - if it shrinks by 9 bytes or more: carve off trailing JUNK + // - try to find adequate JUNK in the current parent + // + // Use child-specific rules as a last resort: + // - if it is LIST:INFO: delete it, must be in first RIFF chunk + // - for others: move to end of last RIFF chunk, make old space JUNK + + // ! Don't create any junk chunks of exactly 8 bytes, just a header and no content. That has a + // ! size field of zero, which hits a crashing bug in some versions of Windows Media Player. + + bool isRIFFContainer = (this->id == kChunk_RIFF); + bool isLISTContainer = (this->id == kChunk_LIST); + XMP_Enforce ( isRIFFContainer | isLISTContainer ); + + XMP_Index childIndex; // Could be local to the loops, this simplifies debuging. Need a signed type! + Chunk * currChild; + + if ( this->children.empty() ) { + if ( isRIFFContainer) { + this->newSize = 12; // Keep a minimal size container. + } else { + this->newSize = 0; // Will get removed from parent in outer call. + } + this->hasChange = true; + return; // Nothing more to do without children. + } + + // Collapse adjacent RIFF junk children, remove all LIST junk children. Work back to front to + // simplify the effect of .erase() on the loop. Purposely ignore the first chunk. + + for ( childIndex = (XMP_Index)this->children.size() - 1; childIndex > 0; --childIndex ) { + + currChild = this->children[childIndex]; + if ( currChild->chunkType != chunk_JUNK ) continue; + + if ( isRIFFContainer ) { + Chunk * prevChild = this->children[childIndex-1]; + if ( prevChild->chunkType != chunk_JUNK ) continue; + prevChild->oldSize += currChild->oldSize; + prevChild->newSize += currChild->newSize; + prevChild->hasChange = true; + } + + this->children.erase ( this->children.begin() + childIndex ); + delete currChild; + this->hasChange = true; + + } + + // Process the children of RIFF and LIST containers to get their final size. Remove empty + // children. Work back to front to simplify the effect of .erase() on the loop. Do not ignore + // the first chunk. + + for ( childIndex = (XMP_Index)this->children.size() - 1; childIndex >= 0; --childIndex ) { + + currChild = this->children[childIndex]; + + ++handler->level; + currChild->changesAndSize ( handler ); + --handler->level; + + if ( (currChild->newSize == 8) || (currChild->newSize == 0) ) { // ! The newSIze is supposed to include the header. + this->children.erase ( this->children.begin() + childIndex ); + delete currChild; + this->hasChange = true; + } else { + this->hasChange |= currChild->hasChange; + currChild->needSizeFix = (currChild->newSize != currChild->oldSize); + if ( currChild->needSizeFix && (currChild->newSize > currChild->oldSize) && + (this == handler->lastChunk) && (childIndex+1 == (XMP_Index)this->children.size()) ) { + // Let an existing last-in-file chunk grow in-place. Shrinking is conceptually OK, + // but complicates later sanity check that the main AVI chunk is not OK to append + // other chunks later. Ignore new chunks, they might reuse junk space. + if ( currChild->oldSize != 0 ) currChild->needSizeFix = false; + } + } + + } + + // Go through the children of a RIFF container, adjusting the placement as necessary. In brief, + // things can only grow at the end of the last RIFF chunk, and non-junk chunks can't be shifted. + + if ( isRIFFContainer ) { + + for ( childIndex = 0; childIndex < (XMP_Index)this->children.size(); ++childIndex ) { + + currChild = this->children[childIndex]; + if ( ! currChild->needSizeFix ) continue; + currChild->needSizeFix = false; + + XMP_Int64 sizeDiff = currChild->newSize - currChild->oldSize; // Positive for growth. + XMP_Uns8 padSize = (currChild->newSize & 1); // Need a pad for odd size. + + // See if the following chunk is junk that can be utilized. + + Chunk * nextChild = 0; + if ( childIndex+1 < (XMP_Index)this->children.size() ) nextChild = this->children[childIndex+1]; + + if ( (nextChild != 0) && (nextChild->chunkType == chunk_JUNK) ) { + if ( nextChild->newSize >= (9 + sizeDiff + padSize) ) { + + // Incorporate part of the trailing junk, or make the trailing junk grow. + nextChild->newSize -= sizeDiff; + nextChild->newSize -= padSize; + nextChild->hasChange = true; + continue; + + } else if ( nextChild->newSize == (sizeDiff + padSize) ) { + + // Incorporate all of the trailing junk. + this->children.erase ( this->children.begin() + childIndex + 1 ); + delete nextChild; + continue; + + } + } + + // See if the chunk shrinks enough to turn the leftover space into junk. + + if ( (sizeDiff + padSize) <= -9 ) { + this->children.insert ( (this->children.begin() + childIndex + 1), new JunkChunk ( NULL, ((-sizeDiff) - padSize) ) ); + continue; + } + + // Look through the parent for a usable span of junk. + + XMP_Index junkIndex; + Chunk * junkChunk = 0; + for ( junkIndex = 0; junkIndex < (XMP_Index)this->children.size(); ++junkIndex ) { + junkChunk = this->children[junkIndex]; + if ( junkChunk->chunkType != chunk_JUNK ) continue; + if ( (junkChunk->newSize >= (9 + currChild->newSize + padSize)) || + (junkChunk->newSize == (currChild->newSize + padSize)) ) break; + } + + if ( junkIndex < (XMP_Index)this->children.size() ) { + + // Use part or all of the junk for the relocated chunk, replace the old space with junk. + + if ( junkChunk->newSize == (currChild->newSize + padSize) ) { + + // The found junk is an exact fit. + this->children[junkIndex] = currChild; + delete junkChunk; + + } else { + + // The found junk has excess space. Insert the moving chunk and shrink the junk. + XMP_Assert ( junkChunk->newSize >= (9 + currChild->newSize + padSize) ); + junkChunk->newSize -= (currChild->newSize + padSize); + junkChunk->hasChange = true; + this->children.insert ( (this->children.begin() + junkIndex), currChild ); + if ( junkIndex < childIndex ) ++childIndex; // The insertion moved the current child. + + } + + if ( currChild->oldSize != 0 ) { + this->children[childIndex] = new JunkChunk ( 0, currChild->oldSize ); // Replace the old space with junk. + } else { + this->children.erase ( this->children.begin() + childIndex ); // Remove the newly created chunk's old location. + --childIndex; // Make the next loop iteration not skip a chunk. + } + + continue; + + } + + // If this is a LIST:INFO chunk not in the last of multiple RIFF chunks, then give up + // and replace it with oldSize junk. Preserve the first RIFF chunk's original size. + + bool isListInfo = (currChild->id == kChunk_LIST) && (currChild->chunkType == chunk_CONTAINER) && + (((ContainerChunk*)currChild)->containerType == kType_INFO); + + if ( isListInfo && (handler->riffChunks.size() > 1) && + (this->id == kChunk_RIFF) && (this != handler->lastChunk) ) { + + if ( currChild->oldSize != 0 ) { + this->children[childIndex] = new JunkChunk ( 0, currChild->oldSize ); + } else { + this->children.erase ( this->children.begin() + childIndex ); + --childIndex; // Make the next loop iteration not skip a chunk. + } + + delete currChild; + continue; + + } + + // Move the chunk to the end of the last RIFF chunk and make the old space junk. + + if ( (this == handler->lastChunk) && (childIndex+1 == (XMP_Index)this->children.size()) ) continue; // Already last. + + handler->lastChunk->children.push_back( currChild ); + if ( currChild->oldSize != 0 ) { + this->children[childIndex] = new JunkChunk ( 0, currChild->oldSize ); // Replace the old space with junk. + } else { + this->children.erase ( this->children.begin() + childIndex ); // Remove the newly created chunk's old location. + --childIndex; // Make the next loop iteration not skip a chunk. + } + + } + + } + + // Compute the finished container's new size (for both RIFF and LIST). + + this->newSize = 12; // Start with standard container header. + for ( childIndex = 0; childIndex < (XMP_Index)this->children.size(); ++childIndex ) { + currChild = this->children[childIndex]; + this->newSize += currChild->newSize; + this->newSize += (this->newSize & 1); // Round up if odd. + } + + XMP_Validate ( (this->newSize <= 0xFFFFFFFFLL), "No single chunk may be above 4 GB", kXMPErr_Unimplemented ); + +} + +std::string ContainerChunk::toString(XMP_Uns8 level ) +{ + XMP_Int64 offset= 12; // compute offsets, just for informational purposes + // (actually only correct for first chunk) + + char buffer[256]; + snprintf( buffer, 255, "%.4s:%.4s, " + "oldSize: 0x%8llX, " + "newSize: 0x%.8llX, " + "oldPos: 0x%.8llX\n", + (char*)(&this->id), (char*)(&this->containerType), this->oldSize, this->newSize, this->oldPos ); + + std::string r(buffer); + chunkVectIter iter; + for( iter = this->children.begin(); iter != this->children.end(); iter++ ) + { + char buffer[256]; + snprintf( buffer, 250, "offset 0x%.8llX", offset ); + r += std::string ( level*4, ' ' ) + std::string( buffer ) + ":" + (*iter)->toString( level + 1 ); + offset += (*iter)->newSize; + if ( offset % 2 == 1 ) + offset++; + } + return std::string(r); +} + +void ContainerChunk::write( RIFF_MetaHandler* handler, XMP_IO* file, bool isMainChunk ) +{ + if ( isMainChunk ) + file ->Rewind(); + + // enforce even position + XMP_Int64 chunkStart = file->Offset(); + XMP_Int64 chunkEnd = chunkStart + this->newSize; + XMP_Enforce( chunkStart % 2 == 0 ); + chunkVect *rc = &this->children; + + // [2473303] have to write back-to-front to avoid stomp-on-feet + XMP_Int64 childStart = chunkEnd; + for ( XMP_Int32 chunkNo = (XMP_Int32)(rc->size() -1); chunkNo >= 0; chunkNo-- ) + { + Chunk* cur = rc->at(chunkNo); + + // pad byte first + if ( cur->newSize % 2 == 1 ) + { + childStart--; + file->Seek ( childStart, kXMP_SeekFromStart ); + XIO::WriteUns8( file, 0 ); + } + + // then contents + childStart-= cur->newSize; + file->Seek ( childStart, kXMP_SeekFromStart ); + switch ( cur->chunkType ) + { + case chunk_GENERAL: //COULDDO enfore no change, since not write-out-able + if ( cur->oldPos != childStart ) + XIO::Move( file, cur->oldPos, file, childStart, cur->oldSize ); + break; + default: + cur->write( handler, file, false ); + break; + } // switch + + } // for + XMP_Enforce ( chunkStart + 12 == childStart); + file->Seek ( chunkStart, kXMP_SeekFromStart ); + + XIO::WriteUns32_LE( file, this->id ); + XIO::WriteUns32_LE( file, (XMP_Uns32) this->newSize - 8 ); // validated in changesAndSize() above + XIO::WriteUns32_LE( file, this->containerType ); + +} + +void ContainerChunk::release() +{ + // free subchunks + Chunk* curChunk; + while( ! this->children.empty() ) + { + curChunk = this->children.back(); + delete curChunk; + this->children.pop_back(); + } +} + +ContainerChunk::~ContainerChunk() +{ + this->release(); // free resources +} + +// XMP CHUNK /////////////////////////////////////////////// +// a) create + +// a) creation +XMPChunk::XMPChunk( ContainerChunk* parent ) : Chunk( parent, chunk_XMP , kChunk_XMP ) +{ + // nothing +} + +// b) parse +XMPChunk::XMPChunk( ContainerChunk* parent, RIFF_MetaHandler* handler ) : Chunk( parent, handler, false, chunk_XMP ) +{ + chunkType = chunk_XMP; + XMP_IO* file = handler->parent->ioRef; + XMP_Uns8 level = handler->level; + + handler->packetInfo.offset = this->oldPos + 8; + handler->packetInfo.length = (XMP_Int32) this->oldSize - 8; + + handler->xmpPacket.reserve ( handler->packetInfo.length ); + handler->xmpPacket.assign ( handler->packetInfo.length, ' ' ); + file->ReadAll ( (void*)handler->xmpPacket.data(), handler->packetInfo.length ); + + handler->containsXMP = true; // last, after all possible failure + + // pointer for later processing + handler->xmpChunk = this; +} + +void XMPChunk::changesAndSize( RIFF_MetaHandler* handler ) +{ + XMP_Enforce( &handler->xmpPacket != 0 ); + XMP_Enforce( handler->xmpPacket.size() > 0 ); + this->newSize = 8 + handler->xmpPacket.size(); + + XMP_Validate( this->newSize <= 0xFFFFFFFFLL, "no single chunk may be above 4 GB", kXMPErr_InternalFailure ); + + // a complete no-change would have been caught in XMPFiles common code anyway + this->hasChange = true; +} + +void XMPChunk::write( RIFF_MetaHandler* handler, XMP_IO* file, bool isMainChunk ) +{ + XIO::WriteUns32_LE( file, kChunk_XMP ); + XIO::WriteUns32_LE( file, (XMP_Uns32) this->newSize - 8 ); // validated in changesAndSize() above + file->Write ( handler->xmpPacket.data(), (XMP_Int32)handler->xmpPacket.size() ); +} + +// Value CHUNK /////////////////////////////////////////////// +// a) creation +ValueChunk::ValueChunk( ContainerChunk* parent, std::string value, XMP_Uns32 id ) : Chunk( parent, chunk_VALUE, id ) +{ + this->oldValue = std::string(); + this->SetValue( value ); +} + +// b) parsing +ValueChunk::ValueChunk( ContainerChunk* parent, RIFF_MetaHandler* handler ) : Chunk( parent, handler, false, chunk_VALUE ) +{ + // set value: ----------------- + XMP_IO* file = handler->parent->ioRef; + XMP_Uns8 level = handler->level; + + // unless changed through reconciliation, assume for now. + // IMPORTANT to stay true to the original (no \0 cleanup or similar) + // since unknown value chunks might not be fully understood, + // hence must be precisely preserved !!! + + XMP_Int32 length = (XMP_Int32) this->oldSize - 8; + this->oldValue.reserve( length ); + this->oldValue.assign( length + 1, '\0' ); + file->ReadAll ( (void*)this->oldValue.data(), length ); + + this->newValue = this->oldValue; + this->newSize = this->oldSize; +} + +void ValueChunk::SetValue( std::string value, bool optionalNUL /* = false */ ) +{ + this->newValue.assign( value ); + if ( (! optionalNUL) || ((value.size() & 1) == 1) ) { + // ! The NUL should be optional in WAV to avoid a parsing bug in Audition 3 - can't handle implicit pad byte. + this->newValue.append( 1, '\0' ); // append zero termination as explicit part of string + } + this->newSize = this->newValue.size() + 8; +} + +void ValueChunk::changesAndSize( RIFF_MetaHandler* handler ) +{ + // Don't simply assign to this->hasChange, it might already be true. + if ( this->newValue.size() != this->oldValue.size() ) { + this->hasChange = true; + } else if ( strncmp ( this->oldValue.c_str(), this->newValue.c_str(), this->newValue.size() ) != 0 ) { + this->hasChange = true; + } +} + +void ValueChunk::write( RIFF_MetaHandler* handler, XMP_IO* file, bool isMainChunk ) +{ + XIO::WriteUns32_LE( file, this->id ); + XIO::WriteUns32_LE( file, (XMP_Uns32)this->newSize - 8 ); + file->Write ( this->newValue.data() , (XMP_Int32)this->newSize - 8 ); +} + +/* remove value chunk if existing. + return true if it was existing. */ +bool ContainerChunk::removeValue( XMP_Uns32 id ) +{ + valueMap* cm = &this->childmap; + valueMapIter iter = cm->find( id ); + + if( iter == cm->end() ) + return false; //not found + + ValueChunk* propChunk = iter->second; + + // remove from vector (difficult) + chunkVect* cv = &this->children; + chunkVectIter cvIter; + for (cvIter = cv->begin(); cvIter != cv->end(); ++cvIter ) + { + if ( (*cvIter)->id == id ) + break; // found! + } + XMP_Validate( cvIter != cv->end(), "property not found in children vector", kXMPErr_InternalFailure ); + cv->erase( cvIter ); + + // remove from map (easy) + cm->erase( iter ); + + delete propChunk; + return true; // found and removed +} + +/* returns iterator to (first) occurence of this chunk. + iterator to the end of the map if chunk pointer is not found */ +chunkVectIter ContainerChunk::getChild( Chunk* needle ) +{ + chunkVectIter iter; + for( iter = this->children.begin(); iter != this->children.end(); iter++ ) + { + Chunk* temp1 = *iter; + Chunk* temp2 = needle; + if ( (*iter) == needle ) return iter; + } + return this->children.end(); +} + +/* replaces a chunk by a JUNK chunk. + Also frees memory of prior chunk. */ +void ContainerChunk::replaceChildWithJunk( Chunk* child, bool deleteChild ) +{ + chunkVectIter iter = getChild( child ); + if ( iter == this->children.end() ) { + throw new XMP_Error(kXMPErr_InternalFailure, "replaceChildWithJunk: childChunk not found."); + } + + *iter = new JunkChunk ( NULL, child->oldSize ); + if ( deleteChild ) delete child; + + this->hasChange = true; +} + +// JunkChunk /////////////////////////////////////////////////// +// a) creation +JunkChunk::JunkChunk( ContainerChunk* parent, XMP_Int64 size ) : Chunk( parent, chunk_JUNK, kChunk_JUNK ) +{ + XMP_Assert( size >= 8 ); + this->oldSize = size; + this->newSize = size; + this->hasChange = true; +} + +// b) parsing +JunkChunk::JunkChunk( ContainerChunk* parent, RIFF_MetaHandler* handler ) : Chunk( parent, handler, true, chunk_JUNK ) +{ + chunkType = chunk_JUNK; +} + +void JunkChunk::changesAndSize( RIFF_MetaHandler* handler ) +{ + this->newSize = this->oldSize; // optimization at a later stage + XMP_Validate( this->newSize <= 0xFFFFFFFFLL, "no single chunk may be above 4 GB", kXMPErr_InternalFailure ); + if ( this->id == kChunk_JUNQ ) this->hasChange = true; // Force ID change to JUNK. +} + +// zeroBuffer, etc to write out empty native padding +const static XMP_Uns32 kZeroBufferSize64K = 64 * 1024; +static XMP_Uns8 kZeroes64K [ kZeroBufferSize64K ]; // C semantics guarantee zero initialization. + +void JunkChunk::write( RIFF_MetaHandler* handler, XMP_IO* file, bool isMainChunk ) +{ + XIO::WriteUns32_LE( file, kChunk_JUNK ); // write JUNK, never JUNQ + XMP_Enforce( this->newSize < 0xFFFFFFFF ); + XMP_Enforce( this->newSize >= 8 ); // minimum size of any chunk + XMP_Uns32 innerSize = (XMP_Uns32)this->newSize - 8; + XIO::WriteUns32_LE( file, innerSize ); + + // write out in 64K chunks + while ( innerSize > kZeroBufferSize64K ) + { + file->Write ( kZeroes64K , kZeroBufferSize64K ); + innerSize -= kZeroBufferSize64K; + } + file->Write ( kZeroes64K , innerSize ); +} + +} // namespace RIFF diff --git a/XMPFiles/source/FormatSupport/RIFF.hpp b/XMPFiles/source/FormatSupport/RIFF.hpp new file mode 100644 index 0000000..533d9a3 --- /dev/null +++ b/XMPFiles/source/FormatSupport/RIFF.hpp @@ -0,0 +1,322 @@ +#ifndef __RIFF_hpp__ +#define __RIFF_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2009 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" + +#include <vector> +#include <map> + +// ahead declaration: +class RIFF_MetaHandler; + +namespace RIFF { + + enum ChunkType { + chunk_GENERAL, //unknown or not relevant + chunk_CONTAINER, + chunk_XMP, + chunk_VALUE, + chunk_JUNK, + NO_CHUNK // used as precessor to first chunk, etc. + }; + + // ahead declarations + class Chunk; + class ContainerChunk; + class ValueChunk; + class XMPChunk; + + // (scope: only used in RIFF_Support and RIFF_Handler.cpp + // ==> no need to overspecify with lengthy names ) + + typedef std::vector<Chunk*> chunkVect; // coulddo: narrow down toValueChunk (could give trouble with JUNK though) + typedef chunkVect::iterator chunkVectIter; // or refactor ?? + + typedef std::vector<ContainerChunk*> containerVect; + typedef containerVect::iterator containerVectIter; + + typedef std::map<XMP_Uns32,ValueChunk*> valueMap; + typedef valueMap::iterator valueMapIter; + + + // format chunks+types + const XMP_Uns32 kChunk_RIFF = 0x46464952; + const XMP_Uns32 kType_AVI_ = 0x20495641; + const XMP_Uns32 kType_AVIX = 0x58495641; + const XMP_Uns32 kType_WAVE = 0x45564157; + + const XMP_Uns32 kChunk_JUNK = 0x4B4E554A; + const XMP_Uns32 kChunk_JUNQ = 0x514E554A; + + // other container chunks + const XMP_Uns32 kChunk_LIST = 0x5453494C; + const XMP_Uns32 kType_INFO = 0x4F464E49; + const XMP_Uns32 kType_Tdat = 0x74616454; + + // other relevant chunks + const XMP_Uns32 kChunk_XMP = 0x584D505F; // "_PMX" + + // relevant for Index Correction + // LIST: + const XMP_Uns32 kType_hdrl = 0x6C726468; + const XMP_Uns32 kType_strl = 0x6C727473; + const XMP_Uns32 kChunk_indx = 0x78646E69; + const XMP_Uns32 kChunk_ixXX = 0x58587869; + const XMP_Uns32 kType_movi = 0x69766F6D; + + //should occur only in AVI + const XMP_Uns32 kChunk_Cr8r = 0x72387243; + const XMP_Uns32 kChunk_PrmL = 0x4C6D7250; + + //should occur only in WAV + const XMP_Uns32 kChunk_DISP = 0x50534944; + const XMP_Uns32 kChunk_bext = 0x74786562; + + // LIST/INFO constants + const XMP_Uns32 kPropChunkIART = 0x54524149; + const XMP_Uns32 kPropChunkICMT = 0x544D4349; + const XMP_Uns32 kPropChunkICOP = 0x504F4349; + const XMP_Uns32 kPropChunkICRD = 0x44524349; + const XMP_Uns32 kPropChunkIENG = 0x474E4549; + const XMP_Uns32 kPropChunkIGNR = 0x524E4749; + const XMP_Uns32 kPropChunkINAM = 0x4D414E49; + const XMP_Uns32 kPropChunkISFT = 0x54465349; + const XMP_Uns32 kPropChunkIARL = 0x4C524149; + + const XMP_Uns32 kPropChunkIMED = 0x44454D49; + const XMP_Uns32 kPropChunkISRF = 0x46525349; + const XMP_Uns32 kPropChunkICMS = 0x4C524149; + const XMP_Uns32 kPropChunkIPRD = 0x534D4349; + const XMP_Uns32 kPropChunkISRC = 0x44525049; + const XMP_Uns32 kPropChunkITCH = 0x43525349; + + const XMP_Uns32 kPropChunk_tc_O =0x4F5F6374; + const XMP_Uns32 kPropChunk_tc_A =0x415F6374; + const XMP_Uns32 kPropChunk_rn_O =0x4F5F6E72; + const XMP_Uns32 kPropChunk_rn_A =0x415F6E72; + + /////////////////////////////////////////////////////////////// + + enum PropType { // from a simplified, opinionated legacy angle + prop_SIMPLE, + prop_TIMEVALUE, + prop_LOCALIZED_TEXT, + prop_ARRAYITEM, // ( here: a solitary one) + }; + + struct Mapping { + XMP_Uns32 chunkID; + const char* ns; + const char* prop; + PropType propType; + }; + + // bext Mappings, piece-by-piece: + static Mapping bextDescription = { 0, kXMP_NS_BWF, "description", prop_SIMPLE }; + static Mapping bextOriginator = { 0, kXMP_NS_BWF, "originator", prop_SIMPLE }; + static Mapping bextOriginatorRef = { 0, kXMP_NS_BWF, "originatorReference", prop_SIMPLE }; + static Mapping bextOriginationDate = { 0, kXMP_NS_BWF, "originationDate", prop_SIMPLE }; + static Mapping bextOriginationTime = { 0, kXMP_NS_BWF, "originationTime", prop_SIMPLE }; + static Mapping bextTimeReference = { 0, kXMP_NS_BWF, "timeReference", prop_SIMPLE }; + static Mapping bextVersion = { 0, kXMP_NS_BWF, "version", prop_SIMPLE }; + static Mapping bextUMID = { 0, kXMP_NS_BWF, "umid", prop_SIMPLE }; + static Mapping bextCodingHistory = { 0, kXMP_NS_BWF, "codingHistory", prop_SIMPLE }; + + // LIST:INFO properties + static Mapping listInfoProps[] = { + // reconciliations CS4 and before: + { kPropChunkIART, kXMP_NS_DM, "artist" , prop_SIMPLE }, + { kPropChunkICMT, kXMP_NS_DM, "logComment" , prop_SIMPLE }, + { kPropChunkICOP, kXMP_NS_DC, "rights" , prop_LOCALIZED_TEXT }, + { kPropChunkICRD, kXMP_NS_XMP, "CreateDate" , prop_SIMPLE }, + { kPropChunkIENG, kXMP_NS_DM, "engineer" , prop_SIMPLE }, + { kPropChunkIGNR, kXMP_NS_DM, "genre" , prop_SIMPLE }, + { kPropChunkINAM, kXMP_NS_DC, "title" , prop_LOCALIZED_TEXT }, // ( was wrongly dc:album in pre-CS4) + { kPropChunkISFT, kXMP_NS_XMP, "CreatorTool", prop_SIMPLE }, + + // RIFF/*/LIST/INFO properties, new in CS5, both AVI and WAV + + { kPropChunkIMED, kXMP_NS_DC, "source" , prop_SIMPLE }, + { kPropChunkISRF, kXMP_NS_DC, "type" , prop_ARRAYITEM }, + // TO ENABLE { kPropChunkIARL, kXMP_NS_DC, "subject" , prop_SIMPLE }, // array !! (not x-default language alternative) + //{ kPropChunkICMS, to be decided, "" , prop_SIMPLE }, + //{ kPropChunkIPRD, to be decided, "" , prop_SIMPLE }, + //{ kPropChunkISRC, to be decided, "" , prop_SIMPLE }, + //{ kPropChunkITCH, to be decided, "" , prop_SIMPLE }, + + { 0, 0, 0 } // sentinel + }; + + static Mapping listTdatProps[] = { + // reconciliations CS4 and before: + { kPropChunk_tc_O, kXMP_NS_DM, "startTimecode" , prop_TIMEVALUE }, // special: must end up in dm:timeValue child + { kPropChunk_tc_A, kXMP_NS_DM, "altTimecode" , prop_TIMEVALUE }, // special: must end up in dm:timeValue child + { kPropChunk_rn_O, kXMP_NS_DM, "tapeName" , prop_SIMPLE }, + { kPropChunk_rn_A, kXMP_NS_DM, "altTapeName" , prop_SIMPLE }, + { 0, 0, 0 } // sentinel + }; + + // ================================================================================================= + // ImportCr8rItems + // =============== + #pragma pack ( push, 1 ) + struct PrmLBoxContent { + XMP_Uns32 magic; + XMP_Uns32 size; + XMP_Uns16 verAPI; + XMP_Uns16 verCode; + XMP_Uns32 exportType; + XMP_Uns16 MacVRefNum; + XMP_Uns32 MacParID; + char filePath[260]; + }; + + enum { kExportTypeMovie = 0, kExportTypeStill = 1, kExportTypeAudio = 2, kExportTypeCustom = 3 }; + + struct Cr8rBoxContent { + XMP_Uns32 magic; + XMP_Uns32 size; + XMP_Uns16 majorVer; + XMP_Uns16 minorVer; + XMP_Uns32 creatorCode; + XMP_Uns32 appleEvent; + char fileExt[16]; + char appOptions[16]; + char appName[32]; + }; + #pragma pack ( pop ) + + // static getter, determines appropriate chunkType (peeking)and returns + // the respective constructor. It's the caller's responsibility to + // delete obtained chunk. + Chunk* getChunk ( ContainerChunk* parent, RIFF_MetaHandler* handler ); + + class Chunk + { + public: + ChunkType chunkType; // set by constructor + ContainerChunk* parent; // 0 on top-level + + XMP_Uns32 id; // the first four bytes, first byte of highest value + XMP_Int64 oldSize; // actual chunk size INCLUDING the 8/12 header bytes, + XMP_Int64 oldPos; // file position of this chunk + + // both set as part of changesAndSize() + XMP_Int64 newSize; + bool hasChange; + bool needSizeFix; // used in changesAndSize() only + + // Constructors /////////////////////// + // parsing + Chunk( ContainerChunk* parent, RIFF_MetaHandler* handler, bool skip, ChunkType c /*= chunk_GENERAL*/ ); + // ad-hoc creation + Chunk( ContainerChunk* parent, ChunkType c, XMP_Uns32 id ); + + /* returns true, if something has changed in chunk (which needs specific write-out, + this->newSize is expected to be set by this routine */ + virtual void changesAndSize( RIFF_MetaHandler* handler ); + virtual std::string toString(XMP_Uns8 level = 0); + virtual void write( RIFF_MetaHandler* handler, XMP_IO* file, bool isMainChunk = false ); + + virtual ~Chunk(); + + }; // class Chunk + + class XMPChunk : public Chunk + { + public: + XMPChunk( ContainerChunk* parent ); + XMPChunk( ContainerChunk* parent, RIFF_MetaHandler* handler ); + + void changesAndSize( RIFF_MetaHandler* handler ); + void write( RIFF_MetaHandler* handler, XMP_IO* file, bool isMainChunk = false ); + + }; + + // any chunk, whose value should be stored, e.g. LIST:INFO, LIST:Tdat + class ValueChunk : public Chunk + { + public: + std::string oldValue, newValue; + + // for ad-hoc creation (upon write) + ValueChunk( ContainerChunk* parent, std::string value, XMP_Uns32 id ); + + // for parsing + ValueChunk( ContainerChunk* parent, RIFF_MetaHandler* handler ); + + enum { kNULisOptional = true }; + + void SetValue( std::string value, bool optionalNUL = false ); + void changesAndSize( RIFF_MetaHandler* handler ); + void write( RIFF_MetaHandler* handler, XMP_IO* file, bool isMainChunk = false ); + + // own destructor not needed. + }; + + // relevant (level 1) JUNQ and JUNK chunks... + class JunkChunk : public Chunk + { + public: + // construction + JunkChunk( ContainerChunk* parent, XMP_Int64 size ); + // parsing + JunkChunk( ContainerChunk* parent, RIFF_MetaHandler* handler ); + + // own destructor not needed. + + void changesAndSize( RIFF_MetaHandler* handler ); + void write( RIFF_MetaHandler* handler, XMP_IO* file, bool isMainChunk = false ); + }; + + + class ContainerChunk : public Chunk + { + public: + XMP_Uns32 containerType; // e.g. kType_INFO as in "LIST:INFO" + + chunkVect children; // used for cleanup/destruction, ordering... + valueMap childmap; // only for efficient *value* access (inside LIST), *not* used for other containers + + // construct + ContainerChunk( ContainerChunk* parent, XMP_Uns32 id, XMP_Uns32 containerType ); + // parse + ContainerChunk( ContainerChunk* parent, RIFF_MetaHandler* handler ); + + bool removeValue( XMP_Uns32 id ); + + /* returns iterator to (first) occurence of this chunk. + iterator to the end of the map if chunk pointer is not found */ + chunkVectIter getChild( Chunk* needle ); + + void replaceChildWithJunk( Chunk* child, bool deleteChild = true ); + + void changesAndSize( RIFF_MetaHandler* handler ); + std::string toString(XMP_Uns8 level = 0); + void write( RIFF_MetaHandler* handler, XMP_IO* file, bool isMainChunk = false ); + + // destroy + void release(); // used by destructor and on error in constructor + ~ContainerChunk(); + + }; // class ContainerChunk + +} // namespace RIFF + + +#endif // __RIFF_hpp__ diff --git a/XMPFiles/source/FormatSupport/RIFF_Support.cpp b/XMPFiles/source/FormatSupport/RIFF_Support.cpp new file mode 100644 index 0000000..1e2fad1 --- /dev/null +++ b/XMPFiles/source/FormatSupport/RIFF_Support.cpp @@ -0,0 +1,939 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2008 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +// must have access to handler class fields... +#include "XMPFiles/source/FormatSupport/RIFF.hpp" +#include "XMPFiles/source/FileHandlers/RIFF_Handler.hpp" +#include "XMPFiles/source/FormatSupport/RIFF_Support.hpp" +#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp" + +#define MIN(a, b) ((a) < (b) ? (a) : (b)) + +using namespace RIFF; +namespace RIFF { + +// The minimum BEXT chunk size should be 610 (incl. 8 byte header/size field) +XMP_Int32 MIN_BEXT_SIZE = 610; // = > 8 + ( 256+32+32+10+8+4+4+2+64+190+0 ) + +// An assumed secure max value of 100 MB. +XMP_Int32 MAX_BEXT_SIZE = 100 * 1024 * 1024; + +// CR8R, PrmL have fixed sizes +XMP_Int32 CR8R_SIZE = 0x5C; +XMP_Int32 PRML_SIZE = 0x122; + +static const char* sHexChars = "0123456789ABCDEF"; + +// Encode a string of raw data bytes into a HexString (w/o spaces, i.e. "DEADBEEF"). +// No insertation/acceptance of whitespace/linefeeds. No output/tolerance of lowercase. +// returns true, if *all* characters returned are zero (or if 0 bytes are returned). +static bool EncodeToHexString ( XMP_StringPtr rawStr, + XMP_StringLen rawLen, + std::string* encodedStr ) +{ + bool allZero = true; // assume for now + + if ( (rawStr == 0) && (rawLen != 0) ) + XMP_Throw ( "EncodeToHexString: null rawStr", kXMPErr_BadParam ); + if ( encodedStr == 0 ) + XMP_Throw ( "EncodeToHexString: null encodedStr", kXMPErr_BadParam ); + + encodedStr->erase(); + if ( rawLen == 0 ) return allZero; + encodedStr->reserve ( rawLen * 2 ); + + for( XMP_Uns32 i = 0; i < rawLen; i++ ) + { + // first, second nibble + XMP_Uns8 first = rawStr[i] >> 4; + XMP_Uns8 second = rawStr[i] & 0xF; + + if ( allZero && (( first != 0 ) || (second != 0))) + allZero = false; + + encodedStr->append( 1, sHexChars[first] ); + encodedStr->append( 1, sHexChars[second] ); + } + + return allZero; +} // EncodeToHexString + +// ------------------------------------------------------------------------------------------------- +// DecodeFromHexString +// ---------------- +// +// Decode a hex string to raw data bytes. +// * Input must be all uppercase and w/o any whitespace, strictly (0-9A-Z)* (i.e. "DEADBEEF0099AABC") +// * No insertation/acceptance of whitespace/linefeeds. +// * bNo use/tolerance of lowercase. +// * Number of bytes in the encoded String must be even. +// * returns true if everything went well, false if illegal (non 0-9A-F) character encountered + +static bool DecodeFromHexString ( XMP_StringPtr encodedStr, + XMP_StringLen encodedLen, + std::string* rawStr ) +{ + if ( (encodedLen % 2) != 0 ) + return false; + rawStr->erase(); + if ( encodedLen == 0 ) return true; + rawStr->reserve ( encodedLen / 2 ); + + for( XMP_Uns32 i = 0; i < encodedLen; ) + { + XMP_Uns8 upperNibble = encodedStr[i]; + if ( (upperNibble < 48) || ( (upperNibble > 57 ) && ( upperNibble < 65 ) ) || (upperNibble > 70) ) + return false; + if ( upperNibble >= 65 ) + upperNibble -= 7; // shift A-F area adjacent to 0-9 + upperNibble -= 48; // 'shift' to a value [0..15] + upperNibble = ( upperNibble << 4 ); + i++; + + XMP_Uns8 lowerNibble = encodedStr[i]; + if ( (lowerNibble < 48) || ( (lowerNibble > 57 ) && ( lowerNibble < 65 ) ) || (lowerNibble > 70) ) + return false; + if ( lowerNibble >= 65 ) + lowerNibble -= 7; // shift A-F area adjacent to 0-9 + lowerNibble -= 48; // 'shift' to a value [0..15] + i++; + + rawStr->append ( 1, (upperNibble + lowerNibble) ); + } + return true; +} // DecodeFromHexString + +// Converts input string to an ascii output string +// - terminates at first 0 +// - replaces all non ascii with 0x3F ('?') +// - produces up to maxOut characters (note that several UTF-8 character bytes can 'melt' to one byte '?' in ascii.) +static XMP_StringLen convertToASCII( XMP_StringPtr input, XMP_StringLen inputLen, std::string* output, XMP_StringLen maxOutputLen ) +{ + if ( (input == 0) && (inputLen != 0) ) + XMP_Throw ( "convertToASCII: null input string", kXMPErr_BadParam ); + if ( output == 0) + XMP_Throw ( "convertToASCII: null output string", kXMPErr_BadParam ); + if ( maxOutputLen == 0) + XMP_Throw ( "convertToASCII: zero maxOutputLen chars", kXMPErr_BadParam ); + + output->reserve(inputLen); + output->erase(); + + bool isUTF8 = ReconcileUtils::IsUTF8( input, inputLen ); + XMP_StringLen outputLen = 0; + + for ( XMP_Uns32 i=0; i < inputLen; i++ ) + { + XMP_Uns8 c = (XMP_Uns8) input[i]; + if ( c == 0 ) // early 0 termination, leave. + break; + if ( c > 127 ) // uft-8 multi-byte sequence. + { + if ( isUTF8 ) // skip all high bytes + { + // how many bytes in this ? + if ( c >= 0xC2 && c <= 0xDF ) + i+=1; // 2-byte sequence + else if ( c >= 0xE0 && c <= 0xEF ) + i+=2; // 3-byte sequence + else if ( c >= 0xF0 && c <= 0xF4 ) + i+=3; // 4-byte sequence + else + continue; //invalid sequence, look for next 'low' byte .. + } // thereafter and 'else': just append a question mark: + output->append( 1, '?' ); + } + else // regular valid ascii. 1 byte. + { + output->append( 1, input[i] ); + } + outputLen++; + if ( outputLen >= maxOutputLen ) + break; // (may be even or even greater due to UFT-8 multi-byte jumps) + } + + return outputLen; +} + +/** + * ensures that native property gets returned as UTF-8 (may or mayn not already be UTF-8) + * - also takes care of "moot padding" (pre-mature zero termination) + * - propertyExists: it is important to know if there as an existing, non zero property + * even (in the event of serverMode) it is not actually returned, but an empty string instead. + */ +static std::string nativePropertyToUTF8 ( XMP_StringPtr cstring, XMP_StringLen maxSize, bool* propertyExists ) +{ + // the value might be properly 0-terminated, prematurely or not + // at all, hence scan through to find actual size + XMP_StringLen size = 0; + for ( size = 0; size < maxSize; size++ ) + { + if ( cstring[size] == 0 ) + break; + } + + (*propertyExists) = ( size > 0 ); + + std::string utf8(""); + if ( ReconcileUtils::IsUTF8( cstring, size ) ) + utf8 = std::string( cstring, size ); //use utf8 directly + else + { + if ( ! ignoreLocalText ) + { + #if ! UNIX_ENV // n/a anyway, since always ignoreLocalText on Unix + ReconcileUtils::LocalToUTF8( cstring, size, &utf8 ); + #endif + } + } + return utf8; +} + +// reads maxSize bytes from file (not "up to", exactly fullSize) +// puts it into a string, sets respective tree property +static std::string getBextField ( const char* data, XMP_Uns32 offset, XMP_Uns32 maxSize ) +{ + if (data == 0) + XMP_Throw ( "getBextField: null data pointer", kXMPErr_BadParam ); + if ( maxSize == 0) + XMP_Throw ( "getBextField: maxSize must be greater than 0", kXMPErr_BadParam ); + + std::string r; + convertToASCII( data+offset, maxSize, &r, maxSize ); + return r; +} + +static void importBextChunkToXMP( RIFF_MetaHandler* handler, ValueChunk* bextChunk ) +{ + // if there's a bext chunk, there is data... + handler->containsXMP = true; // very important for treatment on caller level + + XMP_Enforce( bextChunk->oldSize >= MIN_BEXT_SIZE ); + XMP_Enforce( bextChunk->oldSize < MAX_BEXT_SIZE ); + + const char* data = bextChunk->oldValue.data(); + std::string value; + + // register bext namespace: + SXMPMeta::RegisterNamespace( kXMP_NS_BWF, "bext:", 0 ); + + // bextDescription ------------------------------------------------ + value = getBextField( data, 0, 256 ); + if ( value.size() > 0 ) + handler->xmpObj.SetProperty( bextDescription.ns, bextDescription.prop, value.c_str() ); + + // bextOriginator ------------------------------------------------- + value = getBextField( data, 256, 32 ); + if ( value.size() > 0 ) + handler->xmpObj.SetProperty( bextOriginator.ns , bextOriginator.prop, value.c_str() ); + + // bextOriginatorRef ---------------------------------------------- + value = getBextField( data, 256+32, 32 ); + if ( value.size() > 0 ) + handler->xmpObj.SetProperty( bextOriginatorRef.ns , bextOriginatorRef.prop, value.c_str() ); + + // bextOriginationDate -------------------------------------------- + value = getBextField( data, 256+32+32, 10 ); + if ( value.size() > 0 ) + handler->xmpObj.SetProperty( bextOriginationDate.ns , bextOriginationDate.prop, value.c_str() ); + + // bextOriginationTime -------------------------------------------- + value = getBextField( data, 256+32+32+10, 8 ); + if ( value.size() > 0 ) + handler->xmpObj.SetProperty( bextOriginationTime.ns , bextOriginationTime.prop, value.c_str() ); + + // bextTimeReference ---------------------------------------------- + // thanx to nice byte order, all 8 bytes can be read as one: + XMP_Uns64 timeReferenceFull = GetUns64LE( &(data[256+32+32+10+8 ] ) ); + value.erase(); + SXMPUtils::ConvertFromInt64( timeReferenceFull, "%llu", &value ); + handler->xmpObj.SetProperty( bextTimeReference.ns, bextTimeReference.prop, value ); + + // bextVersion ---------------------------------------------------- + XMP_Uns16 bwfVersion = GetUns16LE( &(data[256+32+32+10+8+8] ) ); + value.erase(); + SXMPUtils::ConvertFromInt( bwfVersion, "", &value ); + handler->xmpObj.SetProperty( bextVersion.ns, bextVersion.prop, value ); + + // bextUMID ------------------------------------------------------- + // binary string is already in memory, must convert to hex string + std::string umidString; + bool allZero = EncodeToHexString( &(data[256+32+32+10+8+8+2]), 64, &umidString ); + if (! allZero ) + handler->xmpObj.SetProperty( bextUMID.ns, bextUMID.prop, umidString ); + + // bextCodingHistory ---------------------------------------------- + bool hasCodingHistory = bextChunk->oldSize > MIN_BEXT_SIZE; + + if ( hasCodingHistory ) + { + XMP_StringLen codingHistorySize = (XMP_StringLen) (bextChunk->oldSize - MIN_BEXT_SIZE); + std::string codingHistory; + convertToASCII( &data[MIN_BEXT_SIZE-8], codingHistorySize, &codingHistory, codingHistorySize ); + if (! codingHistory.empty() ) + handler->xmpObj.SetProperty( bextCodingHistory.ns, bextCodingHistory.prop, codingHistory ); + } +} // importBextChunkToXMP + +static void importPrmLToXMP( RIFF_MetaHandler* handler, ValueChunk* prmlChunk ) +{ + bool haveXMP = false; + + XMP_Enforce( prmlChunk->oldSize == PRML_SIZE ); + PrmLBoxContent rawPrmL; + XMP_Assert ( sizeof ( rawPrmL ) == PRML_SIZE - 8 ); // double check tight packing. + XMP_Assert ( sizeof ( rawPrmL.filePath ) == 260 ); + memcpy ( &rawPrmL, prmlChunk->oldValue.data(), sizeof (rawPrmL) ); + + if ( rawPrmL.magic != 0xBEEFCAFE ) { + Flip4 ( &rawPrmL.exportType ); // The only numeric field that we care about. + } + + rawPrmL.filePath[259] = 0; // Ensure a terminating nul. + if ( rawPrmL.filePath[0] != 0 ) { + if ( rawPrmL.filePath[0] == '/' ) { + haveXMP = true; + handler->xmpObj.SetStructField ( kXMP_NS_CreatorAtom, "macAtom", + kXMP_NS_CreatorAtom, "posixProjectPath", rawPrmL.filePath ); + } else if ( XMP_LitNMatch ( rawPrmL.filePath, "\\\\?\\", 4 ) ) { + haveXMP = true; + handler->xmpObj.SetStructField ( kXMP_NS_CreatorAtom, "windowsAtom", + kXMP_NS_CreatorAtom, "uncProjectPath", rawPrmL.filePath ); + } + } + + const char * exportStr = 0; + switch ( rawPrmL.exportType ) { + case kExportTypeMovie : exportStr = "movie"; break; + case kExportTypeStill : exportStr = "still"; break; + case kExportTypeAudio : exportStr = "audio"; break; + case kExportTypeCustom : exportStr = "custom"; break; + } + if ( exportStr != 0 ) { + haveXMP = true; + handler->xmpObj.SetStructField ( kXMP_NS_DM, "projectRef", kXMP_NS_DM, "type", exportStr ); + } + + handler->containsXMP |= haveXMP; // mind the '|=' +} // importCr8rToXMP + +static void importCr8rToXMP( RIFF_MetaHandler* handler, ValueChunk* cr8rChunk ) +{ + bool haveXMP = false; + + XMP_Enforce( cr8rChunk->oldSize == CR8R_SIZE ); + Cr8rBoxContent rawCr8r; + XMP_Assert ( sizeof ( rawCr8r ) == CR8R_SIZE - 8 ); // double check tight packing. + memcpy ( &rawCr8r, cr8rChunk->oldValue.data(), sizeof (rawCr8r) ); + + if ( rawCr8r.magic != 0xBEEFCAFE ) { + Flip4 ( &rawCr8r.creatorCode ); // The only numeric fields that we care about. + Flip4 ( &rawCr8r.appleEvent ); + } + + std::string fieldPath; + + SXMPUtils::ComposeStructFieldPath ( kXMP_NS_CreatorAtom, "macAtom", kXMP_NS_CreatorAtom, "applicationCode", &fieldPath ); + if ( rawCr8r.creatorCode != 0 ) { + haveXMP = true; + handler->xmpObj.SetProperty_Int64 ( kXMP_NS_CreatorAtom, fieldPath.c_str(), (XMP_Int64)rawCr8r.creatorCode ); // ! Unsigned trickery. + } + + SXMPUtils::ComposeStructFieldPath ( kXMP_NS_CreatorAtom, "macAtom", kXMP_NS_CreatorAtom, "invocationAppleEvent", &fieldPath ); + if ( rawCr8r.appleEvent != 0 ) { + haveXMP = true; + handler->xmpObj.SetProperty_Int64 ( kXMP_NS_CreatorAtom, fieldPath.c_str(), (XMP_Int64)rawCr8r.appleEvent ); // ! Unsigned trickery. + } + + rawCr8r.fileExt[15] = 0; // Ensure a terminating nul. + if ( rawCr8r.fileExt[0] != 0 ) { + haveXMP = true; + handler->xmpObj.SetStructField ( kXMP_NS_CreatorAtom, "windowsAtom", kXMP_NS_CreatorAtom, "extension", rawCr8r.fileExt ); + } + + rawCr8r.appOptions[15] = 0; // Ensure a terminating nul. + if ( rawCr8r.appOptions[0] != 0 ) { + haveXMP = true; + handler->xmpObj.SetStructField ( kXMP_NS_CreatorAtom, "windowsAtom", kXMP_NS_CreatorAtom, "invocationFlags", rawCr8r.appOptions ); + } + + rawCr8r.appName[31] = 0; // Ensure a terminating nul. + if ( rawCr8r.appName[0] != 0 ) { + haveXMP = true; + handler->xmpObj.SetProperty ( kXMP_NS_XMP, "CreatorTool", rawCr8r.appName ); + } + + handler->containsXMP |= haveXMP; // mind the '|=' +} // importCr8rToXMP + + +static void importListChunkToXMP( RIFF_MetaHandler* handler, ContainerChunk* listChunk, Mapping mapping[], bool xmpHasPriority ) +{ + valueMap* cm = &listChunk->childmap; + for (int p=0; mapping[p].chunkID != 0; p++) // go through legacy chunks + { + valueMapIter result = cm->find(mapping[p].chunkID); + if( result != cm->end() ) // if value found + { + ValueChunk* propChunk = result->second; + + bool propertyExists = false; + std::string utf8 = nativePropertyToUTF8( + propChunk->oldValue.c_str(), + (XMP_StringLen)propChunk->oldValue.size(), &propertyExists ); + + if ( utf8.size() > 0 ) // if property is not-empty, set Property + { + switch ( mapping[p].propType ) + { + case prop_TIMEVALUE: + if ( xmpHasPriority && + handler->xmpObj.DoesStructFieldExist( mapping[p].ns, mapping[p].prop, kXMP_NS_DM, "timeValue" )) + break; // skip if XMP has precedence and exists + handler->xmpObj.SetStructField( mapping[p].ns, mapping[p].prop, + kXMP_NS_DM, "timeValue", utf8.c_str() ); + break; + case prop_LOCALIZED_TEXT: + if ( xmpHasPriority && handler->xmpObj.GetLocalizedText( mapping[p].ns , + mapping[p].prop, "" , "x-default", 0, 0, 0 )) + break; // skip if XMP has precedence and exists + handler->xmpObj.SetLocalizedText( mapping[p].ns , mapping[p].prop, + "" , "x-default" , utf8.c_str() ); + if ( mapping[p].chunkID == kPropChunkINAM ) + handler->hasListInfoINAM = true; // needs to be known for special 3-way merge around dc:title + break; + case prop_ARRAYITEM: + if ( xmpHasPriority && + handler->xmpObj.DoesArrayItemExist( mapping[p].ns, mapping[p].prop, 1 )) + break; // skip if XMP has precedence and exists + handler->xmpObj.DeleteProperty( mapping[p].ns, mapping[p].prop ); + handler->xmpObj.AppendArrayItem( mapping[p].ns, mapping[p].prop, kXMP_PropValueIsArray, utf8.c_str(), kXMP_NoOptions ); + break; + case prop_SIMPLE: + if ( xmpHasPriority && + handler->xmpObj.DoesPropertyExist( mapping[p].ns, mapping[p].prop )) + break; // skip if XMP has precedence and exists + handler->xmpObj.SetProperty( mapping[p].ns, mapping[p].prop, utf8.c_str() ); + break; + default: + XMP_Throw( "internal error" , kXMPErr_InternalFailure ); + } + + handler->containsXMP = true; // very important for treatment on caller level + } + else if ( ! propertyExists) // otherwise remove it. + { // [2389942] don't, if legacy value is existing but non-retrievable (due to server mode) + switch ( mapping[p].propType ) + { + case prop_TIMEVALUE: + if ( (!xmpHasPriority) && // forward deletion only if XMP has no priority + handler->xmpObj.DoesPropertyExist( mapping[p].ns, mapping[p].prop )) + handler->xmpObj.DeleteProperty( mapping[p].ns, mapping[p].prop ); + break; + case prop_LOCALIZED_TEXT: + if ( (!xmpHasPriority) && // forward deletion only if XMP has no priority + handler->xmpObj.DoesPropertyExist( mapping[p].ns, mapping[p].prop )) + handler->xmpObj.DeleteLocalizedText( mapping[p].ns, mapping[p].prop, "", "x-default" ); + break; + case prop_ARRAYITEM: + case prop_SIMPLE: + if ( (!xmpHasPriority) && // forward deletion only if XMP has no priority + handler->xmpObj.DoesPropertyExist( mapping[p].ns, mapping[p].prop )) + handler->xmpObj.DeleteProperty( mapping[p].ns, mapping[p].prop ); + break; + default: + XMP_Throw( "internal error" , kXMPErr_InternalFailure ); + } + } + } + } // for +} +void importProperties( RIFF_MetaHandler* handler ) +{ + bool hasDigest = handler->xmpObj.GetProperty( kXMP_NS_WAV, "NativeDigest", NULL , NULL ); + if ( hasDigest ) + { + // remove! since it now becomse a 'new' handler file + handler->xmpObj.DeleteProperty( kXMP_NS_WAV, "NativeDigest" ); + } + + // BWF Bext extension chunk ----------------------------------------------- + if ( handler->parent->format == kXMP_WAVFile && // applies only to WAV + handler->bextChunk != 0 ) //skip if no BEXT chunk found. + { + importBextChunkToXMP( handler, handler->bextChunk ); + } + + // PrmL chunk -------------------------------------------------------------- + if ( handler->prmlChunk != 0 && handler->prmlChunk->oldSize == PRML_SIZE ) + { + importPrmLToXMP( handler, handler->prmlChunk ); + } + + // Cr8r chunk -------------------------------------------------------------- + if ( handler->cr8rChunk != 0 && handler->cr8rChunk->oldSize == CR8R_SIZE ) + { + importCr8rToXMP( handler, handler->cr8rChunk ); + } + + // LIST:INFO -------------------------------------------------------------- + if ( handler->listInfoChunk != 0) //skip if no LIST:INFO chunk found. + importListChunkToXMP( handler, handler->listInfoChunk, listInfoProps, hasDigest ); + + // LIST:Tdat -------------------------------------------------------------- + if ( handler->listTdatChunk != 0) + importListChunkToXMP( handler, handler->listTdatChunk, listTdatProps, hasDigest ); + + // DISP (do last, higher priority than INAM ) ----------------------------- + bool takeXMP = false; // assume for now + if ( hasDigest ) + { + std::string actualLang, value; + bool r = handler->xmpObj.GetLocalizedText( kXMP_NS_DC, "title", "" , "x-default" , &actualLang, &value, NULL ); + if ( r && (actualLang == "x-default") ) takeXMP = true; + } + + if ( (!takeXMP) && handler->dispChunk != 0) //skip if no LIST:INFO chunk found. + { + std::string* value = &handler->dispChunk->oldValue; + if ( value->size() >= 4 ) // ignore contents if file too small + { + XMP_StringPtr cstring = value->c_str(); + XMP_StringLen size = (XMP_StringLen) value->size(); + + size -= 4; // skip first four bytes known to contain constant + cstring += 4; + + bool propertyExists = false; + std::string utf8 = nativePropertyToUTF8( cstring, size, &propertyExists ); + + if ( utf8.size() > 0 ) + { + handler->xmpObj.SetLocalizedText( kXMP_NS_DC, "title", "" , "x-default" , utf8.c_str() ); + handler->containsXMP = true; // very important for treatment on caller level + } + else + { + // found as part of [2389942] + // forward deletion may only happen if no LIST:INFO/INAM is present: + if ( ! handler->hasListInfoINAM && + ! propertyExists ) // ..[2389942]part2: and if truly no legacy property + { // (not just an unreadable one due to ServerMode). + handler->xmpObj.DeleteProperty( kXMP_NS_DC, "title" ); + } + } + } // if size sufficient + } // handler->dispChunk + +} // importProperties + +//////////////////////////////////////////////////////////////////////////////// +// EXPORT +//////////////////////////////////////////////////////////////////////////////// + +void relocateWronglyPlacedXMPChunk( RIFF_MetaHandler* handler ) +{ + XMP_IO* file = handler->parent->ioRef; + RIFF::containerVect *rc = &handler->riffChunks; + RIFF::ContainerChunk* lastChunk = rc->at( rc->size()-1 ); + + // 1) XMPPacket + // needChunk exists but is not in lastChunk ? + if ( + handler->xmpChunk != 0 && // XMP Chunk existing? + (XMP_Uns32)rc->size() > 1 && // more than 1 top-level chunk (otherwise pointless) + lastChunk->getChild( handler->xmpChunk ) == lastChunk->children.end() // not already in last chunk? + ) + { + RIFF::ContainerChunk* cur; + chunkVectIter child; + XMP_Int32 chunkNo; + + // find and relocate to last chunk: + for ( chunkNo = (XMP_Int32)rc->size()-2 ; chunkNo >= 0; chunkNo-- ) // ==> start with second-last chunk + { + cur = rc->at(chunkNo); + child = cur->getChild( handler->xmpChunk ); + if ( child != cur->children.end() ) // found? + break; + } // for + + if ( chunkNo < 0 ) // already in place? nothing left to do. + return; + + lastChunk->children.push_back( *child ); // nb: order matters! + cur->replaceChildWithJunk( *child, false ); + cur->hasChange = true; // [2414649] initialize early-on i.e: here + } // if +} // relocateWronglyPlacedXMPChunk + +// writes to buffer up to max size, +// 0 termination only if shorter than maxSize +// converts down to ascii +static void setBextField ( std::string* value, XMP_Uns8* data, XMP_Uns32 offset, XMP_Uns32 maxSize ) +{ + XMP_Validate( value != 0, "setBextField: null value string pointer", kXMPErr_BadParam ); + XMP_Validate( data != 0, "setBextField: null data value", kXMPErr_BadParam ); + XMP_Validate( maxSize > 0, "setBextField: maxSize must be greater than 0", kXMPErr_BadParam ); + + std::string ascii; + XMP_StringLen actualSize = convertToASCII( value->data(), (XMP_StringLen) value->size() , &ascii , maxSize ); + strncpy( (char*)(data + offset), ascii.data(), actualSize ); +} + +// add bwf-bext related data to bext chunk, create if not existing yet. +// * in fact, since bext is fully fixed and known, there can be no unknown subchunks worth keeping: +// * prepare bext chunk in buffer +// * value changed/created if needed only, otherways remove chunk +// * remove bext-mapped properties from xmp (non-redundant storage) +// note: ValueChunk**: adress of pointer to allow changing the pointer itself (i.e. chunk creation) +static void exportXMPtoBextChunk( RIFF_MetaHandler* handler, ValueChunk** bextChunk ) +{ + // register bext namespace ( if there was no import, this is news, otherwise harmless moot) + SXMPMeta::RegisterNamespace( kXMP_NS_BWF, "bext:", 0 ); + + bool chunkUsed = false; // assume for now + SXMPMeta* xmp = &handler->xmpObj; + + // prepare buffer, need to know CodingHistory size as the only variable + XMP_Int32 bextBufferSize = MIN_BEXT_SIZE - 8; // -8 because of header + std::string value; + if ( xmp->GetProperty( bextCodingHistory.ns, bextCodingHistory.prop, &value, kXMP_NoOptions )) + { + bextBufferSize += ((XMP_StringLen)value.size()) + 1 ; // add to size (and a trailing zero) + } + + // create and clear buffer + XMP_Uns8* buffer = new XMP_Uns8[bextBufferSize]; + for (XMP_Int32 i = 0; i < bextBufferSize; i++ ) + buffer[i] = 0; + + // grab props, write into buffer, remove from XMP /////////////////////////// + // bextDescription ------------------------------------------------ + if ( xmp->GetProperty( bextDescription.ns, bextDescription.prop, &value, kXMP_NoOptions ) ) + { + setBextField( &value, (XMP_Uns8*) buffer, 0, 256 ); + xmp->DeleteProperty( bextDescription.ns, bextDescription.prop) ; + chunkUsed = true; + } + // bextOriginator ------------------------------------------------- + if ( xmp->GetProperty( bextOriginator.ns , bextOriginator.prop, &value, kXMP_NoOptions ) ) + { + setBextField( &value, (XMP_Uns8*) buffer, 256, 32 ); + xmp->DeleteProperty( bextOriginator.ns , bextOriginator.prop ); + chunkUsed = true; + } + // bextOriginatorRef ---------------------------------------------- + if ( xmp->GetProperty( bextOriginatorRef.ns , bextOriginatorRef.prop, &value, kXMP_NoOptions ) ) + { + setBextField( &value, (XMP_Uns8*) buffer, 256+32, 32 ); + xmp->DeleteProperty( bextOriginatorRef.ns , bextOriginatorRef.prop ); + chunkUsed = true; + } + // bextOriginationDate -------------------------------------------- + if ( xmp->GetProperty( bextOriginationDate.ns , bextOriginationDate.prop, &value, kXMP_NoOptions ) ) + { + setBextField( &value, (XMP_Uns8*) buffer, 256+32+32, 10 ); + xmp->DeleteProperty( bextOriginationDate.ns , bextOriginationDate.prop ); + chunkUsed = true; + } + // bextOriginationTime -------------------------------------------- + if ( xmp->GetProperty( bextOriginationTime.ns , bextOriginationTime.prop, &value, kXMP_NoOptions ) ) + { + setBextField( &value, (XMP_Uns8*) buffer, 256+32+32+10, 8 ); + xmp->DeleteProperty( bextOriginationTime.ns , bextOriginationTime.prop ); + chunkUsed = true; + } + // bextTimeReference ---------------------------------------------- + // thanx to friendly byte order, all 8 bytes can be written in one go: + if ( xmp->GetProperty( bextTimeReference.ns, bextTimeReference.prop, &value, kXMP_NoOptions ) ) + { + try + { + XMP_Int64 v = SXMPUtils::ConvertToInt64( value.c_str() ); + PutUns64LE( v, &(buffer[256+32+32+10+8] )); + chunkUsed = true; + } + catch (XMP_Error& e) + { + if ( e.GetID() != kXMPErr_BadParam ) + throw e; // re-throw on any other error + } // 'else' tolerate ( time reference remains 0x00000000 ) + // valid or not, do not store redundantly: + xmp->DeleteProperty( bextTimeReference.ns, bextTimeReference.prop ); + } + + // bextVersion ---------------------------------------------------- + // set version=1, no matter what. + PutUns16LE( 1, &(buffer[256+32+32+10+8+8]) ); + xmp->DeleteProperty( bextVersion.ns, bextVersion.prop ); + + // bextUMID ------------------------------------------------------- + if ( xmp->GetProperty( bextUMID.ns, bextUMID.prop, &value, kXMP_NoOptions ) ) + { + std::string rawStr; + + if ( !DecodeFromHexString( value.data(), (XMP_StringLen) value.size(), &rawStr ) ) + { + delete [] buffer; // important. + XMP_Throw ( "EncodeFromHexString: illegal umid string. Must contain an even number of 0-9 and uppercase A-F chars.", kXMPErr_BadParam ); + } + + // if UMID is smaller/longer than 64 byte for any reason, + // truncate/do a partial write (just like for any other bext property) + + memcpy( (char*) &(buffer[256+32+32+10+8+8+2]), rawStr.data(), MIN( 64, rawStr.size() ) ); + xmp->DeleteProperty( bextUMID.ns, bextUMID.prop ); + chunkUsed = true; + } + + // bextCodingHistory ---------------------------------------------- + if ( xmp->GetProperty( bextCodingHistory.ns, bextCodingHistory.prop, &value, kXMP_NoOptions ) ) + { + std::string ascii; + convertToASCII( value.data(), (XMP_StringLen) value.size() , &ascii, (XMP_StringLen) value.size() ); + strncpy( (char*) &(buffer[MIN_BEXT_SIZE-8]), ascii.data(), ascii.size() ); + xmp->DeleteProperty( bextCodingHistory.ns, bextCodingHistory.prop ); + chunkUsed = true; + } + + // always delete old, recreate if needed + if ( *bextChunk != 0 ) + { + (*bextChunk)->parent->replaceChildWithJunk( *bextChunk ); + (*bextChunk) = 0; // clear direct Chunk pointer + } + + if ( chunkUsed) + *bextChunk = new ValueChunk( handler->riffChunks.at(0), std::string( (char*)buffer, bextBufferSize ), kChunk_bext ); + + delete [] buffer; // important. +} + +static inline void SetBufferedString ( char * dest, const std::string source, size_t limit ) +{ + memset ( dest, 0, limit ); + size_t count = source.size(); + if ( count >= limit ) count = limit - 1; // Ensure a terminating nul. + memcpy ( dest, source.c_str(), count ); +} + +static void exportXMPtoCr8rChunk ( RIFF_MetaHandler* handler, ValueChunk** cr8rChunk ) +{ + const SXMPMeta & xmp = handler->xmpObj; + + // Make sure an existing Cr8r chunk has the proper fixed length. + bool haveOldCr8r = (*cr8rChunk != 0); + if ( haveOldCr8r && ((*cr8rChunk)->oldSize != sizeof(Cr8rBoxContent)+8) ) { + (*cr8rChunk)->parent->replaceChildWithJunk ( *cr8rChunk ); // Wrong length, the existing chunk must be bad. + (*cr8rChunk) = 0; + haveOldCr8r = false; + } + + bool haveNewCr8r = false; + std::string creatorCode, appleEvent, fileExt, appOptions, appName; + + haveNewCr8r |= xmp.GetStructField ( kXMP_NS_CreatorAtom, "macAtom", kXMP_NS_CreatorAtom, "applicationCode", &creatorCode, 0 ); + haveNewCr8r |= xmp.GetStructField ( kXMP_NS_CreatorAtom, "macAtom", kXMP_NS_CreatorAtom, "invocationAppleEvent", &appleEvent, 0 ); + haveNewCr8r |= xmp.GetStructField ( kXMP_NS_CreatorAtom, "windowsAtom", kXMP_NS_CreatorAtom, "extension", &fileExt, 0 ); + haveNewCr8r |= xmp.GetStructField ( kXMP_NS_CreatorAtom, "windowsAtom", kXMP_NS_CreatorAtom, "invocationFlags", &appOptions, 0 ); + haveNewCr8r |= xmp.GetProperty ( kXMP_NS_XMP, "CreatorTool", &appName, 0 ); + + if ( ! haveNewCr8r ) { // Get rid of an existing Cr8r chunk if there is no new XMP. + if ( haveOldCr8r ) { + (*cr8rChunk)->parent->replaceChildWithJunk ( *cr8rChunk ); + *cr8rChunk = 0; + } + return; + } + + if ( ! haveOldCr8r ) { + *cr8rChunk = new ValueChunk ( handler->lastChunk, std::string(), kChunk_Cr8r ); + } + + std::string strValue; + strValue.assign ( (sizeof(Cr8rBoxContent) - 1), '\0' ); // ! Use size-1 because SetValue appends a trailing 0 byte. + (*cr8rChunk)->SetValue ( strValue ); // ! Just get the space available. + XMP_Assert ( (*cr8rChunk)->newValue.size() == sizeof(Cr8rBoxContent) ); + (*cr8rChunk)->hasChange = true; + + Cr8rBoxContent * newCr8r = (Cr8rBoxContent*) (*cr8rChunk)->newValue.data(); + + if ( ! haveOldCr8r ) { + + newCr8r->magic = MakeUns32LE ( 0xBEEFCAFE ); + newCr8r->size = MakeUns32LE ( sizeof(Cr8rBoxContent) ); + newCr8r->majorVer = MakeUns16LE ( 1 ); + + } else { + + const Cr8rBoxContent * oldCr8r = (Cr8rBoxContent*) (*cr8rChunk)->oldValue.data(); + memcpy ( newCr8r, oldCr8r, sizeof(Cr8rBoxContent) ); + if ( GetUns32LE ( &newCr8r->magic ) != 0xBEEFCAFE ) { // Make sure we write LE numbers. + Flip4 ( &newCr8r->magic ); + Flip4 ( &newCr8r->size ); + Flip2 ( &newCr8r->majorVer ); + Flip2 ( &newCr8r->minorVer ); + Flip4 ( &newCr8r->creatorCode ); + Flip4 ( &newCr8r->appleEvent ); + } + + } + + if ( ! creatorCode.empty() ) { + newCr8r->creatorCode = MakeUns32LE ( (XMP_Uns32) strtoul ( creatorCode.c_str(), 0, 0 ) ); + } + + if ( ! appleEvent.empty() ) { + newCr8r->appleEvent = MakeUns32LE ( (XMP_Uns32) strtoul ( appleEvent.c_str(), 0, 0 ) ); + } + + if ( ! fileExt.empty() ) SetBufferedString ( newCr8r->fileExt, fileExt, sizeof ( newCr8r->fileExt ) ); + if ( ! appOptions.empty() ) SetBufferedString ( newCr8r->appOptions, appOptions, sizeof ( newCr8r->appOptions ) ); + if ( ! appName.empty() ) SetBufferedString ( newCr8r->appName, appName, sizeof ( newCr8r->appName ) ); + +} + +static void exportXMPtoListChunk( XMP_Uns32 id, XMP_Uns32 containerType, + RIFF_MetaHandler* handler, ContainerChunk** listChunk, Mapping mapping[]) +{ + // note: ContainerChunk**: adress of pointer to allow changing the pointer itself (i.e. chunk creation) + SXMPMeta* xmp = &handler->xmpObj; + bool listChunkIsNeeded = false; // assume for now + + // ! The NUL is optional in WAV to avoid a parsing bug in Audition 3 - can't handle implicit pad byte. + bool optionalNUL = (handler->parent->format == kXMP_WAVFile); + + for ( int p=0; mapping[p].chunkID != 0; ++p ) { // go through all potential property mappings + + bool propExists = false; + std::string value, actualLang; + + switch ( mapping[p].propType ) { + + // get property. if existing, remove from XMP (to avoid redundant storage) + case prop_TIMEVALUE: + propExists = xmp->GetStructField ( mapping[p].ns, mapping[p].prop, kXMP_NS_DM, "timeValue", &value, 0 ); + break; + + case prop_LOCALIZED_TEXT: + propExists = xmp->GetLocalizedText ( mapping[p].ns, mapping[p].prop, "", "x-default", &actualLang, &value, 0); + if ( actualLang != "x-default" ) propExists = false; // no "x-default" => nothing to reconcile ! + break; + + case prop_ARRAYITEM: + propExists = xmp->GetArrayItem ( mapping[p].ns, mapping[p].prop, 1, &value, 0 ); + break; + + case prop_SIMPLE: + propExists = xmp->GetProperty ( mapping[p].ns, mapping[p].prop, &value, 0 ); + break; + + default: + XMP_Throw ( "internal error", kXMPErr_InternalFailure ); + + } + + if ( ! propExists ) { + + if ( *listChunk != 0 ) (*listChunk)->removeValue ( mapping[p].chunkID ); + + } else { + + listChunkIsNeeded = true; + if ( *listChunk == 0 ) *listChunk = new ContainerChunk ( handler->riffChunks[0], id, containerType ); + + valueMap* cm = &(*listChunk)->childmap; + valueMapIter result = cm->find( mapping[p].chunkID ); + ValueChunk* propChunk = 0; + + if ( result != cm->end() ) { + propChunk = result->second; + } else { + propChunk = new ValueChunk ( *listChunk, std::string(), mapping[p].chunkID ); + } + + propChunk->SetValue ( value.c_str(), optionalNUL ); + + } + + } // for each property + + if ( (! listChunkIsNeeded) && (*listChunk != 0) && ((*listChunk)->children.size() == 0) ) { + (*listChunk)->parent->replaceChildWithJunk ( *listChunk ); + (*listChunk) = 0; // reset direct Chunk pointer + } + +} + +void exportAndRemoveProperties ( RIFF_MetaHandler* handler ) +{ + SXMPMeta xmpObj = handler->xmpObj; + + exportXMPtoCr8rChunk ( handler, &handler->cr8rChunk ); + + // 1/4 BWF Bext extension chunk ----------------------------------------------- + if ( handler->parent->format == kXMP_WAVFile ) { // applies only to WAV + exportXMPtoBextChunk ( handler, &handler->bextChunk ); + } + + // 2/4 DISP chunk + if ( handler->parent->format == kXMP_WAVFile ) { // create for WAVE only + + std::string actualLang, xmpValue; + bool r = xmpObj.GetLocalizedText ( kXMP_NS_DC, "title", "" , "x-default" , &actualLang, &xmpValue, 0 ); + + if ( r && ( actualLang == "x-default" ) ) { // prop exists? + + // the 'right' DISP is lead by a 32 bit low endian 0x0001 + std::string dispValue = std::string( "\x1\0\0\0", 4 ); + dispValue.append ( xmpValue ); + + if ( handler->dispChunk == 0 ) { + handler->dispChunk = new RIFF::ValueChunk ( handler->riffChunks.at(0), std::string(), kChunk_DISP ); + } + + // ! The NUL is optional in WAV to avoid a parsing bug in Audition 3 - can't handle implicit pad byte. + handler->dispChunk->SetValue ( dispValue, ValueChunk::kNULisOptional ); + + } else { // remove Disp Chunk.. + + if ( handler->dispChunk != 0 ) { // ..if existing + ContainerChunk* mainChunk = handler->riffChunks.at(0); + Chunk* needle = handler->dispChunk; + chunkVectIter iter = mainChunk->getChild ( needle ); + if ( iter != mainChunk->children.end() ) { + mainChunk->replaceChildWithJunk ( *iter ); + handler->dispChunk = 0; + mainChunk->hasChange = true; + } + } + + } + + } + + // 3/4 LIST:INFO + exportXMPtoListChunk ( kChunk_LIST, kType_INFO, handler, &handler->listInfoChunk, listInfoProps ); + + // 4/4 LIST:Tdat + exportXMPtoListChunk ( kChunk_LIST, kType_Tdat, handler, &handler->listTdatChunk, listTdatProps ); + +} + +} // namespace RIFF diff --git a/XMPFiles/source/FormatSupport/RIFF_Support.hpp b/XMPFiles/source/FormatSupport/RIFF_Support.hpp new file mode 100644 index 0000000..c88599b --- /dev/null +++ b/XMPFiles/source/FormatSupport/RIFF_Support.hpp @@ -0,0 +1,42 @@ +#ifndef __RIFF_Support_hpp__ +#define __RIFF_Support_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2009 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. +#include <vector> +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +// ahead declaration: +class RIFF_MetaHandler; + +namespace RIFF { + + // declare ahead + class Chunk; + class ContainerChunk; + class ValueChunk; + class XMPChunk; + + /* This rountines imports the properties found into the + xmp packet. Use after parsing. */ + void importProperties( RIFF_MetaHandler* handler ); + + /* This rountines exports XMP properties to the respective Chunks, + creating those if needed. No writing to file here. */ + void exportAndRemoveProperties( RIFF_MetaHandler* handler ); + + /* will relocated a wrongly placed chunk (one of XMP, LIST:Info, LIST:Tdat= + from RIFF::avix back to main chunk. Chunk itself not touched. */ + void relocateWronglyPlacedXMPChunk( RIFF_MetaHandler* handler ); + +} // namespace RIFF + +#endif // __RIFF_Support_hpp__ diff --git a/XMPFiles/source/FormatSupport/ReconcileIPTC.cpp b/XMPFiles/source/FormatSupport/ReconcileIPTC.cpp new file mode 100644 index 0000000..df9d676 --- /dev/null +++ b/XMPFiles/source/FormatSupport/ReconcileIPTC.cpp @@ -0,0 +1,857 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp" +#include "source/XIO.hpp" + +#include <stdio.h> + +#if XMP_WinBuild + #pragma warning ( disable : 4800 ) // forcing value to bool 'true' or 'false' (performance warning) + #pragma warning ( disable : 4996 ) // '...' was declared deprecated +#endif + +// ================================================================================================= +/// \file ReconcileIPTC.cpp +/// \brief Utilities to reconcile between XMP and legacy IPTC and PSIR metadata. +/// +// ================================================================================================= + +// ================================================================================================= +// NormalizeToCR +// ============= + +static inline void NormalizeToCR ( std::string * value ) +{ + char * strPtr = (char*) value->data(); + char * strEnd = strPtr + value->size(); + + for ( ; strPtr < strEnd; ++strPtr ) { + if ( *strPtr == kLF ) *strPtr = kCR; + } + +} // NormalizeToCR + +// ================================================================================================= +// NormalizeToLF +// ============= + +static inline void NormalizeToLF ( std::string * value ) +{ + char * strPtr = (char*) value->data(); + char * strEnd = strPtr + value->size(); + + for ( ; strPtr < strEnd; ++strPtr ) { + if ( *strPtr == kCR ) *strPtr = kLF; + } + +} // NormalizeToLF + +// ================================================================================================= +// ComputeIPTCDigest +// ================= +// +// Compute a 128 bit (16 byte) MD5 digest of the full IPTC block. + +static inline void ComputeIPTCDigest ( const void * iptcPtr, const XMP_Uns32 iptcLen, MD5_Digest * digest ) +{ + MD5_CTX context; + + MD5Init ( &context ); + MD5Update ( &context, (XMP_Uns8*)iptcPtr, iptcLen ); + MD5Final ( *digest, &context ); + +} // ComputeIPTCDigest; + +// ================================================================================================= +// PhotoDataUtils::CheckIPTCDigest +// =============================== + +int PhotoDataUtils::CheckIPTCDigest ( const void * newPtr, const XMP_Uns32 newLen, const void * oldDigest ) +{ + MD5_Digest newDigest; + ComputeIPTCDigest ( newPtr, newLen, &newDigest ); + if ( memcmp ( &newDigest, oldDigest, 16 ) == 0 ) return kDigestMatches; + return kDigestDiffers; + +} // PhotoDataUtils::CheckIPTCDigest + +// ================================================================================================= +// PhotoDataUtils::SetIPTCDigest +// ============================= + +void PhotoDataUtils::SetIPTCDigest ( void * iptcPtr, XMP_Uns32 iptcLen, PSIR_Manager * psir ) +{ + MD5_Digest newDigest; + + ComputeIPTCDigest ( iptcPtr, iptcLen, &newDigest ); + psir->SetImgRsrc ( kPSIR_IPTCDigest, &newDigest, sizeof(newDigest) ); + +} // PhotoDataUtils::SetIPTCDigest + +// ================================================================================================= +// ================================================================================================= + +// ================================================================================================= +// PhotoDataUtils::ImportIPTC_Simple +// ================================= + +void PhotoDataUtils::ImportIPTC_Simple ( const IPTC_Manager & iptc, SXMPMeta * xmp, + XMP_Uns8 id, const char * xmpNS, const char * xmpProp ) +{ + std::string utf8Str; + size_t count = iptc.GetDataSet_UTF8 ( id, &utf8Str ); + + if ( count != 0 ) { + NormalizeToLF ( &utf8Str ); + xmp->SetProperty ( xmpNS, xmpProp, utf8Str.c_str() ); + } + +} // PhotoDataUtils::ImportIPTC_Simple + +// ================================================================================================= +// PhotoDataUtils::ImportIPTC_LangAlt +// ================================== + +void PhotoDataUtils::ImportIPTC_LangAlt ( const IPTC_Manager & iptc, SXMPMeta * xmp, + XMP_Uns8 id, const char * xmpNS, const char * xmpProp ) +{ + std::string utf8Str; + size_t count = iptc.GetDataSet_UTF8 ( id, &utf8Str ); + + if ( count != 0 ) { + NormalizeToLF ( &utf8Str ); + xmp->SetLocalizedText ( xmpNS, xmpProp, "", "x-default", utf8Str.c_str() ); + } + +} // PhotoDataUtils::ImportIPTC_LangAlt + +// ================================================================================================= +// PhotoDataUtils::ImportIPTC_Array +// ================================ + +void PhotoDataUtils::ImportIPTC_Array ( const IPTC_Manager & iptc, SXMPMeta * xmp, + XMP_Uns8 id, const char * xmpNS, const char * xmpProp ) +{ + std::string utf8Str; + size_t count = iptc.GetDataSet ( id, 0 ); + + xmp->DeleteProperty ( xmpNS, xmpProp ); + + XMP_OptionBits arrayForm = kXMP_PropArrayIsUnordered; + if ( XMP_LitMatch ( xmpNS, kXMP_NS_DC ) && XMP_LitMatch ( xmpProp, "creator" ) ) arrayForm = kXMP_PropArrayIsOrdered; + + for ( size_t ds = 0; ds < count; ++ds ) { + (void) iptc.GetDataSet_UTF8 ( id, &utf8Str, ds ); + NormalizeToLF ( &utf8Str ); + xmp->AppendArrayItem ( xmpNS, xmpProp, arrayForm, utf8Str.c_str() ); + } + +} // PhotoDataUtils::ImportIPTC_Array + +// ================================================================================================= +// PhotoDataUtils::ImportIPTC_Date +// =============================== +// +// An IPTC (IIM) date is 8 characters, YYYYMMDD. Include the time portion if it is present. The IPTC +// time is HHMMSSxHHMM, where 'x' is '+' or '-'. Be tolerant of some ill-formed dates and times. +// Apparently some non-Adobe apps put strings like "YYYY-MM-DD" or "HH:MM:SSxHH:MM" in the IPTC. +// Allow a missing time zone portion. + +// *** The date/time handling differs from the MWG 1.0.1 policy, following a proposed tweak to MWG: +// *** Exif DateTimeOriginal <-> XMP exif:DateTimeOriginal +// *** IPTC DateCreated <-> XMP photoshop:DateCreated +// *** Exif DateTimeDigitized <-> IPTC DigitalCreateDate <-> XMP xmp:CreateDate + +void PhotoDataUtils::ImportIPTC_Date ( XMP_Uns8 dateID, const IPTC_Manager & iptc, SXMPMeta * xmp ) +{ + XMP_Uns8 timeID; + XMP_StringPtr xmpNS, xmpProp; + + if ( dateID == kIPTC_DateCreated ) { + timeID = kIPTC_TimeCreated; + xmpNS = kXMP_NS_Photoshop; + xmpProp = "DateCreated"; + } else if ( dateID == kIPTC_DigitalCreateDate ) { + timeID = kIPTC_DigitalCreateTime; + xmpNS = kXMP_NS_XMP; + xmpProp = "CreateDate"; + } else { + XMP_Throw ( "Unrecognized dateID", kXMPErr_BadParam ); + } + + // First gather the date portion. + + IPTC_Manager::DataSetInfo dsInfo; + size_t count = iptc.GetDataSet ( dateID, &dsInfo ); + if ( count == 0 ) return; + + size_t chPos, digits; + XMP_DateTime xmpDate; + memset ( &xmpDate, 0, sizeof(xmpDate) ); + + chPos = 0; + for ( digits = 0; digits < 4; ++digits, ++chPos ) { + if ( (chPos >= dsInfo.dataLen) || (dsInfo.dataPtr[chPos] < '0') || (dsInfo.dataPtr[chPos] > '9') ) break; + xmpDate.year = (xmpDate.year * 10) + (dsInfo.dataPtr[chPos] - '0'); + } + + if ( dsInfo.dataPtr[chPos] == '-' ) ++chPos; + for ( digits = 0; digits < 2; ++digits, ++chPos ) { + if ( (chPos >= dsInfo.dataLen) || (dsInfo.dataPtr[chPos] < '0') || (dsInfo.dataPtr[chPos] > '9') ) break; + xmpDate.month = (xmpDate.month * 10) + (dsInfo.dataPtr[chPos] - '0'); + } + if ( xmpDate.month < 1 ) xmpDate.month = 1; + if ( xmpDate.month > 12 ) xmpDate.month = 12; + + if ( dsInfo.dataPtr[chPos] == '-' ) ++chPos; + for ( digits = 0; digits < 2; ++digits, ++chPos ) { + if ( (chPos >= dsInfo.dataLen) || (dsInfo.dataPtr[chPos] < '0') || (dsInfo.dataPtr[chPos] > '9') ) break; + xmpDate.day = (xmpDate.day * 10) + (dsInfo.dataPtr[chPos] - '0'); + } + if ( xmpDate.day < 1 ) xmpDate.day = 1; + if ( xmpDate.day > 31 ) xmpDate.day = 28; // Close enough. + + if ( chPos != dsInfo.dataLen ) return; // The DataSet is ill-formed. + xmpDate.hasDate = true; + + // Now add the time portion if present. + + count = iptc.GetDataSet ( timeID, &dsInfo ); + if ( count != 0 ) { + + chPos = 0; + for ( digits = 0; digits < 2; ++digits, ++chPos ) { + if ( (chPos >= dsInfo.dataLen) || (dsInfo.dataPtr[chPos] < '0') || (dsInfo.dataPtr[chPos] > '9') ) break; + xmpDate.hour = (xmpDate.hour * 10) + (dsInfo.dataPtr[chPos] - '0'); + } + if ( xmpDate.hour < 0 ) xmpDate.hour = 0; + if ( xmpDate.hour > 23 ) xmpDate.hour = 23; + + if ( dsInfo.dataPtr[chPos] == ':' ) ++chPos; + for ( digits = 0; digits < 2; ++digits, ++chPos ) { + if ( (chPos >= dsInfo.dataLen) || (dsInfo.dataPtr[chPos] < '0') || (dsInfo.dataPtr[chPos] > '9') ) break; + xmpDate.minute = (xmpDate.minute * 10) + (dsInfo.dataPtr[chPos] - '0'); + } + if ( xmpDate.minute < 0 ) xmpDate.minute = 0; + if ( xmpDate.minute > 59 ) xmpDate.minute = 59; + + if ( dsInfo.dataPtr[chPos] == ':' ) ++chPos; + for ( digits = 0; digits < 2; ++digits, ++chPos ) { + if ( (chPos >= dsInfo.dataLen) || (dsInfo.dataPtr[chPos] < '0') || (dsInfo.dataPtr[chPos] > '9') ) break; + xmpDate.second = (xmpDate.second * 10) + (dsInfo.dataPtr[chPos] - '0'); + } + if ( xmpDate.second < 0 ) xmpDate.second = 0; + if ( xmpDate.second > 59 ) xmpDate.second = 59; + + xmpDate.hasTime = true; + + if ( (dsInfo.dataPtr[chPos] != ' ') && (dsInfo.dataPtr[chPos] != 0) ) { // Tolerate a missing TZ. + + if ( dsInfo.dataPtr[chPos] == '+' ) { + xmpDate.tzSign = kXMP_TimeEastOfUTC; + } else if ( dsInfo.dataPtr[chPos] == '-' ) { + xmpDate.tzSign = kXMP_TimeWestOfUTC; + } else if ( chPos != dsInfo.dataLen ) { + return; // The DataSet is ill-formed. + } + + ++chPos; // Move past the time zone sign. + for ( digits = 0; digits < 2; ++digits, ++chPos ) { + if ( (chPos >= dsInfo.dataLen) || (dsInfo.dataPtr[chPos] < '0') || (dsInfo.dataPtr[chPos] > '9') ) break; + xmpDate.tzHour = (xmpDate.tzHour * 10) + (dsInfo.dataPtr[chPos] - '0'); + } + if ( xmpDate.tzHour < 0 ) xmpDate.tzHour = 0; + if ( xmpDate.tzHour > 23 ) xmpDate.tzHour = 23; + + if ( dsInfo.dataPtr[chPos] == ':' ) ++chPos; + for ( digits = 0; digits < 2; ++digits, ++chPos ) { + if ( (chPos >= dsInfo.dataLen) || (dsInfo.dataPtr[chPos] < '0') || (dsInfo.dataPtr[chPos] > '9') ) break; + xmpDate.tzMinute = (xmpDate.tzMinute * 10) + (dsInfo.dataPtr[chPos] - '0'); + } + if ( xmpDate.tzMinute < 0 ) xmpDate.tzMinute = 0; + if ( xmpDate.tzMinute > 59 ) xmpDate.tzMinute = 59; + + if ( chPos != dsInfo.dataLen ) return; // The DataSet is ill-formed. + xmpDate.hasTimeZone = true; + + } + + } + + // Finally, set the XMP property. + + xmp->SetProperty_Date ( xmpNS, xmpProp, xmpDate ); + +} // PhotoDataUtils::ImportIPTC_Date + +// ================================================================================================= +// ImportIPTC_IntellectualGenre +// ============================ +// +// Import DataSet 2:04. In the IIM this is a 3 digit number, a colon, and an optional text name. +// Even though the number is the more formal part, the IPTC4XMP rule is that the name is imported to +// XMP and the number is dropped. Also, even though IIMv4.1 says that 2:04 is repeatable, the XMP +// property to which it is mapped is simple. + +static void ImportIPTC_IntellectualGenre ( const IPTC_Manager & iptc, SXMPMeta * xmp ) +{ + std::string utf8Str; + size_t count = iptc.GetDataSet_UTF8 ( kIPTC_IntellectualGenre, &utf8Str ); + + if ( count == 0 ) return; + NormalizeToLF ( &utf8Str ); + + XMP_StringPtr namePtr = utf8Str.c_str() + 4; + + if ( utf8Str.size() <= 4 ) { + // No name in the IIM. Look up the number in our list of known genres. + int i; + XMP_StringPtr numPtr = utf8Str.c_str(); + for ( i = 0; kIntellectualGenreMappings[i].refNum != 0; ++i ) { + if ( strncmp ( numPtr, kIntellectualGenreMappings[i].refNum, 3 ) == 0 ) break; + } + if ( kIntellectualGenreMappings[i].refNum == 0 ) return; + namePtr = kIntellectualGenreMappings[i].name; + } + + xmp->SetProperty ( kXMP_NS_IPTCCore, "IntellectualGenre", namePtr ); + +} // ImportIPTC_IntellectualGenre + +// ================================================================================================= +// ImportIPTC_SubjectCode +// ====================== +// +// Import all 2:12 DataSets into an unordered array. In the IIM each DataSet is composed of 5 colon +// separated sections: a provider name, an 8 digit reference number, and 3 optional names for the +// levels of the reference number hierarchy. The IPTC4XMP mapping rule is that only the reference +// number is imported to XMP. + +static void ImportIPTC_SubjectCode ( const IPTC_Manager & iptc, SXMPMeta * xmp ) +{ + std::string utf8Str; + size_t count = iptc.GetDataSet_UTF8 ( kIPTC_SubjectCode, 0 ); + + for ( size_t ds = 0; ds < count; ++ds ) { + + (void) iptc.GetDataSet_UTF8 ( kIPTC_SubjectCode, &utf8Str, ds ); + + char * refNumPtr = (char*) utf8Str.c_str(); + for ( ; (*refNumPtr != ':') && (*refNumPtr != 0); ++refNumPtr ) {} + if ( *refNumPtr == 0 ) continue; // This DataSet is ill-formed. + + char * refNumEnd = refNumPtr + 1; + for ( ; (*refNumEnd != ':') && (*refNumEnd != 0); ++refNumEnd ) {} + if ( (refNumEnd - refNumPtr) != 8 ) continue; // This DataSet is ill-formed. + *refNumEnd = 0; // Ensure a terminating nul for the reference number portion. + + xmp->AppendArrayItem ( kXMP_NS_IPTCCore, "SubjectCode", kXMP_PropArrayIsUnordered, refNumPtr ); + + } + +} // ImportIPTC_SubjectCode + +// ================================================================================================= +// PhotoDataUtils::Import2WayIPTC +// ============================== + +void PhotoDataUtils::Import2WayIPTC ( const IPTC_Manager & iptc, SXMPMeta * xmp, int iptcDigestState ) +{ + if ( iptcDigestState == kDigestMatches ) return; // Ignore the IPTC if the digest matches. + + std::string oldStr, newStr; + IPTC_Writer oldIPTC; + + if ( iptcDigestState == kDigestDiffers ) { + PhotoDataUtils::ExportIPTC ( *xmp, &oldIPTC ); // Predict old IPTC DataSets based on the existing XMP. + } + + size_t newCount; + IPTC_Manager::DataSetInfo newInfo, oldInfo; + + for ( size_t i = 0; kKnownDataSets[i].dsNum != 255; ++i ) { + + const DataSetCharacteristics & thisDS = kKnownDataSets[i]; + if ( thisDS.mapForm >= kIPTC_Map3Way ) continue; // The mapping is handled elsewhere, or not at all. + + bool haveXMP = xmp->DoesPropertyExist ( thisDS.xmpNS, thisDS.xmpProp ); + newCount = PhotoDataUtils::GetNativeInfo ( iptc, thisDS.dsNum, iptcDigestState, haveXMP, &newInfo ); + if ( newCount == 0 ) continue; // GetNativeInfo returns 0 for ignored local text. + + if ( iptcDigestState == kDigestMissing ) { + if ( haveXMP ) continue; // Keep the existing XMP. + } else if ( ! PhotoDataUtils::IsValueDifferent ( iptc, oldIPTC, thisDS.dsNum ) ) { + continue; // Don't import values that match the previous export. + } + + // The IPTC wins. Delete any existing XMP and import the DataSet. + + xmp->DeleteProperty ( thisDS.xmpNS, thisDS.xmpProp ); + + try { // Don't let errors with one stop the others. + + switch ( thisDS.mapForm ) { + + case kIPTC_MapSimple : + ImportIPTC_Simple ( iptc, xmp, thisDS.dsNum, thisDS.xmpNS, thisDS.xmpProp ); + break; + + case kIPTC_MapLangAlt : + ImportIPTC_LangAlt ( iptc, xmp, thisDS.dsNum, thisDS.xmpNS, thisDS.xmpProp ); + break; + + case kIPTC_MapArray : + ImportIPTC_Array ( iptc, xmp, thisDS.dsNum, thisDS.xmpNS, thisDS.xmpProp ); + break; + + case kIPTC_MapSpecial : + if ( thisDS.dsNum == kIPTC_DateCreated ) { + PhotoDataUtils::ImportIPTC_Date ( thisDS.dsNum, iptc, xmp ); + } else if ( thisDS.dsNum == kIPTC_IntellectualGenre ) { + ImportIPTC_IntellectualGenre ( iptc, xmp ); + } else if ( thisDS.dsNum == kIPTC_SubjectCode ) { + ImportIPTC_SubjectCode ( iptc, xmp ); + } else { + XMP_Assert ( false ); // Catch mapping errors. + } + break; + + } + + } catch ( ... ) { + + // Do nothing, let other imports proceed. + // ? Notify client? + + } + + } + +} // PhotoDataUtils::Import2WayIPTC + +// ================================================================================================= +// PhotoDataUtils::ImportPSIR +// ========================== +// +// There are only 2 standalone Photoshop image resources for XMP properties: +// 1034 - Copyright Flag - 0/1 Boolean mapped to xmpRights:Marked. +// 1035 - Copyright URL - Local OS text mapped to xmpRights:WebStatement. + +// ! Photoshop does not use a true/false/missing model for PSIR 1034. Instead it essentially uses a +// ! yes/don't-know model when importing. A missing or 0 value for PSIR 1034 cause xmpRights:Marked +// ! to be deleted. + +void PhotoDataUtils::ImportPSIR ( const PSIR_Manager & psir, SXMPMeta * xmp, int iptcDigestState ) +{ + PSIR_Manager::ImgRsrcInfo rsrcInfo; + bool import; + + if ( iptcDigestState == kDigestMatches ) return; + + try { // Don't let errors with one stop the others. + import = psir.GetImgRsrc ( kPSIR_CopyrightFlag, &rsrcInfo ); + if ( import ) import = (! xmp->DoesPropertyExist ( kXMP_NS_XMP_Rights, "Marked" )); + if ( import && (rsrcInfo.dataLen == 1) && (*((XMP_Uns8*)rsrcInfo.dataPtr) != 0) ) { + xmp->SetProperty_Bool ( kXMP_NS_XMP_Rights, "Marked", true ); + } + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + + try { // Don't let errors with one stop the others. + import = psir.GetImgRsrc ( kPSIR_CopyrightURL, &rsrcInfo ); + if ( import ) import = (! xmp->DoesPropertyExist ( kXMP_NS_XMP_Rights, "WebStatement" )); + if ( import ) { + std::string utf8; + if ( ReconcileUtils::IsUTF8 ( rsrcInfo.dataPtr, rsrcInfo.dataLen ) ) { + utf8.assign ( (char*)rsrcInfo.dataPtr, rsrcInfo.dataLen ); + } else if ( ! ignoreLocalText ) { + ReconcileUtils::LocalToUTF8 ( rsrcInfo.dataPtr, rsrcInfo.dataLen, &utf8 ); + } else { + import = false; // Inhibit the SetProperty call. + } + if ( import ) xmp->SetProperty ( kXMP_NS_XMP_Rights, "WebStatement", utf8.c_str() ); + } + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // PhotoDataUtils::ImportPSIR; + +// ================================================================================================= +// ================================================================================================= + +// ================================================================================================= +// ExportIPTC_Simple +// ================= + +static void ExportIPTC_Simple ( const SXMPMeta & xmp, IPTC_Manager * iptc, + const char * xmpNS, const char * xmpProp, XMP_Uns8 id ) +{ + std::string value; + XMP_OptionBits xmpFlags; + + bool found = xmp.GetProperty ( xmpNS, xmpProp, &value, &xmpFlags ); + if ( ! found ) { + iptc->DeleteDataSet ( id ); + return; + } + + if ( ! XMP_PropIsSimple ( xmpFlags ) ) return; // ? Complain? Delete the DataSet? + + NormalizeToCR ( &value ); + + size_t iptcCount = iptc->GetDataSet ( id, 0 ); + if ( iptcCount > 1 ) iptc->DeleteDataSet ( id ); + + iptc->SetDataSet_UTF8 ( id, value.c_str(), (XMP_Uns32)value.size(), 0 ); // ! Don't append a 2nd DataSet! + +} // ExportIPTC_Simple + +// ================================================================================================= +// ExportIPTC_LangAlt +// ================== + +static void ExportIPTC_LangAlt ( const SXMPMeta & xmp, IPTC_Manager * iptc, + const char * xmpNS, const char * xmpProp, XMP_Uns8 id ) +{ + std::string value; + XMP_OptionBits xmpFlags; + + bool found = xmp.GetProperty ( xmpNS, xmpProp, 0, &xmpFlags ); + if ( ! found ) { + iptc->DeleteDataSet ( id ); + return; + } + + if ( ! XMP_ArrayIsAltText ( xmpFlags ) ) return; // ? Complain? Delete the DataSet? + + found = xmp.GetLocalizedText ( xmpNS, xmpProp, "", "x-default", 0, &value, 0 ); + if ( ! found ) { + iptc->DeleteDataSet ( id ); + return; + } + + NormalizeToCR ( &value ); + + size_t iptcCount = iptc->GetDataSet ( id, 0 ); + if ( iptcCount > 1 ) iptc->DeleteDataSet ( id ); + + iptc->SetDataSet_UTF8 ( id, value.c_str(), (XMP_Uns32)value.size(), 0 ); // ! Don't append a 2nd DataSet! + +} // ExportIPTC_LangAlt + +// ================================================================================================= +// ExportIPTC_Array +// ================ +// +// Array exporting needs a bit of care to preserve the detection of XMP-only updates. If the current +// XMP and IPTC array sizes differ, delete the entire IPTC and append all new values. If they match, +// set the individual values in order - which lets SetDataSet apply its no-change optimization. + +static void ExportIPTC_Array ( const SXMPMeta & xmp, IPTC_Manager * iptc, + const char * xmpNS, const char * xmpProp, XMP_Uns8 id ) +{ + std::string value; + XMP_OptionBits xmpFlags; + + bool found = xmp.GetProperty ( xmpNS, xmpProp, 0, &xmpFlags ); + if ( ! found ) { + iptc->DeleteDataSet ( id ); + return; + } + + if ( ! XMP_PropIsArray ( xmpFlags ) ) return; // ? Complain? Delete the DataSet? + + XMP_Index xmpCount = xmp.CountArrayItems ( xmpNS, xmpProp ); + XMP_Index iptcCount = (XMP_Index) iptc->GetDataSet ( id, 0 ); + + if ( xmpCount != iptcCount ) iptc->DeleteDataSet ( id ); + + for ( XMP_Index ds = 0; ds < xmpCount; ++ds ) { // ! XMP arrays are indexed from 1, IPTC from 0. + + (void) xmp.GetArrayItem ( xmpNS, xmpProp, ds+1, &value, &xmpFlags ); + if ( ! XMP_PropIsSimple ( xmpFlags ) ) continue; // ? Complain? + + NormalizeToCR ( &value ); + + iptc->SetDataSet_UTF8 ( id, value.c_str(), (XMP_Uns32)value.size(), ds ); // ! Appends if necessary. + + } + +} // ExportIPTC_Array + +// ================================================================================================= +// ExportIPTC_IntellectualGenre +// ============================ +// +// Export DataSet 2:04. In the IIM this is a 3 digit number, a colon, and a text name. Even though +// the number is the more formal part, the IPTC4XMP rule is that the name is imported to XMP and the +// number is dropped. Also, even though IIMv4.1 says that 2:04 is repeatable, the XMP property to +// which it is mapped is simple. Look up the XMP value in a list of known genres to get the number. + +static void ExportIPTC_IntellectualGenre ( const SXMPMeta & xmp, IPTC_Manager * iptc ) +{ + std::string xmpValue; + XMP_OptionBits xmpFlags; + + bool found = xmp.GetProperty ( kXMP_NS_IPTCCore, "IntellectualGenre", &xmpValue, &xmpFlags ); + if ( ! found ) { + iptc->DeleteDataSet ( kIPTC_IntellectualGenre ); + return; + } + + if ( ! XMP_PropIsSimple ( xmpFlags ) ) return; // ? Complain? Delete the DataSet? + + NormalizeToCR ( &xmpValue ); + + int i; + XMP_StringPtr namePtr = xmpValue.c_str(); + for ( i = 0; kIntellectualGenreMappings[i].name != 0; ++i ) { + if ( strcmp ( namePtr, kIntellectualGenreMappings[i].name ) == 0 ) break; + } + if ( kIntellectualGenreMappings[i].name == 0 ) return; // Not a known genre, don't export it. + + std::string iimValue = kIntellectualGenreMappings[i].refNum; + iimValue += ':'; + iimValue += xmpValue; + + size_t iptcCount = iptc->GetDataSet ( kIPTC_IntellectualGenre, 0 ); + if ( iptcCount > 1 ) iptc->DeleteDataSet ( kIPTC_IntellectualGenre ); + + iptc->SetDataSet_UTF8 ( kIPTC_IntellectualGenre, iimValue.c_str(), (XMP_Uns32)iimValue.size(), 0 ); // ! Don't append a 2nd DataSet! + +} // ExportIPTC_IntellectualGenre + +// ================================================================================================= +// ExportIPTC_SubjectCode +// ====================== +// +// Export 2:12 DataSets from an unordered array. In the IIM each DataSet is composed of 5 colon +// separated sections: a provider name, an 8 digit reference number, and 3 optional names for the +// levels of the reference number hierarchy. The IPTC4XMP mapping rule is that only the reference +// number is imported to XMP. We export with a fixed provider of "IPTC" and no optional names. + +static void ExportIPTC_SubjectCode ( const SXMPMeta & xmp, IPTC_Manager * iptc ) +{ + std::string xmpValue, iimValue; + XMP_OptionBits xmpFlags; + + bool found = xmp.GetProperty ( kXMP_NS_IPTCCore, "SubjectCode", 0, &xmpFlags ); + if ( ! found ) { + iptc->DeleteDataSet ( kIPTC_SubjectCode ); + return; + } + + if ( ! XMP_PropIsArray ( xmpFlags ) ) return; // ? Complain? Delete the DataSet? + + XMP_Index xmpCount = xmp.CountArrayItems ( kXMP_NS_IPTCCore, "SubjectCode" ); + XMP_Index iptcCount = (XMP_Index) iptc->GetDataSet ( kIPTC_SubjectCode, 0 ); + + if ( xmpCount != iptcCount ) iptc->DeleteDataSet ( kIPTC_SubjectCode ); + + for ( XMP_Index ds = 0; ds < xmpCount; ++ds ) { // ! XMP arrays are indexed from 1, IPTC from 0. + + (void) xmp.GetArrayItem ( kXMP_NS_IPTCCore, "SubjectCode", ds+1, &xmpValue, &xmpFlags ); + if ( ! XMP_PropIsSimple ( xmpFlags ) ) continue; // ? Complain? + if ( xmpValue.size() != 8 ) continue; // ? Complain? + + iimValue = "IPTC:"; + iimValue += xmpValue; + iimValue += ":::"; // Add the separating colons for the empty name portions. + + iptc->SetDataSet_UTF8 ( kIPTC_SubjectCode, iimValue.c_str(), (XMP_Uns32)iimValue.size(), ds ); // ! Appends if necessary. + + } + +} // ExportIPTC_SubjectCode + +// ================================================================================================= +// ExportIPTC_Date +// =============== +// +// The IPTC date and time are "YYYYMMDD" and "HHMMSSxHHMM" where 'x' is '+' or '-'. Export the IPTC +// time only if already present, or if the XMP has a time portion. + +// *** The date/time handling differs from the MWG 1.0 policy, following a proposed tweak to MWG: +// *** Exif DateTimeOriginal <-> IPTC DateCreated <-> XMP photoshop:DateCreated +// *** Exif DateTimeDigitized <-> IPTC DigitalCreateDate <-> XMP xmp:CreateDate + +static void ExportIPTC_Date ( XMP_Uns8 dateID, const SXMPMeta & xmp, IPTC_Manager * iptc ) +{ + XMP_Uns8 timeID; + XMP_StringPtr xmpNS, xmpProp; + + if ( dateID == kIPTC_DateCreated ) { + timeID = kIPTC_TimeCreated; + xmpNS = kXMP_NS_Photoshop; + xmpProp = "DateCreated"; + } else if ( dateID == kIPTC_DigitalCreateDate ) { + timeID = kIPTC_DigitalCreateTime; + xmpNS = kXMP_NS_XMP; + xmpProp = "CreateDate"; + } else { + XMP_Throw ( "Unrecognized dateID", kXMPErr_BadParam ); + } + + iptc->DeleteDataSet ( dateID ); // ! Either the XMP does not exist and we want to + iptc->DeleteDataSet ( timeID ); // ! delete the IPTC, or we're replacing the IPTC. + + XMP_DateTime xmpValue; + bool found = xmp.GetProperty_Date ( xmpNS, xmpProp, &xmpValue, 0 ); + if ( ! found ) return; + + char iimValue[16]; // AUDIT: Big enough for "YYYYMMDD" (8) and "HHMMSS+HHMM" (11). + + // Set the IIM date portion as YYYYMMDD with zeroes for unknown parts. + + snprintf ( iimValue, sizeof(iimValue), "%04d%02d%02d", // AUDIT: Use of sizeof(iimValue) is safe. + xmpValue.year, xmpValue.month, xmpValue.day ); + + iptc->SetDataSet_UTF8 ( dateID, iimValue, 8 ); + + // Set the IIM time portion as HHMMSS+HHMM (or -HHMM). Allow a missing time zone. + + if ( xmpValue.hasTimeZone ) { + snprintf ( iimValue, sizeof(iimValue), "%02d%02d%02d%c%02d%02d", // AUDIT: Use of sizeof(iimValue) is safe. + xmpValue.hour, xmpValue.minute, xmpValue.second, + ((xmpValue.tzSign == kXMP_TimeWestOfUTC) ? '-' : '+'), xmpValue.tzHour, xmpValue.tzMinute ); + iptc->SetDataSet_UTF8 ( timeID, iimValue, 11 ); + } else if ( xmpValue.hasTime ) { + snprintf ( iimValue, sizeof(iimValue), "%02d%02d%02d", // AUDIT: Use of sizeof(iimValue) is safe. + xmpValue.hour, xmpValue.minute, xmpValue.second ); + iptc->SetDataSet_UTF8 ( timeID, iimValue, 6 ); + } else { + iptc->DeleteDataSet ( timeID ); + } + +} // ExportIPTC_Date + +// ================================================================================================= +// PhotoDataUtils::ExportIPTC +// ========================== + +void PhotoDataUtils::ExportIPTC ( const SXMPMeta & xmp, IPTC_Manager * iptc ) +{ + + for ( size_t i = 0; kKnownDataSets[i].dsNum != 255; ++i ) { + + try { // Don't let errors with one stop the others. + + const DataSetCharacteristics & thisDS = kKnownDataSets[i]; + if ( thisDS.mapForm >= kIPTC_UnmappedText ) continue; + + switch ( thisDS.mapForm ) { + + case kIPTC_MapSimple : + ExportIPTC_Simple ( xmp, iptc, thisDS.xmpNS, thisDS.xmpProp, thisDS.dsNum ); + break; + + case kIPTC_MapLangAlt : + ExportIPTC_LangAlt ( xmp, iptc, thisDS.xmpNS, thisDS.xmpProp, thisDS.dsNum ); + break; + + case kIPTC_MapArray : + ExportIPTC_Array ( xmp, iptc, thisDS.xmpNS, thisDS.xmpProp, thisDS.dsNum ); + break; + + case kIPTC_MapSpecial : + if ( thisDS.dsNum == kIPTC_DateCreated ) { + ExportIPTC_Date ( thisDS.dsNum, xmp, iptc ); + } else if ( thisDS.dsNum == kIPTC_IntellectualGenre ) { + ExportIPTC_IntellectualGenre ( xmp, iptc ); + } else if ( thisDS.dsNum == kIPTC_SubjectCode ) { + ExportIPTC_SubjectCode ( xmp, iptc ); + } else { + XMP_Assert ( false ); // Catch mapping errors. + } + break; + + case kIPTC_Map3Way : // The 3 way case is special for import, not for export. + if ( thisDS.dsNum == kIPTC_DigitalCreateDate ) { + // ! Special case: Don't create IIM DigitalCreateDate. This can avoid PSD + // ! full rewrite due to new mapping from xmp:CreateDate. + if ( iptc->GetDataSet ( thisDS.dsNum, 0 ) > 0 ) ExportIPTC_Date ( thisDS.dsNum, xmp, iptc ); + } else if ( thisDS.dsNum == kIPTC_Creator ) { + ExportIPTC_Array ( xmp, iptc, kXMP_NS_DC, "creator", kIPTC_Creator ); + } else if ( thisDS.dsNum == kIPTC_CopyrightNotice ) { + ExportIPTC_LangAlt ( xmp, iptc, kXMP_NS_DC, "rights", kIPTC_CopyrightNotice ); + } else if ( thisDS.dsNum == kIPTC_Description ) { + ExportIPTC_LangAlt ( xmp, iptc, kXMP_NS_DC, "description", kIPTC_Description ); + } else { + XMP_Assert ( false ); // Catch mapping errors. + } + + } + + } catch ( ... ) { + + // Do nothing, let other exports proceed. + // ? Notify client? + + } + + } + +} // PhotoDataUtils::ExportIPTC; + +// ================================================================================================= +// PhotoDataUtils::ExportPSIR +// ========================== +// +// There are only 2 standalone Photoshop image resources for XMP properties: +// 1034 - Copyright Flag - 0/1 Boolean mapped to xmpRights:Marked. +// 1035 - Copyright URL - Local OS text mapped to xmpRights:WebStatement. + +// ! We don't bother with the CR<->LF normalization for xmpRights:WebStatement. Very little chance +// ! of having a raw CR character in a URI. + +void PhotoDataUtils::ExportPSIR ( const SXMPMeta & xmp, PSIR_Manager * psir ) +{ + bool found; + std::string utf8Value; + + try { // Don't let errors with one stop the others. + found = xmp.GetProperty ( kXMP_NS_XMP_Rights, "Marked", &utf8Value, 0 ); + if ( ! found ) { + psir->DeleteImgRsrc ( kPSIR_CopyrightFlag ); + } else { + bool copyrighted = SXMPUtils::ConvertToBool ( utf8Value ); + psir->SetImgRsrc ( kPSIR_CopyrightFlag, ©righted, 1 ); + } + } catch ( ... ) { + // Do nothing, let other exports proceed. + // ? Notify client? + } + + try { // Don't let errors with one stop the others. + found = xmp.GetProperty ( kXMP_NS_XMP_Rights, "WebStatement", &utf8Value, 0 ); + if ( ! found ) { + psir->DeleteImgRsrc ( kPSIR_CopyrightURL ); + } else if ( ! ignoreLocalText ) { + std::string localValue; + ReconcileUtils::UTF8ToLocal ( utf8Value.c_str(), utf8Value.size(), &localValue ); + psir->SetImgRsrc ( kPSIR_CopyrightURL, localValue.c_str(), (XMP_Uns32)localValue.size() ); + } else if ( ReconcileUtils::IsASCII ( utf8Value.c_str(), utf8Value.size() ) ) { + psir->SetImgRsrc ( kPSIR_CopyrightURL, utf8Value.c_str(), (XMP_Uns32)utf8Value.size() ); + } else { + psir->DeleteImgRsrc ( kPSIR_CopyrightURL ); + } + } catch ( ... ) { + // Do nothing, let other exports proceed. + // ? Notify client? + } + +} // PhotoDataUtils::ExportPSIR; diff --git a/XMPFiles/source/FormatSupport/ReconcileLegacy.cpp b/XMPFiles/source/FormatSupport/ReconcileLegacy.cpp new file mode 100644 index 0000000..e71af09 --- /dev/null +++ b/XMPFiles/source/FormatSupport/ReconcileLegacy.cpp @@ -0,0 +1,205 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/ReconcileLegacy.hpp" +#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp" +#include "source/XIO.hpp" + +// ================================================================================================= +/// \file ReconcileLegacy.cpp +/// \brief Top level parts of utilities to reconcile between XMP and legacy metadata forms such as +/// TIFF/Exif and IPTC. +/// +// ================================================================================================= + +// ================================================================================================= +// ImportPhotoData +// =============== +// +// Import legacy metadata for JPEG, TIFF, and Photoshop files into the XMP. The caller must have +// already done the file specific processing to select the appropriate sources of the TIFF stream, +// the Photoshop image resources, and the IPTC. + +#define SaveExifTag(ns,prop) \ + if ( xmp->DoesPropertyExist ( ns, prop ) ) SXMPUtils::DuplicateSubtree ( *xmp, &savedExif, ns, prop ) +#define RestoreExifTag(ns,prop) \ + if ( savedExif.DoesPropertyExist ( ns, prop ) ) SXMPUtils::DuplicateSubtree ( savedExif, xmp, ns, prop ) + +void ImportPhotoData ( const TIFF_Manager & exif, + const IPTC_Manager & iptc, + const PSIR_Manager & psir, + int iptcDigestState, + SXMPMeta * xmp, + XMP_OptionBits options /* = 0 */ ) +{ + bool haveXMP = XMP_OptionIsSet ( options, k2XMP_FileHadXMP ); + bool haveExif = XMP_OptionIsSet ( options, k2XMP_FileHadExif ); + bool haveIPTC = XMP_OptionIsSet ( options, k2XMP_FileHadIPTC ); + + // Save some new Exif writebacks that can be XMP-only from older versions, delete all of the + // XMP's tiff: and exif: namespaces (they should only reflect native Exif), then put back the + // saved writebacks (which might get replaced by the native Exif values in the Import calls). + // The value of exif:ISOSpeedRatings is saved for special case handling of ISO over 65535. + + bool haveOldExif = true; // Default to old Exif if no version tag. + TIFF_Manager::TagInfo tagInfo; + bool found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_ExifVersion, &tagInfo ); + if ( found && (tagInfo.type == kTIFF_UndefinedType) && (tagInfo.count == 4) ) { + haveOldExif = (strncmp ( (char*)tagInfo.dataPtr, "0230", 4 ) < 0); + } + + SXMPMeta savedExif; + + SaveExifTag ( kXMP_NS_EXIF, "DateTimeOriginal" ); + SaveExifTag ( kXMP_NS_EXIF, "GPSLatitude" ); + SaveExifTag ( kXMP_NS_EXIF, "GPSLongitude" ); + SaveExifTag ( kXMP_NS_EXIF, "GPSTimeStamp" ); + SaveExifTag ( kXMP_NS_EXIF, "GPSAltitude" ); + SaveExifTag ( kXMP_NS_EXIF, "GPSAltitudeRef" ); + SaveExifTag ( kXMP_NS_EXIF, "ISOSpeedRatings" ); + + SXMPUtils::RemoveProperties ( xmp, kXMP_NS_TIFF, 0, kXMPUtil_DoAllProperties ); + SXMPUtils::RemoveProperties ( xmp, kXMP_NS_EXIF, 0, kXMPUtil_DoAllProperties ); + if ( ! haveOldExif ) SXMPUtils::RemoveProperties ( xmp, kXMP_NS_ExifEX, 0, kXMPUtil_DoAllProperties ); + + RestoreExifTag ( kXMP_NS_EXIF, "DateTimeOriginal" ); + RestoreExifTag ( kXMP_NS_EXIF, "GPSLatitude" ); + RestoreExifTag ( kXMP_NS_EXIF, "GPSLongitude" ); + RestoreExifTag ( kXMP_NS_EXIF, "GPSTimeStamp" ); + RestoreExifTag ( kXMP_NS_EXIF, "GPSAltitude" ); + RestoreExifTag ( kXMP_NS_EXIF, "GPSAltitudeRef" ); + RestoreExifTag ( kXMP_NS_EXIF, "ISOSpeedRatings" ); + + // Not obvious here, but the logic in PhotoDataUtils follows the MWG reader guidelines. + + PhotoDataUtils::ImportPSIR ( psir, xmp, iptcDigestState ); + + if ( haveIPTC ) PhotoDataUtils::Import2WayIPTC ( iptc, xmp, iptcDigestState ); + if ( haveExif ) PhotoDataUtils::Import2WayExif ( exif, xmp, iptcDigestState ); + + if ( haveExif | haveIPTC ) PhotoDataUtils::Import3WayItems ( exif, iptc, xmp, iptcDigestState ); + + // If photoshop:DateCreated does not exist try to create it from exif:DateTimeOriginal. + + if ( ! xmp->DoesPropertyExist ( kXMP_NS_Photoshop, "DateCreated" ) ) { + std::string exifValue; + bool haveExifDTO = xmp->GetProperty ( kXMP_NS_EXIF, "DateTimeOriginal", &exifValue, 0 ); + if ( haveExifDTO ) xmp->SetProperty ( kXMP_NS_Photoshop, "DateCreated", exifValue.c_str() ); + } + +} // ImportPhotoData + +// ================================================================================================= +// ExportPhotoData +// =============== + +void ExportPhotoData ( XMP_FileFormat destFormat, + SXMPMeta * xmp, + TIFF_Manager * exif, // Pass 0 if not wanted. + IPTC_Manager * iptc, // Pass 0 if not wanted. + PSIR_Manager * psir, // Pass 0 if not wanted. + XMP_OptionBits options /* = 0 */ ) +{ + XMP_Assert ( (destFormat == kXMP_JPEGFile) || (destFormat == kXMP_TIFFFile) || (destFormat == kXMP_PhotoshopFile) ); + + // Do not write IPTC-IIM or PSIR in DNG files (which are a variant of TIFF). + + if ( (destFormat == kXMP_TIFFFile) && (exif != 0) && + exif->GetTag ( kTIFF_PrimaryIFD, kTIFF_DNGVersion, 0 ) ) { + + iptc = 0; // These prevent calls to ExportIPTC and ExportPSIR. + psir = 0; + + exif->DeleteTag ( kTIFF_PrimaryIFD, kTIFF_IPTC ); // These remove any existing IPTC and PSIR. + exif->DeleteTag ( kTIFF_PrimaryIFD, kTIFF_PSIR ); + + } + + // Export the individual metadata items to the non-XMP forms. Set the IPTC digest whether or not + // it changed, it might not have been present or correct before. + + bool iptcChanged = false; // Save explicitly, internal flag is reset by UpdateMemoryDataSets. + + void * iptcPtr = 0; + XMP_Uns32 iptcLen = 0; + + if ( iptc != 0 ) { + PhotoDataUtils::ExportIPTC ( *xmp, iptc ); + iptcChanged = iptc->IsChanged(); + if ( iptcChanged ) iptc->UpdateMemoryDataSets(); + iptcLen = iptc->GetBlockInfo ( &iptcPtr ); + if ( psir != 0 ) PhotoDataUtils::SetIPTCDigest ( iptcPtr, iptcLen, psir ); + } + + if ( exif != 0 ) PhotoDataUtils::ExportExif ( xmp, exif ); + if ( psir != 0 ) PhotoDataUtils::ExportPSIR ( *xmp, psir ); + + // Now update the non-XMP collections of metadata according to the file format. Do not update + // the XMP here, that is done in the file handlers after deciding if an XMP-only in-place + // update should be done. + // - JPEG has the IPTC in PSIR 1028, the Exif and PSIR are marker segments. + // - TIFF has the IPTC and PSIR in primary IFD tags. + // - PSD has everything in PSIRs. + + if ( destFormat == kXMP_JPEGFile ) { + + if ( iptcChanged && (psir != 0) ) psir->SetImgRsrc ( kPSIR_IPTC, iptcPtr, iptcLen ); + + } else if ( destFormat == kXMP_TIFFFile ) { + + XMP_Assert ( exif != 0 ); + + if ( iptcChanged ) exif->SetTag ( kTIFF_PrimaryIFD, kTIFF_IPTC, kTIFF_UndefinedType, iptcLen, iptcPtr ); + + if ( (psir != 0) && psir->IsChanged() ) { + void* psirPtr; + XMP_Uns32 psirLen = psir->UpdateMemoryResources ( &psirPtr ); + exif->SetTag ( kTIFF_PrimaryIFD, kTIFF_PSIR, kTIFF_UndefinedType, psirLen, psirPtr ); + } + + } else if ( destFormat == kXMP_PhotoshopFile ) { + + XMP_Assert ( psir != 0 ); + + if ( iptcChanged ) psir->SetImgRsrc ( kPSIR_IPTC, iptcPtr, iptcLen ); + + if ( (exif != 0) && exif->IsChanged() ) { + void* exifPtr; + XMP_Uns32 exifLen = exif->UpdateMemoryStream ( &exifPtr ); + psir->SetImgRsrc ( kPSIR_Exif, exifPtr, exifLen ); + } + + } + + // Strip the tiff: and exif: namespaces from the XMP, we're done with them. Save the Exif + // ISOSpeedRatings if any of the values are over 0xFFFF, the native tag is SHORT. Lower level + // code already kept or stripped the XMP form. + + bool haveOldExif = true; // Default to old Exif if no version tag. + if ( exif != 0 ) { + TIFF_Manager::TagInfo tagInfo; + bool found = exif->GetTag ( kTIFF_ExifIFD, kTIFF_ExifVersion, &tagInfo ); + if ( found && (tagInfo.type == kTIFF_UndefinedType) && (tagInfo.count == 4) ) { + haveOldExif = (strncmp ( (char*)tagInfo.dataPtr, "0230", 4 ) < 0); + } + } + + SXMPMeta savedExif; + SaveExifTag ( kXMP_NS_EXIF, "ISOSpeedRatings" ); + + SXMPUtils::RemoveProperties ( xmp, kXMP_NS_TIFF, 0, kXMPUtil_DoAllProperties ); + SXMPUtils::RemoveProperties ( xmp, kXMP_NS_EXIF, 0, kXMPUtil_DoAllProperties ); + if ( ! haveOldExif ) SXMPUtils::RemoveProperties ( xmp, kXMP_NS_ExifEX, 0, kXMPUtil_DoAllProperties ); + + RestoreExifTag ( kXMP_NS_EXIF, "ISOSpeedRatings" ); + +} // ExportPhotoData diff --git a/XMPFiles/source/FormatSupport/ReconcileLegacy.hpp b/XMPFiles/source/FormatSupport/ReconcileLegacy.hpp new file mode 100644 index 0000000..16333a3 --- /dev/null +++ b/XMPFiles/source/FormatSupport/ReconcileLegacy.hpp @@ -0,0 +1,272 @@ +#ifndef __ReconcileLegacy_hpp__ +#define __ReconcileLegacy_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. + +#include "XMPFiles/source/FormatSupport/TIFF_Support.hpp" +#include "XMPFiles/source/FormatSupport/PSIR_Support.hpp" +#include "XMPFiles/source/FormatSupport/IPTC_Support.hpp" + +// ================================================================================================= +/// \file ReconcileLegacy.hpp +/// \brief Utilities to reconcile between XMP and photo metadata forms such as TIFF/Exif and IPTC. +/// +// ================================================================================================= + +// ImportPhotoData imports TIFF/Exif and IPTC metadata from JPEG, TIFF, and Photoshop files into +// XMP. The caller must have already done the file specific processing to select the appropriate +// sources of the TIFF stream, the Photoshop image resources, and the IPTC. +// +// The reconciliation logic used here is based on the Metadata Working Group guidelines. This is a +// simpler approach than used previously - which was modeled after historical Photoshop behavior. + +enum { // Bits for the options to ImportJTPtoXMP. + k2XMP_FileHadXMP = 0x0001, // Set if the file had an XMP packet. + k2XMP_FileHadIPTC = 0x0002, // Set if the file had legacy IPTC. + k2XMP_FileHadExif = 0x0004 // Set if the file had legacy Exif. +}; + +extern void ImportPhotoData ( const TIFF_Manager & exif, + const IPTC_Manager & iptc, + const PSIR_Manager & psir, + int iptcDigestState, + SXMPMeta * xmp, + XMP_OptionBits options = 0 ); + +// ExportPhotoData exports XMP into TIFF/Exif and IPTC metadata for JPEG, TIFF, and Photoshop files. + +extern void ExportPhotoData ( XMP_FileFormat destFormat, + SXMPMeta * xmp, + TIFF_Manager * exif, // Pass 0 if not wanted. + IPTC_Manager * iptc, // Pass 0 if not wanted. + PSIR_Manager * psir, // Pass 0 if not wanted. + XMP_OptionBits options = 0 ); + +// *** Mapping notes need revision for MWG related changes. + +// ================================================================================================= +// Summary of TIFF/Exif mappings to XMP +// ==================================== +// +// The mapping for each tag is driven mainly by the tag ID, and secondarily by the type. E.g. there +// is no blanket rule that all ASCII tags are mapped to simple strings in XMP. Some, such as +// SubSecTime or GPSLatitudeRef, are combined with other tags; others, like Flash, are reformated. +// However, most tags are in fact mapped in an obvious manner based on their type and count. +// +// Photoshop practice has been to truncate ASCII tags at the first NUL, not supporting the TIFF +// specification's notion of multi-part ASCII values. +// +// Rational values are mapped to XMP as "num/denom". +// +// The tags of UNDEFINED type that are mapped to XMP text are either special cases like ExifVersion +// or the strings with an explicit encoding like UserComment. +// +// Latitude and logitude are mapped to XMP as "DDD,MM,SSk" or "DDD,MM.mmk"; k is N, S, E, or W. +// +// Flash struct in XMP separates the Fired, Return, Mode, Function, and RedEyeMode portions of the +// Exif value. Fired, Function, and RedEyeMode are Boolean; Return and Mode are integers. +// +// The OECF/SFR, CFA, and DeviceSettings tables are described in the XMP spec. +// +// Instead of iterating through all tags in the various IFDs, it is probably more efficient to have +// explicit processing for the tags that get special treatment, and a static table listing those +// that get mapped by type and count. The type and count processing will verify that the actual +// type and count are as expected, if not the tag is ignored. +// +// Here are the primary (0th) IFD tags that get special treatment: +// +// 270, 33432 - ASCII mapped to alt-text['x-default'] +// 306 - DateTime master +// 315 - ASCII mapped to text seq[1] +// +// Here are the primary (0th) IFD tags that get mapped by type and count: +// +// 256, 257, 258, 259, 262, 271, 272, 274, 277, 282, 283, 284, 296, 301, 305, 318, 319, +// 529, 530, 531, 532 +// +// Here are the Exif IFD tags that get special treatment: +// +// 34856, 41484 - OECF/SFR table +// 36864, 40960 - 4 ASCII chars to text +// 36867, 36868 - DateTime master +// 37121 - 4 UInt8 to integer seq +// 37385 - Flash struct +// 37510 - explicitly encoded text to alt-text['x-default'] +// 41728, 41729 - UInt8 to integer +// 41730 - CFA table +// 41995 - DeviceSettings table +// +// Here are the Exif IFD tags that get mapped by type and count: +// +// 33434, 33437, 34850, 34852, 34855, 37122, 37377, 37378, 37379, 37380, 37381, 37382, 37383, 37384, +// 37386, 37396, 40961, 40962, 40963, 40964, 41483, 41486, 41487, 41488, 41492, 41493, 41495, 41985, +// 41986, 41987, 41988, 41989, 41990, 41991, 41992, 41993, 41994, 41996, 42016 +// +// Here are the GPS IFD tags that get special treatment: +// +// 0 - 4 UInt8 to text "n.n.n.n" +// 2, 4, 20, 22 - Latitude or longitude master +// 7 - special DateTime master, the time part +// 27, 28 - explicitly encoded text +// +// Here are the GPS IFD tags that get mapped by type and count: +// +// 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 23, 24, 25, 26, 30 +// ================================================================================================= + +// *** What about the Camera Raw tags that MDKit maps: +// *** 0xFDE8, 0xFDE9, 0xFDEA, 0xFE4C, 0xFE4D, 0xFE4E, 0xFE4F, 0xFE50, 0xFE51, 0xFE52, 0xFE53, +// *** 0xFE54, 0xFE55, 0xFE56, 0xFE57, 0xFE58 + +// ================================================================================================= +// Summary of TIFF/Exif mappings from XMP +// ====================================== +// +// Only a small number of properties are written back from XMP to TIFF/Exif. Most of the TIFF/Exif +// tags mapped into XMP are information about the image or capture process, not things that users +// should be editing. The tags that can be edited and written back to TIFF/Exif are: +// +// 270, 274, 282, 283, 296, 305, 306, 315, 33432; 36867, 36868, 37510, 40964 +// ================================================================================================= + +// ================================================================================================= +// Details of TIFF/Exif mappings +// ============================= +// +// General (primary and thumbnail, 0th and 1st) IFD tags +// tag TIFF type count Name XMP mapping +// +// 256 SHORTorLONG 1 ImageWidth integer +// 257 SHORTorLONG 1 ImageLength integer +// 258 SHORT 3 BitsPerSample integer seq +// 259 SHORT 1 Compression integer +// 262 SHORT 1 PhotometricInterpretation integer +// 270 ASCII Any ImageDescription text, dc:description['x-default'] +// 271 ASCII Any Make text +// 272 ASCII Any Model text +// 274 SHORT 1 Orientation integer +// 277 SHORT 1 SamplesPerPixel integer +// 282 RATIONAL 1 XResolution rational +// 283 RATIONAL 1 YResolution rational +// 284 SHORT 1 PlanarConfiguration integer +// 296 SHORT 1 ResolutionUnit integer +// 301 SHORT 3*256 TransferFunction integer seq +// 305 ASCII Any Software text, xmp:CreatorTool +// 306 ASCII 20 DateTime date, master of 37520, xmp:DateTime +// 315 ASCII Any Artist text, dc:creator[1] +// 318 RATIONAL 2 WhitePoint rational seq +// 319 RATIONAL 6 PrimaryChromaticities rational seq +// 529 RATIONAL 3 YCbCrCoefficients rational seq +// 530 SHORT 2 YCbCrSubSampling integer seq +// 531 SHORT 1 YCbCrPositioning integer +// 532 RATIONAL 6 ReferenceBlackWhite rational seq +// 33432 ASCII Any Copyright text, dc:rights['x-default'] +// +// Exif IFD tags +// tag TIFF type count Name XMP mapping +// +// 33434 RATIONAL 1 ExposureTime rational +// 33437 RATIONAL 1 FNumber rational +// 34850 SHORT 1 ExposureProgram integer +// 34852 ASCII Any SpectralSensitivity text +// 34855 SHORT Any ISOSpeedRatings integer seq +// 34856 UNDEFINED Any OECF OECF/SFR table +// 36864 UNDEFINED 4 ExifVersion text, Exif has 4 ASCII chars +// 36867 ASCII 20 DateTimeOriginal date, master of 37521 +// 36868 ASCII 20 DateTimeDigitized date, master of 37522 +// 37121 UNDEFINED 4 ComponentsConfiguration integer seq, Exif has 4 UInt8 +// 37122 RATIONAL 1 CompressedBitsPerPixel rational +// 37377 SRATIONAL 1 ShutterSpeedValue rational +// 37378 RATIONAL 1 ApertureValue rational +// 37379 SRATIONAL 1 BrightnessValue rational +// 37380 SRATIONAL 1 ExposureBiasValue rational +// 37381 RATIONAL 1 MaxApertureValue rational +// 37382 RATIONAL 1 SubjectDistance rational +// 37383 SHORT 1 MeteringMode integer +// 37384 SHORT 1 LightSource integer +// 37385 SHORT 1 Flash Flash struct +// 37386 RATIONAL 1 FocalLength rational +// 37396 SHORT 2..4 SubjectArea integer seq +// 37510 UNDEFINED Any UserComment text, explicit encoding, exif:UserComment['x-default] +// 37520 ASCII Any SubSecTime date, with 306 +// 37521 ASCII Any SubSecTimeOriginal date, with 36867 +// 37522 ASCII Any SubSecTimeDigitized date, with 36868 +// 40960 UNDEFINED 4 FlashpixVersion text, Exif has 4 ASCII chars +// 40961 SHORT 1 ColorSpace integer +// 40962 SHORTorLONG 1 PixelXDimension integer +// 40963 SHORTorLONG 1 PixelYDimension integer +// 40964 ASCII 13 RelatedSoundFile text +// 41483 RATIONAL 1 FlashEnergy rational +// 41484 UNDEFINED Any SpatialFrequencyResponse OECF/SFR table +// 41486 RATIONAL 1 FocalPlaneXResolution rational +// 41487 RATIONAL 1 FocalPlaneYResolution rational +// 41488 SHORT 1 FocalPlaneResolutionUnit integer +// 41492 SHORT 2 SubjectLocation integer seq +// 41493 RATIONAL 1 ExposureIndex rational +// 41495 SHORT 1 SensingMethod integer +// 41728 UNDEFINED 1 FileSource integer, Exif has UInt8 +// 41729 UNDEFINED 1 SceneType integer, Exif has UInt8 +// 41730 UNDEFINED Any CFAPattern CFA table +// 41985 SHORT 1 CustomRendered integer +// 41986 SHORT 1 ExposureMode integer +// 41987 SHORT 1 WhiteBalance integer +// 41988 RATIONAL 1 DigitalZoomRatio rational +// 41989 SHORT 1 FocalLengthIn35mmFilm integer +// 41990 SHORT 1 SceneCaptureType integer +// 41991 SHORT 1 GainControl integer +// 41992 SHORT 1 Contrast integer +// 41993 SHORT 1 Saturation integer +// 41994 SHORT 1 Sharpness integer +// 41995 UNDEFINED Any DeviceSettingDescription DeviceSettings table +// 41996 SHORT 1 SubjectDistanceRange integer +// 42016 ASCII 33 ImageUniqueID text +// +// GPS IFD tags +// tag TIFF type count Name XMP mapping +// +// 0 BYTE 4 GPSVersionID text, "n.n.n.n", Exif has 4 UInt8 +// 1 ASCII 2 GPSLatitudeRef latitude, with 2 +// 2 RATIONAL 3 GPSLatitude latitude, master of 2 +// 3 ASCII 2 GPSLongitudeRef longitude, with 4 +// 4 RATIONAL 3 GPSLongitude longitude, master of 3 +// 5 BYTE 1 GPSAltitudeRef integer +// 6 RATIONAL 1 GPSAltitude rational +// 7 RATIONAL 3 GPSTimeStamp date, master of 29 +// 8 ASCII Any GPSSatellites text +// 9 ASCII 2 GPSStatus text +// 10 ASCII 2 GPSMeasureMode text +// 11 RATIONAL 1 GPSDOP rational +// 12 ASCII 2 GPSSpeedRef text +// 13 RATIONAL 1 GPSSpeed rational +// 14 ASCII 2 GPSTrackRef text +// 15 RATIONAL 1 GPSTrack rational +// 16 ASCII 2 GPSImgDirectionRef text +// 17 RATIONAL 1 GPSImgDirection rational +// 18 ASCII Any GPSMapDatum text +// 19 ASCII 2 GPSDestLatitudeRef latitude, with 20 +// 20 RATIONAL 3 GPSDestLatitude latitude, master of 19 +// 21 ASCII 2 GPSDestLongitudeRef longitude, with 22 +// 22 RATIONAL 3 GPSDestLongitude logitude, master of 21 +// 23 ASCII 2 GPSDestBearingRef text +// 24 RATIONAL 1 GPSDestBearing rational +// 25 ASCII 2 GPSDestDistanceRef text +// 26 RATIONAL 1 GPSDestDistance rational +// 27 UNDEFINED Any GPSProcessingMethod text, explicit encoding +// 28 UNDEFINED Any GPSAreaInformation text, explicit encoding +// 29 ASCII 11 GPSDateStamp date, with 29 +// 30 SHORT 1 GPSDifferential integer +// +// ================================================================================================= + +// ================================================================================================= + +#endif // #ifndef __ReconcileLegacy_hpp__ diff --git a/XMPFiles/source/FormatSupport/ReconcileTIFF.cpp b/XMPFiles/source/FormatSupport/ReconcileTIFF.cpp new file mode 100644 index 0000000..225c91f --- /dev/null +++ b/XMPFiles/source/FormatSupport/ReconcileTIFF.cpp @@ -0,0 +1,3443 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp" +#include "source/UnicodeConversions.hpp" +#include "source/XIO.hpp" + +#include <stdio.h> +#if XMP_WinBuild + #define snprintf _snprintf +#endif + +#if XMP_WinBuild + #pragma warning ( disable : 4146 ) // unary minus operator applied to unsigned type + #pragma warning ( disable : 4800 ) // forcing value to bool 'true' or 'false' + #pragma warning ( disable : 4996 ) // '...' was declared deprecated +#endif + +// ================================================================================================= +/// \file ReconcileTIFF.cpp +/// \brief Utilities to reconcile between XMP and legacy TIFF/Exif metadata. +/// +// ================================================================================================= + +// ================================================================================================= + +#ifndef SupportOldExifProperties + #define SupportOldExifProperties 1 + // This controls support of the old Adobe names for things that have official names as of Exif 2.3. +#endif + +// ================================================================================================= +// Tables of the TIFF/Exif tags that are mapped into XMP. For the most part, the tags have obvious +// mappings based on their IFD, tag number, type and count. These tables do not list tags that are +// mapped as subsidiary parts of others, e.g. TIFF SubSecTime or GPS Info GPSDateStamp. Tags that +// have special mappings are marked by having an empty string for the XMP property name. + +// ! These tables have the tags listed in the order of tables 3, 4, 5, and 12 of Exif 2.2, with the +// ! exception of ImageUniqueID (which is listed at the end of the Exif mappings). This order is +// ! very important to consistent checking of the legacy status. The NativeDigest properties list +// ! all possible mapped tags in this order. The NativeDigest strings are compared as a whole, so +// ! the same tags listed in a different order would compare as different. + +// ! The sentinel tag value can't be 0, that is a valid GPS Info tag, 0xFFFF is unused so far. + +enum { + kExport_Never = 0, // Never export. + kExport_Always = 1, // Add, modify, or delete. + kExport_NoDelete = 2, // Add or modify, do not delete if no XMP. + kExport_InjectOnly = 3 // Add tag if new, never modify or delete existing values. +}; + +struct TIFF_MappingToXMP { + XMP_Uns16 id; + XMP_Uns16 type; + XMP_Uns32 count; // Zero means any. + XMP_Uns8 exportMode; + const char * ns; // The namespace of the mapped XMP property. + const char * name; // The name of the mapped XMP property. +}; + +enum { kAnyCount = 0 }; + +static const TIFF_MappingToXMP sPrimaryIFDMappings[] = { // A blank name indicates a special mapping. + { /* 256 */ kTIFF_ImageWidth, kTIFF_ShortOrLongType, 1, kExport_Never, kXMP_NS_TIFF, "ImageWidth" }, + { /* 257 */ kTIFF_ImageLength, kTIFF_ShortOrLongType, 1, kExport_Never, kXMP_NS_TIFF, "ImageLength" }, + { /* 258 */ kTIFF_BitsPerSample, kTIFF_ShortType, 3, kExport_Never, kXMP_NS_TIFF, "BitsPerSample" }, + { /* 259 */ kTIFF_Compression, kTIFF_ShortType, 1, kExport_Never, kXMP_NS_TIFF, "Compression" }, + { /* 262 */ kTIFF_PhotometricInterpretation, kTIFF_ShortType, 1, kExport_Never, kXMP_NS_TIFF, "PhotometricInterpretation" }, + { /* 274 */ kTIFF_Orientation, kTIFF_ShortType, 1, kExport_NoDelete, kXMP_NS_TIFF, "Orientation" }, + { /* 277 */ kTIFF_SamplesPerPixel, kTIFF_ShortType, 1, kExport_Never, kXMP_NS_TIFF, "SamplesPerPixel" }, + { /* 284 */ kTIFF_PlanarConfiguration, kTIFF_ShortType, 1, kExport_Never, kXMP_NS_TIFF, "PlanarConfiguration" }, + { /* 529 */ kTIFF_YCbCrCoefficients, kTIFF_RationalType, 3, kExport_Never, kXMP_NS_TIFF, "YCbCrCoefficients" }, + { /* 530 */ kTIFF_YCbCrSubSampling, kTIFF_ShortType, 2, kExport_Never, kXMP_NS_TIFF, "YCbCrSubSampling" }, + { /* 282 */ kTIFF_XResolution, kTIFF_RationalType, 1, kExport_NoDelete, kXMP_NS_TIFF, "XResolution" }, + { /* 283 */ kTIFF_YResolution, kTIFF_RationalType, 1, kExport_NoDelete, kXMP_NS_TIFF, "YResolution" }, + { /* 296 */ kTIFF_ResolutionUnit, kTIFF_ShortType, 1, kExport_NoDelete, kXMP_NS_TIFF, "ResolutionUnit" }, + { /* 301 */ kTIFF_TransferFunction, kTIFF_ShortType, 3*256, kExport_Never, kXMP_NS_TIFF, "TransferFunction" }, + { /* 318 */ kTIFF_WhitePoint, kTIFF_RationalType, 2, kExport_Never, kXMP_NS_TIFF, "WhitePoint" }, + { /* 319 */ kTIFF_PrimaryChromaticities, kTIFF_RationalType, 6, kExport_Never, kXMP_NS_TIFF, "PrimaryChromaticities" }, + { /* 531 */ kTIFF_YCbCrPositioning, kTIFF_ShortType, 1, kExport_Never, kXMP_NS_TIFF, "YCbCrPositioning" }, + { /* 532 */ kTIFF_ReferenceBlackWhite, kTIFF_RationalType, 6, kExport_Never, kXMP_NS_TIFF, "ReferenceBlackWhite" }, + { /* 306 */ kTIFF_DateTime, kTIFF_ASCIIType, 20, kExport_Always, "", "" }, // ! Has a special mapping. + { /* 270 */ kTIFF_ImageDescription, kTIFF_ASCIIType, kAnyCount, kExport_Always, "", "" }, // ! Has a special mapping. + { /* 271 */ kTIFF_Make, kTIFF_ASCIIType, kAnyCount, kExport_InjectOnly, kXMP_NS_TIFF, "Make" }, + { /* 272 */ kTIFF_Model, kTIFF_ASCIIType, kAnyCount, kExport_InjectOnly, kXMP_NS_TIFF, "Model" }, + { /* 305 */ kTIFF_Software, kTIFF_ASCIIType, kAnyCount, kExport_Always, kXMP_NS_TIFF, "Software" }, // Has alias to xmp:CreatorTool. + { /* 315 */ kTIFF_Artist, kTIFF_ASCIIType, kAnyCount, kExport_Always, "", "" }, // ! Has a special mapping. + { /* 33432 */ kTIFF_Copyright, kTIFF_ASCIIType, kAnyCount, kExport_Always, "", "" }, // ! Has a special mapping. + { 0xFFFF, 0, 0, 0 } // ! Must end with sentinel. +}; + +static const TIFF_MappingToXMP sExifIFDMappings[] = { + + // From Exif 2.3 table 7: + { /* 36864 */ kTIFF_ExifVersion, kTIFF_UndefinedType, 4, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 40960 */ kTIFF_FlashpixVersion, kTIFF_UndefinedType, 4, kExport_Never, "", "" }, // ! Has a special mapping. + { /* 40961 */ kTIFF_ColorSpace, kTIFF_ShortType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "ColorSpace" }, + { /* 42240 */ kTIFF_Gamma, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_ExifEX, "Gamma" }, + { /* 37121 */ kTIFF_ComponentsConfiguration, kTIFF_UndefinedType, 4, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 37122 */ kTIFF_CompressedBitsPerPixel, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "CompressedBitsPerPixel" }, + { /* 40962 */ kTIFF_PixelXDimension, kTIFF_ShortOrLongType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "PixelXDimension" }, + { /* 40963 */ kTIFF_PixelYDimension, kTIFF_ShortOrLongType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "PixelYDimension" }, + { /* 37510 */ kTIFF_UserComment, kTIFF_UndefinedType, kAnyCount, kExport_Always, "", "" }, // ! Has a special mapping. + { /* 40964 */ kTIFF_RelatedSoundFile, kTIFF_ASCIIType, kAnyCount, kExport_Always, kXMP_NS_EXIF, "RelatedSoundFile" }, // ! Exif spec says count of 13. + { /* 36867 */ kTIFF_DateTimeOriginal, kTIFF_ASCIIType, 20, kExport_Always, "", "" }, // ! Has a special mapping. + { /* 36868 */ kTIFF_DateTimeDigitized, kTIFF_ASCIIType, 20, kExport_Always, "", "" }, // ! Has a special mapping. + { /* 42016 */ kTIFF_ImageUniqueID, kTIFF_ASCIIType, 33, kExport_InjectOnly, kXMP_NS_EXIF, "ImageUniqueID" }, + { /* 42032 */ kTIFF_CameraOwnerName, kTIFF_ASCIIType, kAnyCount, kExport_InjectOnly, kXMP_NS_ExifEX, "CameraOwnerName" }, + { /* 42033 */ kTIFF_BodySerialNumber, kTIFF_ASCIIType, kAnyCount, kExport_InjectOnly, kXMP_NS_ExifEX, "BodySerialNumber" }, + { /* 42034 */ kTIFF_LensSpecification, kTIFF_RationalType, 4, kExport_InjectOnly, kXMP_NS_ExifEX, "LensSpecification" }, + { /* 42035 */ kTIFF_LensMake, kTIFF_ASCIIType, kAnyCount, kExport_InjectOnly, kXMP_NS_ExifEX, "LensMake" }, + { /* 42036 */ kTIFF_LensModel, kTIFF_ASCIIType, kAnyCount, kExport_InjectOnly, kXMP_NS_ExifEX, "LensModel" }, + { /* 42037 */ kTIFF_LensSerialNumber, kTIFF_ASCIIType, kAnyCount, kExport_InjectOnly, kXMP_NS_ExifEX, "LensSerialNumber" }, + + // From Exif 2.3 table 8: + { /* 33434 */ kTIFF_ExposureTime, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "ExposureTime" }, + { /* 33437 */ kTIFF_FNumber, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "FNumber" }, + { /* 34850 */ kTIFF_ExposureProgram, kTIFF_ShortType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "ExposureProgram" }, + { /* 34852 */ kTIFF_SpectralSensitivity, kTIFF_ASCIIType, kAnyCount, kExport_InjectOnly, kXMP_NS_EXIF, "SpectralSensitivity" }, + { /* 34855 */ kTIFF_PhotographicSensitivity, kTIFF_ShortType, 1, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 34856 */ kTIFF_OECF, kTIFF_UndefinedType, kAnyCount, kExport_Never, "", "" }, // ! Has a special mapping. + { /* 34864 */ kTIFF_SensitivityType, kTIFF_ShortType, 1, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 34865 */ kTIFF_StandardOutputSensitivity, kTIFF_LongType, 1, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 34866 */ kTIFF_RecommendedExposureIndex, kTIFF_LongType, 1, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 34867 */ kTIFF_ISOSpeed, kTIFF_LongType, 1, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 34868 */ kTIFF_ISOSpeedLatitudeyyy, kTIFF_LongType, 1, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 34869 */ kTIFF_ISOSpeedLatitudezzz, kTIFF_LongType, 1, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 37377 */ kTIFF_ShutterSpeedValue, kTIFF_SRationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "ShutterSpeedValue" }, + { /* 37378 */ kTIFF_ApertureValue, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "ApertureValue" }, + { /* 37379 */ kTIFF_BrightnessValue, kTIFF_SRationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "BrightnessValue" }, + { /* 37380 */ kTIFF_ExposureBiasValue, kTIFF_SRationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "ExposureBiasValue" }, + { /* 37381 */ kTIFF_MaxApertureValue, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "MaxApertureValue" }, + { /* 37382 */ kTIFF_SubjectDistance, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "SubjectDistance" }, + { /* 37383 */ kTIFF_MeteringMode, kTIFF_ShortType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "MeteringMode" }, + { /* 37384 */ kTIFF_LightSource, kTIFF_ShortType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "LightSource" }, + { /* 37385 */ kTIFF_Flash, kTIFF_ShortType, 1, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 37386 */ kTIFF_FocalLength, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "FocalLength" }, + { /* 37396 */ kTIFF_SubjectArea, kTIFF_ShortType, kAnyCount, kExport_Never, kXMP_NS_EXIF, "SubjectArea" }, // ! Actually 2..4. + { /* 41483 */ kTIFF_FlashEnergy, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "FlashEnergy" }, + { /* 41484 */ kTIFF_SpatialFrequencyResponse, kTIFF_UndefinedType, kAnyCount, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 41486 */ kTIFF_FocalPlaneXResolution, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "FocalPlaneXResolution" }, + { /* 41487 */ kTIFF_FocalPlaneYResolution, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "FocalPlaneYResolution" }, + { /* 41488 */ kTIFF_FocalPlaneResolutionUnit, kTIFF_ShortType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "FocalPlaneResolutionUnit" }, + { /* 41492 */ kTIFF_SubjectLocation, kTIFF_ShortType, 2, kExport_Never, kXMP_NS_EXIF, "SubjectLocation" }, + { /* 41493 */ kTIFF_ExposureIndex, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "ExposureIndex" }, + { /* 41495 */ kTIFF_SensingMethod, kTIFF_ShortType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "SensingMethod" }, + { /* 41728 */ kTIFF_FileSource, kTIFF_UndefinedType, 1, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 41729 */ kTIFF_SceneType, kTIFF_UndefinedType, 1, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 41730 */ kTIFF_CFAPattern, kTIFF_UndefinedType, kAnyCount, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 41985 */ kTIFF_CustomRendered, kTIFF_ShortType, 1, kExport_Never, kXMP_NS_EXIF, "CustomRendered" }, + { /* 41986 */ kTIFF_ExposureMode, kTIFF_ShortType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "ExposureMode" }, + { /* 41987 */ kTIFF_WhiteBalance, kTIFF_ShortType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "WhiteBalance" }, + { /* 41988 */ kTIFF_DigitalZoomRatio, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "DigitalZoomRatio" }, + { /* 41989 */ kTIFF_FocalLengthIn35mmFilm, kTIFF_ShortType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "FocalLengthIn35mmFilm" }, + { /* 41990 */ kTIFF_SceneCaptureType, kTIFF_ShortType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "SceneCaptureType" }, + { /* 41991 */ kTIFF_GainControl, kTIFF_ShortType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "GainControl" }, + { /* 41992 */ kTIFF_Contrast, kTIFF_ShortType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "Contrast" }, + { /* 41993 */ kTIFF_Saturation, kTIFF_ShortType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "Saturation" }, + { /* 41994 */ kTIFF_Sharpness, kTIFF_ShortType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "Sharpness" }, + { /* 41995 */ kTIFF_DeviceSettingDescription, kTIFF_UndefinedType, kAnyCount, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 41996 */ kTIFF_SubjectDistanceRange, kTIFF_ShortType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "SubjectDistanceRange" }, + + { 0xFFFF, 0, 0, 0 } // ! Must end with sentinel. +}; + +static const TIFF_MappingToXMP sGPSInfoIFDMappings[] = { + { /* 0 */ kTIFF_GPSVersionID, kTIFF_ByteType, 4, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 2 */ kTIFF_GPSLatitude, kTIFF_RationalType, 3, kExport_Always, "", "" }, // ! Has a special mapping. + { /* 4 */ kTIFF_GPSLongitude, kTIFF_RationalType, 3, kExport_Always, "", "" }, // ! Has a special mapping. + { /* 5 */ kTIFF_GPSAltitudeRef, kTIFF_ByteType, 1, kExport_Always, kXMP_NS_EXIF, "GPSAltitudeRef" }, + { /* 6 */ kTIFF_GPSAltitude, kTIFF_RationalType, 1, kExport_Always, kXMP_NS_EXIF, "GPSAltitude" }, + { /* 7 */ kTIFF_GPSTimeStamp, kTIFF_RationalType, 3, kExport_Always, "", "" }, // ! Has a special mapping. + { /* 8 */ kTIFF_GPSSatellites, kTIFF_ASCIIType, kAnyCount, kExport_InjectOnly, kXMP_NS_EXIF, "GPSSatellites" }, + { /* 9 */ kTIFF_GPSStatus, kTIFF_ASCIIType, 2, kExport_InjectOnly, kXMP_NS_EXIF, "GPSStatus" }, + { /* 10 */ kTIFF_GPSMeasureMode, kTIFF_ASCIIType, 2, kExport_InjectOnly, kXMP_NS_EXIF, "GPSMeasureMode" }, + { /* 11 */ kTIFF_GPSDOP, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "GPSDOP" }, + { /* 12 */ kTIFF_GPSSpeedRef, kTIFF_ASCIIType, 2, kExport_InjectOnly, kXMP_NS_EXIF, "GPSSpeedRef" }, + { /* 13 */ kTIFF_GPSSpeed, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "GPSSpeed" }, + { /* 14 */ kTIFF_GPSTrackRef, kTIFF_ASCIIType, 2, kExport_InjectOnly, kXMP_NS_EXIF, "GPSTrackRef" }, + { /* 15 */ kTIFF_GPSTrack, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "GPSTrack" }, + { /* 16 */ kTIFF_GPSImgDirectionRef, kTIFF_ASCIIType, 2, kExport_InjectOnly, kXMP_NS_EXIF, "GPSImgDirectionRef" }, + { /* 17 */ kTIFF_GPSImgDirection, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "GPSImgDirection" }, + { /* 18 */ kTIFF_GPSMapDatum, kTIFF_ASCIIType, kAnyCount, kExport_InjectOnly, kXMP_NS_EXIF, "GPSMapDatum" }, + { /* 20 */ kTIFF_GPSDestLatitude, kTIFF_RationalType, 3, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 22 */ kTIFF_GPSDestLongitude, kTIFF_RationalType, 3, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 23 */ kTIFF_GPSDestBearingRef, kTIFF_ASCIIType, 2, kExport_InjectOnly, kXMP_NS_EXIF, "GPSDestBearingRef" }, + { /* 24 */ kTIFF_GPSDestBearing, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "GPSDestBearing" }, + { /* 25 */ kTIFF_GPSDestDistanceRef, kTIFF_ASCIIType, 2, kExport_InjectOnly, kXMP_NS_EXIF, "GPSDestDistanceRef" }, + { /* 26 */ kTIFF_GPSDestDistance, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "GPSDestDistance" }, + { /* 27 */ kTIFF_GPSProcessingMethod, kTIFF_UndefinedType, kAnyCount, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 28 */ kTIFF_GPSAreaInformation, kTIFF_UndefinedType, kAnyCount, kExport_InjectOnly, "", "" }, // ! Has a special mapping. + { /* 30 */ kTIFF_GPSDifferential, kTIFF_ShortType, 1, kExport_InjectOnly, kXMP_NS_EXIF, "GPSDifferential" }, + { /* 31 */ kTIFF_GPSHPositioningError, kTIFF_RationalType, 1, kExport_InjectOnly, kXMP_NS_ExifEX, "GPSHPositioningError" }, + { 0xFFFF, 0, 0, 0 } // ! Must end with sentinel. +}; + +// ================================================================================================= + +static void // ! Needed by Import2WayExif +ExportTIFF_Date ( const SXMPMeta & xmp, const char * xmpNS, const char * xmpProp, TIFF_Manager * tiff, XMP_Uns16 mainID ); + +// ================================================================================================= + +static XMP_Uns32 GatherInt ( const char * strPtr, size_t count ) +{ + XMP_Uns32 value = 0; + const char * strEnd = strPtr + count; + + while ( strPtr < strEnd ) { + char ch = *strPtr; + if ( (ch < '0') || (ch > '9') ) break; + value = value*10 + (ch - '0'); + ++strPtr; + } + + return value; + +} // GatherInt + +// ================================================================================================= + +static size_t TrimTrailingSpaces ( char * firstChar, size_t origLen ) +{ + if ( origLen == 0 ) return 0; + + char * lastChar = firstChar + origLen - 1; + if ( (*lastChar != ' ') && (*lastChar != 0) ) return origLen; // Nothing to do. + + while ( (firstChar <= lastChar) && ((*lastChar == ' ') || (*lastChar == 0)) ) --lastChar; + + XMP_Assert ( (lastChar == firstChar-1) || + ((lastChar >= firstChar) && (*lastChar != ' ') && (*lastChar != 0)) ); + + size_t newLen = (size_t)((lastChar+1) - firstChar); + XMP_Assert ( newLen <= origLen ); + + if ( newLen < origLen ) { + ++lastChar; + *lastChar = 0; + } + + return newLen; + +} // TrimTrailingSpaces + +static void TrimTrailingSpaces ( TIFF_Manager::TagInfo * info ) +{ + info->dataLen = (XMP_Uns32) TrimTrailingSpaces ( (char*)info->dataPtr, (size_t)info->dataLen ); +} + +static void TrimTrailingSpaces ( std::string * stdstr ) +{ + size_t origLen = stdstr->size(); + size_t newLen = TrimTrailingSpaces ( (char*)stdstr->c_str(), origLen ); + if ( newLen != origLen ) stdstr->erase ( newLen ); +} + +// ================================================================================================= + +bool PhotoDataUtils::GetNativeInfo ( const TIFF_Manager & exif, XMP_Uns8 ifd, XMP_Uns16 id, TIFF_Manager::TagInfo * info ) +{ + bool haveExif = exif.GetTag ( ifd, id, info ); + + if ( haveExif ) { + + XMP_Uns32 i; + char * chPtr; + + XMP_Assert ( (info->dataPtr != 0) || (info->dataLen == 0) ); // Null pointer requires zero length. + + bool isDate = ((id == kTIFF_DateTime) || (id == kTIFF_DateTimeOriginal) || (id == kTIFF_DateTimeOriginal)); + + for ( i = 0, chPtr = (char*)info->dataPtr; i < info->dataLen; ++i, ++chPtr ) { + if ( isDate && (*chPtr == ':') ) continue; // Ignore colons, empty dates have spaces and colons. + if ( (*chPtr != ' ') && (*chPtr != 0) ) break; // Break if the Exif value is non-empty. + } + + if ( i == info->dataLen ) { + haveExif = false; // Ignore empty Exif. + } else { + TrimTrailingSpaces ( info ); + if ( info->dataLen == 0 ) haveExif = false; + } + + } + + return haveExif; + +} // PhotoDataUtils::GetNativeInfo + +// ================================================================================================= + +size_t PhotoDataUtils::GetNativeInfo ( const IPTC_Manager & iptc, XMP_Uns8 id, int digestState, bool haveXMP, IPTC_Manager::DataSetInfo * info ) +{ + size_t iptcCount = 0; + + if ( (digestState == kDigestDiffers) || ((digestState == kDigestMissing) && (! haveXMP)) ) { + iptcCount = iptc.GetDataSet ( id, info ); + } + + if ( ignoreLocalText && (iptcCount > 0) && (! iptc.UsingUTF8()) ) { + // Check to see if the new value(s) should be ignored. + size_t i; + IPTC_Manager::DataSetInfo tmpInfo; + for ( i = 0; i < iptcCount; ++i ) { + (void) iptc.GetDataSet ( id, &tmpInfo, i ); + if ( ReconcileUtils::IsASCII ( tmpInfo.dataPtr, tmpInfo.dataLen ) ) break; + } + if ( i == iptcCount ) iptcCount = 0; // Return 0 if value(s) should be ignored. + } + + return iptcCount; + +} // PhotoDataUtils::GetNativeInfo + +// ================================================================================================= + +bool PhotoDataUtils::IsValueDifferent ( const TIFF_Manager::TagInfo & exifInfo, const std::string & xmpValue, std::string * exifValue ) +{ + if ( exifInfo.dataLen == 0 ) return false; // Ignore empty Exif values. + + if ( ReconcileUtils::IsUTF8 ( exifInfo.dataPtr, exifInfo.dataLen ) ) { // ! Note that ASCII is UTF-8. + exifValue->assign ( (char*)exifInfo.dataPtr, exifInfo.dataLen ); + } else { + if ( ignoreLocalText ) return false; + ReconcileUtils::LocalToUTF8 ( exifInfo.dataPtr, exifInfo.dataLen, exifValue ); + } + + return (*exifValue != xmpValue); + +} // PhotoDataUtils::IsValueDifferent + +// ================================================================================================= + +bool PhotoDataUtils::IsValueDifferent ( const IPTC_Manager & newIPTC, const IPTC_Manager & oldIPTC, XMP_Uns8 id ) +{ + IPTC_Manager::DataSetInfo newInfo; + size_t newCount = newIPTC.GetDataSet ( id, &newInfo ); + if ( newCount == 0 ) return false; // Ignore missing new IPTC values. + + IPTC_Manager::DataSetInfo oldInfo; + size_t oldCount = oldIPTC.GetDataSet ( id, &oldInfo ); + if ( oldCount == 0 ) return true; // Missing old IPTC values differ. + + if ( newCount != oldCount ) return true; + + std::string oldStr, newStr; + + for ( newCount = 0; newCount < oldCount; ++newCount ) { + + if ( ignoreLocalText & (! newIPTC.UsingUTF8()) ) { // Check to see if the new value should be ignored. + (void) newIPTC.GetDataSet ( id, &newInfo, newCount ); + if ( ! ReconcileUtils::IsASCII ( newInfo.dataPtr, newInfo.dataLen ) ) continue; + } + + (void) newIPTC.GetDataSet_UTF8 ( id, &newStr, newCount ); + (void) oldIPTC.GetDataSet_UTF8 ( id, &oldStr, newCount ); + if ( newStr.size() == 0 ) continue; // Ignore empty new IPTC. + if ( newStr != oldStr ) break; + + } + + return ( newCount != oldCount ); // Not different if all values matched. + +} // PhotoDataUtils::IsValueDifferent + +// ================================================================================================= +// ================================================================================================= + +// ================================================================================================= +// ImportSingleTIFF_Short +// ====================== + +static void +ImportSingleTIFF_Short ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Uns16 binValue = *((XMP_Uns16*)tagInfo.dataPtr); + if ( ! nativeEndian ) binValue = Flip2 ( binValue ); + + char strValue[20]; + snprintf ( strValue, sizeof(strValue), "%hu", binValue ); // AUDIT: Using sizeof(strValue) is safe. + + xmp->SetProperty ( xmpNS, xmpProp, strValue ); + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportSingleTIFF_Short + +// ================================================================================================= +// ImportSingleTIFF_Long +// ===================== + +static void +ImportSingleTIFF_Long ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Uns32 binValue = *((XMP_Uns32*)tagInfo.dataPtr); + if ( ! nativeEndian ) binValue = Flip4 ( binValue ); + + char strValue[20]; + snprintf ( strValue, sizeof(strValue), "%lu", (unsigned long)binValue ); // AUDIT: Using sizeof(strValue) is safe. + + xmp->SetProperty ( xmpNS, xmpProp, strValue ); + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportSingleTIFF_Long + +// ================================================================================================= +// ImportSingleTIFF_Rational +// ========================= + +static void +ImportSingleTIFF_Rational ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Uns32 * binPtr = (XMP_Uns32*)tagInfo.dataPtr; + XMP_Uns32 binNum = binPtr[0]; + XMP_Uns32 binDenom = binPtr[1]; + if ( ! nativeEndian ) { + binNum = Flip4 ( binNum ); + binDenom = Flip4 ( binDenom ); + } + + char strValue[40]; + snprintf ( strValue, sizeof(strValue), "%lu/%lu", (unsigned long)binNum, (unsigned long)binDenom ); // AUDIT: Using sizeof(strValue) is safe. + + xmp->SetProperty ( xmpNS, xmpProp, strValue ); + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportSingleTIFF_Rational + +// ================================================================================================= +// ImportSingleTIFF_SRational +// ========================== + +static void +ImportSingleTIFF_SRational ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Int32 * binPtr = (XMP_Int32*)tagInfo.dataPtr; + XMP_Int32 binNum = binPtr[0]; + XMP_Int32 binDenom = binPtr[1]; + if ( ! nativeEndian ) { + Flip4 ( &binNum ); + Flip4 ( &binDenom ); + } + + char strValue[40]; + snprintf ( strValue, sizeof(strValue), "%ld/%ld", (unsigned long)binNum, (unsigned long)binDenom ); // AUDIT: Using sizeof(strValue) is safe. + + xmp->SetProperty ( xmpNS, xmpProp, strValue ); + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportSingleTIFF_SRational + +// ================================================================================================= +// ImportSingleTIFF_ASCII +// ====================== + +static void +ImportSingleTIFF_ASCII ( const TIFF_Manager::TagInfo & tagInfo, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + TrimTrailingSpaces ( (TIFF_Manager::TagInfo*) &tagInfo ); + if ( tagInfo.dataLen == 0 ) return; // Ignore empty tags. + + const char * chPtr = (const char *)tagInfo.dataPtr; + const bool hasNul = (chPtr[tagInfo.dataLen-1] == 0); + const bool isUTF8 = ReconcileUtils::IsUTF8 ( chPtr, tagInfo.dataLen ); + + if ( isUTF8 && hasNul ) { + xmp->SetProperty ( xmpNS, xmpProp, chPtr ); + } else { + std::string strValue; + if ( isUTF8 ) { + strValue.assign ( chPtr, tagInfo.dataLen ); + } else { + if ( ignoreLocalText ) return; + ReconcileUtils::LocalToUTF8 ( chPtr, tagInfo.dataLen, &strValue ); + } + xmp->SetProperty ( xmpNS, xmpProp, strValue.c_str() ); + } + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportSingleTIFF_ASCII + +// ================================================================================================= +// ImportSingleTIFF_Byte +// ===================== + +static void +ImportSingleTIFF_Byte ( const TIFF_Manager::TagInfo & tagInfo, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Uns8 binValue = *((XMP_Uns8*)tagInfo.dataPtr); + + char strValue[20]; + snprintf ( strValue, sizeof(strValue), "%hu", binValue ); // AUDIT: Using sizeof(strValue) is safe. + + xmp->SetProperty ( xmpNS, xmpProp, strValue ); + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportSingleTIFF_Byte + +// ================================================================================================= +// ImportSingleTIFF_SByte +// ====================== + +static void +ImportSingleTIFF_SByte ( const TIFF_Manager::TagInfo & tagInfo, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Int8 binValue = *((XMP_Int8*)tagInfo.dataPtr); + + char strValue[20]; + snprintf ( strValue, sizeof(strValue), "%hd", binValue ); // AUDIT: Using sizeof(strValue) is safe. + + xmp->SetProperty ( xmpNS, xmpProp, strValue ); + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportSingleTIFF_SByte + +// ================================================================================================= +// ImportSingleTIFF_SShort +// ======================= + +static void +ImportSingleTIFF_SShort ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Int16 binValue = *((XMP_Int16*)tagInfo.dataPtr); + if ( ! nativeEndian ) Flip2 ( &binValue ); + + char strValue[20]; + snprintf ( strValue, sizeof(strValue), "%hd", binValue ); // AUDIT: Using sizeof(strValue) is safe. + + xmp->SetProperty ( xmpNS, xmpProp, strValue ); + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportSingleTIFF_SShort + +// ================================================================================================= +// ImportSingleTIFF_SLong +// ====================== + +static void +ImportSingleTIFF_SLong ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Int32 binValue = *((XMP_Int32*)tagInfo.dataPtr); + if ( ! nativeEndian ) Flip4 ( &binValue ); + + char strValue[20]; + snprintf ( strValue, sizeof(strValue), "%ld", (long)binValue ); // AUDIT: Using sizeof(strValue) is safe. + + xmp->SetProperty ( xmpNS, xmpProp, strValue ); + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportSingleTIFF_SLong + +// ================================================================================================= +// ImportSingleTIFF_Float +// ====================== + +static void +ImportSingleTIFF_Float ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + float binValue = *((float*)tagInfo.dataPtr); + if ( ! nativeEndian ) Flip4 ( &binValue ); + + xmp->SetProperty_Float ( xmpNS, xmpProp, binValue ); + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportSingleTIFF_Float + +// ================================================================================================= +// ImportSingleTIFF_Double +// ======================= + +static void +ImportSingleTIFF_Double ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + double binValue = *((double*)tagInfo.dataPtr); + if ( ! nativeEndian ) Flip8 ( &binValue ); + + xmp->SetProperty_Float ( xmpNS, xmpProp, binValue ); // ! Yes, SetProperty_Float. + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportSingleTIFF_Double + +// ================================================================================================= +// ImportSingleTIFF +// ================ + +static void +ImportSingleTIFF ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + + // We've got a tag to map to XMP, decide how based on actual type and the expected count. Using + // the actual type eliminates a ShortOrLong case. Using the expected count is needed to know + // whether to create an XMP array. The actual count for an array could be 1. Put the most + // common cases first for better iCache utilization. + + switch ( tagInfo.type ) { + + case kTIFF_ShortType : + ImportSingleTIFF_Short ( tagInfo, nativeEndian, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_LongType : + ImportSingleTIFF_Long ( tagInfo, nativeEndian, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_RationalType : + ImportSingleTIFF_Rational ( tagInfo, nativeEndian, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_SRationalType : + ImportSingleTIFF_SRational ( tagInfo, nativeEndian, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_ASCIIType : + ImportSingleTIFF_ASCII ( tagInfo, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_ByteType : + ImportSingleTIFF_Byte ( tagInfo, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_SByteType : + ImportSingleTIFF_SByte ( tagInfo, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_SShortType : + ImportSingleTIFF_SShort ( tagInfo, nativeEndian, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_SLongType : + ImportSingleTIFF_SLong ( tagInfo, nativeEndian, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_FloatType : + ImportSingleTIFF_Float ( tagInfo, nativeEndian, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_DoubleType : + ImportSingleTIFF_Double ( tagInfo, nativeEndian, xmp, xmpNS, xmpProp ); + break; + + } + +} // ImportSingleTIFF + +// ================================================================================================= +// ================================================================================================= + +// ================================================================================================= +// ImportArrayTIFF_Short +// ===================== + +static void +ImportArrayTIFF_Short ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Uns16 * binPtr = (XMP_Uns16*)tagInfo.dataPtr; + + xmp->DeleteProperty ( xmpNS, xmpProp ); // ! Don't keep appending, create a new array. + + for ( size_t i = 0; i < tagInfo.count; ++i, ++binPtr ) { + + XMP_Uns16 binValue = *binPtr; + if ( ! nativeEndian ) binValue = Flip2 ( binValue ); + + char strValue[20]; + snprintf ( strValue, sizeof(strValue), "%hu", binValue ); // AUDIT: Using sizeof(strValue) is safe. + + xmp->AppendArrayItem ( xmpNS, xmpProp, kXMP_PropArrayIsOrdered, strValue ); + + } + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportArrayTIFF_Short + +// ================================================================================================= +// ImportArrayTIFF_Long +// ==================== + +static void +ImportArrayTIFF_Long ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Uns32 * binPtr = (XMP_Uns32*)tagInfo.dataPtr; + + xmp->DeleteProperty ( xmpNS, xmpProp ); // ! Don't keep appending, create a new array. + + for ( size_t i = 0; i < tagInfo.count; ++i, ++binPtr ) { + + XMP_Uns32 binValue = *binPtr; + if ( ! nativeEndian ) binValue = Flip4 ( binValue ); + + char strValue[20]; + snprintf ( strValue, sizeof(strValue), "%lu", (unsigned long)binValue ); // AUDIT: Using sizeof(strValue) is safe. + + xmp->AppendArrayItem ( xmpNS, xmpProp, kXMP_PropArrayIsOrdered, strValue ); + + } + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportArrayTIFF_Long + +// ================================================================================================= +// ImportArrayTIFF_Rational +// ======================== + +static void +ImportArrayTIFF_Rational ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Uns32 * binPtr = (XMP_Uns32*)tagInfo.dataPtr; + + xmp->DeleteProperty ( xmpNS, xmpProp ); // ! Don't keep appending, create a new array. + + for ( size_t i = 0; i < tagInfo.count; ++i, binPtr += 2 ) { + + XMP_Uns32 binNum = binPtr[0]; + XMP_Uns32 binDenom = binPtr[1]; + if ( ! nativeEndian ) { + binNum = Flip4 ( binNum ); + binDenom = Flip4 ( binDenom ); + } + + char strValue[40]; + snprintf ( strValue, sizeof(strValue), "%lu/%lu", (unsigned long)binNum, (unsigned long)binDenom ); // AUDIT: Using sizeof(strValue) is safe. + + xmp->AppendArrayItem ( xmpNS, xmpProp, kXMP_PropArrayIsOrdered, strValue ); + + } + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportArrayTIFF_Rational + +// ================================================================================================= +// ImportArrayTIFF_SRational +// ========================= + +static void +ImportArrayTIFF_SRational ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Int32 * binPtr = (XMP_Int32*)tagInfo.dataPtr; + + xmp->DeleteProperty ( xmpNS, xmpProp ); // ! Don't keep appending, create a new array. + + for ( size_t i = 0; i < tagInfo.count; ++i, binPtr += 2 ) { + + XMP_Int32 binNum = binPtr[0]; + XMP_Int32 binDenom = binPtr[1]; + if ( ! nativeEndian ) { + Flip4 ( &binNum ); + Flip4 ( &binDenom ); + } + + char strValue[40]; + snprintf ( strValue, sizeof(strValue), "%ld/%ld", (long)binNum, (long)binDenom ); // AUDIT: Using sizeof(strValue) is safe. + + xmp->AppendArrayItem ( xmpNS, xmpProp, kXMP_PropArrayIsOrdered, strValue ); + + } + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportArrayTIFF_SRational + +// ================================================================================================= +// ImportArrayTIFF_ASCII +// ===================== + +static void +ImportArrayTIFF_ASCII ( const TIFF_Manager::TagInfo & tagInfo, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + TrimTrailingSpaces ( (TIFF_Manager::TagInfo*) &tagInfo ); + if ( tagInfo.dataLen == 0 ) return; // Ignore empty tags. + + const char * chPtr = (const char *)tagInfo.dataPtr; + const char * chEnd = chPtr + tagInfo.dataLen; + const bool hasNul = (chPtr[tagInfo.dataLen-1] == 0); + const bool isUTF8 = ReconcileUtils::IsUTF8 ( chPtr, tagInfo.dataLen ); + + std::string strValue; + + if ( (! isUTF8) || (! hasNul) ) { + if ( isUTF8 ) { + strValue.assign ( chPtr, tagInfo.dataLen ); + } else { + if ( ignoreLocalText ) return; + ReconcileUtils::LocalToUTF8 ( chPtr, tagInfo.dataLen, &strValue ); + } + chPtr = strValue.c_str(); + chEnd = chPtr + strValue.size(); + } + + xmp->DeleteProperty ( xmpNS, xmpProp ); // ! Don't keep appending, create a new array. + + for ( ; chPtr < chEnd; chPtr += (strlen(chPtr) + 1) ) { + xmp->AppendArrayItem ( xmpNS, xmpProp, kXMP_PropArrayIsOrdered, chPtr ); + } + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportArrayTIFF_ASCII + +// ================================================================================================= +// ImportArrayTIFF_Byte +// ==================== + +static void +ImportArrayTIFF_Byte ( const TIFF_Manager::TagInfo & tagInfo, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Uns8 * binPtr = (XMP_Uns8*)tagInfo.dataPtr; + + xmp->DeleteProperty ( xmpNS, xmpProp ); // ! Don't keep appending, create a new array. + + for ( size_t i = 0; i < tagInfo.count; ++i, ++binPtr ) { + + XMP_Uns8 binValue = *binPtr; + + char strValue[20]; + snprintf ( strValue, sizeof(strValue), "%hu", binValue ); // AUDIT: Using sizeof(strValue) is safe. + + xmp->AppendArrayItem ( xmpNS, xmpProp, kXMP_PropArrayIsOrdered, strValue ); + + } + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportArrayTIFF_Byte + +// ================================================================================================= +// ImportArrayTIFF_SByte +// ===================== + +static void +ImportArrayTIFF_SByte ( const TIFF_Manager::TagInfo & tagInfo, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Int8 * binPtr = (XMP_Int8*)tagInfo.dataPtr; + + xmp->DeleteProperty ( xmpNS, xmpProp ); // ! Don't keep appending, create a new array. + + for ( size_t i = 0; i < tagInfo.count; ++i, ++binPtr ) { + + XMP_Int8 binValue = *binPtr; + + char strValue[20]; + snprintf ( strValue, sizeof(strValue), "%hd", binValue ); // AUDIT: Using sizeof(strValue) is safe. + + xmp->AppendArrayItem ( xmpNS, xmpProp, kXMP_PropArrayIsOrdered, strValue ); + + } + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportArrayTIFF_SByte + +// ================================================================================================= +// ImportArrayTIFF_SShort +// ====================== + +static void +ImportArrayTIFF_SShort ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Int16 * binPtr = (XMP_Int16*)tagInfo.dataPtr; + + xmp->DeleteProperty ( xmpNS, xmpProp ); // ! Don't keep appending, create a new array. + + for ( size_t i = 0; i < tagInfo.count; ++i, ++binPtr ) { + + XMP_Int16 binValue = *binPtr; + if ( ! nativeEndian ) Flip2 ( &binValue ); + + char strValue[20]; + snprintf ( strValue, sizeof(strValue), "%hd", binValue ); // AUDIT: Using sizeof(strValue) is safe. + + xmp->AppendArrayItem ( xmpNS, xmpProp, kXMP_PropArrayIsOrdered, strValue ); + + } + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportArrayTIFF_SShort + +// ================================================================================================= +// ImportArrayTIFF_SLong +// ===================== + +static void +ImportArrayTIFF_SLong ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Int32 * binPtr = (XMP_Int32*)tagInfo.dataPtr; + + xmp->DeleteProperty ( xmpNS, xmpProp ); // ! Don't keep appending, create a new array. + + for ( size_t i = 0; i < tagInfo.count; ++i, ++binPtr ) { + + XMP_Int32 binValue = *binPtr; + if ( ! nativeEndian ) Flip4 ( &binValue ); + + char strValue[20]; + snprintf ( strValue, sizeof(strValue), "%ld", (long)binValue ); // AUDIT: Using sizeof(strValue) is safe. + + xmp->AppendArrayItem ( xmpNS, xmpProp, kXMP_PropArrayIsOrdered, strValue ); + + } + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportArrayTIFF_SLong + +// ================================================================================================= +// ImportArrayTIFF_Float +// ===================== + +static void +ImportArrayTIFF_Float ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + float * binPtr = (float*)tagInfo.dataPtr; + + xmp->DeleteProperty ( xmpNS, xmpProp ); // ! Don't keep appending, create a new array. + + for ( size_t i = 0; i < tagInfo.count; ++i, ++binPtr ) { + + float binValue = *binPtr; + if ( ! nativeEndian ) Flip4 ( &binValue ); + + std::string strValue; + SXMPUtils::ConvertFromFloat ( binValue, "", &strValue ); + + xmp->AppendArrayItem ( xmpNS, xmpProp, kXMP_PropArrayIsOrdered, strValue.c_str() ); + + } + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportArrayTIFF_Float + +// ================================================================================================= +// ImportArrayTIFF_Double +// ====================== + +static void +ImportArrayTIFF_Double ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + double * binPtr = (double*)tagInfo.dataPtr; + + xmp->DeleteProperty ( xmpNS, xmpProp ); // ! Don't keep appending, create a new array. + + for ( size_t i = 0; i < tagInfo.count; ++i, ++binPtr ) { + + double binValue = *binPtr; + if ( ! nativeEndian ) Flip8 ( &binValue ); + + std::string strValue; + SXMPUtils::ConvertFromFloat ( binValue, "", &strValue ); // ! Yes, ConvertFromFloat. + + xmp->AppendArrayItem ( xmpNS, xmpProp, kXMP_PropArrayIsOrdered, strValue.c_str() ); + + } + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportArrayTIFF_Double + +// ================================================================================================= +// ImportArrayTIFF +// =============== + +static void +ImportArrayTIFF ( const TIFF_Manager::TagInfo & tagInfo, const bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + + // We've got a tag to map to XMP, decide how based on actual type and the expected count. Using + // the actual type eliminates a ShortOrLong case. Using the expected count is needed to know + // whether to create an XMP array. The actual count for an array could be 1. Put the most + // common cases first for better iCache utilization. + + switch ( tagInfo.type ) { + + case kTIFF_ShortType : + ImportArrayTIFF_Short ( tagInfo, nativeEndian, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_LongType : + ImportArrayTIFF_Long ( tagInfo, nativeEndian, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_RationalType : + ImportArrayTIFF_Rational ( tagInfo, nativeEndian, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_SRationalType : + ImportArrayTIFF_SRational ( tagInfo, nativeEndian, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_ASCIIType : + ImportArrayTIFF_ASCII ( tagInfo, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_ByteType : + ImportArrayTIFF_Byte ( tagInfo, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_SByteType : + ImportArrayTIFF_SByte ( tagInfo, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_SShortType : + ImportArrayTIFF_SShort ( tagInfo, nativeEndian, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_SLongType : + ImportArrayTIFF_SLong ( tagInfo, nativeEndian, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_FloatType : + ImportArrayTIFF_Float ( tagInfo, nativeEndian, xmp, xmpNS, xmpProp ); + break; + + case kTIFF_DoubleType : + ImportArrayTIFF_Double ( tagInfo, nativeEndian, xmp, xmpNS, xmpProp ); + break; + + } + +} // ImportArrayTIFF + +// ================================================================================================= +// ImportTIFF_CheckStandardMapping +// =============================== + +static bool +ImportTIFF_CheckStandardMapping ( const TIFF_Manager::TagInfo & tagInfo, const TIFF_MappingToXMP & mapInfo ) +{ + XMP_Assert ( (kTIFF_ByteType <= tagInfo.type) && (tagInfo.type <= kTIFF_LastType) ); + XMP_Assert ( mapInfo.type <= kTIFF_LastType ); + + if ( (tagInfo.type < kTIFF_ByteType) || (tagInfo.type > kTIFF_LastType) ) return false; + + if ( tagInfo.type != mapInfo.type ) { + // Be tolerant of reasonable mismatches among numeric types. + if ( kTIFF_IsIntegerType[mapInfo.type] ) { + if ( ! kTIFF_IsIntegerType[tagInfo.type] ) return false; + } else if ( kTIFF_IsRationalType[mapInfo.type] ) { + if ( ! kTIFF_IsRationalType[tagInfo.type] ) return false; + } else if ( kTIFF_IsFloatType[mapInfo.type] ) { + if ( ! kTIFF_IsFloatType[tagInfo.type] ) return false; + } else { + return false; + } + } + + if ( (tagInfo.count != mapInfo.count) && // Maybe there is a problem because the counts don't match. + // (mapInfo.count != kAnyCount) && ... don't need this because of the new check below ... + (mapInfo.count == 1) ) return false; // Be tolerant of mismatch in expected array size. + + return true; + +} // ImportTIFF_CheckStandardMapping + +// ================================================================================================= +// ImportTIFF_StandardMappings +// =========================== + +static void +ImportTIFF_StandardMappings ( XMP_Uns8 ifd, const TIFF_Manager & tiff, SXMPMeta * xmp ) +{ + const bool nativeEndian = tiff.IsNativeEndian(); + TIFF_Manager::TagInfo tagInfo; + + const TIFF_MappingToXMP * mappings = 0; + + if ( ifd == kTIFF_PrimaryIFD ) { + mappings = sPrimaryIFDMappings; + } else if ( ifd == kTIFF_ExifIFD ) { + mappings = sExifIFDMappings; + } else if ( ifd == kTIFF_GPSInfoIFD ) { + mappings = sGPSInfoIFDMappings; + } else { + XMP_Throw ( "Invalid IFD for standard mappings", kXMPErr_InternalFailure ); + } + + for ( size_t i = 0; mappings[i].id != 0xFFFF; ++i ) { + + try { // Don't let errors with one stop the others. + + const TIFF_MappingToXMP & mapInfo = mappings[i]; + const bool mapSingle = ((mapInfo.count == 1) || (mapInfo.type == kTIFF_ASCIIType)); + + if ( mapInfo.name[0] == 0 ) continue; // Skip special mappings, handled higher up. + + bool found = tiff.GetTag ( ifd, mapInfo.id, &tagInfo ); + if ( ! found ) continue; + + XMP_Assert ( tagInfo.type != kTIFF_UndefinedType ); // These must have a special mapping. + if ( tagInfo.type == kTIFF_UndefinedType ) continue; + if ( ! ImportTIFF_CheckStandardMapping ( tagInfo, mapInfo ) ) continue; + + if ( mapSingle ) { + ImportSingleTIFF ( tagInfo, nativeEndian, xmp, mapInfo.ns, mapInfo.name ); + } else { + ImportArrayTIFF ( tagInfo, nativeEndian, xmp, mapInfo.ns, mapInfo.name ); + } + + } catch ( ... ) { + + // Do nothing, let other imports proceed. + // ? Notify client? + + } + + } + +} // ImportTIFF_StandardMappings + +// ================================================================================================= +// ================================================================================================= + +// ================================================================================================= +// ImportTIFF_Date +// =============== +// +// Convert an Exif 2.2 master date/time tag plus associated fractional seconds to an XMP date/time. +// The Exif date/time part is a 20 byte ASCII value formatted as "YYYY:MM:DD HH:MM:SS" with a +// terminating nul. Any of the numeric portions can be blanks if unknown. The fractional seconds +// are a nul terminated ASCII string with possible space padding. They are literally the fractional +// part, the digits that would be to the right of the decimal point. + +static void +ImportTIFF_Date ( const TIFF_Manager & tiff, const TIFF_Manager::TagInfo & dateInfo, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + XMP_Uns16 secID; + switch ( dateInfo.id ) { + case kTIFF_DateTime : secID = kTIFF_SubSecTime; break; + case kTIFF_DateTimeOriginal : secID = kTIFF_SubSecTimeOriginal; break; + case kTIFF_DateTimeDigitized : secID = kTIFF_SubSecTimeDigitized; break; + } + + try { // Don't let errors with one stop the others. + + if ( (dateInfo.type != kTIFF_ASCIIType) || (dateInfo.count != 20) ) return; + + const char * dateStr = (const char *) dateInfo.dataPtr; + if ( (dateStr[4] != ':') || (dateStr[7] != ':') || + (dateStr[10] != ' ') || (dateStr[13] != ':') || (dateStr[16] != ':') ) return; + + XMP_DateTime binValue; + + binValue.year = GatherInt ( &dateStr[0], 4 ); + binValue.month = GatherInt ( &dateStr[5], 2 ); + binValue.day = GatherInt ( &dateStr[8], 2 ); + if ( (binValue.year != 0) | (binValue.month != 0) | (binValue.day != 0) ) binValue.hasDate = true; + + binValue.hour = GatherInt ( &dateStr[11], 2 ); + binValue.minute = GatherInt ( &dateStr[14], 2 ); + binValue.second = GatherInt ( &dateStr[17], 2 ); + binValue.nanoSecond = 0; // Get the fractional seconds later. + if ( (binValue.hour != 0) | (binValue.minute != 0) | (binValue.second != 0) ) binValue.hasTime = true; + + binValue.tzSign = 0; // ! Separate assignment, avoid VS integer truncation warning. + binValue.tzHour = binValue.tzMinute = 0; + binValue.hasTimeZone = false; // Exif times have no zone. + + // *** Consider looking at the TIFF/EP TimeZoneOffset tag? + + TIFF_Manager::TagInfo secInfo; + bool found = tiff.GetTag ( kTIFF_ExifIFD, secID, &secInfo ); // ! Subseconds are all in the Exif IFD. + + if ( found && (secInfo.type == kTIFF_ASCIIType) ) { + const char * fracPtr = (const char *) secInfo.dataPtr; + binValue.nanoSecond = GatherInt ( fracPtr, secInfo.dataLen ); + size_t digits = 0; + for ( ; (('0' <= *fracPtr) && (*fracPtr <= '9')); ++fracPtr ) ++digits; + for ( ; digits < 9; ++digits ) binValue.nanoSecond *= 10; + if ( binValue.nanoSecond != 0 ) binValue.hasTime = true; + } + + xmp->SetProperty_Date ( xmpNS, xmpProp, binValue ); + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportTIFF_Date + +// ================================================================================================= +// ImportTIFF_LocTextASCII +// ======================= + +static void +ImportTIFF_LocTextASCII ( const TIFF_Manager & tiff, XMP_Uns8 ifd, XMP_Uns16 tagID, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + TIFF_Manager::TagInfo tagInfo; + + bool found = tiff.GetTag ( ifd, tagID, &tagInfo ); + if ( (! found) || (tagInfo.type != kTIFF_ASCIIType) ) return; + + TrimTrailingSpaces ( (TIFF_Manager::TagInfo*) &tagInfo ); + if ( tagInfo.dataLen == 0 ) return; // Ignore empty tags. + + const char * chPtr = (const char *)tagInfo.dataPtr; + const bool hasNul = (chPtr[tagInfo.dataLen-1] == 0); + const bool isUTF8 = ReconcileUtils::IsUTF8 ( chPtr, tagInfo.dataLen ); + + if ( isUTF8 && hasNul ) { + xmp->SetLocalizedText ( xmpNS, xmpProp, "", "x-default", chPtr ); + } else { + std::string strValue; + if ( isUTF8 ) { + strValue.assign ( chPtr, tagInfo.dataLen ); + } else { + if ( ignoreLocalText ) return; + ReconcileUtils::LocalToUTF8 ( chPtr, tagInfo.dataLen, &strValue ); + } + xmp->SetLocalizedText ( xmpNS, xmpProp, "", "x-default", strValue.c_str() ); + } + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportTIFF_LocTextASCII + +// ================================================================================================= +// ImportTIFF_EncodedString +// ======================== + +static void +ImportTIFF_EncodedString ( const TIFF_Manager & tiff, const TIFF_Manager::TagInfo & tagInfo, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp, bool isLangAlt = false ) +{ + try { // Don't let errors with one stop the others. + + std::string strValue; + + bool ok = tiff.DecodeString ( tagInfo.dataPtr, tagInfo.dataLen, &strValue ); + if ( ! ok ) return; + + TrimTrailingSpaces ( &strValue ); + if ( strValue.empty() ) return; + + if ( ! isLangAlt ) { + xmp->SetProperty ( xmpNS, xmpProp, strValue.c_str() ); + } else { + xmp->SetLocalizedText ( xmpNS, xmpProp, "", "x-default", strValue.c_str() ); + } + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportTIFF_EncodedString + +// ================================================================================================= +// ImportTIFF_Flash +// ================ + +static void +ImportTIFF_Flash ( const TIFF_Manager::TagInfo & tagInfo, bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + XMP_Uns16 binValue = *((XMP_Uns16*)tagInfo.dataPtr); + if ( ! nativeEndian ) binValue = Flip2 ( binValue ); + + bool fired = (bool)(binValue & 1); // Avoid implicit 0/1 conversion. + int rtrn = (binValue >> 1) & 3; + int mode = (binValue >> 3) & 3; + bool function = (bool)((binValue >> 5) & 1); + bool redEye = (bool)((binValue >> 6) & 1); + + static const char * sTwoBits[] = { "0", "1", "2", "3" }; + + xmp->SetStructField ( kXMP_NS_EXIF, "Flash", kXMP_NS_EXIF, "Fired", (fired ? kXMP_TrueStr : kXMP_FalseStr) ); + xmp->SetStructField ( kXMP_NS_EXIF, "Flash", kXMP_NS_EXIF, "Return", sTwoBits[rtrn] ); + xmp->SetStructField ( kXMP_NS_EXIF, "Flash", kXMP_NS_EXIF, "Mode", sTwoBits[mode] ); + xmp->SetStructField ( kXMP_NS_EXIF, "Flash", kXMP_NS_EXIF, "Function", (function ? kXMP_TrueStr : kXMP_FalseStr) ); + xmp->SetStructField ( kXMP_NS_EXIF, "Flash", kXMP_NS_EXIF, "RedEyeMode", (redEye ? kXMP_TrueStr : kXMP_FalseStr) ); + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportTIFF_Flash + +// ================================================================================================= +// ImportConversionTable +// ===================== +// +// Although the XMP for the OECF and SFR tables is the same, the Exif is not. The OECF table has +// signed rational values and the SFR table has unsigned. But they are otherwise the same. + +static void +ImportConversionTable ( const TIFF_Manager::TagInfo & tagInfo, bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + const bool isSigned = (tagInfo.id == kTIFF_OECF); + XMP_Assert ( (tagInfo.id == kTIFF_OECF) || (tagInfo.id == kTIFF_SpatialFrequencyResponse) ); + + xmp->DeleteProperty ( xmpNS, xmpProp ); + + try { // Don't let errors with one stop the others. + + const XMP_Uns8 * bytePtr = (XMP_Uns8*)tagInfo.dataPtr; + const XMP_Uns8 * byteEnd = bytePtr + tagInfo.dataLen; + + XMP_Uns16 columns = *((XMP_Uns16*)bytePtr); + XMP_Uns16 rows = *((XMP_Uns16*)(bytePtr+2)); + if ( ! nativeEndian ) { + columns = Flip2 ( columns ); + rows = Flip2 ( rows ); + } + + char buffer[40]; + + snprintf ( buffer, sizeof(buffer), "%d", columns ); // AUDIT: Use of sizeof(buffer) is safe. + xmp->SetStructField ( xmpNS, xmpProp, kXMP_NS_EXIF, "Columns", buffer ); + snprintf ( buffer, sizeof(buffer), "%d", rows ); // AUDIT: Use of sizeof(buffer) is safe. + xmp->SetStructField ( xmpNS, xmpProp, kXMP_NS_EXIF, "Rows", buffer ); + + std::string arrayPath; + + SXMPUtils::ComposeStructFieldPath ( xmpNS, xmpProp, kXMP_NS_EXIF, "Names", &arrayPath ); + + bytePtr += 4; // Move to the list of names. Don't convert from local text, should really be ASCII. + for ( size_t i = columns; i > 0; --i ) { + size_t nameLen = strlen((XMP_StringPtr)bytePtr) + 1; // ! Include the terminating nul. + if ( (bytePtr + nameLen) > byteEnd ) XMP_Throw ( "OECF-SFR name overflow", kXMPErr_BadValue ); + if ( ! ReconcileUtils::IsUTF8 ( bytePtr, nameLen ) ) XMP_Throw ( "OECF-SFR name error", kXMPErr_BadValue ); + xmp->AppendArrayItem ( xmpNS, arrayPath.c_str(), kXMP_PropArrayIsOrdered, (XMP_StringPtr)bytePtr ); + bytePtr += nameLen; + } + + if ( (byteEnd - bytePtr) != (8 * columns * rows) ) XMP_Throw ( "OECF-SFR data overflow", kXMPErr_BadValue ); + SXMPUtils::ComposeStructFieldPath ( xmpNS, xmpProp, kXMP_NS_EXIF, "Values", &arrayPath ); + + XMP_Uns32 * binPtr = (XMP_Uns32*)bytePtr; + for ( size_t i = (columns * rows); i > 0; --i, binPtr += 2 ) { + + XMP_Uns32 binNum = binPtr[0]; + XMP_Uns32 binDenom = binPtr[1]; + if ( ! nativeEndian ) { + Flip4 ( &binNum ); + Flip4 ( &binDenom ); + } + + if ( (binDenom == 0) && (binNum != 0) ) XMP_Throw ( "OECF-SFR data overflow", kXMPErr_BadValue ); + if ( isSigned ) { + snprintf ( buffer, sizeof(buffer), "%ld/%ld", (long)binNum, (long)binDenom ); // AUDIT: Use of sizeof(buffer) is safe. + } else { + snprintf ( buffer, sizeof(buffer), "%lu/%lu", (unsigned long)binNum, (unsigned long)binDenom ); // AUDIT: Use of sizeof(buffer) is safe. + } + + xmp->AppendArrayItem ( xmpNS, arrayPath.c_str(), kXMP_PropArrayIsOrdered, buffer ); + + } + + return; + + } catch ( ... ) { + xmp->DeleteProperty ( xmpNS, xmpProp ); + // ? Notify client? + } + +} // ImportConversionTable + +// ================================================================================================= +// ImportTIFF_CFATable +// =================== + +static void +ImportTIFF_CFATable ( const TIFF_Manager::TagInfo & tagInfo, bool nativeEndian, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + const XMP_Uns8 * bytePtr = (XMP_Uns8*)tagInfo.dataPtr; + const XMP_Uns8 * byteEnd = bytePtr + tagInfo.dataLen; + + XMP_Uns16 columns = *((XMP_Uns16*)bytePtr); + XMP_Uns16 rows = *((XMP_Uns16*)(bytePtr+2)); + if ( ! nativeEndian ) { + columns = Flip2 ( columns ); + rows = Flip2 ( rows ); + } + + char buffer[20]; + std::string arrayPath; + + snprintf ( buffer, sizeof(buffer), "%d", columns ); // AUDIT: Use of sizeof(buffer) is safe. + xmp->SetStructField ( xmpNS, xmpProp, kXMP_NS_EXIF, "Columns", buffer ); + snprintf ( buffer, sizeof(buffer), "%d", rows ); // AUDIT: Use of sizeof(buffer) is safe. + xmp->SetStructField ( xmpNS, xmpProp, kXMP_NS_EXIF, "Rows", buffer ); + + bytePtr += 4; // Move to the matrix of values. + if ( (byteEnd - bytePtr) != (columns * rows) ) goto BadExif; // Make sure the values are present. + + SXMPUtils::ComposeStructFieldPath ( xmpNS, xmpProp, kXMP_NS_EXIF, "Values", &arrayPath ); + + for ( size_t i = (columns * rows); i > 0; --i, ++bytePtr ) { + snprintf ( buffer, sizeof(buffer), "%hu", *bytePtr ); // AUDIT: Use of sizeof(buffer) is safe. + xmp->AppendArrayItem ( xmpNS, arrayPath.c_str(), kXMP_PropArrayIsOrdered, buffer ); + } + + return; + + BadExif: // Ignore the tag if the table is ill-formed. + xmp->DeleteProperty ( xmpNS, xmpProp ); + return; + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportTIFF_CFATable + +// ================================================================================================= +// ImportTIFF_DSDTable +// =================== + +static void +ImportTIFF_DSDTable ( const TIFF_Manager & tiff, const TIFF_Manager::TagInfo & tagInfo, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + const XMP_Uns8 * bytePtr = (XMP_Uns8*)tagInfo.dataPtr; + const XMP_Uns8 * byteEnd = bytePtr + tagInfo.dataLen; + + XMP_Uns16 columns = *((XMP_Uns16*)bytePtr); + XMP_Uns16 rows = *((XMP_Uns16*)(bytePtr+2)); + if ( ! tiff.IsNativeEndian() ) { + columns = Flip2 ( columns ); + rows = Flip2 ( rows ); + } + + char buffer[20]; + + snprintf ( buffer, sizeof(buffer), "%d", columns ); // AUDIT: Use of sizeof(buffer) is safe. + xmp->SetStructField ( xmpNS, xmpProp, kXMP_NS_EXIF, "Columns", buffer ); + snprintf ( buffer, sizeof(buffer), "%d", rows ); // AUDIT: Use of sizeof(buffer) is safe. + xmp->SetStructField ( xmpNS, xmpProp, kXMP_NS_EXIF, "Rows", buffer ); + + std::string arrayPath; + SXMPUtils::ComposeStructFieldPath ( xmpNS, xmpProp, kXMP_NS_EXIF, "Settings", &arrayPath ); + + bytePtr += 4; // Move to the list of settings. + UTF16Unit * utf16Ptr = (UTF16Unit*)bytePtr; + UTF16Unit * utf16End = (UTF16Unit*)byteEnd; + + std::string utf8; + + // Figure 17 in the Exif 2.2 spec is unclear. It has counts for rows and columns, but the + // settings are listed as 1..n, not as a rectangular matrix. So, ignore the counts and copy + // strings until the end of the Exif value. + + while ( utf16Ptr < utf16End ) { + + size_t nameLen = 0; + while ( utf16Ptr[nameLen] != 0 ) ++nameLen; + ++nameLen; // ! Include the terminating nul. + if ( (utf16Ptr + nameLen) > utf16End ) goto BadExif; + + try { + FromUTF16 ( utf16Ptr, nameLen, &utf8, tiff.IsBigEndian() ); + } catch ( ... ) { + goto BadExif; // Ignore the tag if there are conversion errors. + } + + xmp->AppendArrayItem ( xmpNS, arrayPath.c_str(), kXMP_PropArrayIsOrdered, utf8.c_str() ); + + utf16Ptr += nameLen; + + } + + return; + + BadExif: // Ignore the tag if the table is ill-formed. + xmp->DeleteProperty ( xmpNS, xmpProp ); + return; + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportTIFF_DSDTable + +// ================================================================================================= +// ImportTIFF_GPSCoordinate +// ======================== + +static void +ImportTIFF_GPSCoordinate ( const TIFF_Manager & tiff, const TIFF_Manager::TagInfo & posInfo, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. Tolerate ill-formed values where reasonable. + + const bool nativeEndian = tiff.IsNativeEndian(); + + if ( (posInfo.type != kTIFF_RationalType) || (posInfo.count == 0) ) return; + + XMP_Uns16 refID = posInfo.id - 1; // ! The GPS refs and coordinates are all tag n and n+1. + TIFF_Manager::TagInfo refInfo; + bool found = tiff.GetTag ( kTIFF_GPSInfoIFD, refID, &refInfo ); + if ( (! found) || (refInfo.count == 0) ) return; + char ref = *((char*)refInfo.dataPtr); + if ( (ref != 'N') && (ref != 'S') && (ref != 'E') && (ref != 'W') ) return; + + XMP_Uns32 * binPtr = (XMP_Uns32*)posInfo.dataPtr; + XMP_Uns32 degNum = 0, degDenom = 1; // Defaults for missing parts. + XMP_Uns32 minNum = 0, minDenom = 1; + XMP_Uns32 secNum = 0, secDenom = 1; + if ( ! nativeEndian ) { + degDenom = Flip4 ( degDenom ); // So they can be flipped again below. + minDenom = Flip4 ( minDenom ); + secDenom = Flip4 ( secDenom ); + } + + degNum = binPtr[0]; + degDenom = binPtr[1]; + if ( posInfo.count >= 2 ) { + minNum = binPtr[2]; + minDenom = binPtr[3]; + if ( posInfo.count >= 3 ) { + secNum = binPtr[4]; + secDenom = binPtr[5]; + } + } + + if ( ! nativeEndian ) { + degNum = Flip4 ( degNum ); + degDenom = Flip4 ( degDenom ); + minNum = Flip4 ( minNum ); + minDenom = Flip4 ( minDenom ); + secNum = Flip4 ( secNum ); + secDenom = Flip4 ( secDenom ); + } + + // *** No check is made, whether the numerator exceed the maximum values, + // which is 90 for Latitude and 180 for Longitude + // ! The Exif spec says denominator must not be zero, but in reality they can be + + char buffer[40]; + + // Simplest case: all denominators are 1 + if ( (degDenom == 1) && (minDenom == 1) && (secDenom == 1) ) { + + snprintf ( buffer, sizeof(buffer), "%lu,%lu,%lu%c", (unsigned long)degNum, (unsigned long)minNum, (unsigned long)secNum, ref ); // AUDIT: Using sizeof(buffer) is safe. + + } else if ( (degDenom == 0 && degNum != 0) || (minDenom == 0 && minNum != 0) || (secDenom == 0 && secNum != 0) ) { + + // Error case: a denominator is zero and the numerator is non-zero + return; // Do not continue with import + + } else { + + // Determine precision + // The code rounds up to the next power of 10 to get the number of fractional digits + XMP_Uns32 maxDenom = degDenom; + if ( minDenom > maxDenom ) maxDenom = minDenom; + if ( secDenom > maxDenom ) maxDenom = secDenom; + + int fracDigits = 1; + while ( maxDenom > 10 ) { ++fracDigits; maxDenom = maxDenom/10; } + + // Calculate the values + // At this point we know that the fractions are either 0/0, 0/y or x/y + + double degrees, minutes; + + // Degrees + if ( degDenom == 0 && degNum == 0 ) { + degrees = 0; + } else { + degrees = (double)( (XMP_Uns32)((double)degNum / (double)degDenom) ); // Just the integral number of degrees. + } + + // Minutes + if ( minDenom == 0 && minNum == 0 ) { + minutes = 0; + } else { + double temp = 0; + if( degrees != 0 ) temp = ((double)degNum / (double)degDenom) - degrees; + + minutes = (temp * 60.0) + ((double)minNum / (double)minDenom); + } + + // Seconds, are added to minutes + if ( secDenom != 0 && secNum != 0 ) { + minutes += ((double)secNum / (double)secDenom) / 60.0; + } + + snprintf ( buffer, sizeof(buffer), "%.0f,%.*f%c", degrees, fracDigits, minutes, ref ); // AUDIT: Using sizeof(buffer) is safe. + + } + + xmp->SetProperty ( xmpNS, xmpProp, buffer ); + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportTIFF_GPSCoordinate + +// ================================================================================================= +// ImportTIFF_GPSTimeStamp +// ======================= + +static void +ImportTIFF_GPSTimeStamp ( const TIFF_Manager & tiff, const TIFF_Manager::TagInfo & timeInfo, + SXMPMeta * xmp, const char * xmpNS, const char * xmpProp ) +{ + try { // Don't let errors with one stop the others. + + const bool nativeEndian = tiff.IsNativeEndian(); + + bool haveDate; + TIFF_Manager::TagInfo dateInfo; + haveDate = tiff.GetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSDateStamp, &dateInfo ); + if ( ! haveDate ) haveDate = tiff.GetTag ( kTIFF_ExifIFD, kTIFF_DateTimeOriginal, &dateInfo ); + if ( ! haveDate ) haveDate = tiff.GetTag ( kTIFF_ExifIFD, kTIFF_DateTimeDigitized, &dateInfo ); + if ( ! haveDate ) return; + + const char * dateStr = (const char *) dateInfo.dataPtr; + if ( ((dateStr[4] != ':') && (dateStr[4] != '-')) || ((dateStr[7] != ':') && (dateStr[7] != '-')) ) return; + if ( (dateStr[10] != 0) && (dateStr[10] != ' ') ) return; + + XMP_Uns32 * binPtr = (XMP_Uns32*)timeInfo.dataPtr; + XMP_Uns32 hourNum = binPtr[0]; + XMP_Uns32 hourDenom = binPtr[1]; + XMP_Uns32 minNum = binPtr[2]; + XMP_Uns32 minDenom = binPtr[3]; + XMP_Uns32 secNum = binPtr[4]; + XMP_Uns32 secDenom = binPtr[5]; + if ( ! nativeEndian ) { + hourNum = Flip4 ( hourNum ); + hourDenom = Flip4 ( hourDenom ); + minNum = Flip4 ( minNum ); + minDenom = Flip4 ( minDenom ); + secNum = Flip4 ( secNum ); + secDenom = Flip4 ( secDenom ); + } + + double fHour, fMin, fSec, fNano, temp; + fSec = (double)secNum / (double)secDenom; + temp = (double)minNum / (double)minDenom; + fMin = (double)((XMP_Uns32)temp); + fSec += (temp - fMin) * 60.0; + temp = (double)hourNum / (double)hourDenom; + fHour = (double)((XMP_Uns32)temp); + fSec += (temp - fHour) * 3600.0; + temp = (double)((XMP_Uns32)fSec); + fNano = ((fSec - temp) * (1000.0*1000.0*1000.0)) + 0.5; // Try to avoid n999... problems. + fSec = temp; + + XMP_DateTime binStamp; + binStamp.year = GatherInt ( dateStr, 4 ); + binStamp.month = GatherInt ( dateStr+5, 2 ); + binStamp.day = GatherInt ( dateStr+8, 2 ); + binStamp.hour = (XMP_Int32)fHour; + binStamp.minute = (XMP_Int32)fMin; + binStamp.second = (XMP_Int32)fSec; + binStamp.nanoSecond = (XMP_Int32)fNano; + binStamp.hasTimeZone = true; // Exif GPS TimeStamp is implicitly UTC. + binStamp.tzSign = kXMP_TimeIsUTC; + binStamp.tzHour = binStamp.tzMinute = 0; + + xmp->SetProperty_Date ( xmpNS, xmpProp, binStamp ); + + } catch ( ... ) { + // Do nothing, let other imports proceed. + // ? Notify client? + } + +} // ImportTIFF_GPSTimeStamp + +// ================================================================================================= + +static void ImportTIFF_PhotographicSensitivity ( const TIFF_Manager & exif, SXMPMeta * xmp ) { + + // PhotographicSensitivity has special cases for values over 65534 because the tag is SHORT. It + // has a count of "any", but all known cameras used a count of 1. Exif 2.3 still allows a count + // of "any" but says only 1 value should ever be recorded. We only process the first value if + // the count is greater than 1. + // + // Prior to Exif 2.3 the tag was called ISOSpeedRatings. Some cameras omit the tag and some + // write 65535. Most put the real ISO in MakerNote, which might be in the XMP from ACR. + // + // With Exif 2.3 five speed-related tags were added of type LONG: StandardOutputSensitivity, + // RecommendedExposureIndex, ISOSpeed, ISOSpeedLatitudeyyy, and ISOSpeedLatitudezzz. The tag + // SensitivityType was added to say which of these five is copied to PhotographicSensitivity. + // + // Import logic: + // Save exif:ISOSpeedRatings when cleaning namespaces (done in ImportPhotoData) + // If ExifVersion is less than "0230" (or missing) + // If PhotographicSensitivity[1] < 65535 or no exif:ISOSpeedRatings: set exif:ISOSpeedRatings from tag + // Else (ExifVersion is at least "0230") + // Import SensitivityType and related long tags + // If PhotographicSensitivity[1] < 65535: set exifEX:PhotographicSensitivity and exif:ISOSpeedRatings from tag + // Else (no PhotographicSensitivity tag or value is 65535) + // Set exifEX:PhotographicSensitivity from tag (missing or 65535) + // If have SensitivityType and indicated tag: set exif:ISOSpeedRatings from indicated tag + + try { + + bool found; + TIFF_Manager::TagInfo tagInfo; + + bool haveOldExif = true; // Default to old Exif if no version tag. + bool haveTag34855 = false; + bool haveLowISO = false; // Set for real if haveTag34855 is true. + + XMP_Uns32 valueTag34855; // ! Only care about the first value if more than 1. + + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_ExifVersion, &tagInfo ); + if ( found && (tagInfo.type == kTIFF_UndefinedType) && (tagInfo.count == 4) ) { + haveOldExif = (strncmp ( (char*)tagInfo.dataPtr, "0230", 4 ) < 0); + } + + haveTag34855 = exif.GetTag_Integer ( kTIFF_ExifIFD, kTIFF_PhotographicSensitivity, &valueTag34855 ); + if ( haveTag34855 ) haveLowISO = (valueTag34855 < 65535); + + if ( haveOldExif ) { // Exif version is before 2.3, use just the old tag and property. + + if ( haveTag34855 ) { + if ( haveLowISO || (! xmp->DoesPropertyExist ( kXMP_NS_EXIF, "ISOSpeedRatings" )) ) { + xmp->DeleteProperty ( kXMP_NS_EXIF, "ISOSpeedRatings" ); + xmp->AppendArrayItem ( kXMP_NS_EXIF, "ISOSpeedRatings", kXMP_PropArrayIsOrdered, "" ); + xmp->SetProperty_Int ( kXMP_NS_EXIF, "ISOSpeedRatings[1]", valueTag34855 ); + } + } + + } else { // Exif version is 2.3 or newer, use the Exif 2.3 tags and properties. + + // Import the SensitivityType and related long tags. + + XMP_Uns16 whichLongTag = 0; + XMP_Uns32 sensitivityType, tagValue; + + bool haveSensitivityType = exif.GetTag_Integer ( kTIFF_ExifIFD, kTIFF_SensitivityType, &sensitivityType ); + if ( haveSensitivityType ) { + xmp->SetProperty_Int ( kXMP_NS_ExifEX, "SensitivityType", sensitivityType ); + switch ( sensitivityType ) { + case 1 : // Use StandardOutputSensitivity for both 1 and 4. + case 4 : whichLongTag = kTIFF_StandardOutputSensitivity; break; + case 2 : whichLongTag = kTIFF_RecommendedExposureIndex; break; + case 3 : // Use ISOSpeed for all of 3, 5, 6, and 7. + case 5 : + case 6 : + case 7 : whichLongTag = kTIFF_ISOSpeed; break; + } + } + + found = exif.GetTag_Integer ( kTIFF_ExifIFD, kTIFF_StandardOutputSensitivity, &tagValue ); + if ( found ) { + xmp->SetProperty_Int64 ( kXMP_NS_ExifEX, "StandardOutputSensitivity", tagValue ); + } + + found = exif.GetTag_Integer ( kTIFF_ExifIFD, kTIFF_RecommendedExposureIndex, &tagValue ); + if ( found ) { + xmp->SetProperty_Int64 ( kXMP_NS_ExifEX, "RecommendedExposureIndex", tagValue ); + } + + found = exif.GetTag_Integer ( kTIFF_ExifIFD, kTIFF_ISOSpeed, &tagValue ); + if ( found ) { + xmp->SetProperty_Int64 ( kXMP_NS_ExifEX, "ISOSpeed", tagValue ); + } + + found = exif.GetTag_Integer ( kTIFF_ExifIFD, kTIFF_ISOSpeedLatitudeyyy, &tagValue ); + if ( found ) { + xmp->SetProperty_Int64 ( kXMP_NS_ExifEX, "ISOSpeedLatitudeyyy", tagValue ); + } + + found = exif.GetTag_Integer ( kTIFF_ExifIFD, kTIFF_ISOSpeedLatitudezzz, &tagValue ); + if ( found ) { + xmp->SetProperty_Int64 ( kXMP_NS_ExifEX, "ISOSpeedLatitudezzz", tagValue ); + } + + // Deal with the special cases for exifEX:PhotographicSensitivity and exif:ISOSpeedRatings. + + if ( haveTag34855 & haveLowISO ) { // The easier low ISO case. + + xmp->DeleteProperty ( kXMP_NS_EXIF, "ISOSpeedRatings" ); + xmp->AppendArrayItem ( kXMP_NS_EXIF, "ISOSpeedRatings", kXMP_PropArrayIsOrdered, "" ); + xmp->SetProperty_Int ( kXMP_NS_EXIF, "ISOSpeedRatings[1]", valueTag34855 ); + xmp->SetProperty_Int ( kXMP_NS_ExifEX, "PhotographicSensitivity", valueTag34855 ); + + } else { // The more complex high ISO case, or no PhotographicSensitivity tag. + + if ( haveTag34855 ) { + XMP_Assert ( valueTag34855 == 65535 ); + xmp->SetProperty_Int ( kXMP_NS_ExifEX, "PhotographicSensitivity", valueTag34855 ); + } + + if ( whichLongTag != 0 ) { + found = exif.GetTag ( kTIFF_ExifIFD, whichLongTag, &tagInfo ); + if ( found && (tagInfo.type == kTIFF_LongType) && (tagInfo.count == 1) ){ + xmp->DeleteProperty ( kXMP_NS_EXIF, "ISOSpeedRatings" ); + xmp->AppendArrayItem ( kXMP_NS_EXIF, "ISOSpeedRatings", kXMP_PropArrayIsOrdered, "" ); + xmp->SetProperty_Int ( kXMP_NS_EXIF, "ISOSpeedRatings[1]", exif.GetUns32 ( tagInfo.dataPtr ) ); + } + } + + } + + } + + } catch ( ... ) { + + // Do nothing, don't let failures here stop other exports. + + } + +} // ImportTIFF_PhotographicSensitivity + +// ================================================================================================= +// ================================================================================================= + +// ================================================================================================= +// PhotoDataUtils::Import2WayExif +// ============================== +// +// Import the TIFF/Exif tags that have 2 way mappings to XMP, i.e. no correspondence to IPTC. +// These are always imported for the tiff: and exif: namespaces, but not for others. + +void +PhotoDataUtils::Import2WayExif ( const TIFF_Manager & exif, SXMPMeta * xmp, int iptcDigestState ) +{ + const bool nativeEndian = exif.IsNativeEndian(); + + bool found, foundFromXMP; + TIFF_Manager::TagInfo tagInfo; + XMP_OptionBits flags; + + ImportTIFF_StandardMappings ( kTIFF_PrimaryIFD, exif, xmp ); + ImportTIFF_StandardMappings ( kTIFF_ExifIFD, exif, xmp ); + ImportTIFF_StandardMappings ( kTIFF_GPSInfoIFD, exif, xmp ); + + // -------------------------------------------------------- + // Add the old Adobe names for new Exif 2.3 tags if wanted. + + #if SupportOldExifProperties + + // CameraOwnerName -> aux:OwnerName + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_CameraOwnerName, &tagInfo ); + if ( found && (tagInfo.type == kTIFF_ASCIIType) && (tagInfo.count > 0) ) { + ImportSingleTIFF ( tagInfo, nativeEndian, xmp, kXMP_NS_EXIF_Aux, "OwnerName" ); + } + + // BodySerialNumber -> aux:SerialNumber + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_BodySerialNumber, &tagInfo ); + if ( found && (tagInfo.type == kTIFF_ASCIIType) && (tagInfo.count > 0) ) { + ImportSingleTIFF ( tagInfo, nativeEndian, xmp, kXMP_NS_EXIF_Aux, "SerialNumber" ); + } + + // LensModel -> aux:Lens + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_LensModel, &tagInfo ); + if ( found && (tagInfo.type == kTIFF_ASCIIType) && (tagInfo.count > 0) ) { + ImportSingleTIFF ( tagInfo, nativeEndian, xmp, kXMP_NS_EXIF_Aux, "Lens" ); + } + + // LensSpecification -> aux:LensInfo as a single string - use the XMP form for simplicity + found = xmp->GetProperty ( kXMP_NS_ExifEX, "LensSpecification", 0, &flags ); + if ( found && XMP_PropIsArray(flags) ) { + std::string fullStr, oneItem; + size_t count = (size_t) xmp->CountArrayItems ( kXMP_NS_ExifEX, "LensSpecification" ); + if ( count > 0 ) { + (void) xmp->GetArrayItem ( kXMP_NS_ExifEX, "LensSpecification", 1, &fullStr, 0 ); + for ( size_t i = 2; i <= count; ++i ) { + fullStr += ' '; + (void) xmp->GetArrayItem ( kXMP_NS_ExifEX, "LensSpecification", i, &oneItem, 0 ); + fullStr += oneItem; + } + } + xmp->SetProperty ( kXMP_NS_EXIF_Aux, "LensInfo", fullStr.c_str(), kXMP_DeleteExisting ); + } + + #endif + + // -------------------------------------------------------------------------------------------- + // Fixup erroneous cases that appear to have a negative value for GPSAltitude in the Exif. This + // treats any value with the high bit set as a negative, which is more likely in practice than + // an actual value over 2 billion. + + found = exif.GetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSAltitude, &tagInfo ); + if ( found && (tagInfo.type == kTIFF_RationalType) && (tagInfo.count == 1) ) { + + XMP_Uns32 num = exif.GetUns32 ( tagInfo.dataPtr ); + XMP_Uns32 denom = exif.GetUns32 ( (XMP_Uns8*)tagInfo.dataPtr + 4 ); + + bool fixXMP = false; + bool numNeg = num >> 31; + bool denomNeg = denom >> 31; + + if ( denomNeg ) { // The denominator looks negative, shift the sign to the numerator. + denom = -denom; + num = -num; + numNeg = num >> 31; + fixXMP = true; + } + + if ( numNeg ) { // The numerator looks negative, fix the XMP. + xmp->SetProperty ( kXMP_NS_EXIF, "GPSAltitudeRef", "1" ); + num = -num; + fixXMP = true; + } + + if ( fixXMP ) { + char buffer [32]; + snprintf ( buffer, sizeof(buffer), "%lu/%lu", (unsigned long) num, (unsigned long) denom ); // AUDIT: Using sizeof(buffer) is safe. + xmp->SetProperty ( kXMP_NS_EXIF, "GPSAltitude", buffer ); + } + + } + + // --------------------------------------------------------------- + // Import DateTimeOriginal and DateTime if the XMP doss not exist. + + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_DateTimeOriginal, &tagInfo ); + foundFromXMP = xmp->DoesPropertyExist ( kXMP_NS_EXIF, "DateTimeOriginal" ); + + if ( found && (! foundFromXMP) && (tagInfo.type == kTIFF_ASCIIType) ) { + ImportTIFF_Date ( exif, tagInfo, xmp, kXMP_NS_EXIF, "DateTimeOriginal" ); + } + + found = exif.GetTag ( kTIFF_PrimaryIFD, kTIFF_DateTime, &tagInfo ); + foundFromXMP = xmp->DoesPropertyExist ( kXMP_NS_XMP, "ModifyDate" ); + + if ( found && (! foundFromXMP) && (tagInfo.type == kTIFF_ASCIIType) ) { + ImportTIFF_Date ( exif, tagInfo, xmp, kXMP_NS_XMP, "ModifyDate" ); + } + + // ---------------------------------------------------- + // Import the Exif IFD tags that have special mappings. + + // There are moderately complex import special cases for PhotographicSensitivity. + ImportTIFF_PhotographicSensitivity ( exif, xmp ); + + // 42032 CameraOwnerName has a standard mapping. As a special case also set dc:creator if there + // is no Exif Artist tag and no dc:creator in the XMP. + found = exif.GetTag ( kTIFF_PrimaryIFD, kTIFF_Artist, &tagInfo ); + foundFromXMP = xmp->DoesPropertyExist ( kXMP_NS_DC, "creator" ); + if ( (! found) && (! foundFromXMP) ) { + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_CameraOwnerName, &tagInfo ); + if ( found ) { + std::string xmpValue ( (char*)tagInfo.dataPtr, tagInfo.dataLen ); + xmp->AppendArrayItem ( kXMP_NS_DC, "creator", kXMP_PropArrayIsOrdered, xmpValue.c_str() ); + } + } + + // 36864 ExifVersion is 4 "undefined" ASCII characters. + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_ExifVersion, &tagInfo ); + if ( found && (tagInfo.type == kTIFF_UndefinedType) && (tagInfo.count == 4) ) { + char str[5]; + *((XMP_Uns32*)str) = *((XMP_Uns32*)tagInfo.dataPtr); + str[4] = 0; + xmp->SetProperty ( kXMP_NS_EXIF, "ExifVersion", str ); + } + + // 40960 FlashpixVersion is 4 "undefined" ASCII characters. + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_FlashpixVersion, &tagInfo ); + if ( found && (tagInfo.type == kTIFF_UndefinedType) && (tagInfo.count == 4) ) { + char str[5]; + *((XMP_Uns32*)str) = *((XMP_Uns32*)tagInfo.dataPtr); + str[4] = 0; + xmp->SetProperty ( kXMP_NS_EXIF, "FlashpixVersion", str ); + } + + // 37121 ComponentsConfiguration is an array of 4 "undefined" UInt8 bytes. + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_ComponentsConfiguration, &tagInfo ); + if ( found && (tagInfo.type == kTIFF_UndefinedType) && (tagInfo.count == 4) ) { + ImportArrayTIFF_Byte ( tagInfo, xmp, kXMP_NS_EXIF, "ComponentsConfiguration" ); + } + + // 37510 UserComment is a string with explicit encoding. + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_UserComment, &tagInfo ); + if ( found ) { + ImportTIFF_EncodedString ( exif, tagInfo, xmp, kXMP_NS_EXIF, "UserComment", true /* isLangAlt */ ); + } + + // 34856 OECF is an OECF/SFR table. + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_OECF, &tagInfo ); + if ( found ) { + ImportConversionTable ( tagInfo, nativeEndian, xmp, kXMP_NS_EXIF, "OECF" ); + } + + // 37385 Flash is a UInt16 collection of bit fields and is mapped to a struct in XMP. + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_Flash, &tagInfo ); + if ( found && (tagInfo.type == kTIFF_ShortType) && (tagInfo.count == 1) ) { + ImportTIFF_Flash ( tagInfo, nativeEndian, xmp, kXMP_NS_EXIF, "Flash" ); + } + + // 41484 SpatialFrequencyResponse is an OECF/SFR table. + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_SpatialFrequencyResponse, &tagInfo ); + if ( found ) { + ImportConversionTable ( tagInfo, nativeEndian, xmp, kXMP_NS_EXIF, "SpatialFrequencyResponse" ); + } + + // 41728 FileSource is an "undefined" UInt8. + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_FileSource, &tagInfo ); + if ( found && (tagInfo.type == kTIFF_UndefinedType) && (tagInfo.count == 1) ) { + ImportSingleTIFF_Byte ( tagInfo, xmp, kXMP_NS_EXIF, "FileSource" ); + } + + // 41729 SceneType is an "undefined" UInt8. + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_SceneType, &tagInfo ); + if ( found && (tagInfo.type == kTIFF_UndefinedType) && (tagInfo.count == 1) ) { + ImportSingleTIFF_Byte ( tagInfo, xmp, kXMP_NS_EXIF, "SceneType" ); + } + + // 41730 CFAPattern is a custom table. + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_CFAPattern, &tagInfo ); + if ( found ) { + ImportTIFF_CFATable ( tagInfo, nativeEndian, xmp, kXMP_NS_EXIF, "CFAPattern" ); + } + + // 41995 DeviceSettingDescription is a custom table. + found = exif.GetTag ( kTIFF_ExifIFD, kTIFF_DeviceSettingDescription, &tagInfo ); + if ( found ) { + ImportTIFF_DSDTable ( exif, tagInfo, xmp, kXMP_NS_EXIF, "DeviceSettingDescription" ); + } + + // -------------------------------------------------------- + // Import the GPS Info IFD tags that have special mappings. + + // 0 GPSVersionID is 4 UInt8 bytes and mapped as "n.n.n.n". + found = exif.GetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSVersionID, &tagInfo ); + if ( found && (tagInfo.type == kTIFF_ByteType) && (tagInfo.count == 4) ) { + const XMP_Uns8 * binValue = (const XMP_Uns8 *) tagInfo.dataPtr; + char strOut[20];// ! Value could be up to 16 bytes: "255.255.255.255" plus nul. + snprintf ( strOut, sizeof(strOut), "%u.%u.%u.%u", // AUDIT: Use of sizeof(strOut) is safe. + binValue[0], binValue[1], binValue[2], binValue[3] ); + xmp->SetProperty ( kXMP_NS_EXIF, "GPSVersionID", strOut ); + } + + // 2 GPSLatitude is a GPS coordinate master. + found = exif.GetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSLatitude, &tagInfo ); + if ( found ) { + ImportTIFF_GPSCoordinate ( exif, tagInfo, xmp, kXMP_NS_EXIF, "GPSLatitude" ); + } + + // 4 GPSLongitude is a GPS coordinate master. + found = exif.GetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSLongitude, &tagInfo ); + if ( found ) { + ImportTIFF_GPSCoordinate ( exif, tagInfo, xmp, kXMP_NS_EXIF, "GPSLongitude" ); + } + + // 7 GPSTimeStamp is a UTC time as 3 rationals, mated with the optional GPSDateStamp. + found = exif.GetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSTimeStamp, &tagInfo ); + if ( found && (tagInfo.type == kTIFF_RationalType) && (tagInfo.count == 3) ) { + ImportTIFF_GPSTimeStamp ( exif, tagInfo, xmp, kXMP_NS_EXIF, "GPSTimeStamp" ); + } + + // 20 GPSDestLatitude is a GPS coordinate master. + found = exif.GetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSDestLatitude, &tagInfo ); + if ( found ) { + ImportTIFF_GPSCoordinate ( exif, tagInfo, xmp, kXMP_NS_EXIF, "GPSDestLatitude" ); + } + + // 22 GPSDestLongitude is a GPS coordinate master. + found = exif.GetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSDestLongitude, &tagInfo ); + if ( found ) { + ImportTIFF_GPSCoordinate ( exif, tagInfo, xmp, kXMP_NS_EXIF, "GPSDestLongitude" ); + } + + // 27 GPSProcessingMethod is a string with explicit encoding. + found = exif.GetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSProcessingMethod, &tagInfo ); + if ( found ) { + ImportTIFF_EncodedString ( exif, tagInfo, xmp, kXMP_NS_EXIF, "GPSProcessingMethod" ); + } + + // 28 GPSAreaInformation is a string with explicit encoding. + found = exif.GetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSAreaInformation, &tagInfo ); + if ( found ) { + ImportTIFF_EncodedString ( exif, tagInfo, xmp, kXMP_NS_EXIF, "GPSAreaInformation" ); + } + +} // PhotoDataUtils::Import2WayExif + +// ================================================================================================= +// Import3WayDateTime +// ================== + +static void Import3WayDateTime ( XMP_Uns16 exifTag, const TIFF_Manager & exif, const IPTC_Manager & iptc, + SXMPMeta * xmp, int iptcDigestState, const IPTC_Manager & oldIPTC ) +{ + XMP_Uns8 iptcDS; + XMP_StringPtr xmpNS, xmpProp; + + if ( exifTag == kTIFF_DateTimeOriginal ) { + iptcDS = kIPTC_DateCreated; + xmpNS = kXMP_NS_Photoshop; + xmpProp = "DateCreated"; + } else if ( exifTag == kTIFF_DateTimeDigitized ) { + iptcDS = kIPTC_DigitalCreateDate; + xmpNS = kXMP_NS_XMP; + xmpProp = "CreateDate"; + } else { + XMP_Throw ( "Unrecognized dateID", kXMPErr_BadParam ); + } + + size_t iptcCount; + bool haveXMP, haveExif, haveIPTC; // ! These are manipulated to simplify MWG-compliant logic. + std::string xmpValue, exifValue, iptcValue; + TIFF_Manager::TagInfo exifInfo; + IPTC_Manager::DataSetInfo iptcInfo; + + // Get the basic info about available values. + haveXMP = xmp->GetProperty ( xmpNS, xmpProp, &xmpValue, 0 ); + iptcCount = PhotoDataUtils::GetNativeInfo ( iptc, iptcDS, iptcDigestState, haveXMP, &iptcInfo ); + haveIPTC = (iptcCount > 0); + XMP_Assert ( (iptcDigestState == kDigestMatches) ? (! haveIPTC) : true ); + haveExif = (! haveXMP) && (! haveIPTC) && PhotoDataUtils::GetNativeInfo ( exif, kTIFF_ExifIFD, exifTag, &exifInfo ); + XMP_Assert ( (! (haveExif & haveXMP)) & (! (haveExif & haveIPTC)) ); + + if ( haveIPTC ) { + + PhotoDataUtils::ImportIPTC_Date ( iptcDS, iptc, xmp ); + + } else if ( haveExif && (exifInfo.type == kTIFF_ASCIIType) ) { + + // Only import the Exif form if the non-TZ information differs from the XMP. + + TIFF_FileWriter exifFromXMP; + TIFF_Manager::TagInfo infoFromXMP; + + ExportTIFF_Date ( *xmp, xmpNS, xmpProp, &exifFromXMP, exifTag ); + bool foundFromXMP = exifFromXMP.GetTag ( kTIFF_ExifIFD, exifTag, &infoFromXMP ); + + if ( (! foundFromXMP) || (exifInfo.dataLen != infoFromXMP.dataLen) || + (! XMP_LitNMatch ( (char*)exifInfo.dataPtr, (char*)infoFromXMP.dataPtr, exifInfo.dataLen )) ) { + ImportTIFF_Date ( exif, exifInfo, xmp, xmpNS, xmpProp ); + } + + } + +} // Import3WayDateTime + +// ================================================================================================= +// PhotoDataUtils::Import3WayItems +// =============================== +// +// Handle the imports that involve all 3 of Exif, IPTC, and XMP. There are only 4 properties with +// 3-way mappings, copyright, description, creator, and date/time. Following the MWG guidelines, +// this general policy is applied separately to each: +// +// If the new IPTC digest differs from the stored digest (favor IPTC over Exif and XMP) +// If the IPTC value differs from the predicted old IPTC value +// Import the IPTC value, including deleting the XMP +// Else if the Exif is non-empty and differs from the XMP +// Import the Exif value (does not delete existing XMP) +// Else if the stored IPTC digest is missing (favor Exif over IPTC, or IPTC over missing XMP) +// If the Exif is non-empty and differs from the XMP +// Import the Exif value (does not delete existing XMP) +// Else if the XMP is missing and the Exif is missing or empty +// Import the IPTC value +// Else (the new IPTC digest matches the stored digest - ignore the IPTC) +// If the Exif is non-empty and differs from the XMP +// Import the Exif value (does not delete existing XMP) +// +// Note that missing or empty Exif will never cause existing XMP to be deleted. This is a pragmatic +// choice to improve compatibility with pre-MWG software. There are few Exif-only editors for these +// 3-way properties, there are important existing IPTC-only editors. + +// ------------------------------------------------------------------------------------------------- + +void PhotoDataUtils::Import3WayItems ( const TIFF_Manager & exif, const IPTC_Manager & iptc, SXMPMeta * xmp, int iptcDigestState ) +{ + size_t iptcCount; + + bool haveXMP, haveExif, haveIPTC; // ! These are manipulated to simplify MWG-compliant logic. + std::string xmpValue, exifValue, iptcValue; + + TIFF_Manager::TagInfo exifInfo; + IPTC_Manager::DataSetInfo iptcInfo; + + IPTC_Writer oldIPTC; + if ( iptcDigestState == kDigestDiffers ) { + PhotoDataUtils::ExportIPTC ( *xmp, &oldIPTC ); // Predict old IPTC DataSets based on the existing XMP. + } + + // --------------------------------------------------------------------------------- + // Process the copyright. Replace internal nuls in the Exif to "merge" the portions. + + // Get the basic info about available values. + haveXMP = xmp->GetLocalizedText ( kXMP_NS_DC, "rights", "", "x-default", 0, &xmpValue, 0 ); + iptcCount = PhotoDataUtils::GetNativeInfo ( iptc, kIPTC_CopyrightNotice, iptcDigestState, haveXMP, &iptcInfo ); + haveIPTC = (iptcCount > 0); + XMP_Assert ( (iptcDigestState == kDigestMatches) ? (! haveIPTC) : true ); + haveExif = (! haveXMP) && (! haveIPTC) && PhotoDataUtils::GetNativeInfo ( exif, kTIFF_PrimaryIFD, kTIFF_Copyright, &exifInfo ); + XMP_Assert ( (! (haveExif & haveXMP)) & (! (haveExif & haveIPTC)) ); + + if ( haveExif && (exifInfo.dataLen > 1) ) { // Replace internal nul characters with linefeed. + for ( XMP_Uns32 i = 0; i < exifInfo.dataLen-1; ++i ) { + if ( ((char*)exifInfo.dataPtr)[i] == 0 ) ((char*)exifInfo.dataPtr)[i] = 0x0A; + } + } + + if ( haveIPTC ) { + PhotoDataUtils::ImportIPTC_LangAlt ( iptc, xmp, kIPTC_CopyrightNotice, kXMP_NS_DC, "rights" ); + } else if ( haveExif && PhotoDataUtils::IsValueDifferent ( exifInfo, xmpValue, &exifValue ) ) { + xmp->SetLocalizedText ( kXMP_NS_DC, "rights", "", "x-default", exifValue.c_str() ); + } + + // ------------------------ + // Process the description. + + // Get the basic info about available values. + haveXMP = xmp->GetLocalizedText ( kXMP_NS_DC, "description", "", "x-default", 0, &xmpValue, 0 ); + iptcCount = PhotoDataUtils::GetNativeInfo ( iptc, kIPTC_Description, iptcDigestState, haveXMP, &iptcInfo ); + haveIPTC = (iptcCount > 0); + XMP_Assert ( (iptcDigestState == kDigestMatches) ? (! haveIPTC) : true ); + haveExif = (! haveXMP) && (! haveIPTC) && PhotoDataUtils::GetNativeInfo ( exif, kTIFF_PrimaryIFD, kTIFF_ImageDescription, &exifInfo ); + XMP_Assert ( (! (haveExif & haveXMP)) & (! (haveExif & haveIPTC)) ); + + if ( haveIPTC ) { + PhotoDataUtils::ImportIPTC_LangAlt ( iptc, xmp, kIPTC_Description, kXMP_NS_DC, "description" ); + } else if ( haveExif && PhotoDataUtils::IsValueDifferent ( exifInfo, xmpValue, &exifValue ) ) { + xmp->SetLocalizedText ( kXMP_NS_DC, "description", "", "x-default", exifValue.c_str() ); + } + + // ------------------------------------------------------------------------------------------- + // Process the creator. The XMP and IPTC are arrays, the Exif is a semicolon separated string. + + // Get the basic info about available values. + haveXMP = xmp->DoesPropertyExist ( kXMP_NS_DC, "creator" ); + haveExif = PhotoDataUtils::GetNativeInfo ( exif, kTIFF_PrimaryIFD, kTIFF_Artist, &exifInfo ); + iptcCount = PhotoDataUtils::GetNativeInfo ( iptc, kIPTC_Creator, iptcDigestState, haveXMP, &iptcInfo ); + haveIPTC = (iptcCount > 0); + XMP_Assert ( (iptcDigestState == kDigestMatches) ? (! haveIPTC) : true ); + haveExif = (! haveXMP) && (! haveIPTC) && PhotoDataUtils::GetNativeInfo ( exif, kTIFF_PrimaryIFD, kTIFF_Artist, &exifInfo ); + XMP_Assert ( (! (haveExif & haveXMP)) & (! (haveExif & haveIPTC)) ); + + if ( haveIPTC ) { + PhotoDataUtils::ImportIPTC_Array ( iptc, xmp, kIPTC_Creator, kXMP_NS_DC, "creator" ); + } else if ( haveExif && PhotoDataUtils::IsValueDifferent ( exifInfo, xmpValue, &exifValue ) ) { + SXMPUtils::SeparateArrayItems ( xmp, kXMP_NS_DC, "creator", + (kXMP_PropArrayIsOrdered | kXMPUtil_AllowCommas), exifValue ); + } + + // ------------------------------------------------------------------------------ + // Process DateTimeDigitized; DateTimeOriginal and DateTime are 2-way. + // *** Exif DateTimeOriginal <-> XMP exif:DateTimeOriginal + // *** IPTC DateCreated <-> XMP photoshop:DateCreated + // *** Exif DateTimeDigitized <-> IPTC DigitalCreateDate <-> XMP xmp:CreateDate + // *** TIFF DateTime <-> XMP xmp:ModifyDate + + Import3WayDateTime ( kTIFF_DateTimeDigitized, exif, iptc, xmp, iptcDigestState, oldIPTC ); + +} // PhotoDataUtils::Import3WayItems + +// ================================================================================================= +// ================================================================================================= + +// ================================================================================================= + +static bool DecodeRational ( const char * ratio, XMP_Uns32 * num, XMP_Uns32 * denom ) { + + unsigned long locNum, locDenom; + char nextChar; // Used to make sure sscanf consumes all of the string. + + int items = sscanf ( ratio, "%lu/%lu%c", &locNum, &locDenom, &nextChar ); // AUDIT: This is safe, check the calls. + + if ( items != 2 ) { + if ( items != 1 ) return false; + locDenom = 1; // The XMP was just an integer, assume a denominator of 1. + } + + *num = (XMP_Uns32)locNum; + *denom = (XMP_Uns32)locDenom; + return true; + +} // DecodeRational + +// ================================================================================================= +// ExportSingleTIFF +// ================ +// +// This is only called when the XMP exists and will be exported. And only for standard mappings. + +// ! Only implemented for the types known to be needed. + +static void +ExportSingleTIFF ( TIFF_Manager * tiff, XMP_Uns8 ifd, const TIFF_MappingToXMP & mapInfo, + bool nativeEndian, const std::string & xmpValue ) +{ + XMP_Assert ( (mapInfo.count == 1) || (mapInfo.type == kTIFF_ASCIIType) ); + XMP_Assert ( mapInfo.name[0] != 0 ); // Must be a standard mapping. + + bool ok; + char nextChar; // Used to make sure sscanf consumes all of the string. + + switch ( mapInfo.type ) { + + case kTIFF_ByteType : { + unsigned short binValue; + int items = sscanf ( xmpValue.c_str(), "%hu%c", &binValue, &nextChar ); // AUDIT: Using xmpValue.c_str() is safe. + if ( items != 1 ) return; // ? complain? notify client? + tiff->SetTag_Byte ( ifd, mapInfo.id, (XMP_Uns8)binValue ); + break; + } + + case kTIFF_ShortType : { + unsigned long binValue; + int items = sscanf ( xmpValue.c_str(), "%lu%c", &binValue, &nextChar ); // AUDIT: Using xmpValue.c_str() is safe. + if ( items != 1 ) return; // ? complain? notify client? + tiff->SetTag_Short ( ifd, mapInfo.id, (XMP_Uns16)binValue ); + break; + } + + case kTIFF_LongType : { + unsigned long binValue; + int items = sscanf ( xmpValue.c_str(), "%lu%c", &binValue, &nextChar ); // AUDIT: Using xmpValue.c_str() is safe. + if ( items != 1 ) return; // ? complain? notify client? + tiff->SetTag_Long ( ifd, mapInfo.id, (XMP_Uns32)binValue ); + break; + } + + case kTIFF_ShortOrLongType : { + unsigned long binValue; + int items = sscanf ( xmpValue.c_str(), "%lu%c", &binValue, &nextChar ); // AUDIT: Using xmpValue.c_str() is safe. + if ( items != 1 ) return; // ? complain? notify client? + if ( binValue <= 0xFFFF ) { + tiff->SetTag_Short ( ifd, mapInfo.id, (XMP_Uns16)binValue ); + } else { + tiff->SetTag_Long ( ifd, mapInfo.id, (XMP_Uns32)binValue ); + } + break; + } + + case kTIFF_RationalType : { // The XMP is formatted as "num/denom". + XMP_Uns32 num, denom; + ok = DecodeRational ( xmpValue.c_str(), &num, &denom ); + if ( ! ok ) return; // ? complain? notify client? + tiff->SetTag_Rational ( ifd, mapInfo.id, num, denom ); + break; + } + + case kTIFF_SRationalType : { // The XMP is formatted as "num/denom". + signed long num, denom; + int items = sscanf ( xmpValue.c_str(), "%ld/%ld%c", &num, &denom, &nextChar ); // AUDIT: Using xmpValue.c_str() is safe. + if ( items != 2 ) { + if ( items != 1 ) return; // ? complain? notify client? + denom = 1; // The XMP was just an integer, assume a denominator of 1. + } + tiff->SetTag_SRational ( ifd, mapInfo.id, (XMP_Int32)num, (XMP_Int32)denom ); + break; + } + + case kTIFF_ASCIIType : + tiff->SetTag ( ifd, mapInfo.id, kTIFF_ASCIIType, (XMP_Uns32)(xmpValue.size()+1), xmpValue.c_str() ); + break; + + default: + XMP_Assert ( false ); // Force a debug assert for unexpected types. + + } + +} // ExportSingleTIFF + +// ================================================================================================= +// ExportArrayTIFF +// ================ +// +// This is only called when the XMP exists and will be exported. And only for standard mappings. + +// ! Only implemented for the types known to be needed. + +static void +ExportArrayTIFF ( TIFF_Manager * tiff, XMP_Uns8 ifd, const TIFF_MappingToXMP & mapInfo, bool nativeEndian, + const SXMPMeta & xmp, const char * xmpNS, const char * xmpArray ) +{ + XMP_Assert ( (mapInfo.count != 1) && (mapInfo.type != kTIFF_ASCIIType) ); + XMP_Assert ( mapInfo.name[0] != 0 ); // Must be a standard mapping. + XMP_Assert ( (mapInfo.type == kTIFF_ShortType) || (mapInfo.type == kTIFF_RationalType) ); + XMP_Assert ( xmp.DoesPropertyExist ( xmpNS, xmpArray ) ); + + size_t arraySize = xmp.CountArrayItems ( xmpNS, xmpArray ); + if ( arraySize == 0 ) { + tiff->DeleteTag ( ifd, mapInfo.id ); + return; + } + + if ( mapInfo.type == kTIFF_ShortType ) { + + std::vector<XMP_Uns16> shortVector; + shortVector.assign ( arraySize, 0 ); + XMP_Uns16 * shortPtr = (XMP_Uns16*) &shortVector[0]; + + std::string itemPath; + XMP_Int32 int32; + XMP_Uns16 uns16; + for ( size_t i = 1; i <= arraySize; ++i, ++shortPtr ) { + SXMPUtils::ComposeArrayItemPath ( xmpNS, xmpArray, (XMP_Index)i, &itemPath ); + xmp.GetProperty_Int ( xmpNS, itemPath.c_str(), &int32, 0 ); + uns16 = (XMP_Uns16)int32; + if ( ! nativeEndian ) uns16 = Flip2 ( uns16 ); + *shortPtr = uns16; + } + + tiff->SetTag ( ifd, mapInfo.id, kTIFF_ShortType, (XMP_Uns32)arraySize, &shortVector[0] ); + + } else if ( mapInfo.type == kTIFF_RationalType ) { + + std::vector<XMP_Uns32> rationalVector; + rationalVector.assign ( 2*arraySize, 0 ); + XMP_Uns32 * rationalPtr = (XMP_Uns32*) &rationalVector[0]; + + std::string itemPath, xmpValue; + XMP_Uns32 num, denom; + for ( size_t i = 1; i <= arraySize; ++i, rationalPtr += 2 ) { + SXMPUtils::ComposeArrayItemPath ( xmpNS, xmpArray, (XMP_Index)i, &itemPath ); + xmp.GetProperty ( xmpNS, itemPath.c_str(), &xmpValue, 0 ); + bool ok = DecodeRational ( xmpValue.c_str(), &num, &denom ); + if ( ! ok ) return; + if ( ! nativeEndian ) { num = Flip4 ( num ); denom = Flip4 ( denom ); } + rationalPtr[0] = num; + rationalPtr[1] = denom; + } + + tiff->SetTag ( ifd, mapInfo.id, kTIFF_RationalType, (XMP_Uns32)arraySize, &rationalVector[0] ); + + } + +} // ExportArrayTIFF + +// ================================================================================================= +// ExportTIFF_StandardMappings +// =========================== + +static void +ExportTIFF_StandardMappings ( XMP_Uns8 ifd, TIFF_Manager * tiff, const SXMPMeta & xmp ) +{ + const bool nativeEndian = tiff->IsNativeEndian(); + TIFF_Manager::TagInfo tagInfo; + std::string xmpValue; + XMP_OptionBits xmpForm; + + const TIFF_MappingToXMP * mappings = 0; + + if ( ifd == kTIFF_PrimaryIFD ) { + mappings = sPrimaryIFDMappings; + } else if ( ifd == kTIFF_ExifIFD ) { + mappings = sExifIFDMappings; + } else if ( ifd == kTIFF_GPSInfoIFD ) { + mappings = sGPSInfoIFDMappings; + } else { + XMP_Throw ( "Invalid IFD for standard mappings", kXMPErr_InternalFailure ); + } + + for ( size_t i = 0; mappings[i].id != 0xFFFF; ++i ) { + + try { // Don't let errors with one stop the others. + + const TIFF_MappingToXMP & mapInfo = mappings[i]; + + if ( mapInfo.exportMode == kExport_Never ) continue; + if ( mapInfo.name[0] == 0 ) continue; // Skip special mappings, handled higher up. + + bool haveTIFF = tiff->GetTag ( ifd, mapInfo.id, &tagInfo ); + if ( haveTIFF && (mapInfo.exportMode == kExport_InjectOnly) ) continue; + + bool haveXMP = xmp.GetProperty ( mapInfo.ns, mapInfo.name, &xmpValue, &xmpForm ); + if ( ! haveXMP ) { + + if ( haveTIFF && (mapInfo.exportMode == kExport_Always) ) tiff->DeleteTag ( ifd, mapInfo.id ); + + } else { + + XMP_Assert ( tagInfo.type != kTIFF_UndefinedType ); // These must have a special mapping. + if ( tagInfo.type == kTIFF_UndefinedType ) continue; + + const bool mapSingle = ((mapInfo.count == 1) || (mapInfo.type == kTIFF_ASCIIType)); + if ( mapSingle ) { + if ( ! XMP_PropIsSimple ( xmpForm ) ) continue; // ? Notify client? + ExportSingleTIFF ( tiff, ifd, mapInfo, nativeEndian, xmpValue ); + } else { + if ( ! XMP_PropIsArray ( xmpForm ) ) continue; // ? Notify client? + ExportArrayTIFF ( tiff, ifd, mapInfo, nativeEndian, xmp, mapInfo.ns, mapInfo.name ); + } + + } + + } catch ( ... ) { + + // Do nothing, let other imports proceed. + // ? Notify client? + + } + + } + +} // ExportTIFF_StandardMappings + +// ================================================================================================= +// ExportTIFF_Date +// =============== +// +// Convert an XMP date/time to an Exif 2.2 master date/time tag plus associated fractional seconds. +// The Exif date/time part is a 20 byte ASCII value formatted as "YYYY:MM:DD HH:MM:SS" with a +// terminating nul. The fractional seconds are a nul terminated ASCII string with possible space +// padding. They are literally the fractional part, the digits that would be to the right of the +// decimal point. + +static void +ExportTIFF_Date ( const SXMPMeta & xmp, const char * xmpNS, const char * xmpProp, TIFF_Manager * tiff, XMP_Uns16 mainID ) +{ + XMP_Uns8 mainIFD = kTIFF_ExifIFD; + XMP_Uns16 fracID; + switch ( mainID ) { + case kTIFF_DateTime : mainIFD = kTIFF_PrimaryIFD; fracID = kTIFF_SubSecTime; break; + case kTIFF_DateTimeOriginal : fracID = kTIFF_SubSecTimeOriginal; break; + case kTIFF_DateTimeDigitized : fracID = kTIFF_SubSecTimeDigitized; break; + } + + try { // Don't let errors with one stop the others. + + std::string xmpStr; + bool foundXMP = xmp.GetProperty ( xmpNS, xmpProp, &xmpStr, 0 ); + if ( ! foundXMP ) { + tiff->DeleteTag ( mainIFD, mainID ); + tiff->DeleteTag ( kTIFF_ExifIFD, fracID ); // ! The subseconds are always in the Exif IFD. + return; + } + + // Format using all of the numbers. Then overwrite blanks for missing fields. The fields + // missing from the XMP are detected with length checks: YYYY-MM-DDThh:mm:ss + // < 18 - no seconds + // < 15 - no minutes + // < 12 - no hours + // < 9 - no day + // < 6 - no month + // < 1 - no year + + XMP_DateTime xmpBin; + SXMPUtils::ConvertToDate ( xmpStr.c_str(), &xmpBin ); + + char buffer[24]; + snprintf ( buffer, sizeof(buffer), "%04d:%02d:%02d %02d:%02d:%02d", // AUDIT: Use of sizeof(buffer) is safe. + xmpBin.year, xmpBin.month, xmpBin.day, xmpBin.hour, xmpBin.minute, xmpBin.second ); + + size_t xmpLen = xmpStr.size(); + if ( xmpLen < 18 ) { + buffer[17] = buffer[18] = ' '; + if ( xmpLen < 15 ) { + buffer[14] = buffer[15] = ' '; + if ( xmpLen < 12 ) { + buffer[11] = buffer[12] = ' '; + if ( xmpLen < 9 ) { + buffer[8] = buffer[9] = ' '; + if ( xmpLen < 6 ) { + buffer[5] = buffer[6] = ' '; + if ( xmpLen < 1 ) { + buffer[0] = buffer[1] = buffer[2] = buffer[3] = ' '; + } + } + } + } + } + } + + tiff->SetTag_ASCII ( mainIFD, mainID, buffer ); + + if ( xmpBin.nanoSecond == 0 ) { + + tiff->DeleteTag ( kTIFF_ExifIFD, fracID ); + + } else { + + snprintf ( buffer, sizeof(buffer), "%09d", xmpBin.nanoSecond ); // AUDIT: Use of sizeof(buffer) is safe. + for ( size_t i = strlen(buffer)-1; i > 0; --i ) { + if ( buffer[i] != '0' ) break; + buffer[i] = 0; // Strip trailing zero digits. + } + + tiff->SetTag_ASCII ( kTIFF_ExifIFD, fracID, buffer ); // ! The subseconds are always in the Exif IFD. + + } + + } catch ( ... ) { + // Do nothing, let other exports proceed. + // ? Notify client? + } + +} // ExportTIFF_Date + +// ================================================================================================= +// ExportTIFF_ArrayASCII +// ===================== +// +// Catenate all of the XMP array values into a string. Use a "; " separator for Artist, nul for others. + +static void +ExportTIFF_ArrayASCII ( const SXMPMeta & xmp, const char * xmpNS, const char * xmpProp, + TIFF_Manager * tiff, XMP_Uns8 ifd, XMP_Uns16 id ) +{ + try { // Don't let errors with one stop the others. + + std::string itemValue, fullValue; + XMP_OptionBits xmpFlags; + + bool foundXMP = xmp.GetProperty ( xmpNS, xmpProp, 0, &xmpFlags ); + if ( ! foundXMP ) { + tiff->DeleteTag ( ifd, id ); + return; + } + + if ( ! XMP_PropIsArray ( xmpFlags ) ) return; // ? Complain? Delete the tag? + + if ( id == kTIFF_Artist ) { + SXMPUtils::CatenateArrayItems ( xmp, xmpNS, xmpProp, 0, 0, + (kXMP_PropArrayIsOrdered | kXMPUtil_AllowCommas), &fullValue ); + fullValue += '\x0'; // ! Need explicit final nul for SetTag below. + } else { + size_t count = xmp.CountArrayItems ( xmpNS, xmpProp ); + for ( size_t i = 1; i <= count; ++i ) { // ! XMP arrays are indexed from 1. + (void) xmp.GetArrayItem ( xmpNS, xmpProp, (XMP_Index)i, &itemValue, &xmpFlags ); + if ( ! XMP_PropIsSimple ( xmpFlags ) ) continue; // ? Complain? + fullValue.append ( itemValue ); + fullValue.append ( 1, '\x0' ); + } + } + + tiff->SetTag ( ifd, id, kTIFF_ASCIIType, (XMP_Uns32)fullValue.size(), fullValue.c_str() ); // ! Already have trailing nul. + + } catch ( ... ) { + // Do nothing, let other exports proceed. + // ? Notify client? + } + +} // ExportTIFF_ArrayASCII + +// ================================================================================================= +// ExportTIFF_LocTextASCII +// ====================== + +static void +ExportTIFF_LocTextASCII ( const SXMPMeta & xmp, const char * xmpNS, const char * xmpProp, + TIFF_Manager * tiff, XMP_Uns8 ifd, XMP_Uns16 id ) +{ + try { // Don't let errors with one stop the others. + + std::string xmpValue; + + bool foundXMP = xmp.GetLocalizedText ( xmpNS, xmpProp, "", "x-default", 0, &xmpValue, 0 ); + if ( ! foundXMP ) { + tiff->DeleteTag ( ifd, id ); + return; + } + + tiff->SetTag ( ifd, id, kTIFF_ASCIIType, (XMP_Uns32)( xmpValue.size()+1 ), xmpValue.c_str() ); + + } catch ( ... ) { + // Do nothing, let other exports proceed. + // ? Notify client? + } + +} // ExportTIFF_LocTextASCII + +// ================================================================================================= +// ExportTIFF_EncodedString +// ======================== + +static void +ExportTIFF_EncodedString ( const SXMPMeta & xmp, const char * xmpNS, const char * xmpProp, + TIFF_Manager * tiff, XMP_Uns8 ifd, XMP_Uns16 id, bool isLangAlt = false ) +{ + try { // Don't let errors with one stop the others. + + std::string xmpValue; + XMP_OptionBits xmpFlags; + + bool foundXMP = xmp.GetProperty ( xmpNS, xmpProp, &xmpValue, &xmpFlags ); + if ( ! foundXMP ) { + tiff->DeleteTag ( ifd, id ); + return; + } + + if ( ! isLangAlt ) { + if ( ! XMP_PropIsSimple ( xmpFlags ) ) return; // ? Complain? Delete the tag? + } else { + if ( ! XMP_ArrayIsAltText ( xmpFlags ) ) return; // ? Complain? Delete the tag? + bool ok = xmp.GetLocalizedText ( xmpNS, xmpProp, "", "x-default", 0, &xmpValue, 0 ); + if ( ! ok ) return; // ? Complain? Delete the tag? + } + + XMP_Uns8 encoding = kTIFF_EncodeASCII; + for ( size_t i = 0; i < xmpValue.size(); ++i ) { + if ( (XMP_Uns8)xmpValue[i] >= 0x80 ) { + encoding = kTIFF_EncodeUnicode; + break; + } + } + + tiff->SetTag_EncodedString ( ifd, id, xmpValue.c_str(), encoding ); + + } catch ( ... ) { + // Do nothing, let other exports proceed. + // ? Notify client? + } + +} // ExportTIFF_EncodedString + +// ================================================================================================= +// ExportTIFF_GPSCoordinate +// ======================== +// +// The XMP format is either "deg,min,secR" or "deg,min.fracR", where 'R' is the reference direction, +// 'N', 'S', 'E', or 'W'. The location gets output as ( deg/1, min/1, sec/1 ) for the first form, +// and ( deg/1, minFrac/denom, 0/1 ) for the second form. + +// ! We arbitrarily limit the number of fractional minute digits to 6 to avoid overflow in the +// ! combined numerator. But we don't otherwise check for overflow or range errors. + +static void +ExportTIFF_GPSCoordinate ( const SXMPMeta & xmp, const char * xmpNS, const char * xmpProp, + TIFF_Manager * tiff, XMP_Uns8 ifd, XMP_Uns16 _id ) +{ + XMP_Uns16 refID = _id-1; // ! The GPS refs and locations are all tag N-1 and N pairs. + XMP_Uns16 locID = _id; + + XMP_Assert ( (locID & 1) == 0 ); + + try { // Don't let errors with one stop the others. Tolerate ill-formed values where reasonable. + + std::string xmpValue; + XMP_OptionBits xmpFlags; + + bool foundXMP = xmp.GetProperty ( xmpNS, xmpProp, &xmpValue, &xmpFlags ); + if ( ! foundXMP ) { + tiff->DeleteTag ( ifd, refID ); + tiff->DeleteTag ( ifd, locID ); + return; + } + + if ( ! XMP_PropIsSimple ( xmpFlags ) ) return; + + const char * chPtr = xmpValue.c_str(); // Guaranteed to have a nul terminator. + + XMP_Uns32 deg=0, minNum=0, minDenom=1, sec=0; + + // Extract the degree part, it must be present. + + while ( (*chPtr == ' ') || (*chPtr == '\t') ) ++chPtr; + if ( (*chPtr < '0') || (*chPtr > '9') ) return; // Bad XMP string. + for ( ; ('0' <= *chPtr) && (*chPtr <= '9'); ++chPtr ) deg = deg*10 + (*chPtr - '0'); + while ( (*chPtr == ' ') || (*chPtr == '\t') ) ++chPtr; + if ( (*chPtr == ',') || (*chPtr == ';') ) ++chPtr; + while ( (*chPtr == ' ') || (*chPtr == '\t') ) ++chPtr; + + // Extract the whole minutes if present. + + if ( ('0' <= *chPtr) && (*chPtr <= '9') ) { + + for ( ; ('0' <= *chPtr) && (*chPtr <= '9'); ++chPtr ) minNum = minNum*10 + (*chPtr - '0'); + + // Extract the fractional minutes or seconds if present. + + if ( *chPtr == '.' ) { + + ++chPtr; // Skip the period. + for ( ; ('0' <= *chPtr) && (*chPtr <= '9'); ++chPtr ) { + if ( minDenom > 100*1000 ) continue; // Don't accumulate any more digits. + minDenom *= 10; + minNum = minNum*10 + (*chPtr - '0'); + } + + } else { + + while ( (*chPtr == ' ') || (*chPtr == '\t') ) ++chPtr; + if ( (*chPtr == ',') || (*chPtr == ';') ) ++chPtr; + while ( (*chPtr == ' ') || (*chPtr == '\t') ) ++chPtr; + for ( ; ('0' <= *chPtr) && (*chPtr <= '9'); ++chPtr ) sec = sec*10 + (*chPtr - '0'); + + } + + } + + // The compass direction part is required. But it isn't worth the bother to make sure it is + // appropriate for the tag. Little chance of ever seeing an error, it causes no great harm. + + while ( (*chPtr == ' ') || (*chPtr == '\t') ) ++chPtr; + if ( (*chPtr == ',') || (*chPtr == ';') ) ++chPtr; // Tolerate ',' or ';' here also. + while ( (*chPtr == ' ') || (*chPtr == '\t') ) ++chPtr; + + char ref[2]; + ref[0] = *chPtr; + ref[1] = 0; + + if ( ('a' <= ref[0]) && (ref[0] <= 'z') ) ref[0] -= 0x20; + if ( (ref[0] != 'N') && (ref[0] != 'S') && (ref[0] != 'E') && (ref[0] != 'W') ) return; + + tiff->SetTag ( ifd, refID, kTIFF_ASCIIType, 2, &ref[0] ); + + XMP_Uns32 loc[6]; + tiff->PutUns32 ( deg, &loc[0] ); + tiff->PutUns32 ( 1, &loc[1] ); + tiff->PutUns32 ( minNum, &loc[2] ); + tiff->PutUns32 ( minDenom, &loc[3] ); + tiff->PutUns32 ( sec, &loc[4] ); + tiff->PutUns32 ( 1, &loc[5] ); + + tiff->SetTag ( ifd, locID, kTIFF_RationalType, 3, &loc[0] ); + + } catch ( ... ) { + + // Do nothing, let other exports proceed. + // ? Notify client? + + } + +} // ExportTIFF_GPSCoordinate + +// ================================================================================================= +// ExportTIFF_GPSTimeStamp +// ======================= +// +// The Exif is in 2 tags, GPSTimeStamp and GPSDateStamp. The time is 3 rationals for the hour, minute, +// and second in UTC. The date is a nul terminated string "YYYY:MM:DD". + +static const double kBillion = 1000.0*1000.0*1000.0; +static const double mMaxSec = 4.0*kBillion - 1.0; + +static void +ExportTIFF_GPSTimeStamp ( const SXMPMeta & xmp, const char * xmpNS, const char * xmpProp, TIFF_Manager * tiff ) +{ + + try { // Don't let errors with one stop the others. + + XMP_DateTime binXMP; + bool foundXMP = xmp.GetProperty_Date ( xmpNS, xmpProp, &binXMP, 0 ); + if ( ! foundXMP ) { + tiff->DeleteTag ( kTIFF_GPSInfoIFD, kTIFF_GPSTimeStamp ); + tiff->DeleteTag ( kTIFF_GPSInfoIFD, kTIFF_GPSDateStamp ); + return; + } + + SXMPUtils::ConvertToUTCTime ( &binXMP ); + + XMP_Uns32 exifTime[6]; + tiff->PutUns32 ( binXMP.hour, &exifTime[0] ); + tiff->PutUns32 ( 1, &exifTime[1] ); + tiff->PutUns32 ( binXMP.minute, &exifTime[2] ); + tiff->PutUns32 ( 1, &exifTime[3] ); + if ( binXMP.nanoSecond == 0 ) { + tiff->PutUns32 ( binXMP.second, &exifTime[4] ); + tiff->PutUns32 ( 1, &exifTime[5] ); + } else { + double fSec = (double)binXMP.second + ((double)binXMP.nanoSecond / kBillion ); + XMP_Uns32 denom = 1000*1000; // Choose microsecond resolution by default. + TIFF_Manager::TagInfo oldInfo; + bool hadExif = tiff->GetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSTimeStamp, &oldInfo ); + if ( hadExif && (oldInfo.type == kTIFF_RationalType) && (oldInfo.count == 3) ) { + XMP_Uns32 oldDenom = tiff->GetUns32 ( &(((XMP_Uns32*)oldInfo.dataPtr)[5]) ); + if ( oldDenom != 1 ) denom = oldDenom; + } + fSec *= denom; + while ( fSec > mMaxSec ) { fSec /= 10; denom /= 10; } + tiff->PutUns32 ( (XMP_Uns32)fSec, &exifTime[4] ); + tiff->PutUns32 ( denom, &exifTime[5] ); + } + tiff->SetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSTimeStamp, kTIFF_RationalType, 3, &exifTime[0] ); + + char exifDate[16]; // AUDIT: Long enough, only need 11. + snprintf ( exifDate, 12, "%04d:%02d:%02d", binXMP.year, binXMP.month, binXMP.day ); + if ( exifDate[10] == 0 ) { // Make sure there is no value overflow. + tiff->SetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSDateStamp, kTIFF_ASCIIType, 11, exifDate ); + } + + } catch ( ... ) { + // Do nothing, let other exports proceed. + // ? Notify client? + } + +} // ExportTIFF_GPSTimeStamp + +// ================================================================================================= + +static void ExportTIFF_PhotographicSensitivity ( SXMPMeta * xmp, TIFF_Manager * exif ) { + + // PhotographicSensitivity has special cases for values over 65534 because the tag is SHORT. It + // has a count of "any", but all known cameras used a count of 1. Exif 2.3 still allows a count + // of "any" but says only 1 value should ever be recorded. We only process the first value if + // the count is greater than 1. + // + // Prior to Exif 2.3 the tag was called ISOSpeedRatings. Some cameras omit the tag and some + // write 65535. Most put the real ISO in MakerNote, which be in the XMP from ACR. + // + // With Exif 2.3 five speed-related tags were added of type LONG: StandardOutputSensitivity, + // RecommendedExposureIndex, ISOSpeed, ISOSpeedLatitudeyyy, and ISOSpeedLatitudezzz. The tag + // SensitivityType was added to say which of these five is copied to PhotographicSensitivity. + // + // Export logic: + // If ExifVersion is less than "0230" (or missing) + // If exif:ISOSpeedRatings[1] <= 65535: inject tag (if missing), remove exif:ISOSpeedRatings + // Else (ExifVersion is at least "0230") + // If no exifEX:PhotographicSensitivity: set it from exif:ISOSpeedRatings[1] + // Remove exif:ISOSpeedRatings (to not be saved in namespace cleanup) + // If exifEX:PhotographicSensitivity <= 65535: inject tag (if missing) + // Else if exifEX:PhotographicSensitivity over 65535 + // If no PhotographicSensitivity tag and no SensitivityType tag and no ISOSpeed tag: + // Inject PhotographicSensitivity tag as 65535 + // If no exifEX:SensitivityType and no exifEX:ISOSpeed + // Set exifEX:SensitivityType to 3 + // Set exifEX:ISOSpeed to exifEX:PhotographicSensitivity + // Inject SensitivityType and long tags (if missing) + // Save exif:ISOSpeedRatings when cleaning namespaces (done in ExportPhotoData) + + try { + + bool foundXMP, foundExif; + TIFF_Manager::TagInfo tagInfo; + std::string xmpValue; + XMP_OptionBits flags; + XMP_Int32 binValue; + + bool haveOldExif = true; // Default to old Exif if no version tag. + + foundExif = exif->GetTag ( kTIFF_ExifIFD, kTIFF_ExifVersion, &tagInfo ); + if ( foundExif && (tagInfo.type == kTIFF_UndefinedType) && (tagInfo.count == 4) ) { + haveOldExif = (strncmp ( (char*)tagInfo.dataPtr, "0230", 4 ) < 0); + } + + if ( haveOldExif ) { // Exif version is before 2.3, use just the old tag and property. + + foundXMP = xmp->GetProperty ( kXMP_NS_EXIF, "ISOSpeedRatings", 0, &flags ); + if ( foundXMP && XMP_PropIsArray(flags) && (xmp->CountArrayItems ( kXMP_NS_EXIF, "ISOSpeedRatings" ) > 0) ) { + foundXMP = xmp->GetProperty_Int ( kXMP_NS_EXIF, "ISOSpeedRatings[1]", &binValue, 0 ); + } + + if ( ! foundXMP ) { + foundXMP = xmp->GetProperty_Int ( kXMP_NS_ExifEX, "PhotographicSensitivity", &binValue, 0 ); + } + + if ( foundXMP && (0 <= binValue) && (binValue <= 65535) ) { + xmp->DeleteProperty ( kXMP_NS_EXIF, "ISOSpeedRatings" ); // So namespace cleanup won't keep it. + foundExif = exif->GetTag ( kTIFF_ExifIFD, kTIFF_PhotographicSensitivity, &tagInfo ); + if ( ! foundExif ) { + exif->SetTag_Short ( kTIFF_ExifIFD, kTIFF_PhotographicSensitivity, (XMP_Uns16)binValue ); + } + } + + } else { // Exif version is 2.3 or newer, use the Exif 2.3 tags and properties. + + // Deal with the special cases for exifEX:PhotographicSensitivity and exif:ISOSpeedRatings. + + if ( ! xmp->DoesPropertyExist ( kXMP_NS_ExifEX, "PhotographicSensitivity" ) ) { + foundXMP = xmp->GetProperty ( kXMP_NS_EXIF, "ISOSpeedRatings", 0, &flags ); + if ( foundXMP && XMP_PropIsArray(flags) && + (xmp->CountArrayItems ( kXMP_NS_EXIF, "ISOSpeedRatings" ) > 0) ) { + xmp->GetArrayItem ( kXMP_NS_EXIF, "ISOSpeedRatings", 1, &xmpValue, 0 ); + xmp->SetProperty ( kXMP_NS_ExifEX, "PhotographicSensitivity", xmpValue.c_str() ); + } + } + + xmp->DeleteProperty ( kXMP_NS_EXIF, "ISOSpeedRatings" ); // Don't want it kept after namespace cleanup. + + foundXMP = xmp->GetProperty_Int ( kXMP_NS_ExifEX, "PhotographicSensitivity", &binValue, 0 ); + + if ( foundXMP && (0 <= binValue) && (binValue <= 65535) ) { // The simpler low ISO case. + + foundExif = exif->GetTag ( kTIFF_ExifIFD, kTIFF_PhotographicSensitivity, &tagInfo ); + if ( ! foundExif ) { + exif->SetTag_Short ( kTIFF_ExifIFD, kTIFF_PhotographicSensitivity, (XMP_Uns16)binValue ); + } + + } else if ( foundXMP ) { // The more commplex high ISO case. + + bool havePhotographicSensitivityTag = exif->GetTag ( kTIFF_ExifIFD, kTIFF_PhotographicSensitivity, 0 ); + bool haveSensitivityTypeTag = exif->GetTag ( kTIFF_ExifIFD, kTIFF_SensitivityType, 0 ); + bool haveISOSpeedTag = exif->GetTag ( kTIFF_ExifIFD, kTIFF_ISOSpeed, 0 ); + + bool haveSensitivityTypeXMP = xmp->DoesPropertyExist ( kXMP_NS_ExifEX, "SensitivityType" ); + bool haveISOSpeedXMP = xmp->DoesPropertyExist ( kXMP_NS_ExifEX, "ISOSpeed" ); + + if ( (! havePhotographicSensitivityTag) && (! haveSensitivityTypeTag) && (! haveISOSpeedTag) ) { + + exif->SetTag_Short ( kTIFF_ExifIFD, kTIFF_PhotographicSensitivity, 65535 ); + + if ( (! haveSensitivityTypeXMP) && (! haveISOSpeedXMP) ) { + xmp->SetProperty ( kXMP_NS_ExifEX, "SensitivityType", "3" ); + xmp->SetProperty_Int ( kXMP_NS_ExifEX, "ISOSpeed", binValue ); + } + + } + + } + + // Export SensitivityType and the related long tags. Must be done after the special + // cases because they might set exifEX:SensitivityType and exifEX:ISOSpeed. + + foundExif = exif->GetTag ( kTIFF_ExifIFD, kTIFF_SensitivityType, &tagInfo ); + if ( ! foundExif ) { + foundXMP = xmp->GetProperty_Int ( kXMP_NS_ExifEX, "SensitivityType", &binValue, 0 ); + if ( foundXMP && (0 <= binValue) && (binValue <= 65535) ) { + exif->SetTag_Short ( kTIFF_ExifIFD, kTIFF_SensitivityType, (XMP_Uns16)binValue ); + } + } + + foundExif = exif->GetTag ( kTIFF_ExifIFD, kTIFF_StandardOutputSensitivity, &tagInfo ); + if ( ! foundExif ) { + foundXMP = xmp->GetProperty_Int ( kXMP_NS_ExifEX, "StandardOutputSensitivity", &binValue, 0 ); + if ( foundXMP && (binValue >= 0) ) { + exif->SetTag_Long ( kTIFF_ExifIFD, kTIFF_StandardOutputSensitivity, (XMP_Uns32)binValue ); + } + } + + foundExif = exif->GetTag ( kTIFF_ExifIFD, kTIFF_RecommendedExposureIndex, &tagInfo ); + if ( ! foundExif ) { + foundXMP = xmp->GetProperty_Int ( kXMP_NS_ExifEX, "RecommendedExposureIndex", &binValue, 0 ); + if ( foundXMP && (binValue >= 0) ) { + exif->SetTag_Long ( kTIFF_ExifIFD, kTIFF_RecommendedExposureIndex, (XMP_Uns32)binValue ); + } + } + + foundExif = exif->GetTag ( kTIFF_ExifIFD, kTIFF_ISOSpeed, &tagInfo ); + if ( ! foundExif ) { + foundXMP = xmp->GetProperty_Int ( kXMP_NS_ExifEX, "ISOSpeed", &binValue, 0 ); + if ( foundXMP && (binValue >= 0) ) { + exif->SetTag_Long ( kTIFF_ExifIFD, kTIFF_ISOSpeed, (XMP_Uns32)binValue ); + } + } + + foundExif = exif->GetTag ( kTIFF_ExifIFD, kTIFF_ISOSpeedLatitudeyyy, &tagInfo ); + if ( ! foundExif ) { + foundXMP = xmp->GetProperty_Int ( kXMP_NS_ExifEX, "ISOSpeedLatitudeyyy", &binValue, 0 ); + if ( foundXMP && (binValue >= 0) ) { + exif->SetTag_Long ( kTIFF_ExifIFD, kTIFF_ISOSpeedLatitudeyyy, (XMP_Uns32)binValue ); + } + } + + foundExif = exif->GetTag ( kTIFF_ExifIFD, kTIFF_ISOSpeedLatitudezzz, &tagInfo ); + if ( ! foundExif ) { + foundXMP = xmp->GetProperty_Int ( kXMP_NS_ExifEX, "ISOSpeedLatitudezzz", &binValue, 0 ); + if ( foundXMP && (binValue >= 0) ) { + exif->SetTag_Long ( kTIFF_ExifIFD, kTIFF_ISOSpeedLatitudezzz, (XMP_Uns32)binValue ); + } + } + + } + + } catch ( ... ) { + + // Do nothing, don't let this failure stop other exports. + + } + +} // ExportTIFF_PhotographicSensitivity + +// ================================================================================================= +// ================================================================================================= + +// ================================================================================================= +// PhotoDataUtils::ExportExif +// ========================== + +void +PhotoDataUtils::ExportExif ( SXMPMeta * xmp, TIFF_Manager * exif ) +{ + bool haveXMP; + std::string xmpValue; + + XMP_Int32 int32; + XMP_Uns8 uns8; + + // --------------------------------------------------------- + // Read the old Adobe names for new Exif 2.3 tags if wanted. + + #if SupportOldExifProperties + + XMP_OptionBits flags; + + haveXMP = xmp->DoesPropertyExist ( kXMP_NS_ExifEX, "PhotographicSensitivity" ); + if ( ! haveXMP ) { + haveXMP = xmp->GetProperty ( kXMP_NS_EXIF, "ISOSpeedRatings", 0, &flags ); + if ( haveXMP && XMP_PropIsArray(flags) ) { + haveXMP = xmp->GetArrayItem ( kXMP_NS_EXIF, "ISOSpeedRatings", 1, &xmpValue, 0 ); + if ( haveXMP ) xmp->SetProperty ( kXMP_NS_ExifEX, "PhotographicSensitivity", xmpValue.c_str() ); + } + } + + haveXMP = xmp->DoesPropertyExist ( kXMP_NS_ExifEX, "CameraOwnerName" ); + if ( ! haveXMP ) { + haveXMP = xmp->GetProperty ( kXMP_NS_EXIF_Aux, "OwnerName", &xmpValue, 0 ); + if ( haveXMP ) xmp->SetProperty ( kXMP_NS_ExifEX, "CameraOwnerName", xmpValue.c_str() ); + } + + haveXMP = xmp->DoesPropertyExist ( kXMP_NS_ExifEX, "BodySerialNumber" ); + if ( ! haveXMP ) { + haveXMP = xmp->GetProperty ( kXMP_NS_EXIF_Aux, "SerialNumber", &xmpValue, 0 ); + if ( haveXMP ) xmp->SetProperty ( kXMP_NS_ExifEX, "BodySerialNumber", xmpValue.c_str() ); + } + + haveXMP = xmp->DoesPropertyExist ( kXMP_NS_ExifEX, "LensModel" ); + if ( ! haveXMP ) { + haveXMP = xmp->GetProperty ( kXMP_NS_EXIF_Aux, "Lens", &xmpValue, 0 ); + if ( haveXMP ) xmp->SetProperty ( kXMP_NS_ExifEX, "LensModel", xmpValue.c_str() ); + } + + haveXMP = xmp->DoesPropertyExist ( kXMP_NS_ExifEX, "LensSpecification" ); + if ( ! haveXMP ) { + haveXMP = xmp->GetProperty ( kXMP_NS_EXIF_Aux, "LensInfo", &xmpValue, 0 ); + if ( haveXMP ) { + size_t start, end; + std::string nextItem; + for ( start = 0; start < xmpValue.size(); start = end + 1 ) { + end = xmpValue.find ( ' ', start ); + if ( end == start ) continue; + if ( end == std::string::npos ) end = xmpValue.size(); + nextItem = xmpValue.substr ( start, (end-start) ); + xmp->AppendArrayItem ( kXMP_NS_ExifEX, "LensSpecification", kXMP_PropArrayIsOrdered, nextItem.c_str() ); + } + } + } + + #endif + + // Do all of the table driven standard exports. + + ExportTIFF_StandardMappings ( kTIFF_PrimaryIFD, exif, *xmp ); + ExportTIFF_StandardMappings ( kTIFF_ExifIFD, exif, *xmp ); + ExportTIFF_StandardMappings ( kTIFF_GPSInfoIFD, exif, *xmp ); + + // -------------------------------------------------------------------------------------------- + // Fixup erroneous cases that appear to have a negative value for GPSAltitude in the Exif. This + // treats any value with the high bit set as a negative, which is more likely in practice than + // an actual value over 2 billion. The XMP was exported by the tables and is left alone since it + // won't be kept in the file. + + TIFF_Manager::Rational altValue; + bool haveExif = exif->GetTag_Rational ( kTIFF_GPSInfoIFD, kTIFF_GPSAltitude, &altValue ); + if ( haveExif ) { + + bool fixExif = false; + + if ( altValue.denom >> 31 ) { // Shift the sign to the numerator. + altValue.denom = -altValue.denom; + altValue.num = -altValue.num; + fixExif = true; + } + + if ( altValue.num >> 31 ) { // Fix the numerator and set GPSAltitudeRef. + exif->SetTag_Byte ( kTIFF_GPSInfoIFD, kTIFF_GPSAltitudeRef, 1 ); + altValue.num = -altValue.num; + fixExif = true; + } + + if ( fixExif ) exif->SetTag_Rational ( kTIFF_GPSInfoIFD, kTIFF_GPSAltitude, altValue.num, altValue.denom ); + + } + + // Export dc:description to TIFF ImageDescription, and exif:UserComment to EXIF UserComment. + + // *** This is not following the MWG guidelines. The policy here tries to be more backward compatible. + + ExportTIFF_LocTextASCII ( *xmp, kXMP_NS_DC, "description", + exif, kTIFF_PrimaryIFD, kTIFF_ImageDescription ); + + ExportTIFF_EncodedString ( *xmp, kXMP_NS_EXIF, "UserComment", + exif, kTIFF_ExifIFD, kTIFF_UserComment, true /* isLangAlt */ ); + + // Export all of the date/time tags. + // ! Special case: Don't create Exif DateTimeDigitized. This can avoid PSD full rewrite due to + // ! new mapping from xmp:CreateDate. + + if ( exif->GetTag ( kTIFF_ExifIFD, kTIFF_DateTimeDigitized, 0 ) ) { + ExportTIFF_Date ( *xmp, kXMP_NS_XMP, "CreateDate", exif, kTIFF_DateTimeDigitized ); + } + + ExportTIFF_Date ( *xmp, kXMP_NS_EXIF, "DateTimeOriginal", exif, kTIFF_DateTimeOriginal ); + ExportTIFF_Date ( *xmp, kXMP_NS_XMP, "ModifyDate", exif, kTIFF_DateTime ); + + // Export the remaining TIFF, Exif, and GPS IFD tags. + + ExportTIFF_ArrayASCII ( *xmp, kXMP_NS_DC, "creator", exif, kTIFF_PrimaryIFD, kTIFF_Artist ); + + ExportTIFF_LocTextASCII ( *xmp, kXMP_NS_DC, "rights", exif, kTIFF_PrimaryIFD, kTIFF_Copyright ); + + haveXMP = xmp->GetProperty ( kXMP_NS_EXIF, "ExifVersion", &xmpValue, 0 ); + if ( haveXMP && (xmpValue.size() == 4) && (! exif->GetTag ( kTIFF_ExifIFD, kTIFF_ExifVersion, 0 )) ) { + // 36864 ExifVersion is 4 "undefined" ASCII characters. + exif->SetTag ( kTIFF_ExifIFD, kTIFF_ExifVersion, kTIFF_UndefinedType, 4, xmpValue.data() ); + } + + // There are moderately complex export special cases for PhotographicSensitivity. + ExportTIFF_PhotographicSensitivity ( xmp, exif ); + + haveXMP = xmp->DoesPropertyExist ( kXMP_NS_EXIF, "ComponentsConfiguration" ); + if ( haveXMP && (xmp->CountArrayItems ( kXMP_NS_EXIF, "ComponentsConfiguration" ) == 4) && + (! exif->GetTag ( kTIFF_ExifIFD, kTIFF_ComponentsConfiguration, 0 )) ) { + // 37121 ComponentsConfiguration is an array of 4 "undefined" UInt8 bytes. + XMP_Uns8 compConfig[4]; + xmp->GetProperty_Int ( kXMP_NS_EXIF, "ComponentsConfiguration[1]", &int32, 0 ); + compConfig[0] = (XMP_Uns8)int32; + xmp->GetProperty_Int ( kXMP_NS_EXIF, "ComponentsConfiguration[2]", &int32, 0 ); + compConfig[1] = (XMP_Uns8)int32; + xmp->GetProperty_Int ( kXMP_NS_EXIF, "ComponentsConfiguration[3]", &int32, 0 ); + compConfig[2] = (XMP_Uns8)int32; + xmp->GetProperty_Int ( kXMP_NS_EXIF, "ComponentsConfiguration[4]", &int32, 0 ); + compConfig[3] = (XMP_Uns8)int32; + exif->SetTag ( kTIFF_ExifIFD, kTIFF_ComponentsConfiguration, kTIFF_UndefinedType, 4, &compConfig[0] ); + } + + haveXMP = xmp->DoesPropertyExist ( kXMP_NS_EXIF, "Flash" ); + if ( haveXMP && (! exif->GetTag ( kTIFF_ExifIFD, kTIFF_Flash, 0 )) ) { + // 37385 Flash is a UInt16 collection of bit fields and is mapped to a struct in XMP. + XMP_Uns16 binFlash = 0; + bool field; + haveXMP = xmp->GetProperty_Bool ( kXMP_NS_EXIF, "Flash/exif:Fired", &field, 0 ); + if ( haveXMP & field ) binFlash |= 0x0001; + haveXMP = xmp->GetProperty_Int ( kXMP_NS_EXIF, "Flash/exif:Return", &int32, 0 ); + if ( haveXMP ) binFlash |= (int32 & 3) << 1; + haveXMP = xmp->GetProperty_Int ( kXMP_NS_EXIF, "Flash/exif:Mode", &int32, 0 ); + if ( haveXMP ) binFlash |= (int32 & 3) << 3; + haveXMP = xmp->GetProperty_Bool ( kXMP_NS_EXIF, "Flash/exif:Function", &field, 0 ); + if ( haveXMP & field ) binFlash |= 0x0020; + haveXMP = xmp->GetProperty_Bool ( kXMP_NS_EXIF, "Flash/exif:RedEyeMode", &field, 0 ); + if ( haveXMP & field ) binFlash |= 0x0040; + exif->SetTag_Short ( kTIFF_ExifIFD, kTIFF_Flash, binFlash ); + } + + haveXMP = xmp->GetProperty_Int ( kXMP_NS_EXIF, "FileSource", &int32, 0 ); + if ( haveXMP && (! exif->GetTag ( kTIFF_ExifIFD, kTIFF_FileSource, 0 )) ) { + // 41728 FileSource is an "undefined" UInt8. + uns8 = (XMP_Uns8)int32; + exif->SetTag ( kTIFF_ExifIFD, kTIFF_FileSource, kTIFF_UndefinedType, 1, &uns8 ); + } + + haveXMP = xmp->GetProperty_Int ( kXMP_NS_EXIF, "SceneType", &int32, 0 ); + if ( haveXMP && (! exif->GetTag ( kTIFF_ExifIFD, kTIFF_SceneType, 0 )) ) { + // 41729 SceneType is an "undefined" UInt8. + uns8 = (XMP_Uns8)int32; + exif->SetTag ( kTIFF_ExifIFD, kTIFF_SceneType, kTIFF_UndefinedType, 1, &uns8 ); + } + + // *** Deferred inject-only tags: SpatialFrequencyResponse, DeviceSettingDescription, CFAPattern + + haveXMP = xmp->GetProperty ( kXMP_NS_EXIF, "GPSVersionID", &xmpValue, 0 ); // This is inject-only. + if ( haveXMP && (! exif->GetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSVersionID, 0 )) ) { + XMP_Uns8 gpsID[4]; // 0 GPSVersionID is 4 UInt8 bytes and mapped in XMP as "n.n.n.n". + unsigned int fields[4]; // ! Need ints for output from sscanf. + int count = sscanf ( xmpValue.c_str(), "%u.%u.%u.%u", &fields[0], &fields[1], &fields[2], &fields[3] ); + if ( (count == 4) && (fields[0] <= 255) && (fields[1] <= 255) && (fields[2] <= 255) && (fields[3] <= 255) ) { + gpsID[0] = fields[0]; gpsID[1] = fields[1]; gpsID[2] = fields[2]; gpsID[3] = fields[3]; + exif->SetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSVersionID, kTIFF_ByteType, 4, &gpsID[0] ); + } + } + + ExportTIFF_GPSCoordinate ( *xmp, kXMP_NS_EXIF, "GPSLatitude", exif, kTIFF_GPSInfoIFD, kTIFF_GPSLatitude ); + + ExportTIFF_GPSCoordinate ( *xmp, kXMP_NS_EXIF, "GPSLongitude", exif, kTIFF_GPSInfoIFD, kTIFF_GPSLongitude ); + + ExportTIFF_GPSTimeStamp ( *xmp, kXMP_NS_EXIF, "GPSTimeStamp", exif ); + + // The following GPS tags are inject-only. + + haveXMP = xmp->DoesPropertyExist ( kXMP_NS_EXIF, "GPSDestLatitude" ); + if ( haveXMP && (! exif->GetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSDestLatitude, 0 )) ) { + ExportTIFF_GPSCoordinate ( *xmp, kXMP_NS_EXIF, "GPSDestLatitude", exif, kTIFF_GPSInfoIFD, kTIFF_GPSDestLatitude ); + } + + haveXMP = xmp->DoesPropertyExist ( kXMP_NS_EXIF, "GPSDestLongitude" ); + if ( haveXMP && (! exif->GetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSDestLongitude, 0 )) ) { + ExportTIFF_GPSCoordinate ( *xmp, kXMP_NS_EXIF, "GPSDestLongitude", exif, kTIFF_GPSInfoIFD, kTIFF_GPSDestLongitude ); + } + + haveXMP = xmp->GetProperty ( kXMP_NS_EXIF, "GPSProcessingMethod", &xmpValue, 0 ); + if ( haveXMP && (! xmpValue.empty()) && (! exif->GetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSProcessingMethod, 0 )) ) { + // 27 GPSProcessingMethod is a string with explicit encoding. + ExportTIFF_EncodedString ( *xmp, kXMP_NS_EXIF, "GPSProcessingMethod", exif, kTIFF_GPSInfoIFD, kTIFF_GPSProcessingMethod ); + } + + haveXMP = xmp->GetProperty ( kXMP_NS_EXIF, "GPSAreaInformation", &xmpValue, 0 ); + if ( haveXMP && (! xmpValue.empty()) && (! exif->GetTag ( kTIFF_GPSInfoIFD, kTIFF_GPSAreaInformation, 0 )) ) { + // 28 GPSAreaInformation is a string with explicit encoding. + ExportTIFF_EncodedString ( *xmp, kXMP_NS_EXIF, "GPSAreaInformation", exif, kTIFF_GPSInfoIFD, kTIFF_GPSAreaInformation ); + } + +} // PhotoDataUtils::ExportExif diff --git a/XMPFiles/source/FormatSupport/Reconcile_Impl.cpp b/XMPFiles/source/FormatSupport/Reconcile_Impl.cpp new file mode 100644 index 0000000..9dcef3d --- /dev/null +++ b/XMPFiles/source/FormatSupport/Reconcile_Impl.cpp @@ -0,0 +1,431 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp" +#include "source/UnicodeConversions.hpp" +#include "source/XIO.hpp" + +#if XMP_WinBuild +#elif XMP_MacBuild + #include <CoreServices/CoreServices.h> +#endif + +// ================================================================================================= +/// \file Reconcile_Impl.cpp +/// \brief Implementation utilities for the photo metadata reconciliation support. +/// +// ================================================================================================= + +// ================================================================================================= +// ReconcileUtils::IsASCII +// ======================= +// +// See if a string is 7 bit ASCII. + +bool ReconcileUtils::IsASCII ( const void * textPtr, size_t textLen ) +{ + + for ( const XMP_Uns8 * textPos = (XMP_Uns8*)textPtr; textLen > 0; --textLen, ++textPos ) { + if ( *textPos >= 0x80 ) return false; + } + + return true; + +} // ReconcileUtils::IsASCII + +// ================================================================================================= +// ReconcileUtils::IsUTF8 +// ====================== +// +// See if a string contains valid UTF-8. Allow nul bytes, they can appear inside of multi-part Exif +// strings. We don't use CodePoint_from_UTF8_Multi in UnicodeConversions because it throws an +// exception for non-Unicode and we don't need to actually compute the code points. + +bool ReconcileUtils::IsUTF8 ( const void * textPtr, size_t textLen ) +{ + const XMP_Uns8 * textPos = (XMP_Uns8*)textPtr; + const XMP_Uns8 * textEnd = textPos + textLen; + + while ( textPos < textEnd ) { + + if ( *textPos < 0x80 ) { + + ++textPos; // ASCII is UTF-8, tolerate nuls. + + } else { + + // ------------------------------------------------------------------------------------- + // We've got a multibyte UTF-8 character. The first byte has the number of bytes as the + // number of high order 1 bits. The remaining bytes must have 1 and 0 as the top 2 bits. + + #if 0 // *** This might be a more effcient way to count the bytes. + static XMP_Uns8 kByteCounts[16] = { 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 2, 3, 4 }; + size_t bytesNeeded = kByteCounts [ *textPos >> 4 ]; + if ( (bytesNeeded < 2) || ((bytesNeeded == 4) && ((*textPos & 0x08) != 0)) ) return false; + if ( (textPos + bytesNeeded) > textEnd ) return false; + #endif + + size_t bytesNeeded = 0; // Count the high order 1 bits in the first byte. + for ( XMP_Uns8 temp = *textPos; temp > 0x7F; temp = temp << 1 ) ++bytesNeeded; + // *** Consider CPU-specific assembly inline, e.g. cntlzw on PowerPC. + + if ( (bytesNeeded < 2) || (bytesNeeded > 4) || ((textPos+bytesNeeded) > textEnd) ) return false; + + for ( --bytesNeeded, ++textPos; bytesNeeded > 0; --bytesNeeded, ++textPos ) { + if ( (*textPos >> 6) != 2 ) return false; + } + + } + + } + + return true; // ! Returns true for empty strings. + +} // ReconcileUtils::IsUTF8 + +// ================================================================================================= +// UTF8ToHostEncoding +// ================== + +#if XMP_WinBuild + + void ReconcileUtils::UTF8ToWinEncoding ( UINT codePage, const XMP_Uns8 * utf8Ptr, size_t utf8Len, std::string * host ) + { + + std::string utf16; // WideCharToMultiByte wants native UTF-16. + ToUTF16Native ( (UTF8Unit*)utf8Ptr, utf8Len, &utf16 ); + + LPCWSTR utf16Ptr = (LPCWSTR) utf16.c_str(); + size_t utf16Len = utf16.size() / 2; + + int hostLen = WideCharToMultiByte ( codePage, 0, utf16Ptr, (int)utf16Len, 0, 0, 0, 0 ); + host->assign ( hostLen, ' ' ); // Allocate space for the results. + + (void) WideCharToMultiByte ( codePage, 0, utf16Ptr, (int)utf16Len, (LPSTR)host->data(), hostLen, 0, 0 ); + XMP_Assert ( hostLen == host->size() ); + + } // UTF8ToWinEncoding + +#elif XMP_MacBuild + + void ReconcileUtils::UTF8ToMacEncoding ( XMP_Uns16 macScript, XMP_Uns16 macLang, const XMP_Uns8 * utf8Ptr, size_t utf8Len, std::string * host ) + { + OSStatus err; + + TextEncoding destEncoding; + if ( macLang == langUnspecified ) macLang = kTextLanguageDontCare; + err = UpgradeScriptInfoToTextEncoding ( macScript, macLang, kTextRegionDontCare, 0, &destEncoding ); + if ( err != noErr ) XMP_Throw ( "UpgradeScriptInfoToTextEncoding failed", kXMPErr_ExternalFailure ); + + UnicodeMapping mappingInfo; + mappingInfo.mappingVersion = kUnicodeUseLatestMapping; + mappingInfo.otherEncoding = GetTextEncodingBase ( destEncoding ); + mappingInfo.unicodeEncoding = CreateTextEncoding ( kTextEncodingUnicodeDefault, + kUnicodeNoSubset, kUnicodeUTF8Format ); + + UnicodeToTextInfo converterInfo; + err = CreateUnicodeToTextInfo ( &mappingInfo, &converterInfo ); + if ( err != noErr ) XMP_Throw ( "CreateUnicodeToTextInfo failed", kXMPErr_ExternalFailure ); + + try { // ! Need to call DisposeUnicodeToTextInfo before exiting. + + OptionBits convFlags = kUnicodeUseFallbacksMask | + kUnicodeLooseMappingsMask | kUnicodeDefaultDirectionMask; + ByteCount bytesRead, bytesWritten; + + enum { kBufferLen = 1000 }; // Ought to be enough in practice, without using too much stack. + char buffer [kBufferLen]; + + host->reserve ( utf8Len ); // As good a guess as any. + + while ( utf8Len > 0 ) { + // Ignore all errors from ConvertFromUnicodeToText. It returns info like "output + // buffer full" or "use substitution" as errors. + err = ConvertFromUnicodeToText ( converterInfo, utf8Len, (UniChar*)utf8Ptr, convFlags, + 0, 0, 0, 0, kBufferLen, &bytesRead, &bytesWritten, buffer ); + if ( bytesRead == 0 ) break; // Make sure forward progress happens. + host->append ( &buffer[0], bytesWritten ); + utf8Ptr += bytesRead; + utf8Len -= bytesRead; + } + + DisposeUnicodeToTextInfo ( &converterInfo ); + + } catch ( ... ) { + + DisposeUnicodeToTextInfo ( &converterInfo ); + throw; + + } + + } // UTF8ToMacEncoding + +#elif XMP_UNIXBuild + + // ! Does not exist, must not be called, for Generic UNIX builds. + +#endif + +// ================================================================================================= +// ReconcileUtils::UTF8ToLocal +// =========================== + +void ReconcileUtils::UTF8ToLocal ( const void * _utf8Ptr, size_t utf8Len, std::string * local ) +{ + const XMP_Uns8* utf8Ptr = (XMP_Uns8*)_utf8Ptr; + + local->erase(); + + if ( ReconcileUtils::IsASCII ( utf8Ptr, utf8Len ) ) { + local->assign ( (const char *)utf8Ptr, utf8Len ); + return; + } + + #if XMP_WinBuild + + UTF8ToWinEncoding ( CP_ACP, utf8Ptr, utf8Len, local ); + + #elif XMP_MacBuild + + UTF8ToMacEncoding ( smSystemScript, kTextLanguageDontCare, utf8Ptr, utf8Len, local ); + + #elif XMP_UNIXBuild + + XMP_Throw ( "Generic UNIX does not have conversions between local and Unicode", kXMPErr_Unavailable ); + + #endif + +} // ReconcileUtils::UTF8ToLocal + +// ================================================================================================= +// ReconcileUtils::UTF8ToLatin1 +// ============================ + +void ReconcileUtils::UTF8ToLatin1 ( const void * _utf8Ptr, size_t utf8Len, std::string * latin1 ) +{ + const XMP_Uns8* utf8Ptr = (XMP_Uns8*)_utf8Ptr; + const XMP_Uns8* utf8End = utf8Ptr + utf8Len; + + latin1->erase(); + latin1->reserve ( utf8Len ); // As good a guess as any, at least enough, exact for ASCII. + + bool inBadRun = false; + + while ( utf8Ptr < utf8End ) { + + if ( *utf8Ptr <= 0x7F ) { + + (*latin1) += (char)*utf8Ptr; // Have an ASCII character. + inBadRun = false; + ++utf8Ptr; + + } else if ( utf8Ptr == (utf8End - 1) ) { + + inBadRun = false; + ++utf8Ptr; // Ignore a bad end to the UTF-8. + + } else { + + XMP_Assert ( (utf8End - utf8Ptr) >= 2 ); + XMP_Uns16 ch16 = GetUns16BE ( utf8Ptr ); // A Latin-1 80..FF is 2 UTF-8 bytes. + + if ( (0xC280 <= ch16) && (ch16 <= 0xC2BF) ) { + + (*latin1) += (char)(ch16 & 0xFF); // UTF-8 C280..C2BF are Latin-1 80..BF. + inBadRun = false; + utf8Ptr += 2; + + } else if ( (0xC380 <= ch16) && (ch16 <= 0xC3BF) ) { + + (*latin1) += (char)((ch16 & 0xFF) + 0x40); // UTF-8 C380..C3BF are Latin-1 C0..FF. + inBadRun = false; + utf8Ptr += 2; + + } else { + + if ( ! inBadRun ) { + inBadRun = true; + (*latin1) += "(?)"; // Mark the run of out of scope UTF-8. + } + + ++utf8Ptr; // Skip the presumably well-formed UTF-8 character. + while ( (utf8Ptr < utf8End) && ((*utf8Ptr & 0xC0) == 0x80) ) ++utf8Ptr; + + } + + } + + } + + XMP_Assert ( utf8Ptr == utf8End ); + +} // ReconcileUtils::UTF8ToLatin1 + +// ================================================================================================= +// HostEncodingToUTF8 +// ================== + +#if XMP_WinBuild + + void ReconcileUtils::WinEncodingToUTF8 ( UINT codePage, const XMP_Uns8 * hostPtr, size_t hostLen, std::string * utf8 ) + { + + int utf16Len = MultiByteToWideChar ( codePage, 0, (LPCSTR)hostPtr, (int)hostLen, 0, 0 ); + std::vector<UTF16Unit> utf16 ( utf16Len, 0 ); // MultiByteToWideChar returns native UTF-16. + + (void) MultiByteToWideChar ( codePage, 0, (LPCSTR)hostPtr, (int)hostLen, (LPWSTR)&utf16[0], utf16Len ); + FromUTF16Native ( &utf16[0], (int)utf16Len, utf8 ); + + } // WinEncodingToUTF8 + +#elif XMP_MacBuild + + void ReconcileUtils::MacEncodingToUTF8 ( XMP_Uns16 macScript, XMP_Uns16 macLang, const XMP_Uns8 * hostPtr, size_t hostLen, std::string * utf8 ) + { + OSStatus err; + + TextEncoding srcEncoding; + if ( macLang == langUnspecified ) macLang = kTextLanguageDontCare; + err = UpgradeScriptInfoToTextEncoding ( macScript, macLang, kTextRegionDontCare, 0, &srcEncoding ); + if ( err != noErr ) XMP_Throw ( "UpgradeScriptInfoToTextEncoding failed", kXMPErr_ExternalFailure ); + + UnicodeMapping mappingInfo; + mappingInfo.mappingVersion = kUnicodeUseLatestMapping; + mappingInfo.otherEncoding = GetTextEncodingBase ( srcEncoding ); + mappingInfo.unicodeEncoding = CreateTextEncoding ( kTextEncodingUnicodeDefault, + kUnicodeNoSubset, kUnicodeUTF8Format ); + + TextToUnicodeInfo converterInfo; + err = CreateTextToUnicodeInfo ( &mappingInfo, &converterInfo ); + if ( err != noErr ) XMP_Throw ( "CreateTextToUnicodeInfo failed", kXMPErr_ExternalFailure ); + + try { // ! Need to call DisposeTextToUnicodeInfo before exiting. + + ByteCount bytesRead, bytesWritten; + + enum { kBufferLen = 1000 }; // Ought to be enough in practice, without using too much stack. + char buffer [kBufferLen]; + + utf8->reserve ( hostLen ); // As good a guess as any. + + while ( hostLen > 0 ) { + // Ignore all errors from ConvertFromTextToUnicode. It returns info like "output + // buffer full" or "use substitution" as errors. + err = ConvertFromTextToUnicode ( converterInfo, hostLen, hostPtr, kNilOptions, + 0, 0, 0, 0, kBufferLen, &bytesRead, &bytesWritten, (UniChar*)buffer ); + if ( bytesRead == 0 ) break; // Make sure forward progress happens. + utf8->append ( &buffer[0], bytesWritten ); + hostPtr += bytesRead; + hostLen -= bytesRead; + } + + DisposeTextToUnicodeInfo ( &converterInfo ); + + } catch ( ... ) { + + DisposeTextToUnicodeInfo ( &converterInfo ); + throw; + + } + + } // MacEncodingToUTF8 + +#elif XMP_UNIXBuild + + // ! Does not exist, must not be called, for Generic UNIX builds. + +#endif + +// ================================================================================================= +// ReconcileUtils::LocalToUTF8 +// =========================== + +void ReconcileUtils::LocalToUTF8 ( const void * _localPtr, size_t localLen, std::string * utf8 ) +{ + const XMP_Uns8* localPtr = (XMP_Uns8*)_localPtr; + + utf8->erase(); + + if ( ReconcileUtils::IsASCII ( localPtr, localLen ) ) { + utf8->assign ( (const char *)localPtr, localLen ); + return; + } + + #if XMP_WinBuild + + WinEncodingToUTF8 ( CP_ACP, localPtr, localLen, utf8 ); + + #elif XMP_MacBuild + + MacEncodingToUTF8 ( smSystemScript, kTextLanguageDontCare, localPtr, localLen, utf8 ); + + #elif XMP_UNIXBuild + + XMP_Throw ( "Generic UNIX does not have conversions between local and Unicode", kXMPErr_Unavailable ); + + #endif + +} // ReconcileUtils::LocalToUTF8 + +// ================================================================================================= +// ReconcileUtils::Latin1ToUTF8 +// ============================ + +void ReconcileUtils::Latin1ToUTF8 ( const void * _latin1Ptr, size_t latin1Len, std::string * utf8 ) +{ + const XMP_Uns8* latin1Ptr = (XMP_Uns8*)_latin1Ptr; + const XMP_Uns8* latin1End = latin1Ptr + latin1Len; + + utf8->erase(); + utf8->reserve ( latin1Len ); // As good a guess as any, exact for ASCII. + + for ( ; latin1Ptr < latin1End; ++latin1Ptr ) { + + XMP_Uns8 ch8 = *latin1Ptr; + + if ( ch8 <= 0x7F ) { + (*utf8) += (char)ch8; // Have an ASCII character. + } else if ( ch8 <= 0xBF ) { + (*utf8) += 0xC2; // Latin-1 80..BF are UTF-8 C280..C2BF. + (*utf8) += (char)ch8; + } else { + (*utf8) += 0xC3; // Latin-1 C0..FF are UTF-8 C380..C3BF. + (*utf8) += (char)(ch8 - 0x40); + } + + } + +} // ReconcileUtils::Latin1ToUTF8 + + +// ================================================================================================= +// ReconcileUtils::NativeToUTF8 +// ============================ + +void ReconcileUtils::NativeToUTF8( const std::string & input, std::string & output ) +{ + output.erase(); + // IF it is not UTF-8 + if( ! ReconcileUtils::IsUTF8( input.c_str(), input.length() ) ) + { + // And ServerMode is not active + if( ! ignoreLocalText ) + { + // Convert it to UTF-8 + ReconcileUtils::LocalToUTF8( input.c_str(), input.length(), &output ); + } + } + else // If it is already UTF-8 + { + output = input; + } +} // ReconcileUtils::NativeToUTF8 diff --git a/XMPFiles/source/FormatSupport/Reconcile_Impl.hpp b/XMPFiles/source/FormatSupport/Reconcile_Impl.hpp new file mode 100644 index 0000000..ea5d407 --- /dev/null +++ b/XMPFiles/source/FormatSupport/Reconcile_Impl.hpp @@ -0,0 +1,104 @@ +#ifndef __Reconcile_Impl_hpp__ +#define __Reconcile_Impl_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. + +#include "XMPFiles/source/FormatSupport/ReconcileLegacy.hpp" +#include "third-party/zuid/interfaces/MD5.h" + +// ================================================================================================= +/// \file Reconcile_Impl.hpp +/// \brief Implementation utilities for the legacy metadata reconciliation support. +/// +// ================================================================================================= + +typedef XMP_Uns8 MD5_Digest[16]; // ! Should be in MD5.h. + +enum { + kDigestMissing = -1, // A partial import is done, existing XMP is left alone. + kDigestDiffers = 0, // A full import is done, existing XMP is deleted or replaced. + kDigestMatches = +1 // No importing is done. +}; + +namespace ReconcileUtils { + + // *** These ought to be with the Unicode conversions. + + static const char * kHexDigits = "0123456789ABCDEF"; + + bool IsASCII ( const void * _textPtr, size_t textLen ); + bool IsUTF8 ( const void * _textPtr, size_t textLen ); + + void UTF8ToLocal ( const void * _utf8Ptr, size_t utf8Len, std::string * local ); + void UTF8ToLatin1 ( const void * _utf8Ptr, size_t utf8Len, std::string * latin1 ); + void LocalToUTF8 ( const void * _localPtr, size_t localLen, std::string * utf8 ); + void Latin1ToUTF8 ( const void * _latin1Ptr, size_t latin1Len, std::string * utf8 ); + + // + // Checks if the input string is UTF-8 encoded. If not, it tries to convert it to UTF-8 + // This is only done, if Server Mode is not active! + // @param input the native input string + // @return The input if it is already UTF-8, the converted input + // or an empty string if no conversion is possible because of ServerMode + // + void NativeToUTF8 ( const std::string & input, std::string & output ); + + #if XMP_WinBuild + void UTF8ToWinEncoding ( UINT codePage, const XMP_Uns8 * utf8Ptr, size_t utf8Len, std::string * host ); + void WinEncodingToUTF8 ( UINT codePage, const XMP_Uns8 * hostPtr, size_t hostLen, std::string * utf8 ); + #elif XMP_MacBuild + void UTF8ToMacEncoding ( XMP_Uns16 macScript, XMP_Uns16 macLang, const XMP_Uns8 * utf8Ptr, size_t utf8Len, std::string * host ); + void MacEncodingToUTF8 ( XMP_Uns16 macScript, XMP_Uns16 macLang, const XMP_Uns8 * hostPtr, size_t hostLen, std::string * utf8 ); + #endif + +}; // ReconcileUtils + +namespace PhotoDataUtils { + + int CheckIPTCDigest ( const void * newPtr, const XMP_Uns32 newLen, const void * oldDigest ); + void SetIPTCDigest ( void * iptcPtr, XMP_Uns32 iptcLen, PSIR_Manager * psir ); + + bool GetNativeInfo ( const TIFF_Manager & exif, XMP_Uns8 ifd, XMP_Uns16 id, TIFF_Manager::TagInfo * info ); + size_t GetNativeInfo ( const IPTC_Manager & iptc, XMP_Uns8 id, int digestState, + bool haveXMP, IPTC_Manager::DataSetInfo * info ); + + bool IsValueDifferent ( const TIFF_Manager::TagInfo & exifInfo, + const std::string & xmpValue, std::string * exifValue ); + bool IsValueDifferent ( const IPTC_Manager & newIPTC, const IPTC_Manager & oldIPTC, XMP_Uns8 id ); + + void ImportPSIR ( const PSIR_Manager & psir, SXMPMeta * xmp, int iptcDigestState ); + + void Import2WayIPTC ( const IPTC_Manager & iptc, SXMPMeta * xmp, int iptcDigestState ); + void Import2WayExif ( const TIFF_Manager & exif, SXMPMeta * xmp, int iptcDigestState ); + + void Import3WayItems ( const TIFF_Manager & exif, const IPTC_Manager & iptc, SXMPMeta * xmp, int iptcDigestState ); + + void ExportPSIR ( const SXMPMeta & xmp, PSIR_Manager * psir ); + void ExportIPTC ( const SXMPMeta & xmp, IPTC_Manager * iptc ); + void ExportExif ( SXMPMeta * xmp, TIFF_Manager * exif ); + + // These need to be exposed for use in Import3WayItem: + + void ImportIPTC_Simple ( const IPTC_Manager & iptc, SXMPMeta * xmp, + XMP_Uns8 id, const char * xmpNS, const char * xmpProp ); + + void ImportIPTC_LangAlt ( const IPTC_Manager & iptc, SXMPMeta * xmp, + XMP_Uns8 id, const char * xmpNS, const char * xmpProp ); + + void ImportIPTC_Array ( const IPTC_Manager & iptc, SXMPMeta * xmp, + XMP_Uns8 id, const char * xmpNS, const char * xmpProp ); + + void ImportIPTC_Date ( XMP_Uns8 dateID, const IPTC_Manager & iptc, SXMPMeta * xmp ); + +}; // PhotoDataUtils + +#endif // __Reconcile_Impl_hpp__ diff --git a/XMPFiles/source/FormatSupport/SWF_Support.cpp b/XMPFiles/source/FormatSupport/SWF_Support.cpp new file mode 100644 index 0000000..ec7b9a6 --- /dev/null +++ b/XMPFiles/source/FormatSupport/SWF_Support.cpp @@ -0,0 +1,484 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2007 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FormatSupport/SWF_Support.hpp" + +#include "third-party/zlib/zlib.h" + +// ================================================================================================= + +XMP_Uns32 SWF_IO::FileHeaderSize ( XMP_Uns8 rectBits ) { + + // Return the full size of the SWF header, adding the fixed size to the variable RECT size. + + XMP_Uns8 bitsPerField = rectBits >> 3; + XMP_Uns32 rectBytes = ((5 + (4 * bitsPerField)) / 8) + 1; + + return SWF_IO::HeaderFixedSize + rectBytes; + +} // SWF_IO::FileHeaderSize + +// ================================================================================================= + +bool SWF_IO::GetTagInfo ( const RawDataBlock & swfStream, XMP_Uns32 tagOffset, SWF_IO::TagInfo * info ) { + + if ( tagOffset >= swfStream.size() ) return false; + XMP_Uns32 spaceLeft = swfStream.size() - tagOffset; + XMP_Uns8 headerSize = 2; + if ( spaceLeft < headerSize ) return false; // The minimum empty tag is a 2 byte header. + + XMP_Uns16 tagHeader = GetUns16LE ( &swfStream[tagOffset] ); + + info->tagID = tagHeader >> 6; + info->tagOffset = tagOffset; + info->contentLength = tagHeader & SWF_IO::TagLengthMask; + + if ( info->contentLength != SWF_IO::TagLengthMask ) { + info->hasLongHeader = false; + } else { + headerSize = 6; + if ( spaceLeft < headerSize ) return false; // Make sure there is room for the extended length. + info->contentLength = GetUns32LE ( &swfStream[tagOffset+2] ); + info->hasLongHeader = true; + } + + if ( (spaceLeft - headerSize) < info->contentLength ) return false; + + return true; + +} // SWF_IO::GetTagInfo + +// ================================================================================================= + +static inline XMP_Uns32 TagHeaderSize ( const SWF_IO::TagInfo & info ) { + + XMP_Uns8 headerSize = 2; + if ( info.hasLongHeader ) headerSize = 6; + return headerSize; + +} // TagHeaderSize + +// ================================================================================================= + +XMP_Uns32 SWF_IO::FullTagLength ( const SWF_IO::TagInfo & info ) { + + return TagHeaderSize ( info ) + info.contentLength; + +} // SWF_IO::FullTagLength + +// ================================================================================================= + +XMP_Uns32 SWF_IO::ContentOffset ( const SWF_IO::TagInfo & info ) { + + return info.tagOffset + TagHeaderSize ( info ); + +} // SWF_IO::ContentOffset + +// ================================================================================================= + +XMP_Uns32 SWF_IO::NextTagOffset ( const SWF_IO::TagInfo & info ) { + + return info.tagOffset + FullTagLength ( info ); + +} // SWF_IO::NextTagOffset + +// ================================================================================================= + +static inline void AppendData ( RawDataBlock * dataOut, XMP_Uns8 * buffer, size_t count ) { + + size_t prevSize = dataOut->size(); // ! Don't save a pointer, there might be a reallocation. + dataOut->insert ( dataOut->end(), count, 0 ); // Add space to the RawDataBlock. + memcpy ( &((*dataOut)[prevSize]), buffer, count ); + +} // AppendData + +// ================================================================================================= + +XMP_Int64 SWF_IO::DecompressFileToMemory ( XMP_IO * fileIn, RawDataBlock * dataOut ) { + + fileIn->Rewind(); + dataOut->clear(); + + static const size_t bufferSize = 64*1024; + XMP_Uns8 bufferIn [ bufferSize ]; + XMP_Uns8 bufferOut [ bufferSize ]; + + int err; + z_stream zipState; + memset ( &zipState, 0, sizeof(zipState) ); + err = inflateInit ( &zipState ); + XMP_Enforce ( err == Z_OK ); + + XMP_Int32 ioCount; + XMP_Int64 offsetIn; + const XMP_Int64 lengthIn = fileIn->Length(); + XMP_Enforce ( (SWF_IO::HeaderPrefixSize <= lengthIn) && (lengthIn <= SWF_IO::MaxExpandedSize) ); + + // Set the uncompressed part of the header. Save the expanded size from the file. + + fileIn->ReadAll ( bufferIn, SWF_IO::HeaderPrefixSize ); + offsetIn = SWF_IO::HeaderPrefixSize; + XMP_Uns32 expectedFullSize = GetUns32LE ( &bufferIn[4] ); + + AppendData ( dataOut, bufferIn, SWF_IO::HeaderPrefixSize ); // Copy the compressed stream's prefix. + PutUns32LE ( SWF_IO::ExpandedSignature, &(*dataOut)[0] ); // Change the signature. + (*dataOut)[3] = bufferIn[3]; // Keep the SWF version. + + // Read the input file, feed it to the decompression engine, writing as needed. + + zipState.next_out = &bufferOut[0]; // Initial output conditions. Must be set before the input loop! + zipState.avail_out = bufferSize; + + while ( offsetIn < lengthIn ) { + + // Read the next chunk of input. + ioCount = fileIn->Read ( bufferIn, bufferSize ); + XMP_Enforce ( ioCount > 0 ); + offsetIn += ioCount; + zipState.next_in = &bufferIn[0]; + zipState.avail_in = ioCount; + + // Process all of this input, writing as needed. + + err = Z_OK; + while ( (zipState.avail_in > 0) && (err == Z_OK) ) { + + XMP_Assert ( zipState.avail_out > 0 ); // Sanity check for output buffer space. + err = inflate ( &zipState, Z_NO_FLUSH ); + XMP_Enforce ( (err == Z_OK) || (err == Z_STREAM_END) ); + + if ( zipState.avail_out == 0 ) { + AppendData ( dataOut, bufferOut, bufferSize ); + zipState.next_out = &bufferOut[0]; + zipState.avail_out = bufferSize; + } + + } + + } + + // Finish the decompression and write the final output. + + do { + + ioCount = bufferSize - zipState.avail_out; // Make sure there is room for inflate to do more. + if ( ioCount > 0 ) { + AppendData ( dataOut, bufferOut, ioCount ); + zipState.next_out = &bufferOut[0]; + zipState.avail_out = bufferSize; + } + + err = inflate ( &zipState, Z_NO_FLUSH ); + XMP_Enforce ( (err == Z_OK) || (err == Z_STREAM_END) || (err == Z_BUF_ERROR) ); + + } while ( err == Z_OK ); + + ioCount = bufferSize - zipState.avail_out; // Write any final output. + if ( ioCount > 0 ) { + AppendData ( dataOut, bufferOut, ioCount ); + zipState.next_out = &bufferOut[0]; + zipState.avail_out = bufferSize; + } + + // Done. Make sure the file header has the true decompressed size. + + XMP_Int64 lengthOut = zipState.total_out + SWF_IO::HeaderPrefixSize; + if ( lengthOut != expectedFullSize ) PutUns32LE ( (XMP_Uns32)lengthOut, &((*dataOut)[4]) ); + inflateEnd ( &zipState ); + return lengthOut; + +} // SWF_IO::DecompressFileToMemory + +// ================================================================================================= + +XMP_Int64 SWF_IO::CompressMemoryToFile ( const RawDataBlock & dataIn, XMP_IO* fileOut ) { + + fileOut->Rewind(); + fileOut->Truncate ( 0 ); + + static const size_t bufferSize = 64*1024; + XMP_Uns8 bufferOut [ bufferSize ]; + + int err; + z_stream zipState; + memset ( &zipState, 0, sizeof(zipState) ); + err = deflateInit ( &zipState, Z_DEFAULT_COMPRESSION ); + XMP_Enforce ( err == Z_OK ); + + XMP_Int32 ioCount; + const size_t lengthIn = dataIn.size(); + XMP_Enforce ( SWF_IO::HeaderPrefixSize <= lengthIn ); + + // Write the uncompressed part of the file header. + + PutUns32LE ( SWF_IO::CompressedSignature, &bufferOut[0] ); + bufferOut[3] = dataIn[3]; // Copy the SWF version. + PutUns32LE ( lengthIn, &bufferOut[4] ); + fileOut->Write ( bufferOut, SWF_IO::HeaderPrefixSize ); + + // Feed the input to the compression engine in one step, write the output as available. + + zipState.next_in = (Bytef*)&dataIn[SWF_IO::HeaderPrefixSize]; + zipState.avail_in = lengthIn - SWF_IO::HeaderPrefixSize; + zipState.next_out = &bufferOut[0]; + zipState.avail_out = bufferSize; + + while ( zipState.avail_in > 0 ) { + + XMP_Assert ( zipState.avail_out > 0 ); // Sanity check for output buffer space. + err = deflate ( &zipState, Z_NO_FLUSH ); + XMP_Enforce ( err == Z_OK ); + + if ( zipState.avail_out == 0 ) { + fileOut->Write ( bufferOut, bufferSize ); + zipState.next_out = &bufferOut[0]; + zipState.avail_out = bufferSize; + } + + } + + // Finish the compression and write the final output. + + do { + + err = deflate ( &zipState, Z_FINISH ); + XMP_Enforce ( (err == Z_OK) || (err == Z_STREAM_END) ); + ioCount = bufferSize - zipState.avail_out; // See if there is output to write. + + if ( ioCount > 0 ) { + fileOut->Write ( bufferOut, ioCount ); + zipState.next_out = &bufferOut[0]; + zipState.avail_out = bufferSize; + } + + } while ( err != Z_STREAM_END ); + + // Done. + + XMP_Int64 lengthOut = zipState.total_out; + deflateEnd ( &zipState ); + return lengthOut; + +} // SWF_IO::CompressMemoryToFile + +// ================================================================================================= + +#if 0 // ! Not used, but save it for later transfer to a general ZIP utility file. + +XMP_Int64 SWF_IO::DecompressFileToFile ( XMP_IO * fileIn, XMP_IO * fileOut ) { + + fileIn->Rewind(); + fileOut->Rewind(); + fileOut->Truncate ( 0 ); + + static const size_t bufferSize = 64*1024; + XMP_Uns8 bufferIn [ bufferSize ]; + XMP_Uns8 bufferOut [ bufferSize ]; + + int err; + z_stream zipState; + memset ( zipState, 0, sizeof(zipState) ); + err = inflateInit ( &zipState ); + XMP_Enforce ( err == Z_OK ); + + XMP_Int32 ioCount; + XMP_Int64 offsetIn; + const XMP_Int64 lengthIn = fileIn->Length(); + XMP_Enforce ( (lengthIn >= SWF_IO::HeaderPrefixSize) && (lengthIn <= SWF_IO::MaxExpandedSize) ); + + // Copy the uncompressed part of the file header. Save the expanded size from the header. + + fileIn->ReadAll ( bufferIn, SWF_IO::HeaderPrefixSize ); + fileOut.Write ( bufferIn, SWF_IO::HeaderPrefixSize ); + offsetIn = SWF_IO::HeaderPrefixSize; + + XMP_Uns32 expectedFullSize = GetUns32LE ( &bufferIn[4] ); + + // Read the input file, feed it to the decompression engine, writing as needed. + + zipState.next_out = &bufferOut[0]; // Initial output conditions. Must be set before the input loop! + zipState.avail_out = bufferSize; + + while ( offsetIn < lengthIn ) { + + // Read the next chunk of input. + ioCount = fileIn->Read ( bufferIn, bufferSize ); + XMP_Enforce ( ioCount > 0 ); + offsetIn += ioCount; + zipState.next_in = &bufferIn[0]; + zipState.avail_in = ioCount; + + // Process all of this input, writing as needed. + + err = Z_OK; + while ( (zipState.avail_in > 0) && (err == Z_OK) ) { + + XMP_Assert ( zipState.avail_out > 0 ); // Sanity check for output buffer space. + err = inflate ( &zipState, Z_NO_FLUSH ); + XMP_Enforce ( (err == Z_OK) || (err == Z_STREAM_END) ); + + if ( zipState.avail_out == 0 ) { + fileOut->write ( bufferOut, bufferSize ); + zipState.next_out = &bufferOut[0]; + zipState.avail_out = bufferSize; + } + + } + + } + + // Finish the decompression and write the final output. + + do { + + ioCount = bufferSize - zipState.avail_out; // Make sure there is room for inflate. + if ( ioCount > 0 ) { + fileOut->write ( bufferOut, ioCount ); + zipState.next_out = &bufferOut[0]; + zipState.avail_out = bufferSize; + } + + err = inflate ( &zipState, Z_NO_FLUSH ); + XMP_Enforce ( (err == Z_OK) || (err == Z_STREAM_END) || (err == Z_BUF_ERROR) ); + ioCount = bufferSize - zipState.avail_out; // See if there is output to write. + if ( ioCount > 0 ) { + fileOut->write ( bufferOut, ioCount ); + zipState.next_out = &bufferOut[0]; + zipState.avail_out = bufferSize; + } + + } while ( err == Z_OK ); + + ioCount = bufferSize - zipState.avail_out; // Write any final output. + if ( ioCount > 0 ) { + fileOut->write ( bufferOut, ioCount ); + zipState.next_out = &bufferOut[0]; + zipState.avail_out = bufferSize; + } + + // Done. Make sure the file header has the true decompressed size. + + XMP_Int64 lengthOut = zipState.total_out; + + if ( lengthOut != expectedFullSize ) { + PutUns32LE ( &bufferOut[0], lengthOut ); + fileOut->Seek ( 4, kXMP_SeekFromStart ); + fileOut.Write ( &bufferOut[0], 4 ); + fileOut->Seek ( 0, kXMP_SeekFromEnd ); + } + + inflateEnd ( &zipState ); + return lengthOut; + +} // SWF_IO::DecompressFileToFile + +#endif + +// ================================================================================================= + +#if 0 // ! Not used, but save it for later transfer to a general ZIP utility file. + +XMP_Int64 SWF_IO::CompressFileToFile ( XMP_IO * fileIn, XMP_IO * fileOut ) { + + fileIn->Rewind(); + fileOut->Rewind(); + fileOut->Truncate ( 0 ); + + static const size_t bufferSize = 64*1024; + XMP_Uns8 bufferIn [ bufferSize ]; + XMP_Uns8 bufferOut [ bufferSize ]; + + int err; + z_stream zipState; + memset ( zipState, 0, sizeof(zipState) ); + err = deflateInit ( &zipState, Z_DEFAULT_COMPRESSION ); + XMP_Enforce ( err == Z_OK ); + + XMP_Int32 ioCount; + XMP_Int64 offsetIn; + const XMP_Int64 lengthIn = fileIn->Length(); + XMP_Enforce ( (lengthIn >= SWF_IO::HeaderPrefixSize) && (lengthIn <= SWF_IO::MaxExpandedSize) ); + + // Write the uncompressed part of the file header. + + fileIn->ReadAll ( bufferIn, SWF_IO::HeaderPrefixSize ); + offsetIn = SWF_IO::HeaderPrefixSize; + + PutUns32LE ( SWF_IO::CompressedSignature, &bufferOut[0] ); + bufferOut[3] = bufferIn[3]; // Copy the SWF version. + PutUns32LE ( &bufferOut[4], lengthIn ); + fileOut.Write ( bufferOut, SWF_IO::HeaderPrefixSize ); + + // Read the input file, feed it to the compression engine, writing as needed. + + zipState.next_out = &bufferOut[0]; // Initial output conditions. Must be set before the input loop! + zipState.avail_out = bufferSize; + + while ( offsetIn < lengthIn ) { + + // Read the next chunk of input. + ioCount = fileIn->Read ( bufferIn, bufferSize ); + XMP_Enforce ( ioCount > 0 ); + offsetIn += ioCount; + zipState.next_in = &bufferIn[0]; + zipState.avail_in = ioCount; + + // Process all of this input, writing as needed. Yes, we need a loop. Compression means less + // output than input, but a previous read has probably left partial compression results. + + while ( zipState.avail_in > 0 ) { + + XMP_Assert ( zipState.avail_out > 0 ); // Sanity check for output buffer space. + err = deflate ( &zipState, Z_NO_FLUSH ); + XMP_Enforce ( err == Z_OK ); + + if ( zipState.avail_out == 0 ) { + fileOut->write ( bufferOut, bufferSize ); + zipState.next_out = &bufferOut[0]; + zipState.avail_out = bufferSize; + } + + } + + } + + // Finish the compression and write the final output. + + do { + + err = deflate ( &zipState, Z_FINISH ); + XMP_Enforce ( (err == Z_OK) || (err == Z_STREAM_END) ); + ioCount = bufferSize - zipState.avail_out; // See if there is output to write. + + if ( ioCount > 0 ) { + fileOut->write ( bufferOut, ioCount ); + zipState.next_out = &bufferOut[0]; + zipState.avail_out = bufferSize; + } + + } while ( err != Z_STREAM_END ); + + // Done. + + XMP_Int64 lengthOut = zipState.total_out; + deflateEnd ( &zipState ); + return lengthOut; + +} // SWF_IO::CompressFileToFile + +#endif diff --git a/XMPFiles/source/FormatSupport/SWF_Support.hpp b/XMPFiles/source/FormatSupport/SWF_Support.hpp new file mode 100644 index 0000000..731d64e --- /dev/null +++ b/XMPFiles/source/FormatSupport/SWF_Support.hpp @@ -0,0 +1,86 @@ +#ifndef __SWF_Support_hpp__ +#define __SWF_Support_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2008 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +#include "third-party/zlib/zlib.h" + +// ================================================================================================= + +namespace SWF_IO { + + const XMP_Int64 MaxExpandedSize = 0xFFFFFFFFUL; // The file header has a UInt32 expanded size field. + + const size_t HeaderPrefixSize = 8; // The uncompressed first part of the file header. + const size_t HeaderFixedSize = 12; // The fixed size part of the file header, omits the RECT. + + const XMP_Uns32 CompressedSignature = 0x00535743; // The low 3 bytes are "SWC". + const XMP_Uns32 ExpandedSignature = 0x00535746; // The low 3 bytes are "SWF". + // Note: Can't use char* here, it causes duplicate symbols with xcode. + + const XMP_Uns16 FileAttributesTagID = 69; + const XMP_Uns16 MetadataTagID = 77; + + const XMP_Uns8 TagLengthMask = 0x3F; + const XMP_Uns8 HasMetadataMask = 0x10; + + // A SWF file begins with a variable length header. The header layout is: + // + // UInt8[3] - "FWS" for uncompressed SWF and "CWS" for compressed SWF + // UInt8 - SWF format version + // UInt32 - Length of uncompressed file, little endian + // RECT - packed bit RECT structure + // UInt16 - frame rate, little endian, really 8.8 fixed point + // UInt16 - frame count, little endian + // + // If the first 4 bytes are read as a little endian UInt32 they become "vSWC" and "vSWF", where + // the "v" byte is the version format version. + // + // SWF compression starts 8 bytes into the file, after the length field in the header. + // The length in the header is everything. If compressed this is 8 plus the decompressed size. + // + // Following the header is a sequence of tags. Each tag begins with a little endian UInt16 whose + // upper 10 bits are the tag ID and lower 6 bits are a length for the content. If this length is + // 63 (0x3F) then a little endian Int32 follows with the content length. + // + // The FileAttributes tag, #69, has a flag byte and 3 reserved bytes following the header. There + // is only 1 flag bit that we care about, HasMetadata with the mask 0x10. + // + // The Metadata tag, #77, has content that is the UTF-8 XMP, preferably as small as possible. + + XMP_Uns32 FileHeaderSize ( XMP_Uns8 rectBits ); + + class TagInfo { + public: + bool hasLongHeader; + XMP_Uns16 tagID; + XMP_Uns32 tagOffset, contentLength; + TagInfo() : hasLongHeader(false), tagID(0), tagOffset(0), contentLength(0) {}; + ~TagInfo() {}; + }; + + bool GetTagInfo ( const RawDataBlock & swfStream, XMP_Uns32 tagOffset, TagInfo * info ); + XMP_Uns32 FullTagLength ( const TagInfo & info ); + XMP_Uns32 ContentOffset ( const TagInfo & info ); + XMP_Uns32 NextTagOffset ( const TagInfo & info ); + + XMP_Int64 DecompressFileToMemory ( XMP_IO * fileIn, RawDataBlock * dataOut ); + XMP_Int64 CompressMemoryToFile ( const RawDataBlock & dataIn, XMP_IO* fileOut ); + +}; // SWF_IO + +#endif // __SWF_Support_hpp__ diff --git a/XMPFiles/source/FormatSupport/TIFF_FileWriter.cpp b/XMPFiles/source/FormatSupport/TIFF_FileWriter.cpp new file mode 100644 index 0000000..40cb9bd --- /dev/null +++ b/XMPFiles/source/FormatSupport/TIFF_FileWriter.cpp @@ -0,0 +1,1986 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/TIFF_Support.hpp" +#include "source/XIO.hpp" + +// ================================================================================================= +/// \file TIFF_FileWriter.cpp +/// \brief TIFF_FileWriter is used for memory-based read-write access and all file-based access. +/// +/// \c TIFF_FileWriter is used for memory-based read-write access and all file-based access. The +/// main internal data structure is the InternalTagMap, a std::map that uses the tag number as the +/// key and InternalTagInfo as the value. There are 5 of these maps, one for each of the recognized +/// IFDs. The maps contain an entry for each tag in the IFD, whether we capture the data or not. The +/// dataPtr and dataLen fields in the InternalTagInfo are zero if the tag is not captured. +// ================================================================================================= + +// ================================================================================================= +// TIFF_FileWriter::TIFF_FileWriter +// ================================ +// +// Set big endian Get/Put routines so that routines are in place for creating TIFF without a parse. +// Parsing will reset them to the proper endianness for the stream. Big endian is a good default +// since JPEG and PSD files are big endian overall. + +TIFF_FileWriter::TIFF_FileWriter() : changed(false), legacyDeleted(false), memParsed(false), + fileParsed(false), ownedStream(false), memStream(0), tiffLength(0) +{ + + XMP_Uns8 bogusTIFF [kEmptyTIFFLength]; + + bogusTIFF[0] = 0x4D; + bogusTIFF[1] = 0x4D; + bogusTIFF[2] = 0x00; + bogusTIFF[3] = 0x2A; + bogusTIFF[4] = bogusTIFF[5] = bogusTIFF[6] = bogusTIFF[7] = 0x00; + + (void) this->CheckTIFFHeader ( bogusTIFF, sizeof ( bogusTIFF ) ); + +} // TIFF_FileWriter::TIFF_FileWriter + +// ================================================================================================= +// TIFF_FileWriter::~TIFF_FileWriter +// ================================= + +TIFF_FileWriter::~TIFF_FileWriter() +{ + XMP_Assert ( ! (this->memParsed && this->fileParsed) ); + + if ( this->ownedStream ) { + XMP_Assert ( this->memStream != 0 ); + free ( this->memStream ); + } + +} // TIFF_FileWriter::~TIFF_FileWriter + +// ================================================================================================= +// TIFF_FileWriter::DeleteExistingInfo +// =================================== + +void TIFF_FileWriter::DeleteExistingInfo() +{ + XMP_Assert ( ! (this->memParsed && this->fileParsed) ); + + if ( this->ownedStream ) free ( this->memStream ); // ! Current TIFF might be memory-parsed. + this->memStream = 0; + this->tiffLength = 0; + + for ( int ifd = 0; ifd < kTIFF_KnownIFDCount; ++ifd ) this->containedIFDs[ifd].clear(); + + this->changed = false; + this->legacyDeleted = false; + this->memParsed = false; + this->fileParsed = false; + this->ownedStream = false; + +} // TIFF_FileWriter::DeleteExistingInfo + +// ================================================================================================= +// TIFF_FileWriter::PickIFD +// ======================== + +XMP_Uns8 TIFF_FileWriter::PickIFD ( XMP_Uns8 ifd, XMP_Uns16 id ) +{ + if ( ifd > kTIFF_LastRealIFD ) { + if ( ifd != kTIFF_KnownIFD ) XMP_Throw ( "Invalid IFD number", kXMPErr_BadParam ); + XMP_Throw ( "kTIFF_KnownIFD not yet implemented", kXMPErr_Unimplemented ); + // *** Likely to stay unimplemented until there is a client need. + } + + return ifd; + +} // TIFF_FileWriter::PickIFD + +// ================================================================================================= +// TIFF_FileWriter::FindTagInIFD +// ============================= + +const TIFF_FileWriter::InternalTagInfo* TIFF_FileWriter::FindTagInIFD ( XMP_Uns8 ifd, XMP_Uns16 id ) const +{ + ifd = PickIFD ( ifd, id ); + const InternalTagMap& currIFD = this->containedIFDs[ifd].tagMap; + + InternalTagMap::const_iterator tagPos = currIFD.find ( id ); + if ( tagPos == currIFD.end() ) return 0; + return &tagPos->second; + +} // TIFF_FileWriter::FindTagInIFD + +// ================================================================================================= +// TIFF_FileWriter::GetIFD +// ======================= + +bool TIFF_FileWriter::GetIFD ( XMP_Uns8 ifd, TagInfoMap* ifdMap ) const +{ + if ( ifd > kTIFF_LastRealIFD ) XMP_Throw ( "Invalid IFD number", kXMPErr_BadParam ); + const InternalTagMap& currIFD = this->containedIFDs[ifd].tagMap; + + InternalTagMap::const_iterator tagPos = currIFD.begin(); + InternalTagMap::const_iterator tagEnd = currIFD.end(); + + if ( ifdMap != 0 ) ifdMap->clear(); + if ( tagPos == tagEnd ) return false; // Empty IFD. + + if ( ifdMap != 0 ) { + for ( ; tagPos != tagEnd; ++tagPos ) { + const InternalTagInfo& intInfo = tagPos->second; + TagInfo extInfo ( intInfo.id, intInfo.type, intInfo.count, intInfo.dataPtr, intInfo.dataLen ); + (*ifdMap)[intInfo.id] = extInfo; + } + } + + return true; + +} // TIFF_FileWriter::GetIFD + +// ================================================================================================= +// TIFF_FileWriter::GetValueOffset +// =============================== + +XMP_Uns32 TIFF_FileWriter::GetValueOffset ( XMP_Uns8 ifd, XMP_Uns16 id ) const +{ + const InternalTagInfo* thisTag = this->FindTagInIFD ( ifd, id ); + if ( (thisTag == 0) || (thisTag->origDataLen == 0) ) return 0; + + return thisTag->origDataOffset; + +} // TIFF_FileWriter::GetValueOffset + +// ================================================================================================= +// TIFF_FileWriter::GetTag +// ======================= + +bool TIFF_FileWriter::GetTag ( XMP_Uns8 ifd, XMP_Uns16 id, TagInfo* info ) const +{ + const InternalTagInfo* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + + if ( info != 0 ) { + + info->id = thisTag->id; + info->type = thisTag->type; + info->count = thisTag->dataLen / (XMP_Uns32)kTIFF_TypeSizes[thisTag->type]; + info->dataLen = thisTag->dataLen; + info->dataPtr = (const void*)(thisTag->dataPtr); + + } + + return true; + +} // TIFF_FileWriter::GetTag + +// ================================================================================================= +// TIFF_FileWriter::SetTag +// ======================= + +void TIFF_FileWriter::SetTag ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns16 type, XMP_Uns32 count, const void* clientPtr ) +{ + if ( (type < kTIFF_ByteType) || (type > kTIFF_LastType) ) XMP_Throw ( "Invalid TIFF tag type", kXMPErr_BadParam ); + size_t typeSize = kTIFF_TypeSizes[type]; + size_t fullSize = count * typeSize; + + ifd = PickIFD ( ifd, id ); + InternalTagMap& currIFD = this->containedIFDs[ifd].tagMap; + + InternalTagInfo* tagPtr = 0; + InternalTagMap::iterator tagPos = currIFD.find ( id ); + + if ( tagPos == currIFD.end() ) { + + // The tag does not yet exist, add it. + InternalTagMap::value_type mapValue ( id, InternalTagInfo ( id, type, count, this->fileParsed ) ); + tagPos = currIFD.insert ( tagPos, mapValue ); + tagPtr = &tagPos->second; + + } else { + + tagPtr = &tagPos->second; + + // The tag already exists, make sure the value is actually changing. + if ( (type == tagPtr->type) && (count == tagPtr->count) && + (memcmp ( clientPtr, tagPtr->dataPtr, tagPtr->dataLen ) == 0) ) { + return; // ! The value is unchanged, exit. + } + + tagPtr->FreeData(); // Release any existing data allocation. + + tagPtr->type = type; // These might be changing also. + tagPtr->count = count; + + } + + tagPtr->changed = true; + tagPtr->dataLen = (XMP_Uns32)fullSize; + + if ( fullSize <= 4 ) { + // The data is less than 4 bytes, store it in the smallValue field using native endianness. + tagPtr->dataPtr = (XMP_Uns8*) &tagPtr->smallValue; + } else { + // The data is more than 4 bytes, make a copy. + tagPtr->dataPtr = (XMP_Uns8*) malloc ( fullSize ); + if ( tagPtr->dataPtr == 0 ) XMP_Throw ( "Out of memory", kXMPErr_NoMemory ); + } + memcpy ( tagPtr->dataPtr, clientPtr, fullSize ); // AUDIT: Safe, space guaranteed to be fullSize. + + if ( ! this->nativeEndian ) { + if ( typeSize == 2 ) { + XMP_Uns16* flipPtr = (XMP_Uns16*) tagPtr->dataPtr; + for ( XMP_Uns32 i = 0; i < count; ++i ) Flip2 ( flipPtr[i] ); + } else if ( typeSize == 4 ) { + XMP_Uns32* flipPtr = (XMP_Uns32*) tagPtr->dataPtr; + for ( XMP_Uns32 i = 0; i < count; ++i ) Flip4 ( flipPtr[i] ); + } else if ( typeSize == 8 ) { + XMP_Uns64* flipPtr = (XMP_Uns64*) tagPtr->dataPtr; + for ( XMP_Uns32 i = 0; i < count; ++i ) Flip8 ( flipPtr[i] ); + } + } + + this->containedIFDs[ifd].changed = true; + this->changed = true; + +} // TIFF_FileWriter::SetTag + +// ================================================================================================= +// TIFF_FileWriter::DeleteTag +// ========================== + +void TIFF_FileWriter::DeleteTag ( XMP_Uns8 ifd, XMP_Uns16 id ) +{ + ifd = PickIFD ( ifd, id ); + InternalTagMap& currIFD = this->containedIFDs[ifd].tagMap; + + InternalTagMap::iterator tagPos = currIFD.find ( id ); + if ( tagPos == currIFD.end() ) return; // ! Don't set the changed flags if the tag didn't exist. + + currIFD.erase ( tagPos ); + this->containedIFDs[ifd].changed = true; + this->changed = true; + if ( (ifd != kTIFF_PrimaryIFD) || (id != kTIFF_XMP) ) this->legacyDeleted = true; + +} // TIFF_FileWriter::DeleteTag + +// ================================================================================================= + +static inline XMP_Uns8 GetUns8 ( const void* dataPtr ) +{ + return *((XMP_Uns8*)dataPtr); +} + +// ================================================================================================= +// TIFF_FileWriter::GetTag_Integer +// =============================== + +bool TIFF_FileWriter::GetTag_Integer ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns32* data ) const +{ + const InternalTagInfo* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( thisTag->count != 1 ) return false; + + XMP_Uns32 uns32; + XMP_Int32 int32; + + switch ( thisTag->type ) { + + case kTIFF_ByteType: + uns32 = GetUns8 ( thisTag->dataPtr ); + break; + + case kTIFF_ShortType: + uns32 = this->GetUns16 ( thisTag->dataPtr ); + break; + + case kTIFF_LongType: + uns32 = this->GetUns32 ( thisTag->dataPtr ); + break; + + case kTIFF_SByteType: + int32 = (XMP_Int8) GetUns8 ( thisTag->dataPtr ); + uns32 = (XMP_Uns32) int32; // Make sure sign bits are properly set. + break; + + case kTIFF_SShortType: + int32 = (XMP_Int16) this->GetUns16 ( thisTag->dataPtr ); + uns32 = (XMP_Uns32) int32; // Make sure sign bits are properly set. + break; + + case kTIFF_SLongType: + int32 = (XMP_Int32) this->GetUns32 ( thisTag->dataPtr ); + uns32 = (XMP_Uns32) int32; // Make sure sign bits are properly set. + break; + + default: return false; + + } + + if ( data != 0 ) *data = uns32; + return true; + +} // TIFF_FileWriter::GetTag_Integer + +// ================================================================================================= +// TIFF_FileWriter::GetTag_Byte +// ============================ + +bool TIFF_FileWriter::GetTag_Byte ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns8* data ) const +{ + const InternalTagInfo* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_ByteType) || (thisTag->dataLen != 1) ) return false; + + if ( data != 0 ) *data = *thisTag->dataPtr; + return true; + +} // TIFF_FileWriter::GetTag_Byte + +// ================================================================================================= +// TIFF_FileWriter::GetTag_SByte +// ============================= + +bool TIFF_FileWriter::GetTag_SByte ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int8* data ) const +{ + const InternalTagInfo* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_SByteType) || (thisTag->dataLen != 1) ) return false; + + if ( data != 0 ) *data = *thisTag->dataPtr; + return true; + +} // TIFF_FileWriter::GetTag_SByte + +// ================================================================================================= +// TIFF_FileWriter::GetTag_Short +// ============================= + +bool TIFF_FileWriter::GetTag_Short ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns16* data ) const +{ + const InternalTagInfo* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_ShortType) || (thisTag->dataLen != 2) ) return false; + + if ( data != 0 ) *data = this->GetUns16 ( thisTag->dataPtr ); + return true; + +} // TIFF_FileWriter::GetTag_Short + +// ================================================================================================= +// TIFF_FileWriter::GetTag_SShort +// ============================== + +bool TIFF_FileWriter::GetTag_SShort ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int16* data ) const +{ + const InternalTagInfo* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_SShortType) || (thisTag->dataLen != 2) ) return false; + + if ( data != 0 ) *data = (XMP_Int16) this->GetUns16 ( thisTag->dataPtr ); + return true; + +} // TIFF_FileWriter::GetTag_SShort + +// ================================================================================================= +// TIFF_FileWriter::GetTag_Long +// ============================ + +bool TIFF_FileWriter::GetTag_Long ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns32* data ) const +{ + const InternalTagInfo* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_LongType) || (thisTag->dataLen != 4) ) return false; + + if ( data != 0 ) *data = this->GetUns32 ( thisTag->dataPtr ); + return true; + +} // TIFF_FileWriter::GetTag_Long + +// ================================================================================================= +// TIFF_FileWriter::GetTag_SLong +// ============================= + +bool TIFF_FileWriter::GetTag_SLong ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int32* data ) const +{ + const InternalTagInfo* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_SLongType) || (thisTag->dataLen != 4) ) return false; + + if ( data != 0 ) *data = (XMP_Int32) this->GetUns32 ( thisTag->dataPtr ); + return true; + +} // TIFF_FileWriter::GetTag_SLong + +// ================================================================================================= +// TIFF_FileWriter::GetTag_Rational +// ================================ + +bool TIFF_FileWriter::GetTag_Rational ( XMP_Uns8 ifd, XMP_Uns16 id, Rational* data ) const +{ + const InternalTagInfo* thisTag = this->FindTagInIFD ( ifd, id ); + if ( (thisTag == 0) || (thisTag->dataPtr == 0) ) return false; + if ( (thisTag->type != kTIFF_RationalType) || (thisTag->dataLen != 8) ) return false; + + if ( data != 0 ) { + XMP_Uns32* dataPtr = (XMP_Uns32*)thisTag->dataPtr; + data->num = this->GetUns32 ( dataPtr ); + data->denom = this->GetUns32 ( dataPtr+1 ); + } + + return true; + +} // TIFF_FileWriter::GetTag_Rational + +// ================================================================================================= +// TIFF_FileWriter::GetTag_SRational +// ================================= + +bool TIFF_FileWriter::GetTag_SRational ( XMP_Uns8 ifd, XMP_Uns16 id, SRational* data ) const +{ + const InternalTagInfo* thisTag = this->FindTagInIFD ( ifd, id ); + if ( (thisTag == 0) || (thisTag->dataPtr == 0) ) return false; + if ( (thisTag->type != kTIFF_SRationalType) || (thisTag->dataLen != 8) ) return false; + + if ( data != 0 ) { + XMP_Uns32* dataPtr = (XMP_Uns32*)thisTag->dataPtr; + data->num = (XMP_Int32) this->GetUns32 ( dataPtr ); + data->denom = (XMP_Int32) this->GetUns32 ( dataPtr+1 ); + } + + return true; + +} // TIFF_FileWriter::GetTag_SRational + +// ================================================================================================= +// TIFF_FileWriter::GetTag_Float +// ============================= + +bool TIFF_FileWriter::GetTag_Float ( XMP_Uns8 ifd, XMP_Uns16 id, float* data ) const +{ + const InternalTagInfo* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_FloatType) || (thisTag->dataLen != 4) ) return false; + + if ( data != 0 ) *data = this->GetFloat ( thisTag->dataPtr ); + return true; + +} // TIFF_FileWriter::GetTag_Float + +// ================================================================================================= +// TIFF_FileWriter::GetTag_Double +// ============================== + +bool TIFF_FileWriter::GetTag_Double ( XMP_Uns8 ifd, XMP_Uns16 id, double* data ) const +{ + const InternalTagInfo* thisTag = this->FindTagInIFD ( ifd, id ); + if ( (thisTag == 0) || (thisTag->dataPtr == 0) ) return false; + if ( (thisTag->type != kTIFF_DoubleType) || (thisTag->dataLen != 8) ) return false; + + if ( data != 0 ) *data = this->GetDouble ( thisTag->dataPtr ); + return true; + +} // TIFF_FileWriter::GetTag_Double + +// ================================================================================================= +// TIFF_FileWriter::GetTag_ASCII +// ============================= + +bool TIFF_FileWriter::GetTag_ASCII ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_StringPtr* dataPtr, XMP_StringLen* dataLen ) const +{ + const InternalTagInfo* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->dataLen > 4) && (thisTag->dataPtr == 0) ) return false; + if ( thisTag->type != kTIFF_ASCIIType ) return false; + + if ( dataPtr != 0 ) *dataPtr = (XMP_StringPtr)thisTag->dataPtr; + if ( dataLen != 0 ) *dataLen = thisTag->dataLen; + + return true; + +} // TIFF_FileWriter::GetTag_ASCII + +// ================================================================================================= +// TIFF_FileWriter::GetTag_EncodedString +// ===================================== + +bool TIFF_FileWriter::GetTag_EncodedString ( XMP_Uns8 ifd, XMP_Uns16 id, std::string* utf8Str ) const +{ + const InternalTagInfo* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( thisTag->type != kTIFF_UndefinedType ) return false; + + if ( utf8Str == 0 ) return true; // Return true if the converted string is not wanted. + + bool ok = this->DecodeString ( thisTag->dataPtr, thisTag->dataLen, utf8Str ); + return ok; + +} // TIFF_FileWriter::GetTag_EncodedString + +// ================================================================================================= +// TIFF_FileWriter::SetTag_EncodedString +// ===================================== + +void TIFF_FileWriter::SetTag_EncodedString ( XMP_Uns8 ifd, XMP_Uns16 id, const std::string& utf8Str, XMP_Uns8 encoding ) +{ + std::string encodedStr; + + this->EncodeString ( utf8Str, encoding, &encodedStr ); + this->SetTag ( ifd, id, kTIFF_UndefinedType, (XMP_Uns32)encodedStr.size(), encodedStr.c_str() ); + +} // TIFF_FileWriter::SetTag_EncodedString + +// ================================================================================================= +// TIFF_FileWriter::IsLegacyChanged +// ================================ + +bool TIFF_FileWriter::IsLegacyChanged() +{ + + if ( ! this->changed ) return false; + if ( this->legacyDeleted ) return true; + + for ( int ifd = 0; ifd < kTIFF_KnownIFDCount; ++ifd ) { + + InternalIFDInfo & thisIFD = this->containedIFDs[ifd]; + if ( ! thisIFD.changed ) continue; + + InternalTagMap::iterator tagPos; + InternalTagMap::iterator tagEnd = thisIFD.tagMap.end(); + + for ( tagPos = thisIFD.tagMap.begin(); tagPos != tagEnd; ++tagPos ) { + InternalTagInfo & thisTag = tagPos->second; + if ( thisTag.changed && (thisTag.id != kTIFF_XMP) ) return true; + } + + } + + return false; // Can get here if the XMP tag is the only one changed. + +} // TIFF_FileWriter::IsLegacyChanged + +// ================================================================================================= +// TIFF_FileWriter::ParseMemoryStream +// ================================== + +void TIFF_FileWriter::ParseMemoryStream ( const void* data, XMP_Uns32 length, bool copyData /* = true */ ) +{ + this->DeleteExistingInfo(); + this->memParsed = true; + if ( length == 0 ) return; + + // Allocate space for the full in-memory stream and copy it. + + if ( ! copyData ) { + XMP_Assert ( ! this->ownedStream ); + this->memStream = (XMP_Uns8*) data; + } else { + if ( length > 100*1024*1024 ) XMP_Throw ( "Outrageous length for memory-based TIFF", kXMPErr_BadTIFF ); + this->memStream = (XMP_Uns8*) malloc(length); + if ( this->memStream == 0 ) XMP_Throw ( "Out of memory", kXMPErr_NoMemory ); + memcpy ( this->memStream, data, length ); // AUDIT: Safe, malloc'ed length bytes above. + this->ownedStream = true; + } + + this->tiffLength = length; + XMP_Uns32 ifdLimit = this->tiffLength - 6; // An IFD must start before this offset. + + // Find and process the primary, Exif, GPS, and Interoperability IFDs. + + XMP_Uns32 primaryIFDOffset = this->CheckTIFFHeader ( this->memStream, length ); + + if ( primaryIFDOffset != 0 ) (void) this->ProcessMemoryIFD ( primaryIFDOffset, kTIFF_PrimaryIFD ); + + const InternalTagInfo* exifIFDTag = this->FindTagInIFD ( kTIFF_PrimaryIFD, kTIFF_ExifIFDPointer ); + if ( (exifIFDTag != 0) && (exifIFDTag->type == kTIFF_LongType) && (exifIFDTag->dataLen == 4) ) { + XMP_Uns32 exifOffset = this->GetUns32 ( exifIFDTag->dataPtr ); + (void) this->ProcessMemoryIFD ( exifOffset, kTIFF_ExifIFD ); + } + + const InternalTagInfo* gpsIFDTag = this->FindTagInIFD ( kTIFF_PrimaryIFD, kTIFF_GPSInfoIFDPointer ); + if ( (gpsIFDTag != 0) && (gpsIFDTag->type == kTIFF_LongType) && (gpsIFDTag->dataLen == 4) ) { + XMP_Uns32 gpsOffset = this->GetUns32 ( gpsIFDTag->dataPtr ); + if ( (8 <= gpsOffset) && (gpsOffset < ifdLimit) ) { // Remove a bad GPS IFD offset. + (void) this->ProcessMemoryIFD ( gpsOffset, kTIFF_GPSInfoIFD ); + } else { + this->DeleteTag ( kTIFF_PrimaryIFD, kTIFF_GPSInfoIFDPointer ); + } + } + + const InternalTagInfo* interopIFDTag = this->FindTagInIFD ( kTIFF_ExifIFD, kTIFF_InteroperabilityIFDPointer ); + if ( (interopIFDTag != 0) && (interopIFDTag->type == kTIFF_LongType) && (interopIFDTag->dataLen == 4) ) { + XMP_Uns32 interopOffset = this->GetUns32 ( interopIFDTag->dataPtr ); + if ( (8 <= interopOffset) && (interopOffset < ifdLimit) ) { // Remove a bad Interoperability IFD offset. + (void) this->ProcessMemoryIFD ( interopOffset, kTIFF_InteropIFD ); + } else { + this->DeleteTag ( kTIFF_ExifIFD, kTIFF_InteroperabilityIFDPointer ); + } + } + + #if 0 + { + printf ( "\nExiting TIFF_FileWriter::ParseMemoryStream\n" ); + for ( int ifd = 0; ifd < kTIFF_KnownIFDCount; ++ifd ) { + InternalIFDInfo & thisIFD = this->containedIFDs[ifd]; + printf ( "\n IFD %d, count %d, mapped %d, offset %d (0x%X), next IFD %d (0x%X)\n", + ifd, thisIFD.origCount, thisIFD.tagMap.size(), + thisIFD.origDataOffset, thisIFD.origDataOffset, thisIFD.origNextIFD, thisIFD.origNextIFD ); + InternalTagMap::iterator tagPos; + InternalTagMap::iterator tagEnd = thisIFD.tagMap.end(); + for ( tagPos = thisIFD.tagMap.begin(); tagPos != tagEnd; ++tagPos ) { + InternalTagInfo & thisTag = tagPos->second; + printf ( " Tag %d, smallValue 0x%X, origDataLen %d, origDataOffset %d (0x%X)\n", + thisTag.id, thisTag.smallValue, thisTag.origDataLen, thisTag.origDataOffset, thisTag.origDataOffset ); + } + } + printf ( "\n" ); + } + #endif + +} // TIFF_FileWriter::ParseMemoryStream + +// ================================================================================================= +// TIFF_FileWriter::ProcessMemoryIFD +// ================================= + +XMP_Uns32 TIFF_FileWriter::ProcessMemoryIFD ( XMP_Uns32 ifdOffset, XMP_Uns8 ifd ) +{ + InternalIFDInfo& ifdInfo ( this->containedIFDs[ifd] ); + + if ( (ifdOffset < 8) || (ifdOffset > (this->tiffLength - kEmptyIFDLength)) ) { + XMP_Throw ( "Bad IFD offset", kXMPErr_BadTIFF ); + } + + XMP_Uns8* ifdPtr = this->memStream + ifdOffset; + XMP_Uns16 tagCount = this->GetUns16 ( ifdPtr ); + RawIFDEntry* ifdEntries = (RawIFDEntry*)(ifdPtr+2); + + if ( tagCount >= 0x8000 ) XMP_Throw ( "Outrageous IFD count", kXMPErr_BadTIFF ); + if ( (XMP_Uns32)(2 + tagCount*12 + 4) > (this->tiffLength - ifdOffset) ) { + XMP_Throw ( "Out of bounds IFD", kXMPErr_BadTIFF ); + } + + ifdInfo.origIFDOffset = ifdOffset; + ifdInfo.origCount = tagCount; + + for ( size_t i = 0; i < tagCount; ++i ) { + + RawIFDEntry* rawTag = &ifdEntries[i]; + XMP_Uns16 tagType = this->GetUns16 ( &rawTag->type ); + if ( (tagType < kTIFF_ByteType) || (tagType > kTIFF_LastType) ) continue; // Bad type, skip this tag. + + XMP_Uns16 tagID = this->GetUns16 ( &rawTag->id ); + XMP_Uns32 tagCount = this->GetUns32 ( &rawTag->count ); + + InternalTagMap::value_type mapValue ( tagID, InternalTagInfo ( tagID, tagType, tagCount, kIsMemoryBased ) ); + InternalTagMap::iterator newPos = ifdInfo.tagMap.insert ( ifdInfo.tagMap.end(), mapValue ); + InternalTagInfo& mapTag = newPos->second; + + mapTag.dataLen = mapTag.origDataLen = mapTag.count * (XMP_Uns32)kTIFF_TypeSizes[mapTag.type]; + mapTag.smallValue = rawTag->dataOrOffset; // Keep the value or offset in stream byte ordering. + + if ( mapTag.dataLen <= 4 ) { + mapTag.origDataOffset = ifdOffset + 2 + (12 * (XMP_Uns32)i) + 8; // Compute the data offset. + } else { + mapTag.origDataOffset = this->GetUns32 ( &rawTag->dataOrOffset ); // Extract the data offset. + // printf ( "FW_ProcessMemoryIFD tag %d large value @ %.8X\n", mapTag.id, mapTag.dataPtr ); + if ( (mapTag.origDataOffset < 8) || (mapTag.origDataOffset >= this->tiffLength) ) { + mapTag.count = mapTag.dataLen = mapTag.origDataLen = mapTag.smallValue = 0; + mapTag.origDataOffset = ifdOffset + 2 + (12 * (XMP_Uns32)i) + 8; // Make this bad tag look empty + } + if ( mapTag.dataLen > (this->tiffLength - mapTag.origDataOffset) ) { + mapTag.count = mapTag.dataLen = mapTag.origDataLen = mapTag.smallValue = 0; + mapTag.origDataOffset = ifdOffset + 2 + (12 * (XMP_Uns32)i) + 8; // Make this bad tag look empty + } + } + mapTag.dataPtr = this->memStream + mapTag.origDataOffset; + + } + + ifdPtr += (2 + tagCount*12); + ifdInfo.origNextIFD = this->GetUns32 ( ifdPtr ); + + return ifdInfo.origNextIFD; + +} // TIFF_FileWriter::ProcessMemoryIFD + +// ================================================================================================= +// TIFF_FileWriter::ParseFileStream +// ================================ +// +// The buffered I/O model is worth the logic complexity - as opposed to a simple seek/read for each +// part of the TIFF stream. The vast majority of real-world TIFFs have the primary IFD, Exif IFD, +// and all of their interesting tag values within the first 64K of the file. Well, at least before +// we get around to our edit-by-append approach. + +void TIFF_FileWriter::ParseFileStream ( XMP_IO* fileRef ) +{ + bool ok; + IOBuffer ioBuf; + + this->DeleteExistingInfo(); + this->fileParsed = true; + this->tiffLength = (XMP_Uns32) fileRef->Length(); + if ( this->tiffLength == 0 ) return; + + XMP_Uns32 ifdLimit = this->tiffLength - 6; // An IFD must start before this offset. + + // Find and process the primary, Exif, GPS, and Interoperability IFDs. + + ioBuf.filePos = 0; + fileRef->Rewind ( ); + ok = CheckFileSpace ( fileRef, &ioBuf, 8 ); + if ( ! ok ) XMP_Throw ( "TIFF too small", kXMPErr_BadTIFF ); + + XMP_Uns32 primaryIFDOffset = this->CheckTIFFHeader ( ioBuf.ptr, this->tiffLength ); + + if ( primaryIFDOffset != 0 ) (void) this->ProcessFileIFD ( kTIFF_PrimaryIFD, primaryIFDOffset, fileRef, &ioBuf ); + + const InternalTagInfo* exifIFDTag = this->FindTagInIFD ( kTIFF_PrimaryIFD, kTIFF_ExifIFDPointer ); + if ( (exifIFDTag != 0) && (exifIFDTag->type == kTIFF_LongType) && (exifIFDTag->count == 1) ) { + XMP_Uns32 exifOffset = this->GetUns32 ( exifIFDTag->dataPtr ); + (void) this->ProcessFileIFD ( kTIFF_ExifIFD, exifOffset, fileRef, &ioBuf ); + } + + const InternalTagInfo* gpsIFDTag = this->FindTagInIFD ( kTIFF_PrimaryIFD, kTIFF_GPSInfoIFDPointer ); + if ( (gpsIFDTag != 0) && (gpsIFDTag->type == kTIFF_LongType) && (gpsIFDTag->count == 1) ) { + XMP_Uns32 gpsOffset = this->GetUns32 ( gpsIFDTag->dataPtr ); + if ( (8 <= gpsOffset) && (gpsOffset < ifdLimit) ) { // Remove a bad GPS IFD offset. + (void) this->ProcessFileIFD ( kTIFF_GPSInfoIFD, gpsOffset, fileRef, &ioBuf ); + } else { + this->DeleteTag ( kTIFF_PrimaryIFD, kTIFF_GPSInfoIFDPointer ); + } + } + + const InternalTagInfo* interopIFDTag = this->FindTagInIFD ( kTIFF_ExifIFD, kTIFF_InteroperabilityIFDPointer ); + if ( (interopIFDTag != 0) && (interopIFDTag->type == kTIFF_LongType) && (interopIFDTag->dataLen == 4) ) { + XMP_Uns32 interopOffset = this->GetUns32 ( interopIFDTag->dataPtr ); + if ( (8 <= interopOffset) && (interopOffset < ifdLimit) ) { // Remove a bad Interoperability IFD offset. + (void) this->ProcessFileIFD ( kTIFF_InteropIFD, interopOffset, fileRef, &ioBuf ); + } else { + this->DeleteTag ( kTIFF_ExifIFD, kTIFF_InteroperabilityIFDPointer ); + } + } + + #if 0 + { + printf ( "\nExiting TIFF_FileWriter::ParseFileStream\n" ); + for ( int ifd = 0; ifd < kTIFF_KnownIFDCount; ++ifd ) { + InternalIFDInfo & thisIFD = this->containedIFDs[ifd]; + printf ( "\n IFD %d, count %d, mapped %d, offset %d (0x%X), next IFD %d (0x%X)\n", + ifd, thisIFD.origCount, thisIFD.tagMap.size(), + thisIFD.origDataOffset, thisIFD.origDataOffset, thisIFD.origNextIFD, thisIFD.origNextIFD ); + InternalTagMap::iterator tagPos; + InternalTagMap::iterator tagEnd = thisIFD.tagMap.end(); + for ( tagPos = thisIFD.tagMap.begin(); tagPos != tagEnd; ++tagPos ) { + InternalTagInfo & thisTag = tagPos->second; + printf ( " Tag %d, smallValue 0x%X, origDataLen %d, origDataOffset %d (0x%X)\n", + thisTag.id, thisTag.smallValue, thisTag.origDataLen, thisTag.origDataOffset, thisTag.origDataOffset ); + } + } + printf ( "\n" ); + } + #endif + +} // TIFF_FileWriter::ParseFileStream + +// ================================================================================================= +// TIFF_FileWriter::ProcessFileIFD +// =============================== + +XMP_Uns32 TIFF_FileWriter::ProcessFileIFD ( XMP_Uns8 ifd, XMP_Uns32 ifdOffset, XMP_IO* fileRef, void* _ioBuf ) +{ + IOBuffer* ioBuf = (IOBuffer*)_ioBuf; // *** Temporary hack, _ioBuf is IOBuffer* but don't want to include XIO.hpp. + InternalIFDInfo& ifdInfo ( this->containedIFDs[ifd] ); + + if ( (ifdOffset < 8) || (ifdOffset > (this->tiffLength - kEmptyIFDLength)) ) { + XMP_Throw ( "Bad IFD offset", kXMPErr_BadTIFF ); + } + + MoveToOffset ( fileRef, ifdOffset, ioBuf ); // Move to the start of the IFD. + + bool ok = CheckFileSpace ( fileRef, ioBuf, 2 ); + if ( ! ok ) XMP_Throw ( "IFD count missing", kXMPErr_BadTIFF ); + XMP_Uns16 tagCount = this->GetUns16 ( ioBuf->ptr ); + + if ( tagCount >= 0x8000 ) XMP_Throw ( "Outrageous IFD count", kXMPErr_BadTIFF ); + if ( (XMP_Uns32)(2 + tagCount*12 + 4) > (this->tiffLength - ifdOffset) ) { + XMP_Throw ( "Out of bounds IFD", kXMPErr_BadTIFF ); + } + + ifdInfo.origIFDOffset = ifdOffset; + ifdInfo.origCount = tagCount; + + // --------------------------------------------------------------------------------------------- + // First create all of the IFD map entries, capturing short values, and get the next IFD offset. + // We're using a std::map for storage, it automatically eliminates duplicates and provides + // sorted output. Plus the "map[key] = value" assignment conveniently keeps the last encountered + // value, following Photoshop's behavior. + + ioBuf->ptr += 2; // Move to the first IFD entry. + + for ( XMP_Uns16 i = 0; i < tagCount; ++i, ioBuf->ptr += 12 ) { + + if ( ! CheckFileSpace ( fileRef, ioBuf, 12 ) ) XMP_Throw ( "EOF within IFD", kXMPErr_BadTIFF ); + + RawIFDEntry* rawTag = (RawIFDEntry*)ioBuf->ptr; + XMP_Uns16 tagType = this->GetUns16 ( &rawTag->type ); + if ( (tagType < kTIFF_ByteType) || (tagType > kTIFF_LastType) ) continue; // Bad type, skip this tag. + + XMP_Uns16 tagID = this->GetUns16 ( &rawTag->id ); + XMP_Uns32 tagCount = this->GetUns32 ( &rawTag->count ); + + InternalTagMap::value_type mapValue ( tagID, InternalTagInfo ( tagID, tagType, tagCount, kIsFileBased ) ); + InternalTagMap::iterator newPos = ifdInfo.tagMap.insert ( ifdInfo.tagMap.end(), mapValue ); + InternalTagInfo& mapTag = newPos->second; + + mapTag.dataLen = mapTag.origDataLen = mapTag.count * (XMP_Uns32)kTIFF_TypeSizes[mapTag.type]; + mapTag.smallValue = rawTag->dataOrOffset; // Keep the value or offset in stream byte ordering. + + if ( mapTag.dataLen <= 4 ) { + mapTag.dataPtr = (XMP_Uns8*) &mapTag.smallValue; + mapTag.origDataOffset = ifdOffset + 2 + (12 * i) + 8; // Compute the data offset. + } else { + mapTag.origDataOffset = this->GetUns32 ( &rawTag->dataOrOffset ); // Extract the data offset. + if ( (mapTag.origDataOffset < 8) || (mapTag.origDataOffset >= this->tiffLength) ) { + mapTag.dataPtr = (XMP_Uns8*) &mapTag.smallValue; // Make this bad tag look empty. + mapTag.origDataOffset = ifdOffset + 2 + (12 * i) + 8; + mapTag.count = mapTag.dataLen = mapTag.origDataLen = mapTag.smallValue = 0; + } + if ( mapTag.dataLen > (this->tiffLength - mapTag.origDataOffset) ) { + mapTag.dataPtr = (XMP_Uns8*) &mapTag.smallValue; // Make this bad tag look empty. + mapTag.origDataOffset = ifdOffset + 2 + (12 * i) + 8; + mapTag.count = mapTag.dataLen = mapTag.origDataLen = mapTag.smallValue = 0; + } + } + + } + + if ( ! CheckFileSpace ( fileRef, ioBuf, 4 ) ) XMP_Throw ( "EOF at next IFD offset", kXMPErr_BadTIFF ); + ifdInfo.origNextIFD = this->GetUns32 ( ioBuf->ptr ); + + // --------------------------------------------------------------------------------------------- + // Go back over the tag map and extract the data for large recognized tags. This is done in 2 + // passes, in order to lessen the typical amount of I/O. On the first pass make sure we have at + // least 32K of data following the IFD in the buffer, and extract all of the values in that + // portion. This should cover an original file, or the appended values with an appended IFD. + + if ( (ioBuf->limit - ioBuf->ptr) < 32*1024 ) RefillBuffer ( fileRef, ioBuf ); + + InternalTagMap::iterator tagPos = ifdInfo.tagMap.begin(); + InternalTagMap::iterator tagEnd = ifdInfo.tagMap.end(); + + const XMP_Uns16* knownTagPtr = sKnownTags[ifd]; // Points into the ordered recognized tag list. + + XMP_Uns32 bufBegin = (XMP_Uns32)ioBuf->filePos; // TIFF stream bounds for the current buffer. + XMP_Uns32 bufEnd = bufBegin + (XMP_Uns32)ioBuf->len; + + for ( ; tagPos != tagEnd; ++tagPos ) { + + InternalTagInfo* currTag = &tagPos->second; + + if ( currTag->dataLen <= 4 ) continue; // Short values are already in the smallValue field. + + while ( *knownTagPtr < currTag->id ) ++knownTagPtr; + if ( *knownTagPtr != currTag->id ) continue; // Skip unrecognized tags. + + if ( (bufBegin <= currTag->origDataOffset) && ((currTag->origDataOffset + currTag->dataLen) <= bufEnd) ) { + // This value is already fully within the current I/O buffer, copy it. + MoveToOffset ( fileRef, currTag->origDataOffset, ioBuf ); + currTag->dataPtr = (XMP_Uns8*) malloc ( currTag->dataLen ); + if ( currTag->dataPtr == 0 ) XMP_Throw ( "No data block", kXMPErr_NoMemory ); + memcpy ( currTag->dataPtr, ioBuf->ptr, currTag->dataLen ); // AUDIT: Safe, malloc'ed currTag->dataLen bytes above. + } + + } + + // --------------------------------------------------------------------------------------------- + // Now the second large value pass. This will reposition the I/O buffer as necessary. Hopefully + // just once, to pick up the span of data not covered in the first pass. + + tagPos = ifdInfo.tagMap.begin(); // Reset both map/array positions. + knownTagPtr = sKnownTags[ifd]; + + for ( ; tagPos != tagEnd; ++tagPos ) { + + InternalTagInfo* currTag = &tagPos->second; + + if ( (currTag->dataLen <= 4) || (currTag->dataPtr != 0) ) continue; // Done this tag? + while ( *knownTagPtr < currTag->id ) ++knownTagPtr; + if ( *knownTagPtr != currTag->id ) continue; // Skip unrecognized tags. + + currTag->dataPtr = (XMP_Uns8*) malloc ( currTag->dataLen ); + if ( currTag->dataPtr == 0 ) XMP_Throw ( "No data block", kXMPErr_NoMemory ); + + if ( currTag->dataLen > kIOBufferSize ) { + // This value is bigger than the I/O buffer, read it directly and restore the file position. + fileRef->Seek ( currTag->origDataOffset, kXMP_SeekFromStart ); + fileRef->ReadAll ( currTag->dataPtr, currTag->dataLen ); + fileRef->Seek ( (ioBuf->filePos + ioBuf->len), kXMP_SeekFromStart ); + } else { + // This value can fit in the I/O buffer, so use that. + MoveToOffset ( fileRef, currTag->origDataOffset, ioBuf ); + ok = CheckFileSpace ( fileRef, ioBuf, currTag->dataLen ); + if ( ! ok ) XMP_Throw ( "EOF in data block", kXMPErr_BadTIFF ); + memcpy ( currTag->dataPtr, ioBuf->ptr, currTag->dataLen ); // AUDIT: Safe, malloc'ed currTag->dataLen bytes above. + } + + } + + // Done, return the next IFD offset. + + return ifdInfo.origNextIFD; + +} // TIFF_FileWriter::ProcessFileIFD + +// ================================================================================================= +// TIFF_FileWriter::IntegrateFromPShop6 +// ==================================== +// +// See comments for ProcessPShop6IFD. + +void TIFF_FileWriter::IntegrateFromPShop6 ( const void * buriedPtr, size_t buriedLen ) +{ + TIFF_MemoryReader buriedExif; + buriedExif.ParseMemoryStream ( buriedPtr, (XMP_Uns32) buriedLen ); + + this->ProcessPShop6IFD ( buriedExif, kTIFF_PrimaryIFD ); + this->ProcessPShop6IFD ( buriedExif, kTIFF_ExifIFD ); + this->ProcessPShop6IFD ( buriedExif, kTIFF_GPSInfoIFD ); + +} // TIFF_FileWriter::IntegrateFromPShop6 + +// ================================================================================================= +// TIFF_FileWriter::CopyTagToMasterIFD +// =================================== +// +// Create a new master IFD entry from a buried Photoshop 6 IFD entry. Don't try to get clever with +// large values, just create a new copy. This preserves a clean separation between the memory-based +// and file-based TIFF processing. + +void* TIFF_FileWriter::CopyTagToMasterIFD ( const TagInfo & ps6Tag, InternalIFDInfo * masterIFD ) +{ + InternalTagMap::value_type mapValue ( ps6Tag.id, InternalTagInfo ( ps6Tag.id, ps6Tag.type, ps6Tag.count, this->fileParsed ) ); + InternalTagMap::iterator newPos = masterIFD->tagMap.insert ( masterIFD->tagMap.end(), mapValue ); + InternalTagInfo& newTag = newPos->second; + + newTag.dataLen = ps6Tag.dataLen; + + if ( newTag.dataLen <= 4 ) { + newTag.dataPtr = (XMP_Uns8*) &newTag.smallValue; + newTag.smallValue = *((XMP_Uns32*)ps6Tag.dataPtr); + } else { + newTag.dataPtr = (XMP_Uns8*) malloc ( newTag.dataLen ); + if ( newTag.dataPtr == 0 ) XMP_Throw ( "Out of memory", kXMPErr_NoMemory ); + memcpy ( newTag.dataPtr, ps6Tag.dataPtr, newTag.dataLen ); // AUDIT: Safe, malloc'ed dataLen bytes above. + } + + newTag.changed = true; // ! See comments with ProcessPShop6IFD. + XMP_Assert ( (newTag.origDataLen == 0) && (newTag.origDataOffset == 0) ); + + masterIFD->changed = true; + + return newPos->second.dataPtr; // ! Return the address within the map entry for small values. + +} // TIFF_FileWriter::CopyTagToMasterIFD + +// ================================================================================================= +// FlipCFATable +// ============ +// +// The CFA pattern table is trivial, a pair of short counts followed by n*m bytes. + +static bool FlipCFATable ( void* voidPtr, XMP_Uns32 tagLen, GetUns16_Proc GetUns16 ) +{ + if ( tagLen < 4 ) return false; + + XMP_Uns16* u16Ptr = (XMP_Uns16*)voidPtr; + + Flip2 ( &u16Ptr[0] ); // Flip the counts to match the master TIFF. + Flip2 ( &u16Ptr[1] ); + + XMP_Uns16 columns = GetUns16 ( &u16Ptr[0] ); // Fetch using the master TIFF's routine. + XMP_Uns16 rows = GetUns16 ( &u16Ptr[1] ); + + if ( tagLen != (XMP_Uns32)(4 + columns*rows) ) return false; + + return true; + +} // FlipCFATable + +// ================================================================================================= +// FlipDSDTable +// ============ +// +// The device settings description table is trivial, a pair of short counts followed by UTF-16 +// strings. So the whole value should be flipped as a sequence of 16 bit items. + +// ! The Exif 2.2 description is a bit garbled. It might be wrong. It would be nice to have a real example. + +static bool FlipDSDTable ( void* voidPtr, XMP_Uns32 tagLen, GetUns16_Proc GetUns16 ) +{ + if ( tagLen < 4 ) return false; + + XMP_Uns16* u16Ptr = (XMP_Uns16*)voidPtr; + for ( size_t i = tagLen/2; i > 0; --i, ++u16Ptr ) Flip2 ( u16Ptr ); + + return true; + +} // FlipDSDTable + +// ================================================================================================= +// FlipOECFSFRTable +// ================ +// +// The OECF and SFR tables have the same layout: +// 2 short counts, columns and rows +// c ASCII strings, null terminated, column names +// c*r rationals + +static bool FlipOECFSFRTable ( void* voidPtr, XMP_Uns32 tagLen, GetUns16_Proc GetUns16 ) +{ + XMP_Uns16* u16Ptr = (XMP_Uns16*)voidPtr; + + Flip2 ( &u16Ptr[0] ); // Flip the data to match the master TIFF. + Flip2 ( &u16Ptr[1] ); + + XMP_Uns16 columns = GetUns16 ( &u16Ptr[0] ); // Fetch using the master TIFF's routine. + XMP_Uns16 rows = GetUns16 ( &u16Ptr[1] ); + + XMP_Uns32 minLen = 4 + columns + (8 * columns * rows); // Minimum legit tag size. + if ( tagLen < minLen ) return false; + + // Compute the start of the rationals from the end of value. No need to walk through the names. + XMP_Uns32* u32Ptr = (XMP_Uns32*) ((XMP_Uns8*)voidPtr + tagLen - (8 * columns * rows)); + + for ( size_t i = 2*columns*rows; i > 0; --i, ++u32Ptr ) Flip4 ( u32Ptr ); + + return true; + +} // FlipOECFSFRTable + +// ================================================================================================= +// TIFF_FileWriter::ProcessPShop6IFD +// ================================= +// +// Photoshop 6 wrote wacky TIFF files that have much of the Exif metadata buried inside of image +// resource 1058, which is itself within tag 34377 in the 0th IFD. This routine moves the buried +// tags up to the parent file. Existing tags are not replaced. +// +// While it is tempting to try to directly use the TIFF_MemoryReader's tweaked IFD info, making that +// visible would compromise implementation separation. Better to pay the modest runtime cost of +// using the official GetIFD method, letting it build the map. +// +// The tags that get moved are marked as being changed, as is the IFD they are moved into, but the +// overall TIFF_FileWriter object is not. We don't want this integration on its own to force a file +// update, but a file update should include these changes. + +// ! Be careful to not move tags that are the nasty Exif explicit offsets, e.g. the Exif or GPS IFD +// ! "pointers". These are tags with a LONG type and count of 1, whose value is an offset into the +// ! buried TIFF stream. We can't reliably plant that offset into the outer IFD structure. + +// ! To make things even more fun, the buried Exif might not have the same endianness as the outer! + +void TIFF_FileWriter::ProcessPShop6IFD ( const TIFF_MemoryReader& buriedExif, XMP_Uns8 ifd ) +{ + bool ok, found; + TagInfoMap ps6IFD; + + found = buriedExif.GetIFD ( ifd, &ps6IFD ); + if ( ! found ) return; + + bool needsFlipping = (this->bigEndian != buriedExif.IsBigEndian()); + + InternalIFDInfo* masterIFD = &this->containedIFDs[ifd]; + + TagInfoMap::const_iterator ps6Pos = ps6IFD.begin(); + TagInfoMap::const_iterator ps6End = ps6IFD.end(); + + for ( ; ps6Pos != ps6End; ++ps6Pos ) { + + // Copy buried tags to the master IFD if they don't already exist there. + + const TagInfo& ps6Tag = ps6Pos->second; + + if ( this->FindTagInIFD ( ifd, ps6Tag.id ) != 0 ) continue; // Keep existing master tags. + if ( needsFlipping && (ps6Tag.id == 37500) ) continue; // Don't copy an unflipped MakerNote. + if ( (ps6Tag.id == kTIFF_ExifIFDPointer) || // Skip the tags that are explicit offsets. + (ps6Tag.id == kTIFF_GPSInfoIFDPointer) || + (ps6Tag.id == kTIFF_JPEGInterchangeFormat) || + (ps6Tag.id == kTIFF_InteroperabilityIFDPointer) ) continue; + + void* voidPtr = this->CopyTagToMasterIFD ( ps6Tag, masterIFD ); + + if ( needsFlipping ) { + switch ( ps6Tag.type ) { + + case kTIFF_ByteType: + case kTIFF_SByteType: + case kTIFF_ASCIIType: + // Nothing more to do. + break; + + case kTIFF_ShortType: + case kTIFF_SShortType: + { + XMP_Uns16* u16Ptr = (XMP_Uns16*)voidPtr; + for ( size_t i = ps6Tag.count; i > 0; --i, ++u16Ptr ) Flip2 ( u16Ptr ); + } + break; + + case kTIFF_LongType: + case kTIFF_SLongType: + case kTIFF_FloatType: + { + XMP_Uns32* u32Ptr = (XMP_Uns32*)voidPtr; + for ( size_t i = ps6Tag.count; i > 0; --i, ++u32Ptr ) Flip4 ( u32Ptr ); + } + break; + + case kTIFF_RationalType: + case kTIFF_SRationalType: + { + XMP_Uns32* ratPtr = (XMP_Uns32*)voidPtr; + for ( size_t i = (2 * ps6Tag.count); i > 0; --i, ++ratPtr ) Flip4 ( ratPtr ); + } + break; + + case kTIFF_DoubleType: + { + XMP_Uns64* u64Ptr = (XMP_Uns64*)voidPtr; + for ( size_t i = ps6Tag.count; i > 0; --i, ++u64Ptr ) Flip8 ( u64Ptr ); + } + break; + + case kTIFF_UndefinedType: + // Fix up the few kinds of special tables that Exif 2.2 defines. + ok = true; // Keep everything that isn't a special table. + if ( ps6Tag.id == kTIFF_CFAPattern ) { + ok = FlipCFATable ( voidPtr, ps6Tag.dataLen, this->GetUns16 ); + } else if ( ps6Tag.id == kTIFF_DeviceSettingDescription ) { + ok = FlipDSDTable ( voidPtr, ps6Tag.dataLen, this->GetUns16 ); + } else if ( (ps6Tag.id == kTIFF_OECF) || (ps6Tag.id == kTIFF_SpatialFrequencyResponse) ) { + ok = FlipOECFSFRTable ( voidPtr, ps6Tag.dataLen, this->GetUns16 ); + } + if ( ! ok ) this->DeleteTag ( ifd, ps6Tag.id ); + break; + + default: + // ? XMP_Throw ( "Unexpected tag type", kXMPErr_InternalFailure ); + this->DeleteTag ( ifd, ps6Tag.id ); + break; + + } + } + + } + +} // TIFF_FileWriter::ProcessPShop6IFD + +// ================================================================================================= +// TIFF_FileWriter::PreflightIFDLinkage +// ==================================== +// +// Preflight special cases for the linkage between IFDs. Three of the IFDs are found through an +// explicit tag, the Exif, GPS, and Interop IFDs. The presence or absence of those IFDs affects the +// presence or absence of the linkage tag, which can affect the IFD containing the linkage tag. The +// thumbnail IFD is chained from the primary IFD, so if the thumbnail IFD is present we make sure +// that the primary IFD isn't empty. + +void TIFF_FileWriter::PreflightIFDLinkage() +{ + + // Do the tag-linked IFDs bottom up, Interop then GPS then Exif. + + if ( this->containedIFDs[kTIFF_InteropIFD].tagMap.empty() ) { + this->DeleteTag ( kTIFF_ExifIFD, kTIFF_InteroperabilityIFDPointer ); + } else if ( ! this->GetTag ( kTIFF_ExifIFD, kTIFF_InteroperabilityIFDPointer, 0 ) ) { + this->SetTag_Long ( kTIFF_ExifIFD, kTIFF_InteroperabilityIFDPointer, 0xABADABAD ); + } + + if ( this->containedIFDs[kTIFF_GPSInfoIFD].tagMap.empty() ) { + this->DeleteTag ( kTIFF_PrimaryIFD, kTIFF_GPSInfoIFDPointer ); + } else if ( ! this->GetTag ( kTIFF_PrimaryIFD, kTIFF_GPSInfoIFDPointer, 0 ) ) { + this->SetTag_Long ( kTIFF_PrimaryIFD, kTIFF_GPSInfoIFDPointer, 0xABADABAD ); + } + + if ( this->containedIFDs[kTIFF_ExifIFD].tagMap.empty() ) { + this->DeleteTag ( kTIFF_PrimaryIFD, kTIFF_ExifIFDPointer ); + } else if ( ! this->GetTag ( kTIFF_PrimaryIFD, kTIFF_ExifIFDPointer, 0 ) ) { + this->SetTag_Long ( kTIFF_PrimaryIFD, kTIFF_ExifIFDPointer, 0xABADABAD ); + } + + // Make sure that the primary IFD is not empty if the thumbnail IFD is not empty. + + if ( this->containedIFDs[kTIFF_PrimaryIFD].tagMap.empty() && + (! this->containedIFDs[kTIFF_TNailIFD].tagMap.empty()) ) { + this->SetTag_Short ( kTIFF_PrimaryIFD, kTIFF_ResolutionUnit, 2 ); // Set Resolution unit to inches. + } + +} // TIFF_FileWriter::PreflightIFDLinkage + +// ================================================================================================= +// TIFF_FileWriter::DetermineVisibleLength +// ======================================= + +XMP_Uns32 TIFF_FileWriter::DetermineVisibleLength() +{ + XMP_Uns32 visibleLength = 8; // Start with the TIFF header size. + + for ( int ifd = 0; ifd < kTIFF_KnownIFDCount; ++ifd ) { + + InternalIFDInfo& ifdInfo ( this->containedIFDs[ifd] ); + size_t tagCount = ifdInfo.tagMap.size(); + if ( tagCount == 0 ) continue; + + visibleLength += (XMP_Uns32)( 6 + (12 * tagCount) ); + + InternalTagMap::iterator tagPos = ifdInfo.tagMap.begin(); + InternalTagMap::iterator tagEnd = ifdInfo.tagMap.end(); + + for ( ; tagPos != tagEnd; ++tagPos ) { + InternalTagInfo & currTag ( tagPos->second ); + if ( currTag.dataLen > 4 ) visibleLength += ((currTag.dataLen + 1) & 0xFFFFFFFE); // ! Round to even lengths. + } + + } + + return visibleLength; + +} // TIFF_FileWriter::DetermineVisibleLength + +// ================================================================================================= +// TIFF_FileWriter::DetermineAppendInfo +// ==================================== + +#ifndef Trace_DetermineAppendInfo + #define Trace_DetermineAppendInfo 0 +#endif + +XMP_Uns32 TIFF_FileWriter::DetermineAppendInfo ( XMP_Uns32 appendedOrigin, + bool appendedIFDs[kTIFF_KnownIFDCount], + XMP_Uns32 newIFDOffsets[kTIFF_KnownIFDCount], + bool appendAll /* = false */ ) +{ + XMP_Uns32 appendedLength = 0; + XMP_Assert ( (appendedOrigin & 1) == 0 ); // Make sure it is even. + + #if Trace_DetermineAppendInfo + { + printf ( "\nEntering TIFF_FileWriter::DetermineAppendInfo%s\n", (appendAll ? ", append all" : "") ); + for ( int ifd = 0; ifd < kTIFF_KnownIFDCount; ++ifd ) { + InternalIFDInfo & thisIFD = this->containedIFDs[ifd]; + printf ( "\n IFD %d, origCount %d, map.size %d, origIFDOffset %d (0x%X), origNextIFD %d (0x%X)", + ifd, thisIFD.origCount, thisIFD.tagMap.size(), + thisIFD.origIFDOffset, thisIFD.origIFDOffset, thisIFD.origNextIFD, thisIFD.origNextIFD ); + if ( thisIFD.changed ) printf ( ", changed" ); + if ( thisIFD.origCount < thisIFD.tagMap.size() ) printf ( ", should get appended" ); + printf ( "\n" ); + InternalTagMap::iterator tagPos; + InternalTagMap::iterator tagEnd = thisIFD.tagMap.end(); + for ( tagPos = thisIFD.tagMap.begin(); tagPos != tagEnd; ++tagPos ) { + InternalTagInfo & thisTag = tagPos->second; + printf ( " Tag %d, smallValue 0x%X, origDataLen %d, origDataOffset %d (0x%X)", + thisTag.id, thisTag.smallValue, thisTag.origDataLen, thisTag.origDataOffset, thisTag.origDataOffset ); + if ( thisTag.changed ) printf ( ", changed" ); + if ( (thisTag.dataLen > thisTag.origDataLen) && (thisTag.dataLen > 4) ) printf ( ", should get appended" ); + printf ( "\n" ); + } + } + printf ( "\n" ); + } + #endif + + // Determine which of the IFDs will be appended. If the Exif, GPS, or Interoperability IFDs are + // appended, set dummy values for their offsets in the "owning" IFD. This must be done first + // since this might cause the owning IFD to grow. + + if ( ! appendAll ) { + for ( int i = 0; i < kTIFF_KnownIFDCount ;++i ) appendedIFDs[i] = false; + } else { + for ( int i = 0; i < kTIFF_KnownIFDCount ;++i ) appendedIFDs[i] = (this->containedIFDs[i].tagMap.size() > 0); + } + + appendedIFDs[kTIFF_InteropIFD] |= (this->containedIFDs[kTIFF_InteropIFD].origCount < + this->containedIFDs[kTIFF_InteropIFD].tagMap.size()); + if ( appendedIFDs[kTIFF_InteropIFD] ) { + this->SetTag_Long ( kTIFF_ExifIFD, kTIFF_InteroperabilityIFDPointer, 0xABADABAD ); + } + + appendedIFDs[kTIFF_GPSInfoIFD] |= (this->containedIFDs[kTIFF_GPSInfoIFD].origCount < + this->containedIFDs[kTIFF_GPSInfoIFD].tagMap.size()); + if ( appendedIFDs[kTIFF_GPSInfoIFD] ) { + this->SetTag_Long ( kTIFF_PrimaryIFD, kTIFF_GPSInfoIFDPointer, 0xABADABAD ); + } + + appendedIFDs[kTIFF_ExifIFD] |= (this->containedIFDs[kTIFF_ExifIFD].origCount < + this->containedIFDs[kTIFF_ExifIFD].tagMap.size()); + if ( appendedIFDs[kTIFF_ExifIFD] ) { + this->SetTag_Long ( kTIFF_PrimaryIFD, kTIFF_ExifIFDPointer, 0xABADABAD ); + } + + appendedIFDs[kTIFF_PrimaryIFD] |= (this->containedIFDs[kTIFF_PrimaryIFD].origCount < + this->containedIFDs[kTIFF_PrimaryIFD].tagMap.size()); + + // The appended data (if any) will be a sequence of an IFD followed by its large values. + // Determine the new offsets for the appended IFDs and tag values, and the total amount of + // appended stuff. + + for ( int ifd = 0; ifd < kTIFF_KnownIFDCount ;++ifd ) { + + InternalIFDInfo& ifdInfo ( this->containedIFDs[ifd] ); + size_t tagCount = ifdInfo.tagMap.size(); + + if ( ! (appendAll | ifdInfo.changed) ) continue; + if ( tagCount == 0 ) continue; + + newIFDOffsets[ifd] = ifdInfo.origIFDOffset; + if ( appendedIFDs[ifd] ) { + newIFDOffsets[ifd] = appendedOrigin + appendedLength; + appendedLength += (XMP_Uns32)( 6 + (12 * tagCount) ); + } + + InternalTagMap::iterator tagPos = ifdInfo.tagMap.begin(); + InternalTagMap::iterator tagEnd = ifdInfo.tagMap.end(); + + for ( ; tagPos != tagEnd; ++tagPos ) { + + InternalTagInfo & currTag ( tagPos->second ); + if ( (! (appendAll | currTag.changed)) || (currTag.dataLen <= 4) ) continue; + + if ( (currTag.dataLen <= currTag.origDataLen) && (! appendAll) ) { + this->PutUns32 ( currTag.origDataOffset, &currTag.smallValue ); // Reuse the old space. + } else { + this->PutUns32 ( (appendedOrigin + appendedLength), &currTag.smallValue ); // Set the appended offset. + appendedLength += ((currTag.dataLen + 1) & 0xFFFFFFFEUL); // Round to an even size. + } + + } + + } + + // If the Exif, GPS, or Interoperability IFDs get appended, update the tag values for their new offsets. + + if ( appendedIFDs[kTIFF_ExifIFD] ) { + this->SetTag_Long ( kTIFF_PrimaryIFD, kTIFF_ExifIFDPointer, newIFDOffsets[kTIFF_ExifIFD] ); + } + if ( appendedIFDs[kTIFF_GPSInfoIFD] ) { + this->SetTag_Long ( kTIFF_PrimaryIFD, kTIFF_GPSInfoIFDPointer, newIFDOffsets[kTIFF_GPSInfoIFD] ); + } + if ( appendedIFDs[kTIFF_InteropIFD] ) { + this->SetTag_Long ( kTIFF_ExifIFD, kTIFF_InteroperabilityIFDPointer, newIFDOffsets[kTIFF_InteropIFD] ); + } + + #if Trace_DetermineAppendInfo + { + printf ( "Exiting TIFF_FileWriter::DetermineAppendInfo\n" ); + for ( int ifd = 0; ifd < kTIFF_KnownIFDCount; ++ifd ) { + InternalIFDInfo & thisIFD = this->containedIFDs[ifd]; + printf ( "\n IFD %d, origCount %d, map.size %d, origIFDOffset %d (0x%X), origNextIFD %d (0x%X)", + ifd, thisIFD.origCount, thisIFD.tagMap.size(), + thisIFD.origIFDOffset, thisIFD.origIFDOffset, thisIFD.origNextIFD, thisIFD.origNextIFD ); + if ( thisIFD.changed ) printf ( ", changed" ); + if ( appendedIFDs[ifd] ) printf ( ", will be appended at %d (0x%X)", newIFDOffsets[ifd], newIFDOffsets[ifd] ); + printf ( "\n" ); + InternalTagMap::iterator tagPos; + InternalTagMap::iterator tagEnd = thisIFD.tagMap.end(); + for ( tagPos = thisIFD.tagMap.begin(); tagPos != tagEnd; ++tagPos ) { + InternalTagInfo & thisTag = tagPos->second; + printf ( " Tag %d, smallValue 0x%X, origDataLen %d, origDataOffset %d (0x%X)", + thisTag.id, thisTag.smallValue, thisTag.origDataLen, thisTag.origDataOffset, thisTag.origDataOffset ); + if ( thisTag.changed ) printf ( ", changed" ); + if ( (thisTag.dataLen > thisTag.origDataLen) && (thisTag.dataLen > 4) ) { + XMP_Uns32 newOffset = this->GetUns32 ( &thisTag.smallValue ); + printf ( ", will be appended at %d (0x%X)", newOffset, newOffset ); + } + printf ( "\n" ); + } + } + printf ( "\n" ); + } + #endif + + return appendedLength; + +} // TIFF_FileWriter::DetermineAppendInfo + +// ================================================================================================= +// TIFF_FileWriter::UpdateMemByAppend +// ================================== +// +// Normally we update TIFF in a conservative "by-append" manner. Changes are written in-place where +// they fit, anything requiring growth is appended to the end and the old space is abandoned. The +// end for memory-based TIFF is the end of the data block, the end for file-based TIFF is the end of +// the file. This update-by-append model has the advantage of not perturbing any hidden offsets, a +// common feature of proprietary MakerNotes. +// +// When doing the update-by-append we're only going to be modifying things that have changed. This +// means IFDs with changed, added, or deleted tags, and large values for changed or added tags. The +// IFDs and tag values are updated in-place if they fit, leaving holes in the stream if the new +// value is smaller than the old. + +// ** Someday we might want to use the FreeOffsets and FreeByteCounts tags to track free space. +// ** Probably not a huge win in practice though, and the TIFF spec says they are not recommended +// ** for general interchange use. + +void TIFF_FileWriter::UpdateMemByAppend ( XMP_Uns8** newStream_out, XMP_Uns32* newLength_out, + bool appendAll /* = false */, XMP_Uns32 extraSpace /* = 0 */ ) +{ + bool appendedIFDs[kTIFF_KnownIFDCount]; + XMP_Uns32 newIFDOffsets[kTIFF_KnownIFDCount]; + XMP_Uns32 appendedOrigin = ((this->tiffLength + 1) & 0xFFFFFFFEUL); // Start at an even offset. + XMP_Uns32 appendedLength = DetermineAppendInfo ( appendedOrigin, appendedIFDs, newIFDOffsets, appendAll ); + + // Allocate the new block of memory for the full stream. Copy the original stream. Write the + // modified IFDs and values. Finally rebuild the internal IFD info and tag map. + + XMP_Uns32 newLength = appendedOrigin + appendedLength; + XMP_Uns8* newStream = (XMP_Uns8*) malloc ( newLength + extraSpace ); + if ( newStream == 0 ) XMP_Throw ( "Out of memory", kXMPErr_NoMemory ); + + memcpy ( newStream, this->memStream, this->tiffLength ); // AUDIT: Safe, malloc'ed newLength bytes above. + if ( this->tiffLength < appendedOrigin ) { + XMP_Assert ( appendedOrigin == (this->tiffLength + 1) ); + newStream[this->tiffLength] = 0; // Clear the pad byte. + } + + try { // We might get exceptions from the next part and must delete newStream on the way out. + + // Write the modified IFDs and values. Rewrite the full IFD from scratch to make sure the + // tags are now unique and sorted. Copy large changed values to their appropriate location. + + XMP_Uns32 appendedOffset = appendedOrigin; + + for ( int ifd = 0; ifd < kTIFF_KnownIFDCount; ++ifd ) { + + InternalIFDInfo& ifdInfo ( this->containedIFDs[ifd] ); + size_t tagCount = ifdInfo.tagMap.size(); + + if ( ! (appendAll | ifdInfo.changed) ) continue; + if ( tagCount == 0 ) continue; + + XMP_Uns8* ifdPtr = newStream + newIFDOffsets[ifd]; + + if ( appendedIFDs[ifd] ) { + XMP_Assert ( newIFDOffsets[ifd] == appendedOffset ); + appendedOffset += (XMP_Uns32)( 6 + (12 * tagCount) ); + } + + this->PutUns16 ( (XMP_Uns16)tagCount, ifdPtr ); + ifdPtr += 2; + + InternalTagMap::iterator tagPos = ifdInfo.tagMap.begin(); + InternalTagMap::iterator tagEnd = ifdInfo.tagMap.end(); + + for ( ; tagPos != tagEnd; ++tagPos ) { + + InternalTagInfo & currTag ( tagPos->second ); + + this->PutUns16 ( currTag.id, ifdPtr ); + ifdPtr += 2; + this->PutUns16 ( currTag.type, ifdPtr ); + ifdPtr += 2; + this->PutUns32 ( currTag.count, ifdPtr ); + ifdPtr += 4; + + *((XMP_Uns32*)ifdPtr) = currTag.smallValue; + + if ( (appendAll | currTag.changed) && (currTag.dataLen > 4) ) { + + XMP_Uns32 valueOffset = this->GetUns32 ( &currTag.smallValue ); + + if ( (currTag.dataLen <= currTag.origDataLen) && (! appendAll) ) { + XMP_Assert ( valueOffset == currTag.origDataOffset ); + } else { + XMP_Assert ( valueOffset == appendedOffset ); + appendedOffset += ((currTag.dataLen + 1) & 0xFFFFFFFEUL); + } + + XMP_Assert ( valueOffset <= newLength ); // Provably true, valueOffset is in the old span, newLength is the new bigger span. + if ( currTag.dataLen > (newLength - valueOffset) ) XMP_Throw ( "Buffer overrun", kXMPErr_InternalFailure ); + memcpy ( (newStream + valueOffset), currTag.dataPtr, currTag.dataLen ); // AUDIT: Protected by the above check. + if ( (currTag.dataLen & 1) != 0 ) newStream[valueOffset+currTag.dataLen] = 0; + + } + + ifdPtr += 4; + + } + + this->PutUns32 ( ifdInfo.origNextIFD, ifdPtr ); + ifdPtr += 4; + + } + + XMP_Assert ( appendedOffset == newLength ); + + // Back fill the offsets for the primary and thumnbail IFDs, if they are now appended. + + if ( appendedIFDs[kTIFF_PrimaryIFD] ) { + this->PutUns32 ( newIFDOffsets[kTIFF_PrimaryIFD], (newStream + 4) ); + } + + } catch ( ... ) { + + free ( newStream ); + throw; + + } + + *newStream_out = newStream; + *newLength_out = newLength; + +} // TIFF_FileWriter::UpdateMemByAppend + +// ================================================================================================= +// TIFF_FileWriter::UpdateMemByRewrite +// =================================== +// +// Normally we update TIFF in a conservative "by-append" manner. Changes are written in-place where +// they fit, anything requiring growth is appended to the end and the old space is abandoned. The +// end for memory-based TIFF is the end of the data block, the end for file-based TIFF is the end of +// the file. This update-by-append model has the advantage of not perturbing any hidden offsets, a +// common feature of proprietary MakerNotes. +// +// The condenseStream parameter can be used to rewrite the full stream instead of appending. This +// will discard any MakerNote tag and risks breaking offsets that are hidden. This can be necessary +// though to try to make the TIFF fit in a JPEG file. +// +// We don't do most of the actual rewrite here. We set things up so that UpdateMemByAppend can be +// called to append onto a bare TIFF header. Additional hidden offsets are then handled here. +// +// These tags are recognized as being hidden offsets when composing a condensed stream: +// 273 - StripOffsets, lengths in tag 279 +// 288 - FreeOffsets, lengths in tag 289 +// 324 - TileOffsets, lengths in tag 325 +// 330 - SubIFDs, lengths within the IFDs (Plus subIFD values and possible chaining!) +// 513 - JPEGInterchangeFormat, length in tag 514 +// 519 - JPEGQTables, each table is 64 bytes +// 520 - JPEGDCTables, lengths ??? +// 521 - JPEGACTables, lengths ??? +// Some of these will handled and kept, some will be thrown out, some will cause the rewrite to fail. +// +// The hidden offsets for the Exif, GPS, and Interoperability IFDs (tags 34665, 34853, and 40965) +// are handled by the code in DetermineAppendInfo, which is called from UpdateMemByAppend, which is +// called from here. + +// ! So far, a memory-based TIFF rewrite would only be done for the Exif portion of a JPEG file. +// ! In which case we're probably OK to handle JPEGInterchangeFormat (used for compressed thumbnails) +// ! and complain about any of the other hidden offset tags. + +// tag count type + +// 273 n short or long +// 279 n short or long +// 288 n long +// 289 n long +// 324 n long +// 325 n short or long + +// 330 n long + +// 513 1 long +// 514 1 long + +// 519 n long +// 520 n long +// 521 n long + +static XMP_Uns16 kNoGoTags[] = + { + kTIFF_StripOffsets, // 273 *** Should be handled? + kTIFF_StripByteCounts, // 279 *** Should be handled? + kTIFF_FreeOffsets, // 288 *** Should be handled? + kTIFF_FreeByteCounts, // 289 *** Should be handled? + kTIFF_TileOffsets, // 324 *** Should be handled? + kTIFF_TileByteCounts, // 325 *** Should be handled? + kTIFF_SubIFDs, // 330 *** Should be handled? + kTIFF_JPEGQTables, // 519 + kTIFF_JPEGDCTables, // 520 + kTIFF_JPEGACTables, // 521 + 0xFFFF // Must be last as a sentinel. + }; + +static XMP_Uns16 kBanishedTags[] = + { + kTIFF_MakerNote, // *** Should someday support MakerNoteSafety. + 0xFFFF // Must be last as a sentinel. + }; + +struct SimpleHiddenContentInfo { + XMP_Uns8 ifd; + XMP_Uns16 offsetTag, lengthTag; +}; + +struct SimpleHiddenContentLocations { + XMP_Uns32 length, oldOffset, newOffset; + SimpleHiddenContentLocations() : length(0), oldOffset(0), newOffset(0) {}; +}; + +enum { kSimpleHiddenContentCount = 1 }; + +static const SimpleHiddenContentInfo kSimpleHiddenContentInfo [kSimpleHiddenContentCount] = + { + { kTIFF_TNailIFD, kTIFF_JPEGInterchangeFormat, kTIFF_JPEGInterchangeFormatLength } + }; + +// ------------------------------------------------------------------------------------------------- + +void TIFF_FileWriter::UpdateMemByRewrite ( XMP_Uns8** newStream_out, XMP_Uns32* newLength_out ) +{ + const InternalTagInfo* tagInfo; + + // Check for tags that we don't tolerate because they have data we can't (or refuse to) find. + + for ( XMP_Uns8 ifd = 0; ifd < kTIFF_KnownIFDCount; ++ifd ) { + for ( int i = 0; kNoGoTags[i] != 0xFFFF; ++i ) { + tagInfo = this->FindTagInIFD ( ifd, kNoGoTags[i] ); + if ( tagInfo != 0 ) XMP_Throw ( "Tag not tolerated for TIFF rewrite", kXMPErr_Unimplemented ); + } + } + + // Delete unwanted tags. + + for ( XMP_Uns8 ifd = 0; ifd < kTIFF_KnownIFDCount; ++ifd ) { + for ( int i = 0; kBanishedTags[i] != 0xFFFF; ++i ) { + this->DeleteTag ( ifd, kBanishedTags[i] ); + } + } + + // Determine the offsets and additional size for the hidden offset content. Set the offset tags + // to the new offset. + + XMP_Uns32 hiddenContentLength = 0; + XMP_Uns32 hiddenContentOrigin = this->DetermineVisibleLength(); + + SimpleHiddenContentLocations hiddenLocations [kSimpleHiddenContentCount]; + + for ( int i = 0; i < kSimpleHiddenContentCount; ++i ) { + + const SimpleHiddenContentInfo & hiddenInfo ( kSimpleHiddenContentInfo[i] ); + + bool haveLength = this->GetTag_Integer ( hiddenInfo.ifd, hiddenInfo.lengthTag, &hiddenLocations[i].length ); + bool haveOffset = this->GetTag_Integer ( hiddenInfo.ifd, hiddenInfo.offsetTag, &hiddenLocations[i].oldOffset ); + if ( haveLength != haveOffset ) XMP_Throw ( "Unpaired simple hidden content tag", kXMPErr_BadTIFF ); + if ( (! haveLength) || (hiddenLocations[i].length == 0) ) continue; + + hiddenLocations[i].newOffset = hiddenContentOrigin + hiddenContentLength; + this->SetTag_Long ( hiddenInfo.ifd, hiddenInfo.offsetTag, hiddenLocations[i].newOffset ); + hiddenContentLength += ((hiddenLocations[i].length + 1) & 0xFFFFFFFE); // ! Round up for even offsets. + + } + + // Save any old memory stream for the content behind hidden offsets. Setup a bare TIFF header. + + XMP_Uns8* oldStream = this->memStream; + + XMP_Uns8 bareTIFF [8]; + if ( this->bigEndian ) { + bareTIFF[0] = 0x4D; bareTIFF[1] = 0x4D; bareTIFF[2] = 0x00; bareTIFF[3] = 0x2A; + } else { + bareTIFF[0] = 0x49; bareTIFF[1] = 0x49; bareTIFF[2] = 0x2A; bareTIFF[3] = 0x00; + } + *((XMP_Uns32*)&bareTIFF[4]) = 0; + + this->memStream = &bareTIFF[0]; + this->tiffLength = sizeof ( bareTIFF ); + this->ownedStream = false; + + // Call UpdateMemByAppend to write the new stream, telling it to append everything. + + this->UpdateMemByAppend ( newStream_out, newLength_out, true, hiddenContentLength ); + + // Copy the hidden content and update the output stream length; + + XMP_Assert ( *newLength_out == hiddenContentOrigin ); + *newLength_out += hiddenContentLength; + + for ( int i = 0; i < kSimpleHiddenContentCount; ++i ) { + + if ( hiddenLocations[i].length == 0 ) continue; + + XMP_Uns8* srcPtr = oldStream + hiddenLocations[i].oldOffset; + XMP_Uns8* destPtr = *newStream_out + hiddenLocations[i].newOffset; + memcpy ( destPtr, srcPtr, hiddenLocations[i].length ); // AUDIT: Safe copy, not user data, computed length. + + } + +} // TIFF_FileWriter::UpdateMemByRewrite + +// ================================================================================================= +// TIFF_FileWriter::UpdateMemoryStream +// =================================== +// +// Normally we update TIFF in a conservative "by-append" manner. Changes are written in-place where +// they fit, anything requiring growth is appended to the end and the old space is abandoned. The +// end for memory-based TIFF is the end of the data block, the end for file-based TIFF is the end of +// the file. This update-by-append model has the advantage of not perturbing any hidden offsets, a +// common feature of proprietary MakerNotes. +// +// The condenseStream parameter can be used to rewrite the full stream instead of appending. This +// will discard any MakerNote tags and risks breaking offsets that are hidden. This can be necessary +// though to try to make the TIFF fit in a JPEG file. + +XMP_Uns32 TIFF_FileWriter::UpdateMemoryStream ( void** dataPtr, bool condenseStream /* = false */ ) +{ + if ( this->fileParsed ) XMP_Throw ( "Not memory based", kXMPErr_EnforceFailure ); + + if ( ! this->changed ) { + if ( dataPtr != 0 ) *dataPtr = this->memStream; + return this->tiffLength; + } + + this->PreflightIFDLinkage(); + + bool nowEmpty = true; + for ( size_t i = 0; i < kTIFF_KnownIFDCount; ++i ) { + if ( ! this->containedIFDs[i].tagMap.empty() ) { + nowEmpty = false; + break; + } + } + + XMP_Uns8* newStream = 0; + XMP_Uns32 newLength = 0; + + if ( nowEmpty ) { + + this->DeleteExistingInfo(); // Prepare for an empty reparse. + + } else { + + if ( this->tiffLength == 0 ) { // ! An empty parse does set this->memParsed. + condenseStream = true; // Makes "conjured" TIFF take the full rewrite path. + } + + if ( condenseStream ) this->changed = true; // A prior regular call would have cleared this->changed. + + if ( condenseStream ) { + this->UpdateMemByRewrite ( &newStream, &newLength ); + } else { + this->UpdateMemByAppend ( &newStream, &newLength ); + } + + } + + // Parse the revised stream. This is the cleanest way to rebuild the tag map. + + this->ParseMemoryStream ( newStream, newLength, kDoNotCopyData ); + XMP_Assert ( this->tiffLength == newLength ); + this->ownedStream = (newLength > 0); // ! We really do own the new stream, if not empty. + + if ( dataPtr != 0 ) *dataPtr = this->memStream; + return newLength; + +} // TIFF_FileWriter::UpdateMemoryStream + +// ================================================================================================= +// TIFF_FileWriter::UpdateFileStream +// ================================= +// +// Updating a file stream is done in the same general manner as updating a memory stream, the intro +// comments for UpdateMemoryStream largely apply. The file update happens in 3 phases: +// 1. Determine which IFDs will be appended, and the new offsets for the appended IFDs and tags. +// 2. Do the in-place update for the IFDs and tag values that fit. +// 3. Append the IFDs and tag values that grow. +// +// The file being updated must match the file that was previously parsed. Offsets and lengths saved +// when parsing are used to decide if something can be updated in-place or must be appended. + +// *** The general linked structure of TIFF makes it very difficult to process the file in a single +// *** sequential pass. This implementation uses a simple seek/write model for the in-place updates. +// *** In the future we might want to consider creating a map of what gets updated, allowing use of +// *** a possibly more efficient buffered model. + +// ** Someday we might want to use the FreeOffsets and FreeByteCounts tags to track free space. +// ** Probably not a huge win in practice though, and the TIFF spec says they are not recommended +// ** for general interchange use. + +#ifndef Trace_UpdateFileStream + #define Trace_UpdateFileStream 0 +#endif + +void TIFF_FileWriter::UpdateFileStream ( XMP_IO* fileRef ) +{ + if ( this->memParsed ) XMP_Throw ( "Not file based", kXMPErr_EnforceFailure ); + if ( ! this->changed ) return; + + XMP_Int64 origDataLength = fileRef->Length(); + if ( (origDataLength >> 32) != 0 ) XMP_Throw ( "TIFF files can't exceed 4GB", kXMPErr_BadTIFF ); + + bool appendedIFDs[kTIFF_KnownIFDCount]; + XMP_Uns32 newIFDOffsets[kTIFF_KnownIFDCount]; + + #if Trace_UpdateFileStream + printf ( "\nStarting update of TIFF file stream\n" ); + #endif + + XMP_Uns32 appendedOrigin = (XMP_Uns32)origDataLength; + if ( (appendedOrigin & 1) != 0 ) { + ++appendedOrigin; // Start at an even offset. + fileRef->Seek ( 0, kXMP_SeekFromEnd ); + fileRef->Write ( "\0", 1 ); + } + + this->PreflightIFDLinkage(); + + XMP_Uns32 appendedLength = DetermineAppendInfo ( appendedOrigin, appendedIFDs, newIFDOffsets ); + if ( appendedLength > (0xFFFFFFFFUL - appendedOrigin) ) XMP_Throw ( "TIFF files can't exceed 4GB", kXMPErr_BadTIFF ); + + // Do the in-place update for the IFDs and tag values that fit. This part does separate seeks + // and writes for the IFDs and values. Things to be updated can be anywhere in the file. + + // *** This might benefit from a map of the in-place updates. This would allow use of a possibly + // *** more efficient sequential I/O model. Could even incorporate the safe update file copying. + + for ( int ifd = 0; ifd < kTIFF_KnownIFDCount; ++ifd ) { + + InternalIFDInfo & thisIFD = this->containedIFDs[ifd]; + if ( ! thisIFD.changed ) continue; + + // In order to get a little bit of locality, write the IFD first then the changed tags that + // have large values and fit in-place. + + if ( ! appendedIFDs[ifd] ) { + #if Trace_UpdateFileStream + printf ( " Updating IFD %d in-place at offset %d (0x%X)\n", ifd, thisIFD.origIFDOffset, thisIFD.origIFDOffset ); + #endif + fileRef->Seek ( thisIFD.origIFDOffset, kXMP_SeekFromStart ); + this->WriteFileIFD ( fileRef, thisIFD ); + } + + InternalTagMap::iterator tagPos; + InternalTagMap::iterator tagEnd = thisIFD.tagMap.end(); + + for ( tagPos = thisIFD.tagMap.begin(); tagPos != tagEnd; ++tagPos ) { + InternalTagInfo & thisTag = tagPos->second; + if ( (! thisTag.changed) || (thisTag.dataLen <= 4) || (thisTag.dataLen > thisTag.origDataLen) ) continue; + #if Trace_UpdateFileStream + printf ( " Updating tag %d in IFD %d in-place at offset %d (0x%X)\n", thisTag.id, ifd, thisTag.origDataOffset, thisTag.origDataOffset ); + #endif + fileRef->Seek ( thisTag.origDataOffset, kXMP_SeekFromStart ); + fileRef->Write ( thisTag.dataPtr, thisTag.dataLen ); + } + + } + + // Append the IFDs and tag values that grow. + + XMP_Int64 fileEnd = fileRef->Seek ( 0, kXMP_SeekFromEnd ); + XMP_Assert ( fileEnd == appendedOrigin ); + + for ( int ifd = 0; ifd < kTIFF_KnownIFDCount; ++ifd ) { + + InternalIFDInfo & thisIFD = this->containedIFDs[ifd]; + if ( ! thisIFD.changed ) continue; + + if ( appendedIFDs[ifd] ) { + #if Trace_UpdateFileStream + printf ( " Updating IFD %d by append at offset %d (0x%X)\n", ifd, newIFDOffsets[ifd], newIFDOffsets[ifd] ); + #endif + XMP_Assert ( newIFDOffsets[ifd] == fileRef->Length() ); + this->WriteFileIFD ( fileRef, thisIFD ); + } + + InternalTagMap::iterator tagPos; + InternalTagMap::iterator tagEnd = thisIFD.tagMap.end(); + + for ( tagPos = thisIFD.tagMap.begin(); tagPos != tagEnd; ++tagPos ) { + InternalTagInfo & thisTag = tagPos->second; + if ( (! thisTag.changed) || (thisTag.dataLen <= 4) || (thisTag.dataLen <= thisTag.origDataLen) ) continue; + #if Trace_UpdateFileStream + XMP_Uns32 newOffset = this->GetUns32(&thisTag.origDataOffset); + printf ( " Updating tag %d in IFD %d by append at offset %d (0x%X)\n", thisTag.id, ifd, newOffset, newOffset ); + #endif + XMP_Assert ( this->GetUns32(&thisTag.smallValue) == fileRef->Length() ); + fileRef->Write ( thisTag.dataPtr, thisTag.dataLen ); + if ( (thisTag.dataLen & 1) != 0 ) fileRef->Write ( "\0", 1 ); + } + + } + + // Back-fill the offset for the primary IFD, if it is now appended. + + XMP_Uns32 newOffset; + + if ( appendedIFDs[kTIFF_PrimaryIFD] ) { + this->PutUns32 ( newIFDOffsets[kTIFF_PrimaryIFD], &newOffset ); + #if TraceUpdateFileStream + printf ( " Back-filling offset of primary IFD, pointing to %d (0x%X)\n", newOffset, newOffset ); + #endif + fileRef->Seek ( 4, kXMP_SeekFromStart ); + fileRef->Write ( &newOffset, 4 ); + } + + // Reset the changed flags and original length/offset values. This simulates a reparse of the + // updated file. + + for ( int ifd = 0; ifd < kTIFF_KnownIFDCount; ++ifd ) { + + InternalIFDInfo & thisIFD = this->containedIFDs[ifd]; + if ( ! thisIFD.changed ) continue; + + thisIFD.changed = false; + thisIFD.origCount = (XMP_Uns16)( thisIFD.tagMap.size() ); + thisIFD.origIFDOffset = newIFDOffsets[ifd]; + + InternalTagMap::iterator tagPos; + InternalTagMap::iterator tagEnd = thisIFD.tagMap.end(); + + for ( tagPos = thisIFD.tagMap.begin(); tagPos != tagEnd; ++tagPos ) { + InternalTagInfo & thisTag = tagPos->second; + if ( ! thisTag.changed ) continue; + thisTag.changed = false; + thisTag.origDataLen = thisTag.dataLen; + if ( thisTag.origDataLen > 4 ) thisTag.origDataOffset = this->GetUns32 ( &thisTag.smallValue ); + } + + } + + this->tiffLength = (XMP_Uns32) fileRef->Length(); + fileRef->Seek ( 0, kXMP_SeekFromEnd ); // Can't hurt. + + #if Trace_UpdateFileStream + printf ( "\nFinished update of TIFF file stream\n" ); + #endif + +} // TIFF_FileWriter::UpdateFileStream + +// ================================================================================================= +// TIFF_FileWriter::WriteFileIFD +// ============================= + +void TIFF_FileWriter::WriteFileIFD ( XMP_IO* fileRef, InternalIFDInfo & thisIFD ) +{ + XMP_Uns16 tagCount; + this->PutUns16 ( (XMP_Uns16)thisIFD.tagMap.size(), &tagCount ); + fileRef->Write ( &tagCount, 2 ); + + InternalTagMap::iterator tagPos; + InternalTagMap::iterator tagEnd = thisIFD.tagMap.end(); + + for ( tagPos = thisIFD.tagMap.begin(); tagPos != tagEnd; ++tagPos ) { + + InternalTagInfo & thisTag = tagPos->second; + RawIFDEntry ifdEntry; + + this->PutUns16 ( thisTag.id, &ifdEntry.id ); + this->PutUns16 ( thisTag.type, &ifdEntry.type ); + this->PutUns32 ( thisTag.count, &ifdEntry.count ); + ifdEntry.dataOrOffset = thisTag.smallValue; // ! Already in stream endianness. + + fileRef->Write ( &ifdEntry, sizeof(ifdEntry) ); + XMP_Assert ( sizeof(ifdEntry) == 12 ); + + } + + XMP_Uns32 nextIFD; + this->PutUns32 ( thisIFD.origNextIFD, &nextIFD ); + fileRef->Write ( &nextIFD, 4 ); + +} // TIFF_FileWriter::WriteFileIFD diff --git a/XMPFiles/source/FormatSupport/TIFF_MemoryReader.cpp b/XMPFiles/source/FormatSupport/TIFF_MemoryReader.cpp new file mode 100644 index 0000000..3e96620 --- /dev/null +++ b/XMPFiles/source/FormatSupport/TIFF_MemoryReader.cpp @@ -0,0 +1,645 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/TIFF_Support.hpp" +#include "source/XIO.hpp" + +// ================================================================================================= +/// \file TIFF_MemoryReader.cpp +/// \brief Implementation of the memory-based read-only TIFF_Manager. +/// +/// The read-only forms of TIFF_Manager are derived from TIFF_Reader. The GetTag methods are common +/// implementations in TIFF_Reader. The parsing code is different in the TIFF_MemoryReader and +/// TIFF_FileReader constructors. There are also separate destructors to release captured info. +/// +/// The read-only implementations use runtime data that is simple tweaks on the stored form. The +/// memory-based reader has one block of data for the whole TIFF stream. The file-based reader has +/// one for each IFD, plus one for the collected non-local data for each IFD. Otherwise the logic +/// is the same in both cases. +/// +/// The count for each IFD is extracted and a pointer set to the first entry in each IFD (serving as +/// a normal C array pointer). The IFD entries are tweaked as follows: +/// +/// \li The id and type fields are converted to native values. +/// \li The count field is converted to a native byte count. +/// \li If the data is not inline the offset is converted to a pointer. +/// +/// The tag values, whether inline or not, are not converted to native values. The values returned +/// from the GetTag methods are converted on the fly. The id, type, and count fields are easier to +/// convert because their types are fixed. They are used more, and more valuable to convert. +// ================================================================================================= + +// ================================================================================================= +// TIFF_MemoryReader::SortIFD +// ========================== +// +// Does a fairly simple minded insertion-like sort. This sort is not going to be optimal for edge +// cases like and IFD with lots of duplicates. + +// *** Might be better done using read and write pointers and two loops. The first loop moves out +// *** of order tags by a modified bubble sort, shifting the middle down one at a time in the loop. +// *** The first loop stops when a duplicate is hit. The second loop continues by moving the tail +// *** entries up to the appropriate slot. + +void TIFF_MemoryReader::SortIFD ( TweakedIFDInfo* thisIFD ) +{ + + XMP_Uns16 tagCount = thisIFD->count; + TweakedIFDEntry* ifdEntries = thisIFD->entries; + XMP_Uns16 prevTag = ifdEntries[0].id; + + for ( size_t i = 1; i < tagCount; ++i ) { + + XMP_Uns16 thisTag = ifdEntries[i].id; + + if ( thisTag > prevTag ) { + + // In proper order. + prevTag = thisTag; + + } else if ( thisTag == prevTag ) { + + // Duplicate tag, keep the 2nd copy, move the tail of the array up, prevTag is unchanged. + memcpy ( &ifdEntries[i-1], &ifdEntries[i], 12*(tagCount-i) ); // AUDIT: Safe, moving tail forward, i >= 1. + --tagCount; + --i; // ! Don't move forward in the array, we've moved the unseen part up. + + } else if ( thisTag < prevTag ) { + + // Out of order, move this tag up, prevTag is unchanged. Might still be a duplicate! + XMP_Int32 j; // ! Need a signed value. + for ( j = (XMP_Int32)i-1; j >= 0; --j ) { + if ( ifdEntries[j].id <= thisTag ) break; + } + + if ( (j >= 0) && (ifdEntries[j].id == thisTag) ) { + + // Out of order duplicate, move it to position j, move the tail of the array up. + ifdEntries[j] = ifdEntries[i]; + memcpy ( &ifdEntries[i], &ifdEntries[i+1], 12*(tagCount-(i+1)) ); // AUDIT: Safe, moving tail forward, i >= 1. + --tagCount; + --i; // ! Don't move forward in the array, we've moved the unseen part up. + + } else { + + // Move the out of order entry to position j+1, move the middle of the array down. + TweakedIFDEntry temp = ifdEntries[i]; + ++j; // ! So the insertion index becomes j. + memcpy ( &ifdEntries[j+1], &ifdEntries[j], 12*(i-j) ); // AUDIT: Safe, moving less than i entries to a location before i. + ifdEntries[j] = temp; + + } + + } + + } + + thisIFD->count = tagCount; // Save the final count. + +} // TIFF_MemoryReader::SortIFD + +// ================================================================================================= +// TIFF_MemoryReader::GetIFD +// ========================= + +bool TIFF_MemoryReader::GetIFD ( XMP_Uns8 ifd, TagInfoMap* ifdMap ) const +{ + if ( ifd > kTIFF_LastRealIFD ) XMP_Throw ( "Invalid IFD requested", kXMPErr_InternalFailure ); + const TweakedIFDInfo* thisIFD = &containedIFDs[ifd]; + + if ( ifdMap != 0 ) ifdMap->clear(); + if ( thisIFD->count == 0 ) return false; + + if ( ifdMap != 0 ) { + + for ( size_t i = 0; i < thisIFD->count; ++i ) { + + TweakedIFDEntry* thisTag = &(thisIFD->entries[i]); + if ( (thisTag->type < kTIFF_ByteType) || (thisTag->type > kTIFF_LastType) ) continue; // Bad type, skip this tag. + + TagInfo info ( thisTag->id, thisTag->type, 0, 0, thisTag->bytes ); + info.count = info.dataLen / (XMP_Uns32)kTIFF_TypeSizes[info.type]; + info.dataPtr = this->GetDataPtr ( thisTag ); + + (*ifdMap)[info.id] = info; + + } + + } + + return true; + +} // TIFF_MemoryReader::GetIFD + +// ================================================================================================= +// TIFF_MemoryReader::FindTagInIFD +// =============================== + +const TIFF_MemoryReader::TweakedIFDEntry* TIFF_MemoryReader::FindTagInIFD ( XMP_Uns8 ifd, XMP_Uns16 id ) const +{ + if ( ifd == kTIFF_KnownIFD ) { + // ... lookup the tag in the known tag map + } + + if ( ifd > kTIFF_LastRealIFD ) XMP_Throw ( "Invalid IFD requested", kXMPErr_InternalFailure ); + const TweakedIFDInfo* thisIFD = &containedIFDs[ifd]; + + if ( thisIFD->count == 0 ) return 0; + + XMP_Uns32 spanLength = thisIFD->count; + const TweakedIFDEntry* spanBegin = &(thisIFD->entries[0]); + + while ( spanLength > 1 ) { + + XMP_Uns32 halfLength = spanLength >> 1; // Since spanLength > 1, halfLength > 0. + const TweakedIFDEntry* spanMiddle = spanBegin + halfLength; + + // There are halfLength entries below spanMiddle, then the spanMiddle entry, then + // spanLength-halfLength-1 entries above spanMiddle (which can be none). + + if ( spanMiddle->id == id ) { + spanBegin = spanMiddle; + break; + } else if ( spanMiddle->id > id ) { + spanLength = halfLength; // Discard the middle. + } else { + spanBegin = spanMiddle; // Keep a valid spanBegin for the return check, don't use spanMiddle+1. + spanLength -= halfLength; + } + + } + + if ( spanBegin->id != id ) spanBegin = 0; + return spanBegin; + +} // TIFF_MemoryReader::FindTagInIFD + +// ================================================================================================= +// TIFF_MemoryReader::GetValueOffset +// ================================= + +XMP_Uns32 TIFF_MemoryReader::GetValueOffset ( XMP_Uns8 ifd, XMP_Uns16 id ) const +{ + const TweakedIFDEntry* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return 0; + + XMP_Uns8 * valuePtr = (XMP_Uns8*) this->GetDataPtr ( thisTag ); + + return (XMP_Uns32)(valuePtr - this->tiffStream); // ! TIFF streams can't exceed 4GB. + +} // TIFF_MemoryReader::GetValueOffset + +// ================================================================================================= +// TIFF_MemoryReader::GetTag +// ========================= + +bool TIFF_MemoryReader::GetTag ( XMP_Uns8 ifd, XMP_Uns16 id, TagInfo* info ) const +{ + const TweakedIFDEntry* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type < kTIFF_ByteType) || (thisTag->type > kTIFF_LastType) ) return false; // Bad type, skip this tag. + + if ( info != 0 ) { + + info->id = thisTag->id; + info->type = thisTag->type; + info->count = thisTag->bytes / (XMP_Uns32)kTIFF_TypeSizes[thisTag->type]; + info->dataLen = thisTag->bytes; + + info->dataPtr = this->GetDataPtr ( thisTag ); + + } + + return true; + +} // TIFF_MemoryReader::GetTag + +// ================================================================================================= + +static inline XMP_Uns8 GetUns8 ( const void* dataPtr ) +{ + return *((XMP_Uns8*)dataPtr); +} + +// ================================================================================================= +// TIFF_MemoryReader::GetTag_Integer +// ================================= +// +// Tolerate any size or sign. + +bool TIFF_MemoryReader::GetTag_Integer ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns32* data ) const +{ + const TweakedIFDEntry* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + + if ( thisTag->type > kTIFF_LastType ) return false; // Unknown type. + if ( thisTag->bytes != kTIFF_TypeSizes[thisTag->type] ) return false; // Wrong count. + + XMP_Uns32 uns32; + XMP_Int32 int32; + + switch ( thisTag->type ) { + + case kTIFF_ByteType: + uns32 = GetUns8 ( this->GetDataPtr ( thisTag ) ); + break; + + case kTIFF_ShortType: + uns32 = this->GetUns16 ( this->GetDataPtr ( thisTag ) ); + break; + + case kTIFF_LongType: + uns32 = this->GetUns32 ( this->GetDataPtr ( thisTag ) ); + break; + + case kTIFF_SByteType: + int32 = (XMP_Int8) GetUns8 ( this->GetDataPtr ( thisTag ) ); + uns32 = (XMP_Uns32) int32; // Make sure sign bits are properly set. + break; + + case kTIFF_SShortType: + int32 = (XMP_Int16) this->GetUns16 ( this->GetDataPtr ( thisTag ) ); + uns32 = (XMP_Uns32) int32; // Make sure sign bits are properly set. + break; + + case kTIFF_SLongType: + int32 = (XMP_Int32) this->GetUns32 ( this->GetDataPtr ( thisTag ) ); + uns32 = (XMP_Uns32) int32; // Make sure sign bits are properly set. + break; + + default: return false; + + } + + if ( data != 0 ) *data = uns32; + return true; + +} // TIFF_MemoryReader::GetTag_Integer + +// ================================================================================================= +// TIFF_MemoryReader::GetTag_Byte +// ============================== + +bool TIFF_MemoryReader::GetTag_Byte ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns8* data ) const +{ + const TweakedIFDEntry* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_ByteType) || (thisTag->bytes != 1) ) return false; + + if ( data != 0 ) { + *data = * ( (XMP_Uns8*) this->GetDataPtr ( thisTag ) ); + } + + return true; + +} // TIFF_MemoryReader::GetTag_Byte + +// ================================================================================================= +// TIFF_MemoryReader::GetTag_SByte +// =============================== + +bool TIFF_MemoryReader::GetTag_SByte ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int8* data ) const +{ + const TweakedIFDEntry* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_SByteType) || (thisTag->bytes != 1) ) return false; + + if ( data != 0 ) { + *data = * ( (XMP_Int8*) this->GetDataPtr ( thisTag ) ); + } + + return true; + +} // TIFF_MemoryReader::GetTag_SByte + +// ================================================================================================= +// TIFF_MemoryReader::GetTag_Short +// =============================== + +bool TIFF_MemoryReader::GetTag_Short ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns16* data ) const +{ + const TweakedIFDEntry* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_ShortType) || (thisTag->bytes != 2) ) return false; + + if ( data != 0 ) { + *data = this->GetUns16 ( this->GetDataPtr ( thisTag ) ); + } + + return true; + +} // TIFF_MemoryReader::GetTag_Short + +// ================================================================================================= +// TIFF_MemoryReader::GetTag_SShort +// ================================ + +bool TIFF_MemoryReader::GetTag_SShort ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int16* data ) const +{ + const TweakedIFDEntry* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_SShortType) || (thisTag->bytes != 2) ) return false; + + if ( data != 0 ) { + *data = (XMP_Int16) this->GetUns16 ( this->GetDataPtr ( thisTag ) ); + } + + return true; + +} // TIFF_MemoryReader::GetTag_SShort + +// ================================================================================================= +// TIFF_MemoryReader::GetTag_Long +// ============================== + +bool TIFF_MemoryReader::GetTag_Long ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns32* data ) const +{ + const TweakedIFDEntry* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_LongType) || (thisTag->bytes != 4) ) return false; + + if ( data != 0 ) { + *data = this->GetUns32 ( this->GetDataPtr ( thisTag ) ); + } + + return true; + +} // TIFF_MemoryReader::GetTag_Long + +// ================================================================================================= +// TIFF_MemoryReader::GetTag_SLong +// =============================== + +bool TIFF_MemoryReader::GetTag_SLong ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int32* data ) const +{ + const TweakedIFDEntry* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_SLongType) || (thisTag->bytes != 4) ) return false; + + if ( data != 0 ) { + *data = (XMP_Int32) this->GetUns32 ( this->GetDataPtr ( thisTag ) ); + } + + return true; + +} // TIFF_MemoryReader::GetTag_SLong + +// ================================================================================================= +// TIFF_MemoryReader::GetTag_Rational +// ================================== + +bool TIFF_MemoryReader::GetTag_Rational ( XMP_Uns8 ifd, XMP_Uns16 id, Rational* data ) const +{ + const TweakedIFDEntry* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_RationalType) || (thisTag->bytes != 8) ) return false; + + if ( data != 0 ) { + XMP_Uns32* dataPtr = (XMP_Uns32*) this->GetDataPtr ( thisTag ); + data->num = this->GetUns32 ( dataPtr ); + data->denom = this->GetUns32 ( dataPtr+1 ); + } + + return true; + +} // TIFF_MemoryReader::GetTag_Rational + +// ================================================================================================= +// TIFF_MemoryReader::GetTag_SRational +// =================================== + +bool TIFF_MemoryReader::GetTag_SRational ( XMP_Uns8 ifd, XMP_Uns16 id, SRational* data ) const +{ + const TweakedIFDEntry* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_SRationalType) || (thisTag->bytes != 8) ) return false; + + if ( data != 0 ) { + XMP_Uns32* dataPtr = (XMP_Uns32*) this->GetDataPtr ( thisTag ); + data->num = (XMP_Int32) this->GetUns32 ( dataPtr ); + data->denom = (XMP_Int32) this->GetUns32 ( dataPtr+1 ); + } + + return true; + +} // TIFF_MemoryReader::GetTag_SRational + +// ================================================================================================= +// TIFF_MemoryReader::GetTag_Float +// =============================== + +bool TIFF_MemoryReader::GetTag_Float ( XMP_Uns8 ifd, XMP_Uns16 id, float* data ) const +{ + const TweakedIFDEntry* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_FloatType) || (thisTag->bytes != 4) ) return false; + + if ( data != 0 ) { + *data = this->GetFloat ( this->GetDataPtr ( thisTag ) ); + } + + return true; + +} // TIFF_MemoryReader::GetTag_Float + +// ================================================================================================= +// TIFF_MemoryReader::GetTag_Double +// ================================ + +bool TIFF_MemoryReader::GetTag_Double ( XMP_Uns8 ifd, XMP_Uns16 id, double* data ) const +{ + const TweakedIFDEntry* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( (thisTag->type != kTIFF_DoubleType) || (thisTag->bytes != 8) ) return false; + + if ( data != 0 ) { + double* dataPtr = (double*) this->GetDataPtr ( thisTag ); + *data = this->GetDouble ( dataPtr ); + } + + return true; + +} // TIFF_MemoryReader::GetTag_Double + +// ================================================================================================= +// TIFF_MemoryReader::GetTag_ASCII +// =============================== + +bool TIFF_MemoryReader::GetTag_ASCII ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_StringPtr* dataPtr, XMP_StringLen* dataLen ) const +{ + const TweakedIFDEntry* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( thisTag->type != kTIFF_ASCIIType ) return false; + + if ( dataPtr != 0 ) { + *dataPtr = (XMP_StringPtr) this->GetDataPtr ( thisTag ); + } + + if ( dataLen != 0 ) *dataLen = thisTag->bytes; + + return true; + +} // TIFF_MemoryReader::GetTag_ASCII + +// ================================================================================================= +// TIFF_MemoryReader::GetTag_EncodedString +// ======================================= + +bool TIFF_MemoryReader::GetTag_EncodedString ( XMP_Uns8 ifd, XMP_Uns16 id, std::string* utf8Str ) const +{ + const TweakedIFDEntry* thisTag = this->FindTagInIFD ( ifd, id ); + if ( thisTag == 0 ) return false; + if ( thisTag->type != kTIFF_UndefinedType ) return false; + + if ( utf8Str == 0 ) return true; // Return true if the converted string is not wanted. + + bool ok = this->DecodeString ( this->GetDataPtr ( thisTag ), thisTag->bytes, utf8Str ); + return ok; + +} // TIFF_MemoryReader::GetTag_EncodedString + +// ================================================================================================= +// TIFF_MemoryReader::ParseMemoryStream +// ==================================== + +// *** Need to tell TIFF/Exif from TIFF/EP, DNG files are the latter. + +void TIFF_MemoryReader::ParseMemoryStream ( const void* data, XMP_Uns32 length, bool copyData /* = true */ ) +{ + // Get rid of any current TIFF. + + if ( this->ownedStream ) free ( this->tiffStream ); + this->ownedStream = false; + this->tiffStream = 0; + this->tiffLength = 0; + + for ( size_t i = 0; i < kTIFF_KnownIFDCount; ++i ) { + this->containedIFDs[i].count = 0; + this->containedIFDs[i].entries = 0; + } + + if ( length == 0 ) return; + + // Allocate space for the full in-memory stream and copy it. + + if ( ! copyData ) { + XMP_Assert ( ! this->ownedStream ); + this->tiffStream = (XMP_Uns8*) data; + } else { + if ( length > 100*1024*1024 ) XMP_Throw ( "Outrageous length for memory-based TIFF", kXMPErr_BadTIFF ); + this->tiffStream = (XMP_Uns8*) malloc(length); + if ( this->tiffStream == 0 ) XMP_Throw ( "Out of memory", kXMPErr_NoMemory ); + memcpy ( this->tiffStream, data, length ); // AUDIT: Safe, malloc'ed length bytes above. + this->ownedStream = true; + } + + this->tiffLength = length; + XMP_Uns32 ifdLimit = this->tiffLength - 6; // An IFD must start before this offset. + + // Find and process the primary, Exif, GPS, and Interoperability IFDs. + + XMP_Uns32 primaryIFDOffset = this->CheckTIFFHeader ( this->tiffStream, length ); + XMP_Uns32 tnailIFDOffset = 0; + + if ( primaryIFDOffset != 0 ) tnailIFDOffset = this->ProcessOneIFD ( primaryIFDOffset, kTIFF_PrimaryIFD ); + + // ! Need the thumbnail IFD for checking full Exif APP1 in some JPEG files! + if ( tnailIFDOffset != 0 ) (void) this->ProcessOneIFD ( tnailIFDOffset, kTIFF_TNailIFD ); + + const TweakedIFDEntry* exifIFDTag = this->FindTagInIFD ( kTIFF_PrimaryIFD, kTIFF_ExifIFDPointer ); + if ( (exifIFDTag != 0) && (exifIFDTag->type == kTIFF_LongType) && (exifIFDTag->bytes == 4) ) { + XMP_Uns32 exifOffset = this->GetUns32 ( &exifIFDTag->dataOrPos ); + (void) this->ProcessOneIFD ( exifOffset, kTIFF_ExifIFD ); + } + + const TweakedIFDEntry* gpsIFDTag = this->FindTagInIFD ( kTIFF_PrimaryIFD, kTIFF_GPSInfoIFDPointer ); + if ( (gpsIFDTag != 0) && (gpsIFDTag->type == kTIFF_LongType) && (gpsIFDTag->bytes == 4) ) { + XMP_Uns32 gpsOffset = this->GetUns32 ( &gpsIFDTag->dataOrPos ); + if ( (8 <= gpsOffset) && (gpsOffset < ifdLimit) ) { // Ignore a bad GPS IFD offset. + (void) this->ProcessOneIFD ( gpsOffset, kTIFF_GPSInfoIFD ); + } + } + + const TweakedIFDEntry* interopIFDTag = this->FindTagInIFD ( kTIFF_ExifIFD, kTIFF_InteroperabilityIFDPointer ); + if ( (interopIFDTag != 0) && (interopIFDTag->type == kTIFF_LongType) && (interopIFDTag->bytes == 4) ) { + XMP_Uns32 interopOffset = this->GetUns32 ( &interopIFDTag->dataOrPos ); + if ( (8 <= interopOffset) && (interopOffset < ifdLimit) ) { // Ignore a bad Interoperability IFD offset. + (void) this->ProcessOneIFD ( interopOffset, kTIFF_InteropIFD ); + } + } + +} // TIFF_MemoryReader::ParseMemoryStream + +// ================================================================================================= +// TIFF_MemoryReader::ProcessOneIFD +// ================================ + +XMP_Uns32 TIFF_MemoryReader::ProcessOneIFD ( XMP_Uns32 ifdOffset, XMP_Uns8 ifd ) +{ + TweakedIFDInfo& ifdInfo = this->containedIFDs[ifd]; + + if ( (ifdOffset < 8) || (ifdOffset > (this->tiffLength - kEmptyIFDLength)) ) { + XMP_Throw ( "Bad IFD offset", kXMPErr_BadTIFF ); + } + + XMP_Uns8* ifdPtr = this->tiffStream + ifdOffset; + XMP_Uns16 ifdCount = this->GetUns16 ( ifdPtr ); + TweakedIFDEntry* ifdEntries = (TweakedIFDEntry*)(ifdPtr+2); + + if ( ifdCount >= 0x8000 ) XMP_Throw ( "Outrageous IFD count", kXMPErr_BadTIFF ); + if ( (XMP_Uns32)(2 + ifdCount*12 + 4) > (this->tiffLength - ifdOffset) ) { + XMP_Throw ( "Out of bounds IFD", kXMPErr_BadTIFF ); + } + + ifdInfo.count = ifdCount; + ifdInfo.entries = ifdEntries; + + XMP_Int32 prevTag = -1; // ! The GPS IFD has a tag 0, so we need a signed initial value. + bool needsSorting = false; + for ( size_t i = 0; i < ifdCount; ++i ) { + + TweakedIFDEntry* thisEntry = &ifdEntries[i]; // Tweak the IFD entry to be more useful. + + if ( ! this->nativeEndian ) { + Flip2 ( &thisEntry->id ); + Flip2 ( &thisEntry->type ); + Flip4 ( &thisEntry->bytes ); + } + + if ( thisEntry->id <= prevTag ) needsSorting = true; + prevTag = thisEntry->id; + + if ( (thisEntry->type < kTIFF_ByteType) || (thisEntry->type > kTIFF_LastType) ) continue; // Bad type, skip this tag. + + thisEntry->bytes *= (XMP_Uns32)kTIFF_TypeSizes[thisEntry->type]; + if ( thisEntry->bytes > 4 ) { + if ( ! this->nativeEndian ) Flip4 ( &thisEntry->dataOrPos ); + if ( (thisEntry->dataOrPos < 8) || (thisEntry->dataOrPos >= this->tiffLength) ) { + thisEntry->bytes = thisEntry->dataOrPos = 0; // Make this bad tag look empty. + } + if ( thisEntry->bytes > (this->tiffLength - thisEntry->dataOrPos) ) { + thisEntry->bytes = thisEntry->dataOrPos = 0; // Make this bad tag look empty. + } + } + + } + + ifdPtr += (2 + ifdCount*12); + XMP_Uns32 nextIFDOffset = this->GetUns32 ( ifdPtr ); + + if ( needsSorting ) SortIFD ( &ifdInfo ); // ! Don't perturb the ifdCount used to find the next IFD offset. + + return nextIFDOffset; + +} // TIFF_MemoryReader::ProcessOneIFD + +// ================================================================================================= diff --git a/XMPFiles/source/FormatSupport/TIFF_Support.cpp b/XMPFiles/source/FormatSupport/TIFF_Support.cpp new file mode 100644 index 0000000..9da9b52 --- /dev/null +++ b/XMPFiles/source/FormatSupport/TIFF_Support.cpp @@ -0,0 +1,439 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/TIFF_Support.hpp" +#include "source/EndianUtils.hpp" +#include "source/XIO.hpp" + +#include "source/UnicodeConversions.hpp" + +// ================================================================================================= +/// \file TIFF_Support.cpp +/// \brief Manager for parsing and serializing TIFF streams. +/// +// ================================================================================================= + +// ================================================================================================= +// TIFF_Manager::TIFF_Manager +// ========================== + +static bool sFirstCTor = true; + +TIFF_Manager::TIFF_Manager() + : bigEndian(false), nativeEndian(false), + GetUns16(0), GetUns32(0), GetFloat(0), GetDouble(0), + PutUns16(0), PutUns32(0), PutFloat(0), PutDouble(0) +{ + + if ( sFirstCTor ) { + sFirstCTor = false; + #if XMP_DebugBuild + for ( int ifd = 0; ifd < kTIFF_KnownIFDCount; ++ifd ) { // Make sure the known tag arrays are sorted. + for ( const XMP_Uns16* idPtr = sKnownTags[ifd]; *idPtr != 0xFFFF; ++idPtr ) { + XMP_Assert ( *idPtr < *(idPtr+1) ); + } + } + #endif + } + +} // TIFF_Manager::TIFF_Manager + +// ================================================================================================= +// TIFF_Manager::CheckTIFFHeader +// ============================= +// +// Checks the 4 byte TIFF prefix for validity and endianness. Sets the endian flags and the Get +// function pointers. Returns the 0th IFD offset. + +XMP_Uns32 TIFF_Manager::CheckTIFFHeader ( const XMP_Uns8* tiffPtr, XMP_Uns32 length ) +{ + if ( length < kEmptyTIFFLength ) XMP_Throw ( "The TIFF is too small", kXMPErr_BadTIFF ); + + XMP_Uns32 tiffPrefix = (tiffPtr[0] << 24) | (tiffPtr[1] << 16) | (tiffPtr[2] << 8) | (tiffPtr[3]); + + if ( tiffPrefix == kBigEndianPrefix ) { + this->bigEndian = true; + } else if ( tiffPrefix == kLittleEndianPrefix ) { + this->bigEndian = false; + } else { + XMP_Throw ( "Unrecognized TIFF prefix", kXMPErr_BadTIFF ); + } + + this->nativeEndian = (this->bigEndian == kBigEndianHost); + + if ( this->bigEndian ) { + + this->GetUns16 = GetUns16BE; + this->GetUns32 = GetUns32BE; + this->GetFloat = GetFloatBE; + this->GetDouble = GetDoubleBE; + + this->PutUns16 = PutUns16BE; + this->PutUns32 = PutUns32BE; + this->PutFloat = PutFloatBE; + this->PutDouble = PutDoubleBE; + + } else { + + this->GetUns16 = GetUns16LE; + this->GetUns32 = GetUns32LE; + this->GetFloat = GetFloatLE; + this->GetDouble = GetDoubleLE; + + this->PutUns16 = PutUns16LE; + this->PutUns32 = PutUns32LE; + this->PutFloat = PutFloatLE; + this->PutDouble = PutDoubleLE; + + } + + XMP_Uns32 mainIFDOffset = this->GetUns32 ( tiffPtr+4 ); // ! Do this after setting the Get/Put procs! + + if ( mainIFDOffset != 0 ) { // Tolerate empty TIFF even though formally invalid. + if ( (length < (kEmptyTIFFLength + kEmptyIFDLength)) || + (mainIFDOffset < kEmptyTIFFLength) || (mainIFDOffset > (length - kEmptyIFDLength)) ) { + XMP_Throw ( "Invalid primary IFD offset", kXMPErr_BadTIFF ); + } + } + + return mainIFDOffset; + +} // TIFF_Manager::CheckTIFFHeader + +// ================================================================================================= +// TIFF_Manager::SetTag_Integer +// ============================ + +void TIFF_Manager::SetTag_Integer ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns32 data32 ) +{ + + if ( data32 > 0xFFFF ) { + this->SetTag ( ifd, id, kTIFF_LongType, 1, &data32 ); + } else { + XMP_Uns16 data16 = (XMP_Uns16)data32; + this->SetTag ( ifd, id, kTIFF_ShortType, 1, &data16 ); + } + +} // TIFF_Manager::SetTag_Integer + +// ================================================================================================= +// TIFF_Manager::SetTag_Byte +// ========================= + +void TIFF_Manager::SetTag_Byte ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns8 data ) +{ + + this->SetTag ( ifd, id, kTIFF_ByteType, 1, &data ); + +} // TIFF_Manager::SetTag_Byte + +// ================================================================================================= +// TIFF_Manager::SetTag_SByte +// ========================== + +void TIFF_Manager::SetTag_SByte ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int8 data ) +{ + + this->SetTag ( ifd, id, kTIFF_SByteType, 1, &data ); + +} // TIFF_Manager::SetTag_SByte + +// ================================================================================================= +// TIFF_Manager::SetTag_Short +// ========================== + +void TIFF_Manager::SetTag_Short ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns16 clientData ) +{ + XMP_Uns16 streamData; + + this->PutUns16 ( clientData, &streamData ); + this->SetTag ( ifd, id, kTIFF_ShortType, 1, &streamData ); + +} // TIFF_Manager::SetTag_Short + +// ================================================================================================= +// TIFF_Manager::SetTag_SShort +// =========================== + + void TIFF_Manager::SetTag_SShort ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int16 clientData ) +{ + XMP_Int16 streamData; + + this->PutUns16 ( (XMP_Uns16)clientData, &streamData ); + this->SetTag ( ifd, id, kTIFF_SShortType, 1, &streamData ); + +} // TIFF_Manager::SetTag_SShort + +// ================================================================================================= +// TIFF_Manager::SetTag_Long +// ========================= + + void TIFF_Manager::SetTag_Long ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns32 clientData ) +{ + XMP_Uns32 streamData; + + this->PutUns32 ( clientData, &streamData ); + this->SetTag ( ifd, id, kTIFF_LongType, 1, &streamData ); + +} // TIFF_Manager::SetTag_Long + +// ================================================================================================= +// TIFF_Manager::SetTag_SLong +// ========================== + + void TIFF_Manager::SetTag_SLong ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int32 clientData ) +{ + XMP_Int32 streamData; + + this->PutUns32 ( (XMP_Uns32)clientData, &streamData ); + this->SetTag ( ifd, id, kTIFF_SLongType, 1, &streamData ); + +} // TIFF_Manager::SetTag_SLong + +// ================================================================================================= +// TIFF_Manager::SetTag_Rational +// ============================= + +struct StreamRational { XMP_Uns32 num, denom; }; + + void TIFF_Manager::SetTag_Rational ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns32 clientNum, XMP_Uns32 clientDenom ) +{ + StreamRational streamData; + + this->PutUns32 ( clientNum, &streamData.num ); + this->PutUns32 ( clientDenom, &streamData.denom ); + this->SetTag ( ifd, id, kTIFF_RationalType, 1, &streamData ); + +} // TIFF_Manager::SetTag_Rational + +// ================================================================================================= +// TIFF_Manager::SetTag_SRational +// ============================== + + void TIFF_Manager::SetTag_SRational ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int32 clientNum, XMP_Int32 clientDenom ) +{ + StreamRational streamData; + + this->PutUns32 ( (XMP_Uns32)clientNum, &streamData.num ); + this->PutUns32 ( (XMP_Uns32)clientDenom, &streamData.denom ); + this->SetTag ( ifd, id, kTIFF_SRationalType, 1, &streamData ); + +} // TIFF_Manager::SetTag_SRational + +// ================================================================================================= +// TIFF_Manager::SetTag_Float +// ========================== + + void TIFF_Manager::SetTag_Float ( XMP_Uns8 ifd, XMP_Uns16 id, float clientData ) +{ + float streamData; + + this->PutFloat ( clientData, &streamData ); + this->SetTag ( ifd, id, kTIFF_FloatType, 1, &streamData ); + +} // TIFF_Manager::SetTag_Float + +// ================================================================================================= +// TIFF_Manager::SetTag_Double +// =========================== + + void TIFF_Manager::SetTag_Double ( XMP_Uns8 ifd, XMP_Uns16 id, double clientData ) +{ + double streamData; + + this->PutDouble ( clientData, &streamData ); + this->SetTag ( ifd, id, kTIFF_DoubleType, 1, &streamData ); + +} // TIFF_Manager::SetTag_Double + +// ================================================================================================= +// TIFF_Manager::SetTag_ASCII +// =========================== + +void TIFF_Manager::SetTag_ASCII ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_StringPtr data ) +{ + + this->SetTag ( ifd, id, kTIFF_ASCIIType, (XMP_Uns32)(strlen(data) + 1), data ); // ! Include trailing nul. + +} // TIFF_Manager::SetTag_ASCII + +// ================================================================================================= +// UTF16_to_UTF8 +// ============= + +static void UTF16_to_UTF8 ( const UTF16Unit * utf16Ptr, size_t utf16Len, bool bigEndian, std::string * outStr ) +{ + UTF16_to_UTF8_Proc ToUTF8 = 0; + if ( bigEndian ) { + ToUTF8 = UTF16BE_to_UTF8; + } else { + ToUTF8 = UTF16LE_to_UTF8; + } + + UTF8Unit buffer [1000]; + size_t inCount, outCount; + + outStr->erase(); + outStr->reserve ( utf16Len * 2 ); // As good a guess as any. + + while ( utf16Len > 0 ) { + ToUTF8 ( utf16Ptr, utf16Len, buffer, sizeof(buffer), &inCount, &outCount ); + outStr->append ( (XMP_StringPtr)buffer, outCount ); + utf16Ptr += inCount; + utf16Len -= inCount; + } + +} // UTF16_to_UTF8 + +// ================================================================================================= +// TIFF_Manager::DecodeString +// ========================== +// +// Convert an explicitly encoded string to UTF-8. The input must be encoded according to table 6 of +// the Exif 2.2 specification. The input pointer is to the start of the 8 byte header, the length is +// the full length. In other words, the pointer and length from the IFD entry. Returns true if the +// encoding is supported and the conversion succeeds. + +// *** JIS encoding is not supported yet. Need a static mapping table from JIS X 208-1990 to Unicode. + +bool TIFF_Manager::DecodeString ( const void * encodedPtr, size_t encodedLen, std::string* utf8Str ) const +{ + utf8Str->erase(); + if ( encodedLen < 8 ) return false; // Need to have at least the 8 byte header. + + XMP_StringPtr typePtr = (XMP_StringPtr)encodedPtr; + XMP_StringPtr valuePtr = typePtr + 8; + size_t valueLen = encodedLen - 8; + + if ( *typePtr == 'A' ) { + + utf8Str->assign ( valuePtr, valueLen ); + return true; + + } else if ( *typePtr == 'U' ) { + + try { + + const UTF16Unit * utf16Ptr = (const UTF16Unit *) valuePtr; + size_t utf16Len = valueLen >> 1; // The number of UTF-16 storage units, not bytes. + if ( utf16Len == 0 ) return false; + bool isBigEndian = this->bigEndian; // Default to stream endian, unless there is a BOM ... + if ( (*utf16Ptr == 0xFEFF) || (*utf16Ptr == 0xFFFE) ) { // Check for an explicit BOM + isBigEndian = (*((XMP_Uns8*)utf16Ptr) == 0xFE); + utf16Ptr += 1; // Don't translate the BOM. + utf16Len -= 1; + if ( utf16Len == 0 ) return false; + } + UTF16_to_UTF8 ( utf16Ptr, utf16Len, isBigEndian, utf8Str ); + return true; + + } catch ( ... ) { + return false; // Ignore the tag if there are conversion errors. + } + + } else if ( *typePtr == 'J' ) { + + return false; // ! Ignore JIS for now. + + } + + return false; // ! Ignore all other encodings. + +} // TIFF_Manager::DecodeString + +// ================================================================================================= +// UTF8_to_UTF16 +// ============= + +static void UTF8_to_UTF16 ( const UTF8Unit * utf8Ptr, size_t utf8Len, bool bigEndian, std::string * outStr ) +{ + UTF8_to_UTF16_Proc ToUTF16 = 0; + if ( bigEndian ) { + ToUTF16 = UTF8_to_UTF16BE; + } else { + ToUTF16 = UTF8_to_UTF16LE; + } + + enum { kUTF16Len = 1000 }; + UTF16Unit buffer [kUTF16Len]; + size_t inCount, outCount; + + outStr->erase(); + outStr->reserve ( utf8Len * 2 ); // As good a guess as any. + + while ( utf8Len > 0 ) { + ToUTF16 ( utf8Ptr, utf8Len, buffer, kUTF16Len, &inCount, &outCount ); + outStr->append ( (XMP_StringPtr)buffer, outCount*2 ); + utf8Ptr += inCount; + utf8Len -= inCount; + } + +} // UTF8_to_UTF16 + +// ================================================================================================= +// TIFF_Manager::EncodeString +// ========================== +// +// Convert a UTF-8 string to an explicitly encoded form according to table 6 of the Exif 2.2 +// specification. Returns true if the encoding is supported and the conversion succeeds. + +// *** JIS encoding is not supported yet. Need a static mapping table to JIS X 208-1990 from Unicode. + +bool TIFF_Manager::EncodeString ( const std::string& utf8Str, XMP_Uns8 encoding, std::string* encodedStr ) +{ + encodedStr -> erase(); + + if ( encoding == kTIFF_EncodeASCII ) { + + encodedStr->assign ( "ASCII\0\0\0", 8 ); + XMP_Assert (encodedStr->size() == 8 ); + + encodedStr->append ( utf8Str ); // ! Let the caller filter the value. (?) + + return true; + + } else if ( encoding == kTIFF_EncodeUnicode ) { + + encodedStr->assign ( "UNICODE\0", 8 ); + XMP_Assert (encodedStr->size() == 8 ); + + try { + std::string temp; + UTF8_to_UTF16 ( (const UTF8Unit*)utf8Str.c_str(), utf8Str.size(), this->bigEndian, &temp ); + encodedStr->append ( temp ); + return true; + } catch ( ... ) { + return false; // Ignore the tag if there are conversion errors. + } + + } else if ( encoding == kTIFF_EncodeJIS ) { + + XMP_Throw ( "Encoding to JIS is not implemented", kXMPErr_Unimplemented ); + + // encodedStr->assign ( "JIS\0\0\0\0\0", 8 ); + // XMP_Assert (encodedStr->size() == 8 ); + + // ... + + // return true; + + } else { + + XMP_Throw ( "Invalid TIFF string encoding", kXMPErr_BadParam ); + + } + + return false; // ! Should never get here. + +} // TIFF_Manager::EncodeString + +// ================================================================================================= diff --git a/XMPFiles/source/FormatSupport/TIFF_Support.hpp b/XMPFiles/source/FormatSupport/TIFF_Support.hpp new file mode 100644 index 0000000..447ab79 --- /dev/null +++ b/XMPFiles/source/FormatSupport/TIFF_Support.hpp @@ -0,0 +1,964 @@ +#ifndef __TIFF_Support_hpp__ +#define __TIFF_Support_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. + +#include <map> +#include <stdlib.h> +#include <string.h> + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +#include "source/EndianUtils.hpp" + +// ================================================================================================= +/// \file TIFF_Support.hpp +/// \brief XMPFiles support for TIFF streams. +/// +/// This header provides TIFF stream support specific to the needs of XMPFiles. This is not intended +/// for general purpose TIFF processing. TIFF_Manager is an abstract base class with 2 concrete +/// derived classes, TIFF_MemoryReader and TIFF_FileWriter. +/// +/// TIFF_MemoryReader provides read-only support for TIFF streams that are small enough to be kept +/// entirely in memory. This allows optimizations to reduce heap usage and processing code. It is +/// sufficient for browsing access to the Exif metadata in JPEG and Photoshop files. Think of +/// TIFF_MemoryReader as "memory-based AND read-only". Since the entire TIFF stream is available, +/// GetTag will return information about any tag in the stream. +/// +/// TIFF_FileWriter is for cases where updates are needed or the TIFF stream is too large to be kept +/// entirely in memory. Think of TIFF_FileWriter as "file-based OR read-write". TIFF_FileWriter only +/// maintains information for tags of interest as metadata. +/// +/// The needs of XMPFiles are well defined metadata access. Only 4 IFDs are processed: +/// \li The 0th IFD, for the primary image, the first one in the outer list of IFDs. +/// \li The Exif general metadata IFD, from tag 34665 in the primary image IFD. +/// \li The Exif GPS Info metadata IFD, from tag 34853 in the primary image IFD. +/// \li The Exif Interoperability IFD, from tag 40965 in the Exif general metadata IFD. +/// +/// \note These classes are for use only when directly compiled and linked. They should not be +/// packaged in a DLL by themselves. They do not provide any form of C++ ABI protection. +// ================================================================================================= + + +// ================================================================================================= +// TIFF IFD and type constants +// =========================== +// +// These aren't inside TIFF_Manager because static data members can't be initialized there. + +enum { // Constants for the recognized IFDs. + kTIFF_PrimaryIFD = 0, // The primary image IFD, also called the 0th IFD. + kTIFF_TNailIFD = 1, // The thumbnail image IFD also called the 1st IFD. (not used) + kTIFF_ExifIFD = 2, // The Exif general metadata IFD. + kTIFF_GPSInfoIFD = 3, // The Exif GPS Info IFD. + kTIFF_InteropIFD = 4, // The Exif Interoperability IFD. + kTIFF_LastRealIFD = 4, + kTIFF_KnownIFDCount = 5, + kTIFF_KnownIFD = 9 // The IFD that a tag is known to belong in. +}; + +enum { // Constants for the type field of a tag, as defined by TIFF. + kTIFF_ShortOrLongType = 0, // ! Not part of the TIFF spec, never in a tag! + kTIFF_ByteType = 1, + kTIFF_ASCIIType = 2, + kTIFF_ShortType = 3, + kTIFF_LongType = 4, + kTIFF_RationalType = 5, + kTIFF_SByteType = 6, + kTIFF_UndefinedType = 7, + kTIFF_SShortType = 8, + kTIFF_SLongType = 9, + kTIFF_SRationalType = 10, + kTIFF_FloatType = 11, + kTIFF_DoubleType = 12, + kTIFF_LastType = 12 +}; + +static const size_t kTIFF_TypeSizes[] = { 0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8 }; + +static const bool kTIFF_IsIntegerType[] = { 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0 }; +static const bool kTIFF_IsRationalType[] = { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0 }; +static const bool kTIFF_IsFloatType[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 }; + +static const char * kTIFF_TypeNames[] = { "ShortOrLong", "BYTE", "ASCII", "SHORT", "LONG", "RATIONAL", + "SBYTE", "UNDEFINED", "SSHORT", "SLONG", "SRATIONAL", + "FLOAT", "DOUBLE" }; + +enum { // Encodings for SetTag_EncodedString. + kTIFF_EncodeUndefined = 0, + kTIFF_EncodeASCII = 1, + kTIFF_EncodeUnicode = 2, // UTF-16 in the endianness of the TIFF stream. + kTIFF_EncodeJIS = 3, // Exif 2.2 uses JIS X 208-1990. + kTIFF_EncodeUnknown = 9 +}; + +// ================================================================================================= +// Recognized TIFF tags +// ==================== + +// ----------------------------------------------------------------------------------------------- +// An enum of IDs for all of the tags as potential interest as metadata. The numberical order does +// not matter. These are mostly listed in the order of the Exif specification tables for convenience +// of checking correspondence. + +enum { + + // General 0th IFD tags. Some of these can also be in the thumbnail IFD. + + // General tags from Exif 2.3 table 4: + kTIFF_ImageWidth = 256, + kTIFF_ImageLength = 257, + kTIFF_BitsPerSample = 258, + kTIFF_Compression = 259, + kTIFF_PhotometricInterpretation = 262, + kTIFF_Orientation = 274, + kTIFF_SamplesPerPixel = 277, + kTIFF_PlanarConfiguration = 284, + kTIFF_YCbCrCoefficients = 529, + kTIFF_YCbCrSubSampling = 530, + kTIFF_XResolution = 282, + kTIFF_YResolution = 283, + kTIFF_ResolutionUnit = 296, + kTIFF_TransferFunction = 301, + kTIFF_WhitePoint = 318, + kTIFF_PrimaryChromaticities = 319, + kTIFF_YCbCrPositioning = 531, + kTIFF_ReferenceBlackWhite = 532, + kTIFF_DateTime = 306, + kTIFF_ImageDescription = 270, + kTIFF_Make = 271, + kTIFF_Model = 272, + kTIFF_Software = 305, + kTIFF_Artist = 315, + kTIFF_Copyright = 33432, + + // Tags defined by Adobe: + kTIFF_XMP = 700, + kTIFF_IPTC = 33723, + kTIFF_PSIR = 34377, + kTIFF_DNGVersion = 50706, + kTIFF_DNGBackwardVersion = 50707, + + // Additional thumbnail IFD tags. We also care about 256, 257, and 259 in thumbnails. + kTIFF_JPEGInterchangeFormat = 513, + kTIFF_JPEGInterchangeFormatLength = 514, + + // Tags that need special handling when rewriting memory-based TIFF. + kTIFF_StripOffsets = 273, + kTIFF_StripByteCounts = 279, + kTIFF_FreeOffsets = 288, + kTIFF_FreeByteCounts = 289, + kTIFF_TileOffsets = 324, + kTIFF_TileByteCounts = 325, + kTIFF_SubIFDs = 330, + kTIFF_JPEGQTables = 519, + kTIFF_JPEGDCTables = 520, + kTIFF_JPEGACTables = 521, + + // Exif IFD tags defined in Exif 2.3 table 7. + + kTIFF_ExifVersion = 36864, + kTIFF_FlashpixVersion = 40960, + kTIFF_ColorSpace = 40961, + kTIFF_Gamma = 42240, + kTIFF_ComponentsConfiguration = 37121, + kTIFF_CompressedBitsPerPixel = 37122, + kTIFF_PixelXDimension = 40962, + kTIFF_PixelYDimension = 40963, + kTIFF_MakerNote = 37500, // Gets deleted when rewriting memory-based TIFF. + kTIFF_UserComment = 37510, + kTIFF_RelatedSoundFile = 40964, + kTIFF_DateTimeOriginal = 36867, + kTIFF_DateTimeDigitized = 36868, + kTIFF_SubSecTime = 37520, + kTIFF_SubSecTimeOriginal = 37521, + kTIFF_SubSecTimeDigitized = 37522, + kTIFF_ImageUniqueID = 42016, + kTIFF_CameraOwnerName = 42032, + kTIFF_BodySerialNumber = 42033, + kTIFF_LensSpecification = 42034, + kTIFF_LensMake = 42035, + kTIFF_LensModel = 42036, + kTIFF_LensSerialNumber = 42037, + + // Exif IFD tags defined in Exif 2.3 table 8. + + kTIFF_ExposureTime = 33434, + kTIFF_FNumber = 33437, + kTIFF_ExposureProgram = 34850, + kTIFF_SpectralSensitivity = 34852, + kTIFF_PhotographicSensitivity = 34855, // ! Called kTIFF_ISOSpeedRatings before Exif 2.3. + kTIFF_OECF = 34856, + kTIFF_SensitivityType = 34864, + kTIFF_StandardOutputSensitivity = 34865, + kTIFF_RecommendedExposureIndex = 34866, + kTIFF_ISOSpeed = 34867, + kTIFF_ISOSpeedLatitudeyyy = 34868, + kTIFF_ISOSpeedLatitudezzz = 34869, + kTIFF_ShutterSpeedValue = 37377, + kTIFF_ApertureValue = 37378, + kTIFF_BrightnessValue = 37379, + kTIFF_ExposureBiasValue = 37380, + kTIFF_MaxApertureValue = 37381, + kTIFF_SubjectDistance = 37382, + kTIFF_MeteringMode = 37383, + kTIFF_LightSource = 37384, + kTIFF_Flash = 37385, + kTIFF_FocalLength = 37386, + kTIFF_SubjectArea = 37396, + kTIFF_FlashEnergy = 41483, + kTIFF_SpatialFrequencyResponse = 41484, + kTIFF_FocalPlaneXResolution = 41486, + kTIFF_FocalPlaneYResolution = 41487, + kTIFF_FocalPlaneResolutionUnit = 41488, + kTIFF_SubjectLocation = 41492, + kTIFF_ExposureIndex = 41493, + kTIFF_SensingMethod = 41495, + kTIFF_FileSource = 41728, + kTIFF_SceneType = 41729, + kTIFF_CFAPattern = 41730, + kTIFF_CustomRendered = 41985, + kTIFF_ExposureMode = 41986, + kTIFF_WhiteBalance = 41987, + kTIFF_DigitalZoomRatio = 41988, + kTIFF_FocalLengthIn35mmFilm = 41989, + kTIFF_SceneCaptureType = 41990, + kTIFF_GainControl = 41991, + kTIFF_Contrast = 41992, + kTIFF_Saturation = 41993, + kTIFF_Sharpness = 41994, + kTIFF_DeviceSettingDescription = 41995, + kTIFF_SubjectDistanceRange = 41996, + + // GPS IFD tags. + + kTIFF_GPSVersionID = 0, + kTIFF_GPSLatitudeRef = 1, + kTIFF_GPSLatitude = 2, + kTIFF_GPSLongitudeRef = 3, + kTIFF_GPSLongitude = 4, + kTIFF_GPSAltitudeRef = 5, + kTIFF_GPSAltitude = 6, + kTIFF_GPSTimeStamp = 7, + kTIFF_GPSSatellites = 8, + kTIFF_GPSStatus = 9, + kTIFF_GPSMeasureMode = 10, + kTIFF_GPSDOP = 11, + kTIFF_GPSSpeedRef = 12, + kTIFF_GPSSpeed = 13, + kTIFF_GPSTrackRef = 14, + kTIFF_GPSTrack = 15, + kTIFF_GPSImgDirectionRef = 16, + kTIFF_GPSImgDirection = 17, + kTIFF_GPSMapDatum = 18, + kTIFF_GPSDestLatitudeRef = 19, + kTIFF_GPSDestLatitude = 20, + kTIFF_GPSDestLongitudeRef = 21, + kTIFF_GPSDestLongitude = 22, + kTIFF_GPSDestBearingRef = 23, + kTIFF_GPSDestBearing = 24, + kTIFF_GPSDestDistanceRef = 25, + kTIFF_GPSDestDistance = 26, + kTIFF_GPSProcessingMethod = 27, + kTIFF_GPSAreaInformation = 28, + kTIFF_GPSDateStamp = 29, + kTIFF_GPSDifferential = 30, + kTIFF_GPSHPositioningError = 31, + + // Special tags that are links to other IFDs. + + kTIFF_ExifIFDPointer = 34665, // Found in 0th IFD + kTIFF_GPSInfoIFDPointer = 34853, // Found in 0th IFD + kTIFF_InteroperabilityIFDPointer = 40965 // Found in Exif IFD + +}; + +// *** Temporary hack: +#define kTIFF_ISOSpeedRatings kTIFF_PhotographicSensitivity + +// ------------------------------------------------------------------ +// Sorted arrays of the tags that are recognized in the various IFDs. + +static const XMP_Uns16 sKnownPrimaryIFDTags[] = +{ + kTIFF_ImageWidth, // 256 + kTIFF_ImageLength, // 257 + kTIFF_BitsPerSample, // 258 + kTIFF_Compression, // 259 + kTIFF_PhotometricInterpretation, // 262 + kTIFF_ImageDescription, // 270 + kTIFF_Make, // 271 + kTIFF_Model, // 272 + kTIFF_Orientation, // 274 + kTIFF_SamplesPerPixel, // 277 + kTIFF_XResolution, // 282 + kTIFF_YResolution, // 283 + kTIFF_PlanarConfiguration, // 284 + kTIFF_ResolutionUnit, // 296 + kTIFF_TransferFunction, // 301 + kTIFF_Software, // 305 + kTIFF_DateTime, // 306 + kTIFF_Artist, // 315 + kTIFF_WhitePoint, // 318 + kTIFF_PrimaryChromaticities, // 319 + kTIFF_YCbCrCoefficients, // 529 + kTIFF_YCbCrSubSampling, // 530 + kTIFF_YCbCrPositioning, // 531 + kTIFF_ReferenceBlackWhite, // 532 + kTIFF_XMP, // 700 + kTIFF_Copyright, // 33432 + kTIFF_IPTC, // 33723 + kTIFF_PSIR, // 34377 + kTIFF_ExifIFDPointer, // 34665 + kTIFF_GPSInfoIFDPointer, // 34853 + kTIFF_DNGVersion, // 50706 + kTIFF_DNGBackwardVersion, // 50707 + 0xFFFF // Must be last as a sentinel. +}; + +static const XMP_Uns16 sKnownThumbnailIFDTags[] = +{ + kTIFF_ImageWidth, // 256 + kTIFF_ImageLength, // 257 + kTIFF_Compression, // 259 + kTIFF_JPEGInterchangeFormat, // 513 + kTIFF_JPEGInterchangeFormatLength, // 514 + 0xFFFF // Must be last as a sentinel. +}; + +static const XMP_Uns16 sKnownExifIFDTags[] = +{ + kTIFF_ExposureTime, // 33434 + kTIFF_FNumber, // 33437 + kTIFF_ExposureProgram, // 34850 + kTIFF_SpectralSensitivity, // 34852 + kTIFF_PhotographicSensitivity, // 34855 + kTIFF_OECF, // 34856 + kTIFF_SensitivityType, // 34864 + kTIFF_StandardOutputSensitivity, // 34865 + kTIFF_RecommendedExposureIndex, // 34866 + kTIFF_ISOSpeed, // 34867 + kTIFF_ISOSpeedLatitudeyyy, // 34868 + kTIFF_ISOSpeedLatitudezzz, // 34869 + kTIFF_ExifVersion, // 36864 + kTIFF_DateTimeOriginal, // 36867 + kTIFF_DateTimeDigitized, // 36868 + kTIFF_ComponentsConfiguration, // 37121 + kTIFF_CompressedBitsPerPixel, // 37122 + kTIFF_ShutterSpeedValue, // 37377 + kTIFF_ApertureValue, // 37378 + kTIFF_BrightnessValue, // 37379 + kTIFF_ExposureBiasValue, // 37380 + kTIFF_MaxApertureValue, // 37381 + kTIFF_SubjectDistance, // 37382 + kTIFF_MeteringMode, // 37383 + kTIFF_LightSource, // 37384 + kTIFF_Flash, // 37385 + kTIFF_FocalLength, // 37386 + kTIFF_SubjectArea, // 37396 + kTIFF_UserComment, // 37510 + kTIFF_SubSecTime, // 37520 + kTIFF_SubSecTimeOriginal, // 37521 + kTIFF_SubSecTimeDigitized, // 37522 + kTIFF_FlashpixVersion, // 40960 + kTIFF_ColorSpace, // 40961 + kTIFF_PixelXDimension, // 40962 + kTIFF_PixelYDimension, // 40963 + kTIFF_RelatedSoundFile, // 40964 + kTIFF_FlashEnergy, // 41483 + kTIFF_SpatialFrequencyResponse, // 41484 + kTIFF_FocalPlaneXResolution, // 41486 + kTIFF_FocalPlaneYResolution, // 41487 + kTIFF_FocalPlaneResolutionUnit, // 41488 + kTIFF_SubjectLocation, // 41492 + kTIFF_ExposureIndex, // 41493 + kTIFF_SensingMethod, // 41495 + kTIFF_FileSource, // 41728 + kTIFF_SceneType, // 41729 + kTIFF_CFAPattern, // 41730 + kTIFF_CustomRendered, // 41985 + kTIFF_ExposureMode, // 41986 + kTIFF_WhiteBalance, // 41987 + kTIFF_DigitalZoomRatio, // 41988 + kTIFF_FocalLengthIn35mmFilm, // 41989 + kTIFF_SceneCaptureType, // 41990 + kTIFF_GainControl, // 41991 + kTIFF_Contrast, // 41992 + kTIFF_Saturation, // 41993 + kTIFF_Sharpness, // 41994 + kTIFF_DeviceSettingDescription, // 41995 + kTIFF_SubjectDistanceRange, // 41996 + kTIFF_ImageUniqueID, // 42016 + kTIFF_CameraOwnerName, // 42032 + kTIFF_BodySerialNumber, // 42033 + kTIFF_LensSpecification, // 42034 + kTIFF_LensMake, // 42035 + kTIFF_LensModel, // 42036 + kTIFF_LensSerialNumber, // 42037 + kTIFF_Gamma, // 42240 + 0xFFFF // Must be last as a sentinel. +}; + +static const XMP_Uns16 sKnownGPSInfoIFDTags[] = +{ + kTIFF_GPSVersionID, // 0 + kTIFF_GPSLatitudeRef, // 1 + kTIFF_GPSLatitude, // 2 + kTIFF_GPSLongitudeRef, // 3 + kTIFF_GPSLongitude, // 4 + kTIFF_GPSAltitudeRef, // 5 + kTIFF_GPSAltitude, // 6 + kTIFF_GPSTimeStamp, // 7 + kTIFF_GPSSatellites, // 8 + kTIFF_GPSStatus, // 9 + kTIFF_GPSMeasureMode, // 10 + kTIFF_GPSDOP, // 11 + kTIFF_GPSSpeedRef, // 12 + kTIFF_GPSSpeed, // 13 + kTIFF_GPSTrackRef, // 14 + kTIFF_GPSTrack, // 15 + kTIFF_GPSImgDirectionRef, // 16 + kTIFF_GPSImgDirection, // 17 + kTIFF_GPSMapDatum, // 18 + kTIFF_GPSDestLatitudeRef, // 19 + kTIFF_GPSDestLatitude, // 20 + kTIFF_GPSDestLongitudeRef, // 21 + kTIFF_GPSDestLongitude, // 22 + kTIFF_GPSDestBearingRef, // 23 + kTIFF_GPSDestBearing, // 24 + kTIFF_GPSDestDistanceRef, // 25 + kTIFF_GPSDestDistance, // 26 + kTIFF_GPSProcessingMethod, // 27 + kTIFF_GPSAreaInformation, // 28 + kTIFF_GPSDateStamp, // 29 + kTIFF_GPSDifferential, // 30 + kTIFF_GPSHPositioningError, // 31 + 0xFFFF // Must be last as a sentinel. +}; + +static const XMP_Uns16 sKnownInteroperabilityIFDTags[] = +{ + // ! Yes, there are none at present. + 0xFFFF // Must be last as a sentinel. +}; + +// ! Make sure these are in the same order as the IFD enum! +static const XMP_Uns16* sKnownTags[kTIFF_KnownIFDCount] = { sKnownPrimaryIFDTags, + sKnownThumbnailIFDTags, + sKnownExifIFDTags, + sKnownGPSInfoIFDTags, + sKnownInteroperabilityIFDTags }; + + +// ================================================================================================= +// ================================================================================================= + + +// ================================================================================================= +// TIFF_Manager +// ============ + +class TIFF_Manager { // The abstract base class. +public: + + // --------------------------------------------------------------------------------------------- + // Types and constants + + static const XMP_Uns32 kBigEndianPrefix = 0x4D4D002AUL; + static const XMP_Uns32 kLittleEndianPrefix = 0x49492A00UL; + + static const size_t kEmptyTIFFLength = 8; // Just the header. + static const size_t kEmptyIFDLength = 2 + 4; // Entry count and next-IFD offset. + static const size_t kIFDEntryLength = 12; + + struct TagInfo { + XMP_Uns16 id; + XMP_Uns16 type; + XMP_Uns32 count; + const void* dataPtr; // ! The data must not be changed! Stream endian format! + XMP_Uns32 dataLen; // The length in bytes. + TagInfo() : id(0), type(0), count(0), dataPtr(0), dataLen(0) {}; + TagInfo ( XMP_Uns16 _id, XMP_Uns16 _type, XMP_Uns32 _count, const void* _dataPtr, XMP_Uns32 _dataLen ) + : id(_id), type(_type), count(_count), dataPtr(_dataPtr), dataLen(_dataLen) {}; + }; + + typedef std::map<XMP_Uns16,TagInfo> TagInfoMap; + + struct Rational { XMP_Uns32 num, denom; }; + struct SRational { XMP_Int32 num, denom; }; + + // --------------------------------------------------------------------------------------------- + // The IsXyzEndian methods return the external endianness of the original parsed TIFF stream. + // The \c GetTag methods return native endian values, the \c SetTag methods take native values. + // The original endianness is preserved in output. + + bool IsBigEndian() const { return this->bigEndian; }; + bool IsLittleEndian() const { return (! this->bigEndian); }; + bool IsNativeEndian() const { return this->nativeEndian; }; + + // --------------------------------------------------------------------------------------------- + // The TIFF_Manager only keeps explicit knowledge of up to 4 IFDs: + // - The primary image IFD, also known as the 0th IFD. This must be present. + // - A possible Exif general metadata IFD, found from tag 34665 in the primary image IFD. + // - A possible Exif GPS metadata IFD, found from tag 34853 in the primary image IFD. + // - A possible Exif Interoperability IFD, found from tag 40965 in the Exif general metadata IFD. + // + // Parsing will silently forget about certain aspects of ill-formed streams. If any tags are + // repeated in an IFD, only the last is kept. Any known tags that are in the wrong IFD are + // removed. Parsing will sort the tags into ascending order, AppendTIFF and ComposeTIFF will + // preserve the sorted order. These fixes do not cause IsChanged to return true, that only + // happens if the client makes explicit changes using SetTag or DeleteTag. + + virtual bool HasExifIFD() const = 0; + virtual bool HasGPSInfoIFD() const = 0; + + // --------------------------------------------------------------------------------------------- + // These are the basic methods to get a map of all of the tags in an IFD, to get or set a tag, + // or to delete a tag. The dataPtr returned by \c GetTag is consided read-only, the client must + // not change it. The ifd parameter to \c GetIFD must be for one of the recognized actual IFDs. + // The ifd parameter for the GetTag or SetTag methods can be a specific IFD of kTIFF_KnownIFD. + // Using the specific IFD will be slightly faster, saving a lookup in the known tag map. An + // exception is thrown if kTIFF_KnownIFD is passed to GetTag or SetTag and the tag is not known. + // \c SetTag replaces an existing tag regardless of type or count. \c DeleteTag deletes a tag, + // it is a no-op if the tag does not exist. \c GetValueOffset returns the offset within the + // parsed stream of the tag's value. It returns 0 if the tag was not in the parsed input. + + virtual bool GetIFD ( XMP_Uns8 ifd, TagInfoMap* ifdMap ) const = 0; + + virtual bool GetTag ( XMP_Uns8 ifd, XMP_Uns16 id, TagInfo* info ) const = 0; + + virtual void SetTag ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns16 type, XMP_Uns32 count, const void* dataPtr ) = 0; + + virtual void DeleteTag ( XMP_Uns8 ifd, XMP_Uns16 id ) = 0; + + virtual XMP_Uns32 GetValueOffset ( XMP_Uns8 ifd, XMP_Uns16 id ) const = 0; + + // --------------------------------------------------------------------------------------------- + // These methods are for tags whose type can be short or long, depending on the actual value. + // \c GetTag_Integer returns false if an existing tag's type is not short, or long, or if the + // count is not 1. \c SetTag_Integer replaces an existing tag regardless of type or count, the + // new tag will have type short or long. + + virtual bool GetTag_Integer ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns32* data ) const = 0; + + void SetTag_Integer ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns32 data ); + + // --------------------------------------------------------------------------------------------- + // These are customized forms of GetTag that verify the type and return a typed value. False is + // returned if the type does not match or if the count is not 1. + + // *** Should also add support for ASCII multi-strings? + + virtual bool GetTag_Byte ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns8* data ) const = 0; + virtual bool GetTag_SByte ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int8* data ) const = 0; + virtual bool GetTag_Short ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns16* data ) const = 0; + virtual bool GetTag_SShort ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int16* data ) const = 0; + virtual bool GetTag_Long ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns32* data ) const = 0; + virtual bool GetTag_SLong ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int32* data ) const = 0; + + virtual bool GetTag_Rational ( XMP_Uns8 ifd, XMP_Uns16 id, Rational* data ) const = 0; + virtual bool GetTag_SRational ( XMP_Uns8 ifd, XMP_Uns16 id, SRational* data ) const = 0; + + virtual bool GetTag_Float ( XMP_Uns8 ifd, XMP_Uns16 id, float* data ) const = 0; + virtual bool GetTag_Double ( XMP_Uns8 ifd, XMP_Uns16 id, double* data ) const = 0; + + virtual bool GetTag_ASCII ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_StringPtr* dataPtr, XMP_StringLen* dataLen ) const = 0; + + // --------------------------------------------------------------------------------------------- + + void SetTag_Byte ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns8 data ); + void SetTag_SByte ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int8 data ); + void SetTag_Short ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns16 data ); + void SetTag_SShort ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int16 data ); + void SetTag_Long ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns32 data ); + void SetTag_SLong ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int32 data ); + + void SetTag_Rational ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns32 num, XMP_Uns32 denom ); + void SetTag_SRational ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int32 num, XMP_Int32 denom ); + + void SetTag_Float ( XMP_Uns8 ifd, XMP_Uns16 id, float data ); + void SetTag_Double ( XMP_Uns8 ifd, XMP_Uns16 id, double data ); + + void SetTag_ASCII ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_StringPtr dataPtr ); + + // --------------------------------------------------------------------------------------------- + + virtual bool GetTag_EncodedString ( XMP_Uns8 ifd, XMP_Uns16 id, std::string* utf8Str ) const = 0; + virtual void SetTag_EncodedString ( XMP_Uns8 ifd, XMP_Uns16 id, const std::string& utf8Str, XMP_Uns8 encoding ) = 0; + + bool DecodeString ( const void * encodedPtr, size_t encodedLen, std::string* utf8Str ) const; + bool EncodeString ( const std::string& utf8Str, XMP_Uns8 encoding, std::string* encodedStr ); + + // --------------------------------------------------------------------------------------------- + // \c IsChanged returns true if a read-write stream has changes that need to be saved. This is + // only the case when a \c SetTag method has been called. It is not true for changes related to + // parsing normalization such as sorting of tags. \c IsChanged returns false for read-only streams. + + virtual bool IsChanged() = 0; + + // --------------------------------------------------------------------------------------------- + // \c IsLegacyChanged returns true if a read-write stream has changes that need to be saved to + // tags other than the XMP (tag 700). This only the case when a \c SetTag method has been + // called. It is not true for changes related to parsing normalization such as sorting of tags. + // \c IsLegacyChanged returns false for read-only streams. + + virtual bool IsLegacyChanged() = 0; + + // --------------------------------------------------------------------------------------------- + // \c UpdateMemoryStream is mainly applicable to memory-based read-write streams. It recomposes + // the memory stream to incorporate all changes. The new length and data pointer are returned. + // \c UpdateMemoryStream can be used with a read-only memory stream to get the raw stream info. + // + // \c UpdateFileStream updates file-based TIFF. The client must guarantee that the TIFF portion + // of the file matches that from the parse in the file-based constructor. Offsets saved from that + // parse must still be valid. The open file reference need not be the same, e.g. the client can + // be doing a crash-safe update into a temporary copy. + // + // Both \c UpdateMemoryStream and \c UpdateFileStream use an update-by-append model. Changes are + // written in-place where they fit, anything requiring growth is appended to the end and the old + // space is abandoned. The end for memory-based TIFF is the end of the data block, the end for + // file-based TIFF is the end of the file. This update-by-append model has the advantage of not + // perturbing any hidden offsets, a common feature of proprietary MakerNotes. + // + // The condenseStream parameter to UpdateMemoryStream can be used to rewrite the full stream + // instead of appending. This will discard any MakerNote tags and risks breaking offsets that + // are hidden. This can be necessary though to try to make the TIFF fit in a JPEG file. + + virtual void ParseMemoryStream ( const void* data, XMP_Uns32 length, bool copyData = true ) = 0; + virtual void ParseFileStream ( XMP_IO* fileRef ) = 0; + + virtual void IntegrateFromPShop6 ( const void * buriedPtr, size_t buriedLen ) = 0; + + virtual XMP_Uns32 UpdateMemoryStream ( void** dataPtr, bool condenseStream = false ) = 0; + virtual void UpdateFileStream ( XMP_IO* fileRef ) = 0; + + // --------------------------------------------------------------------------------------------- + + GetUns16_Proc GetUns16; // Get values from the TIFF stream. + GetUns32_Proc GetUns32; // Always native endian on the outside, stream endian in the stream. + GetFloat_Proc GetFloat; + GetDouble_Proc GetDouble; + + PutUns16_Proc PutUns16; // Put values into the TIFF stream. + PutUns32_Proc PutUns32; // Always native endian on the outside, stream endian in the stream. + PutFloat_Proc PutFloat; + PutDouble_Proc PutDouble; + + virtual ~TIFF_Manager() {}; + +protected: + + bool bigEndian, nativeEndian; + + XMP_Uns32 CheckTIFFHeader ( const XMP_Uns8* tiffPtr, XMP_Uns32 length ); + + TIFF_Manager(); // Force clients to use the reader or writer derived classes. + + struct RawIFDEntry { + XMP_Uns16 id; + XMP_Uns16 type; + XMP_Uns32 count; + XMP_Uns32 dataOrOffset; + }; + +}; // TIFF_Manager + + +// ================================================================================================= +// ================================================================================================= + + +// ================================================================================================= +// TIFF_MemoryReader +// ================= + +class TIFF_MemoryReader : public TIFF_Manager { // The derived class for memory-based read-only access. +public: + + bool HasExifIFD() const { return (containedIFDs[kTIFF_ExifIFD].count != 0); }; + bool HasGPSInfoIFD() const { return (containedIFDs[kTIFF_GPSInfoIFD].count != 0); }; + + bool GetIFD ( XMP_Uns8 ifd, TagInfoMap* ifdMap ) const; + + bool GetTag ( XMP_Uns8 ifd, XMP_Uns16 id, TagInfo* info ) const; + + void SetTag ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns16 type, XMP_Uns32 count, const void* dataPtr ) { NotAppropriate(); }; + + void DeleteTag ( XMP_Uns8 ifd, XMP_Uns16 id ) { NotAppropriate(); }; + + XMP_Uns32 GetValueOffset ( XMP_Uns8 ifd, XMP_Uns16 id ) const; + + bool GetTag_Integer ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns32* data ) const; + + bool GetTag_Byte ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns8* data ) const; + bool GetTag_SByte ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int8* data ) const; + bool GetTag_Short ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns16* data ) const; + bool GetTag_SShort ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int16* data ) const; + bool GetTag_Long ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns32* data ) const; + bool GetTag_SLong ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int32* data ) const; + + bool GetTag_Rational ( XMP_Uns8 ifd, XMP_Uns16 id, Rational* data ) const; + bool GetTag_SRational ( XMP_Uns8 ifd, XMP_Uns16 id, SRational* data ) const; + + bool GetTag_Float ( XMP_Uns8 ifd, XMP_Uns16 id, float* data ) const; + bool GetTag_Double ( XMP_Uns8 ifd, XMP_Uns16 id, double* data ) const; + + bool GetTag_ASCII ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_StringPtr* dataPtr, XMP_StringLen* dataLen ) const; + + bool GetTag_EncodedString ( XMP_Uns8 ifd, XMP_Uns16 id, std::string* utf8Str ) const; + + void SetTag_EncodedString ( XMP_Uns8 ifd, XMP_Uns16 id, const std::string& utf8Str, XMP_Uns8 encoding ) { NotAppropriate(); }; + + bool IsChanged() { return false; }; + bool IsLegacyChanged() { return false; }; + + void ParseMemoryStream ( const void* data, XMP_Uns32 length, bool copyData = true ); + void ParseFileStream ( XMP_IO* fileRef ) { NotAppropriate(); }; + + void IntegrateFromPShop6 ( const void * buriedPtr, size_t buriedLen ) { NotAppropriate(); }; + + XMP_Uns32 UpdateMemoryStream ( void** dataPtr, bool condenseStream = false ) { if ( dataPtr != 0 ) *dataPtr = tiffStream; return tiffLength; }; + void UpdateFileStream ( XMP_IO* fileRef ) { NotAppropriate(); }; + + TIFF_MemoryReader() : ownedStream(false), tiffStream(0), tiffLength(0) {}; + + virtual ~TIFF_MemoryReader() { if ( this->ownedStream ) free ( this->tiffStream ); }; + +private: + + bool ownedStream; + + XMP_Uns8* tiffStream; + XMP_Uns32 tiffLength; + + // Memory usage notes: TIFF_MemoryReader is for memory-based read-only usage (both apply). There + // is no need to ever allocate separate blocks of memory, everything is used directly from the + // TIFF stream. Data pointers are computed on the fly, the offset field is 4 bytes and pointers + // will be 8 bytes for 64-bit platforms. + + struct TweakedIFDEntry { // ! Most fields are in native byte order, dataOrPos is for offsets only. + XMP_Uns16 id; + XMP_Uns16 type; + XMP_Uns32 bytes; + XMP_Uns32 dataOrPos; + TweakedIFDEntry() : id(0), type(0), bytes(0), dataOrPos(0) {}; + }; + + struct TweakedIFDInfo { + XMP_Uns16 count; + TweakedIFDEntry* entries; + TweakedIFDInfo() : count(0), entries(0) {}; + }; + + TweakedIFDInfo containedIFDs[kTIFF_KnownIFDCount]; + + static void SortIFD ( TweakedIFDInfo* thisIFD ); + + XMP_Uns32 ProcessOneIFD ( XMP_Uns32 ifdOffset, XMP_Uns8 ifd ); + + const TweakedIFDEntry* FindTagInIFD ( XMP_Uns8 ifd, XMP_Uns16 id ) const; + + const inline void* GetDataPtr ( const TweakedIFDEntry* tifdEntry ) const + { if ( tifdEntry->bytes <= 4 ) return &tifdEntry->dataOrPos; else return (this->tiffStream + tifdEntry->dataOrPos); }; + + static inline void NotAppropriate() { XMP_Throw ( "Not appropriate for TIFF_Reader", kXMPErr_InternalFailure ); }; + +}; // TIFF_MemoryReader + + +// ================================================================================================= +// ================================================================================================= + + +// ================================================================================================= +// TIFF_FileWriter +// =============== + +class TIFF_FileWriter : public TIFF_Manager { // The derived class for file-based or read-write access. +public: + + bool HasExifIFD() const { return this->containedIFDs[kTIFF_ExifIFD].tagMap.size() != 0; }; + bool HasGPSInfoIFD() const { return this->containedIFDs[kTIFF_GPSInfoIFD].tagMap.size() != 0; }; + + bool GetIFD ( XMP_Uns8 ifd, TagInfoMap* ifdMap ) const; + + bool GetTag ( XMP_Uns8 ifd, XMP_Uns16 id, TagInfo* info ) const; + + void SetTag ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns16 type, XMP_Uns32 count, const void* dataPtr ); + + void DeleteTag ( XMP_Uns8 ifd, XMP_Uns16 id ); + + XMP_Uns32 GetValueOffset ( XMP_Uns8 ifd, XMP_Uns16 id ) const; + + bool GetTag_Integer ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns32* data ) const; + + bool GetTag_Byte ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns8* data ) const; + bool GetTag_SByte ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int8* data ) const; + bool GetTag_Short ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns16* data ) const; + bool GetTag_SShort ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int16* data ) const; + bool GetTag_Long ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Uns32* data ) const; + bool GetTag_SLong ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_Int32* data ) const; + + bool GetTag_Rational ( XMP_Uns8 ifd, XMP_Uns16 id, Rational* data ) const; + bool GetTag_SRational ( XMP_Uns8 ifd, XMP_Uns16 id, SRational* data ) const; + + bool GetTag_Float ( XMP_Uns8 ifd, XMP_Uns16 id, float* data ) const; + bool GetTag_Double ( XMP_Uns8 ifd, XMP_Uns16 id, double* data ) const; + + bool GetTag_ASCII ( XMP_Uns8 ifd, XMP_Uns16 id, XMP_StringPtr* dataPtr, XMP_StringLen* dataLen ) const; + + bool GetTag_EncodedString ( XMP_Uns8 ifd, XMP_Uns16 id, std::string* utf8Str ) const; + + void SetTag_EncodedString ( XMP_Uns8 ifd, XMP_Uns16 id, const std::string& utf8Str, XMP_Uns8 encoding ); + + bool IsChanged() { return this->changed; }; + + bool IsLegacyChanged(); + + enum { kDoNotCopyData = false }; + + void ParseMemoryStream ( const void* data, XMP_Uns32 length, bool copyData = true ); + void ParseFileStream ( XMP_IO* fileRef ); + + void IntegrateFromPShop6 ( const void * buriedPtr, size_t buriedLen ); + + XMP_Uns32 UpdateMemoryStream ( void** dataPtr, bool condenseStream = false ); + void UpdateFileStream ( XMP_IO* fileRef ); + + TIFF_FileWriter(); + + virtual ~TIFF_FileWriter(); + +private: + + bool changed, legacyDeleted; + bool memParsed, fileParsed; + bool ownedStream; + + XMP_Uns8* memStream; + XMP_Uns32 tiffLength; + + // Memory usage notes: TIFF_FileWriter is for file-based OR read/write usage. For memory-based + // streams the dataPtr is initially into the stream, regardless of size. For file-based streams + // the dataPtr is initially a separate allocation for large values (over 4 bytes), and points to + // the smallValue field for small values. This is also the usage when a tag is changed (for both + // memory and file cases), the dataPtr is a separate allocation for large values (over 4 bytes), + // and points to the smallValue field for small values. + + // ! The working data values are always stream endian, no matter where stored. They are flipped + // ! as necessary by GetTag and SetTag. + + static const bool kIsFileBased = true; // For use in the InternalTagInfo constructor. + static const bool kIsMemoryBased = false; + + class InternalTagInfo { + public: + + XMP_Uns16 id; + XMP_Uns16 type; + XMP_Uns32 count; + XMP_Uns32 dataLen; + XMP_Uns32 smallValue; // Small value in stream endianness, but "left" justified. + XMP_Uns8* dataPtr; // Parsing captures all small values, only large ones that we care about. + XMP_Uns32 origDataLen; // The original (parse time) data length in bytes. + XMP_Uns32 origDataOffset; // The original data offset, regardless of length. + bool changed; + bool fileBased; + + inline void FreeData() { + if ( this->fileBased || this->changed ) { + if ( (this->dataLen > 4) && (this->dataPtr != 0) ) { free ( this->dataPtr ); this->dataPtr = 0; } + } + } + + InternalTagInfo ( XMP_Uns16 _id, XMP_Uns16 _type, XMP_Uns32 _count, bool _fileBased ) + : id(_id), type(_type), count(_count), dataLen(0), smallValue(0), dataPtr(0), + origDataLen(0), origDataOffset(0), changed(false), fileBased(_fileBased) {}; + ~InternalTagInfo() { this->FreeData(); }; + + void operator= ( const InternalTagInfo & in ) + { + // ! Gag! Transfer ownership of the dataPtr! + this->FreeData(); + memcpy ( this, &in, sizeof(*this) ); // AUDIT: Use of sizeof(InternalTagInfo) is safe. + if ( this->dataLen <= 4 ) { + this->dataPtr = (XMP_Uns8*) &this->smallValue; // Don't use the copied pointer. + } else { + *((XMP_Uns8**)&in.dataPtr) = 0; // The pointer is now owned by "this". + } + }; + + private: + + InternalTagInfo() // Hidden on purpose, fileBased must be properly set. + : id(0), type(0), count(0), dataLen(0), smallValue(0), dataPtr(0), + origDataLen(0), origDataOffset(0), changed(false), fileBased(false) {}; + + }; + + typedef std::map<XMP_Uns16,InternalTagInfo> InternalTagMap; + + struct InternalIFDInfo { + bool changed; + XMP_Uns16 origCount; // Original number of IFD entries. + XMP_Uns32 origIFDOffset; // Original stream offset of the IFD. + XMP_Uns32 origNextIFD; // Original stream offset of the following IFD. + InternalTagMap tagMap; + InternalIFDInfo() : changed(false), origCount(0), origIFDOffset(0), origNextIFD(0) {}; + inline void clear() + { + this->changed = false; + this->origCount = 0; + this->origIFDOffset = this->origNextIFD = 0; + this->tagMap.clear(); + }; + }; + + InternalIFDInfo containedIFDs[kTIFF_KnownIFDCount]; + + static XMP_Uns8 PickIFD ( XMP_Uns8 ifd, XMP_Uns16 id ); + const InternalTagInfo* FindTagInIFD ( XMP_Uns8 ifd, XMP_Uns16 id ) const; + + void DeleteExistingInfo(); + + XMP_Uns32 ProcessMemoryIFD ( XMP_Uns32 ifdOffset, XMP_Uns8 ifd ); + XMP_Uns32 ProcessFileIFD ( XMP_Uns8 ifd, XMP_Uns32 ifdOffset, XMP_IO* fileRef, void* _ioBuf ); + // *** Temporary hack above, _ioBuf is IOBuffer* but don't want to include XIO.hpp. + + void ProcessPShop6IFD ( const TIFF_MemoryReader& buriedExif, XMP_Uns8 ifd ); + + void* CopyTagToMasterIFD ( const TagInfo& ps6Tag, InternalIFDInfo* masterIFD ); + + void PreflightIFDLinkage(); + + XMP_Uns32 DetermineVisibleLength(); + + XMP_Uns32 DetermineAppendInfo ( XMP_Uns32 appendedOrigin, + bool appendedIFDs[kTIFF_KnownIFDCount], + XMP_Uns32 newIFDOffsets[kTIFF_KnownIFDCount], + bool appendAll = false ); + + void UpdateMemByAppend ( XMP_Uns8** newStream_out, XMP_Uns32* newLength_out, + bool appendAll = false, XMP_Uns32 extraSpace = 0 ); + void UpdateMemByRewrite ( XMP_Uns8** newStream_out, XMP_Uns32* newLength_out ); + + void WriteFileIFD ( XMP_IO* fileRef, InternalIFDInfo & thisIFD ); + +}; // TIFF_FileWriter + + +// ================================================================================================= + +#endif // __TIFF_Support_hpp__ diff --git a/XMPFiles/source/FormatSupport/WAVE/BEXTMetadata.cpp b/XMPFiles/source/FormatSupport/WAVE/BEXTMetadata.cpp new file mode 100644 index 0000000..a11e481 --- /dev/null +++ b/XMPFiles/source/FormatSupport/WAVE/BEXTMetadata.cpp @@ -0,0 +1,347 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include <string.h> + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/WAVE/BEXTMetadata.h" +#include "source/Endian.h" + +using namespace IFF_RIFF; + +static const XMP_Uns32 kBEXTSizeMin = 602; // at minimum 602 bytes + +static const XMP_Uns32 kSizeDescription = 256; +static const XMP_Uns32 kSizeOriginator = 32; +static const XMP_Uns32 kSizeOriginatorReference = 32; +static const XMP_Uns32 kSizeOriginationDate = 10; +static const XMP_Uns32 kSizeOriginationTime = 8; + +// Needed to be able to memcpy directly to this struct. +#pragma pack ( push, 1 ) + struct BEXT + { + char mDescription[256]; + char mOriginator[32]; + char mOriginatorReference[32]; + char mOriginationDate[10]; + char mOriginationTime[8]; + XMP_Uns32 mTimeReferenceLow; + XMP_Uns32 mTimeReferenceHigh; + XMP_Uns16 mVersion; + XMP_Uns8 mUMID[64]; + XMP_Uns8 mReserved[190]; + }; +#pragma pack ( pop ) + +//----------------------------------------------------------------------------- +// +// [static] convertLF(...) +// +// Purpose: Convert Mac/Unix line feeds to CR/LF +// +//----------------------------------------------------------------------------- + +void BEXTMetadata::NormalizeLF( std::string& str ) +{ + XMP_Uns32 i = 0; + while( i < str.length() ) + { + char ch = str[i]; + + if( ch == 0x0d ) + { + // + // possible Mac lf + // + if( i+1 < str.length() ) + { + if( str[i+1] != 0x0a ) + { + // + // insert missing LF character + // + str.insert( i+1, 1, 0x0a ); + } + + i += 2; + } + else + { + str.push_back( 0x0a ); + } + } + else if( ch == 0x0a ) + { + // + // possible Unix LF + // + if( i == 0 || str[i-1] != 0x0d ) + { + // + // insert missing CR character + // + str.insert( i, 1, 0x0d ); + i += 2; + } + else + { + i++; + } + } + else + { + i++; + } + } +} + +//----------------------------------------------------------------------------- +// +// BEXTMetadata::BEXTMetadata(...) +// +// Purpose: ctor/dtor +// +//----------------------------------------------------------------------------- + +BEXTMetadata::BEXTMetadata() +{ +} + +BEXTMetadata::~BEXTMetadata() +{ +} + +//----------------------------------------------------------------------------- +// +// BEXTMetadata::parse(...) +// +// Purpose: Parses the given memory block and creates a data model representation +// The implementation expects that the memory block is the data area of +// the BEXT chunk and its size is at least as big as the minimum size +// of a BEXT data block. +// Throws exceptions if parsing is not possible +// +//----------------------------------------------------------------------------- + +void BEXTMetadata::parse( const XMP_Uns8* chunkData, XMP_Uns64 size ) +{ + if( size >= kBEXTSizeMin ) + { + const LittleEndian& LE = LittleEndian::getInstance(); + + BEXT bext; + memset( &bext, 0, kBEXTSizeMin ); + + // + // copy input data into BEXT block (except CodingHistory field) + // Safe as fixed size matches size of struct that is #pragma packed(1) + // + memcpy( &bext, chunkData, kBEXTSizeMin ); + + // + // copy CodingHistory + // + if( size > kBEXTSizeMin ) + { + this->setValue<std::string>( kCodingHistory, std::string( reinterpret_cast<const char*>(&chunkData[kBEXTSizeMin]), static_cast<std::string::size_type>(size - kBEXTSizeMin) ) ); + } + + // + // copy values to map + // + this->setValue<std::string>( kDescription, std::string( bext.mDescription, kSizeDescription ) ); + this->setValue<std::string>( kOriginator, std::string( bext.mOriginator, kSizeOriginator ) ); + this->setValue<std::string>( kOriginatorReference, std::string( bext.mOriginatorReference, kSizeOriginatorReference ) ); + this->setValue<std::string>( kOriginationDate, std::string( bext.mOriginationDate, kSizeOriginationDate ) ); + this->setValue<std::string>( kOriginationTime, std::string( bext.mOriginationTime, kSizeOriginationTime ) ); + + this->setValue<XMP_Uns64>( kTimeReference, LE.getUns64( &bext.mTimeReferenceLow ) ); + this->setValue<XMP_Uns16>( kVersion, LE.getUns16( &bext.mVersion ) ); + + this->setArray<XMP_Uns8>( kUMID, bext.mUMID, 64 ); + + this->resetChanges(); + } + else + { + XMP_Throw ( "Not a valid BEXT chunk", kXMPErr_BadFileFormat ); + } +} + +//----------------------------------------------------------------------------- +// +// BEXTMetadata::serialize(...) +// +// Purpose: Serializes the data model to a memory block. +// The memory block will be the data area of a BEXT chunk. +// Throws exceptions if serializing is not possible +// +//----------------------------------------------------------------------------- + +XMP_Uns64 BEXTMetadata::serialize( XMP_Uns8** outBuffer ) +{ + XMP_Uns64 size = 0; + + if( outBuffer != NULL ) + { + const LittleEndian& LE = LittleEndian::getInstance(); + + size = kBEXTSizeMin; + + std::string codingHistory; + + if( this->valueExists( kCodingHistory ) ) + { + codingHistory = this->getValue<std::string>( kCodingHistory ); + NormalizeLF( codingHistory ); + + size += codingHistory.length(); + } + + // + // setup buffer + // + XMP_Uns8* buffer = new XMP_Uns8[static_cast<size_t>(size)]; + + // + // copy values and strings back to BEXT block + // + // ! Safe use of strncpy as the fixed size is consistent with the size of the destination buffer + // But it is intentional here that the string might not be null terminated if + // the size of the source is equal to the fixed size of the destination + // + BEXT bext; + memset( &bext, 0, kBEXTSizeMin ); + + if( this->valueExists( kDescription ) ) + { + strncpy( bext.mDescription, this->getValue<std::string>( kDescription ).c_str(), kSizeDescription ); + } + if( this->valueExists( kOriginator ) ) + { + strncpy( bext.mOriginator, this->getValue<std::string>( kOriginator ).c_str(), kSizeOriginator ); + } + if( this->valueExists( kOriginatorReference ) ) + { + strncpy( bext.mOriginatorReference, this->getValue<std::string>( kOriginatorReference ).c_str(), kSizeOriginatorReference ); + } + if( this->valueExists( kOriginationDate ) ) + { + strncpy( bext.mOriginationDate, this->getValue<std::string>( kOriginationDate ).c_str(), kSizeOriginationDate ); + } + if( this->valueExists( kOriginationTime ) ) + { + strncpy( bext.mOriginationTime, this->getValue<std::string>( kOriginationTime ).c_str(), kSizeOriginationTime ); + } + + if( this->valueExists( kTimeReference ) ) + { + LE.putUns64( this->getValue<XMP_Uns64>( kTimeReference ), &bext.mTimeReferenceLow ); + } + + if( this->valueExists( kVersion ) ) + { + LE.putUns16( this->getValue<XMP_Uns16>( kVersion ), &bext.mVersion ); + } + else // Special case: If no value is given, a value of "1" is the default! + { + LE.putUns16( 1, &bext.mVersion ); + } + + if( this->valueExists( kUMID ) ) + { + XMP_Uns32 muidSize = 0; + const XMP_Uns8* const muid = this->getArray<XMP_Uns8>( kUMID, muidSize ); + + // Make sure to copy 64 bytes max. + muidSize = muidSize > 64 ? 64 : muidSize; + memcpy( bext.mUMID, muid, muidSize ); + } + // + // set input buffer to zero + // + memset( buffer, 0, static_cast<size_t>(size) ); + + // + // copy BEXT block into buffer (except CodingHistory field) + // + memcpy( buffer, &bext, kBEXTSizeMin ); + + // + // copy CodingHistory field into buffer + // + if( ! codingHistory.empty() ) + { + memcpy( buffer + kBEXTSizeMin, codingHistory.c_str(), static_cast<size_t>(size - kBEXTSizeMin) ); + } + + *outBuffer = buffer; + } + else + { + XMP_Throw ( "Invalid buffer", kXMPErr_InternalFailure ); + } + + return size; +} + +//----------------------------------------------------------------------------- +// +// BEXTMetadata::isEmptyValue(...) +// +// Purpose: Is the value of the passed ValueObject and its id "empty"? +// +//----------------------------------------------------------------------------- + +bool BEXTMetadata::isEmptyValue( XMP_Uns32 id, ValueObject& valueObj ) +{ + bool ret = true; + + switch( id ) + { + case kDescription: + case kOriginator: + case kOriginatorReference: + case kOriginationDate: + case kOriginationTime: + case kCodingHistory: + { + TValueObject<std::string>* strObj = dynamic_cast<TValueObject<std::string>*>(&valueObj); + + ret = ( strObj == NULL || ( strObj != NULL && strObj->getValue().empty() ) ); + } + break; + + case kTimeReference: + case kVersion: + ret = false; + break; + case kUMID: + { + TArrayObject<XMP_Uns8>* obj = dynamic_cast<TArrayObject<XMP_Uns8>*>(&valueObj); + + if( obj != NULL ) + { + XMP_Uns32 size = 0; + const XMP_Uns8* const buffer = obj->getArray( size ); + + ret = ( size == 0 ); + } + } + break; + + default: + ret = true; + } + + return ret; +} diff --git a/XMPFiles/source/FormatSupport/WAVE/BEXTMetadata.h b/XMPFiles/source/FormatSupport/WAVE/BEXTMetadata.h new file mode 100644 index 0000000..0f57246 --- /dev/null +++ b/XMPFiles/source/FormatSupport/WAVE/BEXTMetadata.h @@ -0,0 +1,99 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _BEXTMetadata_h_ +#define _BEXTMetadata_h_ + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/NativeMetadataSupport/IMetadata.h" + +namespace IFF_RIFF +{ + +/** + * BEXT Metadata model. + * Implements the IMetadata interface + */ +class BEXTMetadata : public IMetadata +{ +public: + enum + { + kDescription, // std::string + kOriginator, // std::string + kOriginatorReference, // std::string + kOriginationDate, // std::string + kOriginationTime, // std::string + kTimeReference, // XMP_Uns64 + kVersion, // XMP_Uns16 + kUMID, // XMP_Uns8[64] + kCodingHistory // std::string + }; + +public: + /** + *ctor/dtor + */ + BEXTMetadata(); + ~BEXTMetadata(); + + /** + * Parses the given memory block and creates a data model representation + * The implementation expects that the memory block is the data area of + * the BEXT chunk. + * Throws exceptions if parsing is not possible + * + * @param input The byte buffer to parse + * @param size Size of the given byte buffer + */ + void parse( const XMP_Uns8* chunkData, XMP_Uns64 size ); + + /** + * See IMetadata::parse( const LFA_FileRef input ) + */ + void parse( XMP_IO* input ) { IMetadata::parse( input ); } + + /** + * Serializes the data model to a memory block. + * The memory block will be the data area of a BEXT chunk. + * Throws exceptions if serializing is not possible + * + * @param buffer Buffer that gets filled with serialized data + * @param size Size of passed in buffer + * + * @return Size of serialzed data (might be smaller than buffer size) + */ + XMP_Uns64 serialize( XMP_Uns8** buffer ); + +protected: + /** + * @see IMetadata::isEmptyValue + */ + virtual bool isEmptyValue( XMP_Uns32 id, ValueObject& valueObj ); + + /** + * Normalize line feeds to \CR\LF + * Mac <OS9: \r + * Linux: \n + * Win: \r\n + */ + static void NormalizeLF( std::string& str ); + +private: + // Operators hidden on purpose + BEXTMetadata( const BEXTMetadata& ) {}; + BEXTMetadata& operator=( const BEXTMetadata& ) { return *this; }; +}; + +} + +#endif diff --git a/XMPFiles/source/FormatSupport/WAVE/CartMetadata.cpp b/XMPFiles/source/FormatSupport/WAVE/CartMetadata.cpp new file mode 100644 index 0000000..07c8969 --- /dev/null +++ b/XMPFiles/source/FormatSupport/WAVE/CartMetadata.cpp @@ -0,0 +1,316 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp" +#include "XMPFiles/source/FormatSupport/WAVE/CartMetadata.h" + +#include "source/EndianUtils.hpp" + +using namespace IFF_RIFF; + +// ============================================================================================= + +// Types and globals for the stored form of the cart chunk. + +#pragma pack ( push, 1 ) + +struct StoredCartChunk { + char Version[4]; // All of the fixed size text fields are null-filled local text, + char Title[64]; // but need not have a terminating null. + char Artist[64]; + char CutID[64]; + char ClientID[64]; + char Category[64]; + char Classification[64]; + char OutCue[64]; + char StartDate[10]; + char StartTime[8]; + char EndDate[10]; + char EndTime[8]; + char ProducerAppID[64]; + char ProducerAppVersion[64]; + char UserDef[64]; + XMP_Int32 LevelReference; // Little endian in the file. + CartMetadata::StoredCartTimer PostTimer[CartMetadata::kPostTimerLength]; + char Reserved[276]; + char URL[1024]; + // char TagText[]; - fills the tail of the chunk +}; + +static const size_t kMinimumCartChunkSize = sizeof(StoredCartChunk); + +#pragma pack ( pop ) + +static const size_t kFixedTextCount = (CartMetadata::kLastFixedTextField - CartMetadata::kFirstFixedTextField + 1); + +struct FixedTextFieldInfo { + size_t length; + size_t offset; +}; + +static const FixedTextFieldInfo kFixedTextFields [kFixedTextCount] = { + // ! The order of the items must match the order of the mapped field enum in the header. + { 4, offsetof ( StoredCartChunk, Version ) }, + { 64, offsetof ( StoredCartChunk, Title ) }, + { 64, offsetof ( StoredCartChunk, Artist ) }, + { 64, offsetof ( StoredCartChunk, CutID ) }, + { 64, offsetof ( StoredCartChunk, ClientID ) }, + { 64, offsetof ( StoredCartChunk, Category ) }, + { 64, offsetof ( StoredCartChunk, Classification ) }, + { 64, offsetof ( StoredCartChunk, OutCue ) }, + { 10, offsetof ( StoredCartChunk, StartDate ) }, + { 8, offsetof ( StoredCartChunk, StartTime ) }, + { 10, offsetof ( StoredCartChunk, EndDate ) }, + { 8, offsetof ( StoredCartChunk, EndTime ) }, + { 64, offsetof ( StoredCartChunk, ProducerAppID ) }, + { 64, offsetof ( StoredCartChunk, ProducerAppVersion ) }, + { 64, offsetof ( StoredCartChunk, UserDef ) }, + { 1024, offsetof ( StoredCartChunk, URL ) } +}; + +// ================================================================================================= + +CartMetadata::CartMetadata() { + // Nothing to do. +} + +// ================================================================================================= + +CartMetadata::~CartMetadata() { + // Nothing to do. +} + +// ================================================================================================= + +static size_t FindZeroByte ( const char * textPtr, size_t maxLength ) { + // Return the offset of the first zero byte, up to maxLength. + size_t length = 0; + while ( (length < maxLength) && (textPtr[length] != 0) ) ++length; + return length; +} + +// ================================================================================================= + +void CartMetadata::parse ( const XMP_Uns8* chunkData, XMP_Uns64 chunkSize ) +{ + // Make sure the chunk has a reasonable size. + if ( chunkSize > 1000*1000*1000 ) + { + XMP_Throw ( "Not a valid Cart chunk", kXMPErr_BadFileFormat ); + } + + StoredCartChunk* fileChunk; + + // If the chunk is too small, copy and pad with zeros + if ( chunkSize < kMinimumCartChunkSize ) + { + fileChunk = new StoredCartChunk; + memset( fileChunk, 0, kMinimumCartChunkSize ); + memcpy( fileChunk, chunkData, static_cast<size_t>(chunkSize) ); // AUDIT: safe, chunkSize is smaller than buffer + } + else + { + fileChunk = (StoredCartChunk*)chunkData; + } + + try + { + std::string localStr; + + // Extract the binary LevelReference field. + + this->setValue<XMP_Int32> ( kLevelReference, (XMP_Int32) GetUns32LE ( &fileChunk->LevelReference ) ); + + // Extract the PostTimer Array + // first ensure the correct endianess + StoredCartTimer timerArray[CartMetadata::kPostTimerLength]; + for ( XMP_Uns32 i = 0; i < CartMetadata::kPostTimerLength; i++ ) + { + timerArray[i].usage = GetUns32BE( &fileChunk->PostTimer[i].usage ); + timerArray[i].value = GetUns32LE( &fileChunk->PostTimer[i].value ); + } + this->setArray<StoredCartTimer> (kPostTimer, timerArray, CartMetadata::kPostTimerLength); + + // Extract the trailing TagText portion, if any. Keep the local encoding, the conversion to + // Unicode is done later when importing to XMP. + + if ( chunkSize > kMinimumCartChunkSize ) { + + const char * tagTextPtr = (char*)fileChunk + sizeof(StoredCartChunk); + const size_t trailerSize = (size_t)chunkSize - kMinimumCartChunkSize; + const size_t tagTextSize = FindZeroByte ( tagTextPtr, trailerSize ); + localStr.assign ( tagTextPtr, tagTextSize ); + this->setValue<std::string> ( kTagText, localStr ); + + } + + // Extract the fixed length text fields. Keep the local encoding, the conversion to Unicode is + // done later when importing to XMP. + + for ( int i = CartMetadata::kFirstFixedTextField; i <= CartMetadata::kLastFixedTextField; ++i ) { + + const FixedTextFieldInfo& currField = kFixedTextFields[i]; + const char * textPtr = (char*)fileChunk + currField.offset; + size_t textLen = FindZeroByte ( textPtr, currField.length ); + if ( textLen > 0 ) { + localStr.assign ( textPtr, textLen ); + this->setValue<std::string> ( i, localStr ); + } + + } + + this->resetChanges(); + } + catch ( ... ) // setValue/setArray might throw + { + // If the chunk had been too small, it has been copied to a new buffer padded with zeros + if ( chunkSize < kMinimumCartChunkSize ) + { + delete fileChunk; + } + throw; + } + + if ( chunkSize < kMinimumCartChunkSize ) + { + delete fileChunk; + } + +} // CartMetadata::parse + +// ================================================================================================= + +XMP_Uns64 CartMetadata::serialize ( XMP_Uns8** outBuffer ) +{ + + if ( outBuffer == NULL ) + { + XMP_Throw ( "Invalid buffer", kXMPErr_InternalFailure ); + } + + *outBuffer = 0; // Default in case of early exit. + + // Set up the output buffer. + + size_t trailerSize = 0; + std::string tagTextStr; + if ( this->valueExists ( kTagText ) ) { + tagTextStr = this->getValue<std::string> ( kTagText ); + trailerSize = tagTextStr.size() + 1; // Include the final nul. + } + + size_t chunkSize = sizeof(StoredCartChunk) + trailerSize; + XMP_Uns8* buffer = new XMP_Uns8 [ chunkSize ]; + if ( buffer == 0 ) XMP_Throw ( "Cannot allocate cart chunk buffer", kXMPErr_NoMemory ); + memset ( buffer, 0, chunkSize ); // Fill with zeros for missing or short fields. + + StoredCartChunk* newChunk = (StoredCartChunk*)buffer; + + // Insert the binary LevelReference field. + + if ( this->valueExists ( kLevelReference ) ) { + newChunk->LevelReference = MakeUns32LE ( (XMP_Uns32) this->getValue<XMP_Int32> ( kLevelReference ) ); + } + + // Insert the PostTimer Array + if ( this->valueExists ( kPostTimer ) ) { + XMP_Uns32 size = 0; + const StoredCartTimer* timerArray = this->getArray<StoredCartTimer> ( kPostTimer, size ); + XMP_Assert (size == CartMetadata::kPostTimerLength ); + + for ( XMP_Uns32 i = 0; i<CartMetadata::kPostTimerLength; i++ ) + { + newChunk->PostTimer[i].usage = MakeUns32BE(timerArray[i].usage); // ensure the FOURCC is written in Big Endian + newChunk->PostTimer[i].value = MakeUns32LE(timerArray[i].value); // ensure the value is written as Little Endian + } + } + + // Insert the trailing TagText portion, if any. The conversion to local encoding should have + // been done when exporting from XMP. Include the final nul. + + if ( ! tagTextStr.empty() ) { + XMP_Uns8* tagTextPtr = buffer + sizeof(StoredCartChunk); + strncpy ( (char*)tagTextPtr, tagTextStr.c_str(), trailerSize ); // AUDIT: Safe, see prior allocation. + } + + // Insert the fixed length text fields. The conversion to local encoding should have been done + // when exporting from XMP. + + std::string currStr; + + for ( int i = CartMetadata::kFirstFixedTextField; i <= CartMetadata::kLastFixedTextField; ++i ) { + + if ( ! this->valueExists ( i ) ) continue; + const FixedTextFieldInfo& currField = kFixedTextFields[i]; + + currStr = this->getValue<std::string> ( i ); + if ( currStr.empty() ) continue; + if ( currStr.size() > currField.length ) currStr.erase ( currField.length ); + XMP_Assert ( currStr.size() <= currField.length ); + + strncpy ( (char*)(buffer + currField.offset), currStr.c_str(), currStr.size() ); // // AUDIT: Safe, within fixed bounds. + + } + + // Done. + + *outBuffer = buffer; + return chunkSize; + +} // CartMetadata::serialize + +// ================================================================================================= + +bool CartMetadata::isEmptyValue ( XMP_Uns32 id, ValueObject& valueObj ) { + + bool isEmpty = true; + + switch( id ) + { + case kLevelReference: + { + //XMP_Int32 + TValueObject<XMP_Int32>* binObj = dynamic_cast<TValueObject<XMP_Int32>*>(&valueObj); + isEmpty = ( binObj == 0 ); // All values are valid. + } + break; + + case kPostTimer: + { + // Array[8]*2 + TArrayObject<StoredCartTimer>* obj = dynamic_cast<TArrayObject<StoredCartTimer>*>(&valueObj); + + if( obj != NULL ) + { + XMP_Uns32 size = 0; + const StoredCartTimer* const buffer = obj->getArray( size ); + + isEmpty = ( size == 0 ); + } + } + break; + + default: + { + // String values + TValueObject<std::string>* strObj = dynamic_cast<TValueObject<std::string>*>(&valueObj); + isEmpty = ( (strObj == 0) || strObj->getValue().empty() ); + } + break; + } + + return isEmpty; + +} // CartMetadata::isEmptyValue + +// ================================================================================================= diff --git a/XMPFiles/source/FormatSupport/WAVE/CartMetadata.h b/XMPFiles/source/FormatSupport/WAVE/CartMetadata.h new file mode 100644 index 0000000..29266a7 --- /dev/null +++ b/XMPFiles/source/FormatSupport/WAVE/CartMetadata.h @@ -0,0 +1,87 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _CartMetadata_h_ +#define _CartMetadata_h_ + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/NativeMetadataSupport/IMetadata.h" + +namespace IFF_RIFF { + +// ================================================================================================= + +class CartMetadata : public IMetadata { + +public: + + enum { + + // IDs for the mapped cart chunk fields. + kVersion, // Local text, max 4 bytes + kTitle, // Local text, max 64 bytes + kArtist, // Local text, max 64 bytes + kCutID, // Local text, max 64 bytes + kClientID, // Local text, max 64 bytes + kCategory, // Local text, max 64 bytes + kClassification, // Local text, max 64 bytes + kOutCue, // Local text, max 64 bytes + kStartDate, // Local text, max 10 bytes + kStartTime, // Local text, max 8 bytes + kEndDate, // Local text, max 10 bytes + kEndTime, // Local text, max 8 bytes + kProducerAppID, // Local text, max 64 bytes + kProducerAppVersion, // Local text, max 64 bytes + kUserDef, // Local text, max 64 bytes + kURL, // Local text, max 1024 bytes + kTagText, // Local text, no limit + kLevelReference, // Little endian unsigned 32 + kPostTimer, // array[8] of usage(Uns32), value(Uns32) + kReserved, // 276 reserved bytes + + // Constants for the range of fixed length text fields. + kFirstFixedTextField = kVersion, + kLastFixedTextField = kURL + + }; + + struct StoredCartTimer { + XMP_Uns32 usage; + XMP_Uns32 value; + }; + + enum { kPostTimerLength = 8 }; + + CartMetadata(); + virtual ~CartMetadata(); + + void parse ( const XMP_Uns8* chunkData, XMP_Uns64 chunkSize ); + void parse ( XMP_IO* input ) { IMetadata::parse ( input ); } + + XMP_Uns64 serialize ( XMP_Uns8** buffer ); + +protected: + + virtual bool isEmptyValue ( XMP_Uns32 id, ValueObject& valueObj ); + +private: + + // Operators hidden on purpose. + CartMetadata ( const CartMetadata& ) {}; + CartMetadata& operator= ( const CartMetadata& ) { return *this; }; + +}; // CartMetadata + +} // namespace IFF_RIFF + +#endif diff --git a/XMPFiles/source/FormatSupport/WAVE/Cr8rMetadata.cpp b/XMPFiles/source/FormatSupport/WAVE/Cr8rMetadata.cpp new file mode 100644 index 0000000..c9cbc23 --- /dev/null +++ b/XMPFiles/source/FormatSupport/WAVE/Cr8rMetadata.cpp @@ -0,0 +1,240 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include <string.h> + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/WAVE/Cr8rMetadata.h" +#include "source/Endian.h" + +using namespace IFF_RIFF; + +static const XMP_Uns32 kCr8rSizeFix = 84; // always 84 bytes + +static const XMP_Uns32 kSizeFileExt = 16; +static const XMP_Uns32 kSizeAppOtions = 16; +static const XMP_Uns32 kSizeAppName = 32; + +// Needed to be able to memcpy directly to this struct. +#pragma pack ( push, 1 ) + struct Cr8rBoxContent + { + XMP_Uns32 mMagic; + XMP_Uns32 mSize; + XMP_Uns16 mMajorVer; + XMP_Uns16 mMinorVer; + XMP_Uns32 mCreatorCode; + XMP_Uns32 mAppleEvent; + char mFileExt[kSizeFileExt]; + char mAppOptions[kSizeAppOtions]; + char mAppName[kSizeAppName]; + }; +#pragma pack ( pop ) + +//----------------------------------------------------------------------------- +// +// Cr8rMetadata::Cr8rMetadata(...) +// +// Purpose: ctor/dtor +// +//----------------------------------------------------------------------------- + +Cr8rMetadata::Cr8rMetadata() +{ +} + +Cr8rMetadata::~Cr8rMetadata() +{ +} + +//----------------------------------------------------------------------------- +// +// Cr8rMetadata::parse(...) +// +// Purpose: Parses the given memory block and creates a data model representation +// The implementation expects that the memory block is the data area of +// the BEXT chunk and its size is at least as big as the minimum size +// of a BEXT data block. +// Throws exceptions if parsing is not possible +// +//----------------------------------------------------------------------------- + +void Cr8rMetadata::parse( const XMP_Uns8* chunkData, XMP_Uns64 size ) +{ + if( size >= kCr8rSizeFix ) + { + const LittleEndian& LE = LittleEndian::getInstance(); + + Cr8rBoxContent cr8r; + memset( &cr8r, 0, kCr8rSizeFix ); + + // + // copy input data into Cr8r block + // Safe as fixed size matches size of struct that is #pragma packed(1) + // + memcpy( &cr8r, chunkData, kCr8rSizeFix ); + + // + // copy values to map + // + this->setValue<XMP_Uns32>( kMagic, cr8r.mMagic ); + this->setValue<XMP_Uns32>( kSize, cr8r.mSize ); + this->setValue<XMP_Uns16>( kMajorVer, cr8r.mMajorVer ); + this->setValue<XMP_Uns16>( kMinorVer, cr8r.mMinorVer ); + this->setValue<XMP_Uns32>( kCreatorCode, cr8r.mCreatorCode ); + this->setValue<XMP_Uns32>( kAppleEvent, cr8r.mAppleEvent ); + this->setValue<std::string>( kFileExt, std::string( cr8r.mFileExt, kSizeFileExt ) ); + this->setValue<std::string>( kAppOptions, std::string( cr8r.mAppOptions, kSizeAppOtions ) ); + this->setValue<std::string>( kAppName, std::string( cr8r.mAppName, kSizeAppName ) ); + + this->resetChanges(); + } + else + { + XMP_Throw ( "Not a valid Cr8r chunk", kXMPErr_BadFileFormat ); + } +} + +//----------------------------------------------------------------------------- +// +// Cr8rMetadata::serialize(...) +// +// Purpose: Serializes the data model to a memory block. +// The memory block will be the data area of a BEXT chunk. +// Throws exceptions if serializing is not possible +// +//----------------------------------------------------------------------------- + +XMP_Uns64 Cr8rMetadata::serialize( XMP_Uns8** outBuffer ) +{ + XMP_Uns64 size = 0; + + if( outBuffer != NULL ) + { + const LittleEndian& LE = LittleEndian::getInstance(); + + size = kCr8rSizeFix; + + // + // setup buffer + // + XMP_Uns8* buffer = new XMP_Uns8[static_cast<size_t>(size)]; + + // + // copy values and strings back to BEXT block + // + // ! Safe use of strncpy as the fixed size is consistent with the size of the destination buffer + // But it is intentional here that the string might not be null terminated if + // the size of the source is equal to the fixed size of the destination + // + Cr8rBoxContent cr8r; + memset( &cr8r, 0, kCr8rSizeFix ); + + if( this->valueExists( kMagic ) ) + { + LE.putUns32( this->getValue<XMP_Uns32>( kMagic ), &cr8r.mMagic ); + } + if( this->valueExists( kSize ) ) + { + LE.putUns32( this->getValue<XMP_Uns32>( kSize ), &cr8r.mSize ); + } + if( this->valueExists( kMajorVer ) ) + { + LE.putUns16( this->getValue<XMP_Uns16>( kMajorVer ), &cr8r.mMajorVer ); + } + if( this->valueExists( kMinorVer ) ) + { + LE.putUns16( this->getValue<XMP_Uns16>( kMinorVer ), &cr8r.mMinorVer ); + } + if( this->valueExists( kCreatorCode ) ) + { + LE.putUns32( this->getValue<XMP_Uns32>( kCreatorCode ), &cr8r.mCreatorCode ); + } + if( this->valueExists( kAppleEvent ) ) + { + LE.putUns32( this->getValue<XMP_Uns32>( kAppleEvent ), &cr8r.mAppleEvent ); + } + if( this->valueExists( kFileExt ) ) + { + strncpy( cr8r.mFileExt, this->getValue<std::string>( kFileExt ).c_str(), kSizeFileExt ); + } + if( this->valueExists( kAppOptions ) ) + { + strncpy( cr8r.mAppOptions, this->getValue<std::string>( kAppOptions ).c_str(), kSizeAppOtions ); + } + if( this->valueExists( kAppName ) ) + { + strncpy( cr8r.mAppName, this->getValue<std::string>( kAppName ).c_str(), kSizeAppName ); + } + + // + // set input buffer to zero + // + memset( buffer, 0, static_cast<size_t>(size) ); + + // + // copy Cr8r block into buffer + // + memcpy( buffer, &cr8r, kCr8rSizeFix ); + + *outBuffer = buffer; + } + else + { + XMP_Throw ( "Invalid buffer", kXMPErr_BadParam ); + } + + return size; +} + +//----------------------------------------------------------------------------- +// +// Cr8rMetadata::isEmptyValue(...) +// +// Purpose: Is the value of the passed ValueObject and its id "empty"? +// +//----------------------------------------------------------------------------- + +bool Cr8rMetadata::isEmptyValue( XMP_Uns32 id, ValueObject& valueObj ) +{ + bool ret = true; + + switch( id ) + { + case kFileExt: + case kAppOptions: + case kAppName: + { + TValueObject<std::string>* strObj = dynamic_cast<TValueObject<std::string>*>(&valueObj); + + ret = ( strObj == NULL || ( strObj != NULL && strObj->getValue().empty() ) ); + } + break; + + case kMagic: + case kSize: + case kMajorVer: + case kMinorVer: + case kCreatorCode: + case kAppleEvent: + { + ret = false; + } + break; + + default: + { + ret = true; + } + } + + return ret; +} diff --git a/XMPFiles/source/FormatSupport/WAVE/Cr8rMetadata.h b/XMPFiles/source/FormatSupport/WAVE/Cr8rMetadata.h new file mode 100644 index 0000000..451f430 --- /dev/null +++ b/XMPFiles/source/FormatSupport/WAVE/Cr8rMetadata.h @@ -0,0 +1,91 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _Cr8rMetadata_h_ +#define _Cr8rMetadata_h_ + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/NativeMetadataSupport/IMetadata.h" + +namespace IFF_RIFF +{ + +/** + * Cr8r Metadata model. + * Implements the IMetadata interface + */ +class Cr8rMetadata : public IMetadata +{ +public: + enum + { + kMagic, // XMP_Uns32 + kSize, // XMP_Uns32 + kMajorVer, // XMP_Uns16 + kMinorVer, // XMP_Uns16 + kCreatorCode, // XMP_Uns32 + kAppleEvent, // XMP_Uns32 + kFileExt, // char[16] + kAppOptions, // char[16] + kAppName // char[32] + }; + +public: + /** + *ctor/dtor + */ + Cr8rMetadata(); + ~Cr8rMetadata(); + + /** + * Parses the given memory block and creates a data model representation + * The implementation expects that the memory block is the data area of + * the Cr8rMetadata chunk. + * Throws exceptions if parsing is not possible + * + * @param input The byte buffer to parse + * @param size Size of the given byte buffer + */ + void parse( const XMP_Uns8* chunkData, XMP_Uns64 size ); + + /** + * See IMetadata::parse( const LFA_FileRef input ) + */ + void parse( XMP_IO* input ) { IMetadata::parse( input ); } + + /** + * Serializes the data model to a memory block. + * The memory block will be the data area of a Cr8rMetadata chunk. + * Throws exceptions if serializing is not possible + * + * @param buffer Buffer that gets filled with serialized data + * @param size Size of passed in buffer + * + * @return Size of serialzed data (might be smaller than buffer size) + */ + XMP_Uns64 serialize( XMP_Uns8** buffer ); + +protected: + /** + * @see IMetadata::isEmptyValue + */ + virtual bool isEmptyValue( XMP_Uns32 id, ValueObject& valueObj ); + +private: + // Operators hidden on purpose + Cr8rMetadata( const Cr8rMetadata& ) {}; + Cr8rMetadata& operator=( const Cr8rMetadata& ) { return *this; }; +}; + +} + +#endif diff --git a/XMPFiles/source/FormatSupport/WAVE/DISPMetadata.cpp b/XMPFiles/source/FormatSupport/WAVE/DISPMetadata.cpp new file mode 100644 index 0000000..9e821ef --- /dev/null +++ b/XMPFiles/source/FormatSupport/WAVE/DISPMetadata.cpp @@ -0,0 +1,138 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include <string.h> + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/WAVE/DISPMetadata.h" +#include "source/Endian.h" + +using namespace IFF_RIFF; + +//----------------------------------------------------------------------------- +// +// DISPMetadata::isValidDISP(...) +// +// Purpose: [static] Check if the passed data is a valid DISP chunk. Valid in +// of is it a DISP chunk that XMP should process. +// +//----------------------------------------------------------------------------- + +bool DISPMetadata::isValidDISP( const XMP_Uns8* chunkData, XMP_Uns64 size ) +{ + return ( ( size >= 4 ) && ( LittleEndian::getInstance().getUns32( chunkData ) == 0x0001 ) ); +} + +//----------------------------------------------------------------------------- +// +// DISPMetadata::DISPMetadata(...) +// +// Purpose: ctor/dtor +// +//----------------------------------------------------------------------------- + +DISPMetadata::DISPMetadata() +{ +} + +DISPMetadata::~DISPMetadata() +{ +} + +//----------------------------------------------------------------------------- +// +// DISPMetadata::parse(...) +// +// Purpose: Parses the given memory block and creates a data model representation +// The implementation expects that the memory block is the data area of +// the DISP chunk. +// Throws exceptions if parsing is not possible +// +//----------------------------------------------------------------------------- + +void DISPMetadata::parse( const XMP_Uns8* chunkData, XMP_Uns64 size ) +{ + if( DISPMetadata::isValidDISP( chunkData, size ) ) + { + this->setValue<std::string>( kTitle, std::string( (char*)&chunkData[4], static_cast<std::string::size_type>(size-4) ) ); + this->resetChanges(); + } + else + { + XMP_Throw ( "Not a valid DISP chunk", kXMPErr_BadFileFormat ); + } +} + +//----------------------------------------------------------------------------- +// +// DISPMetadata::serialize(...) +// +// Purpose: Serializes the data model to a memory block. +// The memory block will be the data area of a DISP chunk. +// Throws exceptions if serializing is not possible +// +//----------------------------------------------------------------------------- + +XMP_Uns64 DISPMetadata::serialize( XMP_Uns8** outBuffer ) +{ + XMP_Uns64 size = 0; + + if( outBuffer != NULL && this->valueExists( kTitle ) ) + { + std::string title = this->getValue<std::string>( kTitle ); + + size = 4 + title.length(); // at least 4bytes for the type value of the DISP chunk + + // [2500563] DISP chunk must be of even length for WAVE, + // as pad byte is not interpreted correctly by third-party tools + if ( size % 2 != 0 ) + { + size++; + } + XMP_Uns8* buffer = new XMP_Uns8[static_cast<size_t>(size)]; + + memset( buffer, 0, static_cast<size_t>(size) ); + + // + // DISP type + // + buffer[0] = 1; + + // + // copy string into buffer + // + + memcpy( &buffer[4], title.c_str(), title.length() ); + + *outBuffer = buffer; + } + else + { + XMP_Throw ( "Invalid buffer", kXMPErr_InternalFailure ); + } + + return size; +} + +//----------------------------------------------------------------------------- +// +// DISPMetadata::isEmptyValue(...) +// +// Purpose: Is the value of the passed ValueObject and its id "empty"? +// +//----------------------------------------------------------------------------- + +bool DISPMetadata::isEmptyValue( XMP_Uns32 id, ValueObject& valueObj ) +{ + TValueObject<std::string>* strObj = dynamic_cast<TValueObject<std::string>*>(&valueObj); + + return ( strObj == NULL || ( strObj != NULL && strObj->getValue().empty() ) ); +} diff --git a/XMPFiles/source/FormatSupport/WAVE/DISPMetadata.h b/XMPFiles/source/FormatSupport/WAVE/DISPMetadata.h new file mode 100644 index 0000000..7db1891 --- /dev/null +++ b/XMPFiles/source/FormatSupport/WAVE/DISPMetadata.h @@ -0,0 +1,97 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _DISPMetadata_h_ +#define _DISPMetadata_h_ + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/NativeMetadataSupport/IMetadata.h" + +namespace IFF_RIFF +{ + +/** + * DISP Metadata model. + * Implements the IMetadata interface + */ +class DISPMetadata : public IMetadata +{ +public: + enum + { + kTitle // std::string + }; + +public: + /** + * Return true if the type is 0x0001 and it's large enough for + * more content + * + * @param chunkData Data area of chunk + * @param size Size of data area + * + * @return True if it's a valid DISP chunk + */ + static bool isValidDISP( const XMP_Uns8* chunkData, XMP_Uns64 size ); + +public: + /** + *ctor/dtor + */ + DISPMetadata(); + virtual ~DISPMetadata(); + + /** + * Parses the given memory block and creates a data model representation + * The implementation expects that the memory block is the data area of + * the DISP chunk. + * Throws exceptions if parsing is not possible + * + * @param input The byte buffer to parse + * @param size Size of the given byte buffer + */ + void parse( const XMP_Uns8* chunkData, XMP_Uns64 size ); + + /** + * See IMetadata::parse( const LFA_FileRef input ) + */ + void parse( XMP_IO* input ) { IMetadata::parse( input ); } + + /** + * Serializes the data model to a memory block. + * The method creates a buffer and pass it to the parameter 'buffer'. The callee of + * the method is responsible to delete the buffer later on. + * The memory block will be the data area of a DISP chunk. + * Throws exceptions if serializing is not possible + * + * @param buffer Buffer that gets filled with serialized data + * @param size Size of passed in buffer + * + * @ return Buffer size + */ + virtual XMP_Uns64 serialize( XMP_Uns8** buffer ); + +protected: + /** + * @see IMetadata::isEmptyValue + */ + virtual bool isEmptyValue( XMP_Uns32 id, ValueObject& valueObj ); + +private: + // Operators hidden on purpose + DISPMetadata( const DISPMetadata& ) {}; + DISPMetadata& operator=( const DISPMetadata& ) { return *this; }; +}; + +} + +#endif diff --git a/XMPFiles/source/FormatSupport/WAVE/INFOMetadata.cpp b/XMPFiles/source/FormatSupport/WAVE/INFOMetadata.cpp new file mode 100644 index 0000000..76faaa3 --- /dev/null +++ b/XMPFiles/source/FormatSupport/WAVE/INFOMetadata.cpp @@ -0,0 +1,260 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include <string.h> + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/WAVE/INFOMetadata.h" +#include "source/Endian.h" + +using namespace IFF_RIFF; + +typedef std::map<XMP_Uns32, std::string>::const_iterator MapIterator; + +static const XMP_Uns32 kSizeChunkID = 4; +static const XMP_Uns32 kSizeChunkSize = 4; +static const XMP_Uns32 kSizeChunkType = 4; +static const XMP_Uns32 kChunkHeaderSize = kSizeChunkID + kSizeChunkSize; + +static const XMP_Uns32 kType_INFO = 0x494E464F; + +//----------------------------------------------------------------------------- +// +// INFOMetadata::INFOMetadata(...) +// +// Purpose: ctor/dtor +// +//----------------------------------------------------------------------------- + +INFOMetadata::INFOMetadata() +{ +} + +INFOMetadata::~INFOMetadata() +{ +} + +//----------------------------------------------------------------------------- +// +// INFOMetadata::parse(...) +// +// Purpose: @see IMetadata::parse +// +//----------------------------------------------------------------------------- + +void INFOMetadata::parse( const XMP_Uns8* input, XMP_Uns64 size ) +{ + if( input != NULL && size >= kSizeChunkType ) + { + const BigEndian& BE = BigEndian::getInstance(); + const LittleEndian& LE = LittleEndian::getInstance(); + + XMP_Uns32 type = BE.getUns32( &input[0] ); + + XMP_Validate( type == kType_INFO, "Invalid LIST:INFO data", kXMPErr_InternalFailure ); + + XMP_Uns64 offset = kSizeChunkType; // pointer into the buffer + + while( offset < size ) + { + // + // continue parsing only if the remaing buffer is greater than + // the chunk header size + // + if( size - offset >= kChunkHeaderSize ) + { + // + // read: chunk id + // chunk size + // chunk data + XMP_Uns32 id = BE.getUns32( &input[offset] ); + XMP_Uns32 datasize = LE.getUns32( &input[offset+kSizeChunkID] ); + + if( offset + kChunkHeaderSize + datasize <= size ) + { + if( datasize > 0 ) + { + // + // don't store empty values + // + std::string value( reinterpret_cast<const char*>( &input[offset+kChunkHeaderSize] ), datasize ); + + // + // set new value + // + this->setValue<std::string>( id, value ); + } + + // + // update pointer + // + offset = offset + datasize + kChunkHeaderSize; + + if( datasize & 1 ) + { + // pad byte + offset++; + } + } + else + { + // + // invalid chunk, clean up and throw exception + // + this->deleteAll(); + XMP_Throw ( "Not a valid LIST:INFO chunk", kXMPErr_BadFileFormat ); + } + } + else + { + // + // invalid chunk, clean up and throw exception + // + this->deleteAll(); + XMP_Throw ( "Not a valid LIST:INFO chunk", kXMPErr_BadFileFormat ); + } + } + + this->resetChanges(); + } + else + { + XMP_Throw ( "Not a valid LIST:INFO chunk", kXMPErr_BadFileFormat ); + } +} + +//----------------------------------------------------------------------------- +// +// INFOMetadata::serialize(...) +// +// Purpose: @see IMetadata::serialize +// +//----------------------------------------------------------------------------- + +XMP_Uns64 INFOMetadata::serialize( XMP_Uns8** outBuffer ) +{ + XMP_Uns64 size = 0; + + if( outBuffer != NULL ) + { + // + // calculate required buffer size + // + for( ValueMap::iterator iter=mValues.begin(); iter!=mValues.end(); iter++ ) + { + TValueObject<std::string>* strObj = dynamic_cast<TValueObject<std::string>*>(iter->second); + + XMP_Uns32 chunkSize = kChunkHeaderSize + strObj->getValue().length(); + + if( chunkSize & 1 ) + { + // take account of pad byte + chunkSize++; + } + + size += chunkSize; + } + + size += kSizeChunkType; // add size of type "INFO" + + if( size > 0 ) + { + XMP_Uns8* buffer = new XMP_Uns8[static_cast<size_t>(size)]; // output buffer + + memset( buffer, 0, static_cast<size_t>(size) ); + + const BigEndian& BE = BigEndian::getInstance(); + const LittleEndian& LE = LittleEndian::getInstance(); + + // Put type "INFO" in front of the buffer + XMP_Uns32 typeInfo = BE.getUns32(&kType_INFO); + memcpy( buffer, &typeInfo, kSizeChunkType ); + XMP_Uns64 offset = kSizeChunkType; // pointer into the buffer + + // + // for each stored value + // + for( ValueMap::iterator iter=mValues.begin(); iter!=mValues.end(); iter++ ) + { + // + // get: chunk data + // chunk id + // chunk size + // + TValueObject<std::string>* strObj = dynamic_cast<TValueObject<std::string>*>(iter->second); + std::string value = strObj->getValue(); + XMP_Uns32 id = iter->first; + XMP_Uns32 size = value.length(); + + if( size & 1 && strObj->hasChanged() ) + { + // + // if we modified the value of this entry + // then fill chunk data with zero bytes + // rather than use a pad byte, i.e. + // size of each LIST:INFO entry has + // an odd size + // + size++; + } + + // + // chunk id and chunk size are stored in little endian format + // + id = BE.getUns32( &id ); + size = LE.getUns32( &size ); + + // + // copy values into output buffer + // + memcpy( buffer+offset, &id, kSizeChunkID ); + memcpy( buffer+offset+kSizeChunkID, &size, kSizeChunkSize ); + memcpy( buffer+offset+kChunkHeaderSize, value.c_str(), size ); + + // + // update pointer + // + offset += kChunkHeaderSize; + offset += size; + + if( size & 1 ) + { + // + // take account of pad byte + offset++; + } + } + + *outBuffer = buffer; + } + } + else + { + XMP_Throw ( "Invalid buffer", kXMPErr_InternalFailure ); + } + + return size; +} + +//----------------------------------------------------------------------------- +// +// INFOMetadata::isEmptyValue(...) +// +// Purpose: Is the value of the passed ValueObject and its id "empty"? +// +//----------------------------------------------------------------------------- + +bool INFOMetadata::isEmptyValue( XMP_Uns32 id, ValueObject& valueObj ) +{ + TValueObject<std::string>* strObj = dynamic_cast<TValueObject<std::string>*>(&valueObj); + + return ( strObj == NULL || ( strObj != NULL && strObj->getValue().empty() ) ); +} diff --git a/XMPFiles/source/FormatSupport/WAVE/INFOMetadata.h b/XMPFiles/source/FormatSupport/WAVE/INFOMetadata.h new file mode 100644 index 0000000..74d024c --- /dev/null +++ b/XMPFiles/source/FormatSupport/WAVE/INFOMetadata.h @@ -0,0 +1,88 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _INFOMetadata_h_ +#define _INFOMetadata_h_ + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/NativeMetadataSupport/IMetadata.h" + +namespace IFF_RIFF +{ + +/** + * LIST INFO Metadata model. + * Implements the IMetadata interface + */ +class INFOMetadata : public IMetadata +{ +public: + enum + { + kArtist = 0x49415254, // 'IART' std::string + kComments = 0x49434d54, // 'ICMT' std::string + kCopyright = 0x49434f50, // 'ICOP' std::string + kCreationDate = 0x49435244, // 'ICRD' std::string + kEngineer = 0x49454e47, // 'IENG' std::string + kGenre = 0x49474e52, // 'IGNR' std::string + kName = 0x494e414d, // 'INAM' std::string + kSoftware = 0x49534654, // 'ISFT' std::string + kMedium = 0x494d4544, // 'IMED' std::string + kSourceForm = 0x49535246, // 'ISRF' std::string + + // new mappings + kArchivalLocation = 0x4941524C, // 'IARL' + kCommissioned = 0x49434D53, // 'ICMS' + kKeywords = 0x494B4559, // 'IKEY' + kProduct = 0x49505244, // 'IPRD' + kSubject = 0x4953424A, // 'ISBJ' + kSource = 0x49535243, // 'ISRC' + kTechnican = 0x49544348, // 'ITCH' + }; + +public: + /** + *ctor/dtor + */ + INFOMetadata(); + ~INFOMetadata(); + + /** + * @see IMetadata::parse + */ + void parse( const XMP_Uns8* input, XMP_Uns64 size ); + + /** + * See IMetadata::parse( const LFA_FileRef input ) + */ + void parse( XMP_IO* input ) { IMetadata::parse( input ); } + + /** + * @see IMetadata::serialize + */ + XMP_Uns64 serialize( XMP_Uns8** outBuffer ); + +protected: + /** + * @see IMetadata::isEmptyValue + */ + virtual bool isEmptyValue( XMP_Uns32 id, ValueObject& valueObj ); + +private: + // Operators hidden on purpose + INFOMetadata( const INFOMetadata& ) {}; + INFOMetadata& operator=( const INFOMetadata& ) { return *this; }; +}; + +} + +#endif diff --git a/XMPFiles/source/FormatSupport/WAVE/PrmLMetadata.cpp b/XMPFiles/source/FormatSupport/WAVE/PrmLMetadata.cpp new file mode 100644 index 0000000..c5dc42e --- /dev/null +++ b/XMPFiles/source/FormatSupport/WAVE/PrmLMetadata.cpp @@ -0,0 +1,231 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include <string.h> + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/WAVE/PrmLMetadata.h" +#include "source/Endian.h" + +using namespace IFF_RIFF; + +static const XMP_Uns32 kPrmlSizeFix = 282; // always 282 bytes + +static const XMP_Uns32 kSizeFilePath = 260; + +// Needed to be able to memcpy directly to this struct. +#pragma pack ( push, 1 ) + struct PrmlBoxContent + { + XMP_Uns32 mMagic; + XMP_Uns32 mSize; + XMP_Uns16 mVerAPI; + XMP_Uns16 mVerCode; + XMP_Uns32 mExportType; + XMP_Uns16 mMacVRefNum; + XMP_Uns32 mMacParID; + char mFilePath[kSizeFilePath]; + }; +#pragma pack ( pop ) + +//----------------------------------------------------------------------------- +// +// PrmLMetadata::PrmLMetadata(...) +// +// Purpose: ctor/dtor +// +//----------------------------------------------------------------------------- + +PrmLMetadata::PrmLMetadata() +{ +} + +PrmLMetadata::~PrmLMetadata() +{ +} + +//----------------------------------------------------------------------------- +// +// PrmLMetadata::parse(...) +// +// Purpose: Parses the given memory block and creates a data model representation +// The implementation expects that the memory block is the data area of +// the BEXT chunk and its size is at least as big as the minimum size +// of a BEXT data block. +// Throws exceptions if parsing is not possible +// +//----------------------------------------------------------------------------- + +void PrmLMetadata::parse( const XMP_Uns8* chunkData, XMP_Uns64 size ) +{ + if( size >= kPrmlSizeFix ) + { + const LittleEndian& LE = LittleEndian::getInstance(); + + PrmlBoxContent prml; + memset( &prml, 0, kPrmlSizeFix ); + + // + // copy input data into Prml block + // Safe as fixed size matches size of struct that is #pragma packed(1) + // + memcpy( &prml, chunkData, kPrmlSizeFix ); + + // + // copy values to map + // + this->setValue<XMP_Uns32>( kMagic, prml.mMagic ); + this->setValue<XMP_Uns32>( kSize, prml.mSize ); + this->setValue<XMP_Uns16>( kVerAPI, prml.mVerAPI ); + this->setValue<XMP_Uns16>( kVerCode, prml.mVerCode ); + this->setValue<XMP_Uns32>( kExportType, prml.mExportType ); + this->setValue<XMP_Uns16>( kMacVRefNum, prml.mMacVRefNum ); + this->setValue<XMP_Uns32>( kMacParID, prml.mMacParID ); + this->setValue<std::string>( kFilePath, std::string( prml.mFilePath, kSizeFilePath ) ); + + this->resetChanges(); + } + else + { + XMP_Throw ( "Not a valid Prml chunk", kXMPErr_BadFileFormat ); + } +} + +//----------------------------------------------------------------------------- +// +// PrmLMetadata::serialize(...) +// +// Purpose: Serializes the data model to a memory block. +// The memory block will be the data area of a BEXT chunk. +// Throws exceptions if serializing is not possible +// +//----------------------------------------------------------------------------- + +XMP_Uns64 PrmLMetadata::serialize( XMP_Uns8** outBuffer ) +{ + XMP_Uns64 size = 0; + + if( outBuffer != NULL ) + { + const LittleEndian& LE = LittleEndian::getInstance(); + + size = kPrmlSizeFix; + + // + // setup buffer + // + XMP_Uns8* buffer = new XMP_Uns8[static_cast<size_t>(size)]; + + // + // copy values and strings back to BEXT block + // + // ! Safe use of strncpy as the fixed size is consistent with the size of the destination buffer + // But it is intentional here that the string might not be null terminated if + // the size of the source is equal to the fixed size of the destination + // + PrmlBoxContent prml; + memset( &prml, 0, kPrmlSizeFix ); + + if( this->valueExists( kMagic ) ) + { + LE.putUns32( this->getValue<XMP_Uns32>( kMagic ), &prml.mMagic ); + } + if( this->valueExists( kSize ) ) + { + LE.putUns32( this->getValue<XMP_Uns32>( kSize ), &prml.mSize ); + } + if( this->valueExists( kVerAPI ) ) + { + LE.putUns16( this->getValue<XMP_Uns16>( kVerAPI ), &prml.mVerAPI ); + } + if( this->valueExists( kVerCode ) ) + { + LE.putUns16( this->getValue<XMP_Uns16>( kVerCode ), &prml.mVerCode ); + } + if( this->valueExists( kExportType ) ) + { + LE.putUns32( this->getValue<XMP_Uns32>( kExportType ), &prml.mExportType ); + } + if( this->valueExists( kMacVRefNum ) ) + { + LE.putUns16( this->getValue<XMP_Uns16>( kMacVRefNum ), &prml.mMacVRefNum ); + } + if( this->valueExists( kMacParID ) ) + { + LE.putUns32( this->getValue<XMP_Uns32>( kMacParID ), &prml.mMacParID ); + } + if( this->valueExists( kFilePath ) ) + { + strncpy( prml.mFilePath, this->getValue<std::string>( kFilePath ).c_str(), kSizeFilePath ); + } + + // + // set input buffer to zero + // + memset( buffer, 0, static_cast<size_t>(size) ); + + // + // copy Prml block into buffer + // + memcpy( buffer, &prml, kPrmlSizeFix ); + + *outBuffer = buffer; + } + else + { + XMP_Throw ( "Invalid buffer", kXMPErr_BadParam ); + } + + return size; +} + +//----------------------------------------------------------------------------- +// +// PrmLMetadata::isEmptyValue(...) +// +// Purpose: Is the value of the passed ValueObject and its id "empty"? +// +//----------------------------------------------------------------------------- + +bool PrmLMetadata::isEmptyValue( XMP_Uns32 id, ValueObject& valueObj ) +{ + bool ret = true; + + switch( id ) + { + case kFilePath: + { + TValueObject<std::string>* strObj = dynamic_cast<TValueObject<std::string>*>(&valueObj); + + ret = ( strObj == NULL || ( strObj != NULL && strObj->getValue().empty() ) ); + } + break; + + case kMagic: + case kSize: + case kVerAPI: + case kVerCode: + case kExportType: + case kMacVRefNum: + case kMacParID: + { + ret = false; + } + break; + + default: + { + ret = true; + } + } + + return ret; +} diff --git a/XMPFiles/source/FormatSupport/WAVE/PrmLMetadata.h b/XMPFiles/source/FormatSupport/WAVE/PrmLMetadata.h new file mode 100644 index 0000000..8eb7102 --- /dev/null +++ b/XMPFiles/source/FormatSupport/WAVE/PrmLMetadata.h @@ -0,0 +1,90 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _PrmlMetadata_h_ +#define _PrmlMetadata_h_ + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/NativeMetadataSupport/IMetadata.h" + +namespace IFF_RIFF +{ + +/** + * PrmL Metadata model. + * Implements the IMetadata interface + */ +class PrmLMetadata : public IMetadata +{ +public: + enum + { + kMagic, // XMP_Uns32 + kSize, // XMP_Uns32 + kVerAPI, // XMP_Uns16 + kVerCode, // XMP_Uns16 + kExportType, // XMP_Uns32 + kMacVRefNum, // XMP_Uns16 + kMacParID, // XMP_Uns32 + kFilePath, // char[260] + }; + +public: + /** + *ctor/dtor + */ + PrmLMetadata(); + ~PrmLMetadata(); + + /** + * Parses the given memory block and creates a data model representation + * The implementation expects that the memory block is the data area of + * the PrmL chunk. + * Throws exceptions if parsing is not possible + * + * @param input The byte buffer to parse + * @param size Size of the given byte buffer + */ + void parse( const XMP_Uns8* chunkData, XMP_Uns64 size ); + + /** + * See IMetadata::parse( const LFA_FileRef input ) + */ + void parse( XMP_IO* input ) { IMetadata::parse( input ); } + + /** + * Serializes the data model to a memory block. + * The memory block will be the data area of a PrmL chunk. + * Throws exceptions if serializing is not possible + * + * @param buffer Buffer that gets filled with serialized data + * @param size Size of passed in buffer + * + * @return Size of serialzed data (might be smaller than buffer size) + */ + XMP_Uns64 serialize( XMP_Uns8** buffer ); + +protected: + /** + * @see IMetadata::isEmptyValue + */ + virtual bool isEmptyValue( XMP_Uns32 id, ValueObject& valueObj ); + +private: + // Operators hidden on purpose + PrmLMetadata( const PrmLMetadata& ) {}; + PrmLMetadata& operator=( const PrmLMetadata& ) { return *this; }; +}; + +} + +#endif diff --git a/XMPFiles/source/FormatSupport/WAVE/WAVEBehavior.cpp b/XMPFiles/source/FormatSupport/WAVE/WAVEBehavior.cpp new file mode 100644 index 0000000..1e98f4f --- /dev/null +++ b/XMPFiles/source/FormatSupport/WAVE/WAVEBehavior.cpp @@ -0,0 +1,682 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FormatSupport/WAVE/WAVEBehavior.h" +#include "XMPFiles/source/FormatSupport/IFF/Chunk.h" + +#include <algorithm> + +using namespace IFF_RIFF; + +// +// Static init +// +const LittleEndian& WAVEBehavior::mEndian = LittleEndian::getInstance(); + + +//----------------------------------------------------------------------------- +// +// WAVEBehavior::getRealSize(...) +// +// Purpose: Validate the passed in size value, identify the valid size if the +// passed in isn't valid and return the valid size. +// Throw an exception if the passed in size isn't valid and there's +// no way to identify a valid size. +// +//----------------------------------------------------------------------------- + +XMP_Uns64 WAVEBehavior::getRealSize( const XMP_Uns64 size, const ChunkIdentifier& id, IChunkContainer& tree, XMP_IO* stream ) +{ + XMP_Uns64 realSize = size; + + if( size >= kNormalRF64ChunkSize ) // 4GB + { + if( this->isRF64( tree ) ) + { + // + // RF64 supports sizes beyond 4GB + // + DS64* rf64 = this->getDS64( tree, stream ); + + if( rf64 != NULL ) + { + // + // get 64bit size from RF64 structure + // + switch( id.id ) + { + case kChunk_RF64: realSize = rf64->riffSize; break; + case kChunk_data: realSize = rf64->dataSize; break; + + default: + { + bool found = false; + + // + // try to find size value for passed chunk id in the ds64 table + // + if( rf64->tableLength > 0 ) + { + for( std::vector<ChunkSize64>::iterator iter=rf64->table.begin(); iter!=rf64->table.end(); iter++ ) + { + if( iter->id == id.id ) + { + realSize = iter->size; + found = true; + break; + } + } + } + + if( !found ) + { + // + // no size for passed id available + // + XMP_Throw( "Unknown size value", kXMPErr_BadFileFormat ); + } + } + } + } + else + { + // + // no RF64 size info available + // + XMP_Throw( "Unknown size value", kXMPErr_BadFileFormat ); + } + } + else + { + // + // WAVE doesn't support that size + // + XMP_Throw( "Unknown size value", kXMPErr_BadFileFormat ); + } + } + + return realSize; +} + +//----------------------------------------------------------------------------- +// +// WAVEBehavior::getMaxChunkSize(...) +// +// Purpose: Return the maximum size of a single chunk, i.e. the maximum size +// of a top-level chunk. +// +//----------------------------------------------------------------------------- + +XMP_Uns64 WAVEBehavior::getMaxChunkSize() const +{ + // simple WAVE 4GByte + XMP_Uns64 ret = 0x00000000FFFFFFFFLL; + + if( mIsRF64 ) + { + // RF64: full possible 64bit size + ret = 0xFFFFFFFFFFFFFFFFLL; + } + + return ret; +} + +//----------------------------------------------------------------------------- +// +// WAVEBehavior::isValidTopLevelChunk(...) +// +// Purpose: Return true if the passed identifier is valid for top-level chunks +// of a certain format. +// +//----------------------------------------------------------------------------- + +bool WAVEBehavior::isValidTopLevelChunk( const ChunkIdentifier& id, XMP_Uns32 chunkNo ) +{ + return ( chunkNo == 0 ) && + ( ( ( id.id == kChunk_RIFF ) && ( id.type == kType_WAVE ) ) || + ( ( id.id == kChunk_RF64 ) && ( id.type == kType_WAVE ) ) ); +} + +//----------------------------------------------------------------------------- +// +// WAVEBehavior::fixHierarchy(...) +// +// Purpose: Fix the hierarchy of chunks depending ones based on size changes of +// one or more chunks and second based on format specific rules. +// Throw an exception if the hierarchy can't be fixed. +// +//----------------------------------------------------------------------------- + +void WAVEBehavior::fixHierarchy( IChunkContainer& tree ) +{ + XMP_Validate( tree.numChildren() == 1, "WAVE files should only have one top level chunk (RIFF)", kXMPErr_BadFileFormat); + + Chunk* riffChunk = tree.getChildAt(0); + + XMP_Validate( (riffChunk->getType() == kType_WAVE || riffChunk->getType() == kChunk_RF64) , "Invalid type for WAVE/RF64 top level chunk (RIFF)", kXMPErr_BadFileFormat); + + if( riffChunk->hasChanged() ) + { + // + // move new added chunks to temporary container + // + Chunk* tmpContainer = Chunk::createChunk( mEndian ); + this->moveChunks( *riffChunk, *tmpContainer, riffChunk->numChildren() - mChunksAdded ); + + // + // try to arrange chunks at their current position + // + this->arrangeChunksInPlace( *riffChunk, *tmpContainer ); + + // + // for all chunks that were moved to the end try to find a FREE chunk for them + // + this->arrangeChunksInTree( *tmpContainer, *riffChunk ); + + // + // append all remaining new added chunks to the end of the tree + // + this->moveChunks( *tmpContainer, *riffChunk, 0 ); + delete tmpContainer; + + // + // check for FREE chunks at the end + // + Chunk* endFREE = this->mergeFreeChunks( *riffChunk, riffChunk->numChildren() - 1 ); + + if( endFREE != NULL ) + { + riffChunk->removeChildAt( riffChunk->numChildren() - 1 ); + delete endFREE; + } + + // + // Fix the offset values of all chunks. Throw an exception in the case that + // the offset of a non-modified chunk needs to be reset. + // + XMP_Validate( riffChunk->getOffset() == 0, "Invalid offset for RIFF top level chunk", kXMPErr_InternalFailure ); + + this->validateOffsets( tree ); + + // + // update the RF64 chunk (if this is RF64) based on the current chunk sizes + // + this->updateRF64( tree ); + } +} + +void WAVEBehavior::insertChunk( IChunkContainer& tree, Chunk& chunk ) +{ + XMP_Validate( tree.numChildren() == 1, "WAVE files should only have one top level chunk (RIFF)", kXMPErr_BadFileFormat); + Chunk* riffChunk = tree.getChildAt(0); + + XMP_Validate( riffChunk->getType() == kType_WAVE , "Invalid type for WAVE top level chunk (RIFF)", kXMPErr_BadFileFormat); + + // + // add new chunk to the end of the RIFF:WAVE + // + riffChunk->appendChild(&chunk); + + mChunksAdded++; +} + +bool WAVEBehavior::removeChunk( IChunkContainer& tree, Chunk& chunk ) +{ + // + // validate parameter + // + XMP_Validate( chunk.getID() != kChunk_RIFF, "Can't remove RIFF chunk!", kXMPErr_InternalFailure ); + XMP_Validate( chunk.getChunkMode() != CHUNK_UNKNOWN, "Cant' remove UNKNOWN Chunk", kXMPErr_InternalFailure ); + XMP_Validate( tree.numChildren() == 1, "WAVE files should only have one top level chunk (RIFF)", kXMPErr_BadFileFormat); + + // + // get top-level chunk + // + Chunk* riffChunk = tree.getChildAt(0); + + // + // validate top-level chunk + // + XMP_Validate( (riffChunk->getType() == kType_WAVE || riffChunk->getType() == kChunk_RF64) , "Invalid type for WAVE/RF64 top level chunk (RIFF)", kXMPErr_BadFileFormat); + + // + // calculate index of chunk to remove + // + XMP_Uns32 i = std::find( riffChunk->firstChild(), riffChunk->lastChild(), &chunk ) - riffChunk->firstChild(); + + // + // validate index + // + XMP_Validate( i < riffChunk->numChildren(), "Invalid chunk in tree", kXMPErr_InternalFailure ); + + // + // adjust new chunks counter + // + if( i > riffChunk->numChildren() - mChunksAdded - 1 ) + { + mChunksAdded--; + } + + if( i < riffChunk->numChildren()-1 ) + { + // + // fill gap with free chunk + // + Chunk* free = this->createFREE( chunk.getPadSize( true ) ); + riffChunk->replaceChildAt( i, free ); + free->setAsNew(); + + // + // merge JUNK chunks + // + this->mergeFreeChunks( *riffChunk, i ); + } + else + { + // + // remove chunk from tree + // + riffChunk->removeChildAt( i ); + } + + // + // if there is an entry in the ds64 table for the removed chunk + // then update the ds64 table entry + // + if( mDS64Data != NULL && mDS64Data->tableLength > 0 ) + { + for( std::vector<ChunkSize64>::iterator iter=mDS64Data->table.begin(); iter!=mDS64Data->table.end(); iter++ ) + { + if( iter->id == chunk.getID() ) + { + // + // don't remove entry but set its size to zero + // + iter->size = 0LL; + break; + } + } + } + + return true; +} + +Chunk* WAVEBehavior::createFREE( XMP_Uns64 chunkSize ) +{ + XMP_Int64 alloc = chunkSize - Chunk::HEADER_SIZE; + + Chunk* chunk = NULL; + + // + // create a 'JUNK' chunk + // + if( alloc > 0 ) + { + XMP_Uns8* data = new XMP_Uns8[static_cast<size_t>( alloc )]; + memset( data, 0, static_cast<size_t>( alloc ) ); + + chunk = Chunk::createUnknownChunk( mEndian, kChunk_JUNK, kType_NONE, alloc ); + + chunk->setData( data, alloc ); + + delete[] data; + } + else + { + chunk = Chunk::createHeaderChunk( mEndian, kChunk_JUNK ); + } + + // force set dirty flag + chunk->setChanged(); + + return chunk; +} + +XMP_Bool WAVEBehavior::isFREEChunk( const Chunk& chunk ) const +{ + // Check for sigature JUNK and JUNQ + return ( chunk.getID() == kChunk_JUNK || chunk.getID() == kChunk_JUNQ ); +} + + +XMP_Uns64 WAVEBehavior::getMinFREESize() const +{ + // avoid creation of chunks with size==0 + return static_cast<XMP_Uns64>( Chunk::HEADER_SIZE ) + 2; +} + +//----------------------------------------------------------------------------- +// +// WAVEBehavior::isRF64(...) +// +// Purpose: Is the current file a RF64 file +// +//----------------------------------------------------------------------------- + +bool WAVEBehavior::isRF64( const IChunkContainer& tree ) +{ + // The file format will not change at runtime + // So if the flag is not already set, have a look at the tree + if( ! mIsRF64 && tree.numChildren() != 0 ) + { + Chunk *chunk = tree.getChildAt(0); + // Only the TopLevel chunk is interesting + mIsRF64 = chunk->getID() == kChunk_RF64 && + chunk->getType() == kType_WAVE; + } + + return mIsRF64; +} + +//----------------------------------------------------------------------------- +// +// WAVEBehavior::getDS64(...) +// +// Purpose: Return RF64 structure. +// +//----------------------------------------------------------------------------- + +WAVEBehavior::DS64* WAVEBehavior::getDS64( IChunkContainer& tree, XMP_IO* stream ) +{ + DS64* ret = mDS64Data; + + if( ret == NULL ) + { + // + // try to find 'ds64' chunk in the tree + // + Chunk* ds64 = NULL; + Chunk* rf64 = NULL; + + if( tree.numChildren() > 0 ) + { + rf64 = tree.getChildAt(0); + + if( rf64 != NULL && rf64->getID() == kChunk_RF64 && rf64->numChildren() > 0 ) + { + // + // 'ds64' chunk needs to be the very first child of the 'RF64' chunk + // + ds64 = rf64->getChildAt(0); + } + + // + // Try to create 'ds64' chunk by parsing the stream + // + if( ds64 == NULL && stream != NULL ) + { + // + // remember file position before start reading from the stream + // + XMP_Uns64 filePos = stream->Offset(); + + try + { + ds64 = Chunk::createChunk( mEndian ); + ds64->readChunk( stream ); + } + catch( ... ) + { + delete ds64; + ds64 = NULL; + } + + if( ds64 != NULL && ds64->getID() == kChunk_ds64 ) + { + // + // Successfully read 'ds64' chunk. + // Now read its data area as well and + // add chunk to the 'RF64' chunk + // + ds64->cacheChunkData( stream ); + + rf64->appendChild( ds64, false ); + } + else + { + // + // Either the reading failed or the 'ds64' chunk + // doesn't exists at the expected position. + // Now clean up and reject the stream position. + // + delete ds64; + ds64 = NULL; + + stream->Seek( filePos, kXMP_SeekFromStart ); + } + } + else if( ds64 != NULL && ds64->getID() != kChunk_ds64 ) + { + // + // first child of 'RF64' chunk is NOT 'ds64'! + // + ds64 = NULL; + } + } + + // + // parse 'ds64' chunk, store the RF64 struct and return it + // + if( ds64 != NULL ) + { + DS64* ds64data = new DS64(); + + if( this->parseDS64Chunk( *ds64, *ds64data ) ) + { + mDS64Data = ds64data; + ret = mDS64Data; + } + else + { + delete ds64data; + } + } + } + + return ret; +} + +//----------------------------------------------------------------------------- +// +// WAVEBehavior::updateRF64(...) +// +// Purpose: update the RF64 chunk (if this is RF64) based on the current chunk sizes +// +//----------------------------------------------------------------------------- + +void WAVEBehavior::updateRF64( IChunkContainer& tree ) +{ + if( this->isRF64( tree ) ) + { + XMP_Validate( mDS64Data != NULL, "Missing DS64 structure", kXMPErr_InternalFailure ); + XMP_Validate( tree.numChildren() == 1, "Invalid RF64 tree", kXMPErr_InternalFailure ); + + // + // Check all chunks that sizes have changed and update their related value in the DS64 chunk + // + Chunk* rf64 = tree.getChildAt(0); + XMP_Validate( rf64 != NULL && rf64->getID() == kChunk_RF64 && rf64->numChildren() > 0, "Invalid RF64 chunk", kXMPErr_InternalFailure ); + + this->doUpdateRF64( *rf64 ); + + // + // try to find 'ds64' chunk in the tree + // (needs to be the very first child of the 'RF64' chunk) + // + Chunk* ds64 = rf64->getChildAt(0); + XMP_Validate( ds64 != NULL && ds64->getID() == kChunk_ds64, "Missing 'ds64' chunk", kXMPErr_InternalFailure ); + + // + // serialize DS64 structure and write into ds64 chunk + // + this->serializeDS64Chunk( *mDS64Data, *ds64 ); + } +} + +void WAVEBehavior::doUpdateRF64( Chunk& chunk ) +{ + // + // update ds64 entry for chunk if its size has changed + // + if( chunk.hasChanged() && chunk.getOriginalSize() > kNormalRF64ChunkSize ) + { + switch( chunk.getID() ) + { + case kChunk_RF64: mDS64Data->riffSize = chunk.getSize(); break; + case kChunk_data: + if( chunk.getSize() != chunk.getOriginalSize() ) + { + XMP_Throw( "Data chunk must not change", kXMPErr_InternalFailure ); + } + break; + default: + { + bool requireEntry = ( chunk.getSize() > kNormalRF64ChunkSize ); + bool found = false; + + // + // try to find entry for passed chunk id in the ds64 table + // + if( mDS64Data->tableLength > 0 ) + { + for( std::vector<ChunkSize64>::iterator iter=mDS64Data->table.begin(); iter!=mDS64Data->table.end(); iter++ ) + { + if( iter->id == chunk.getID() ) + { + // always set new size even if it's less than 4GB + iter->size = chunk.getSize(); + found = true; + break; + } + } + } + + // + // We can't add new entries to the table. So if we found no entry within 'ds64' + // for the passed chunk ID and the size of the chunk is larger than 4GB then + // we have to throw an exception + // + XMP_Validate( found || ( ! found && ! requireEntry ), "Can't update 'ds64' chunk", kXMPErr_Unimplemented ); + } + } + } + + // + // go through all children to update ds64 data + // + for( XMP_Uns32 i=0; i<chunk.numChildren(); i++ ) + { + Chunk* child = chunk.getChildAt(i); + + this->doUpdateRF64( *child ); + } +} + +//----------------------------------------------------------------------------- +// +// WAVEBehavior::parseRF64Chunk(...) +// +// Purpose: Parses the data block of the given RF64 chunk into the internal data structures +// +//----------------------------------------------------------------------------- + +bool WAVEBehavior::parseDS64Chunk( const Chunk& ds64Chunk, WAVEBehavior::DS64& ds64 ) +{ + bool ret = false; + + // It is a valid ds64 chunk + if( ds64Chunk.getID() == kChunk_ds64 && ds64Chunk.getSize() >= kMinimumDS64ChunkSize ) + { + const XMP_Uns8* data; + XMP_Uns64 size = ds64Chunk.getData(&data); + + memset( &ds64, 0, kMinimumDS64ChunkSize ); + + // + // copy fix input data into RF64 block (except chunk size table) + // Safe as fixed size matches size of struct that is #pragma packed(1) + // + memcpy( &ds64, data, kMinimumDS64ChunkSize ); + + // If there is more data but the table length is <= 0 then this is not a valid ds64 chunk + if( size > kMinimumDS64ChunkSize && ds64.tableLength > 0 ) + { + // copy chunk sizes table + // + XMP_Assert( size - kMinimumDS64ChunkSize >= ds64.tableLength * sizeof(ChunkSize64)); + + XMP_Uns32 offset = kMinimumDS64ChunkSize; + ChunkSize64 chunkSize; + + for( XMP_Uns32 i = 0 ; i < ds64.tableLength ; i++, offset += sizeof(ChunkSize64) ) + { + chunkSize.id = mEndian.getUns32( data + offset ); + chunkSize.size = mEndian.getUns64( data + offset + 4 ); + + ds64.table.push_back( chunkSize ); + } + } + + // remember any existing table buffer + ds64.trailingBytes = static_cast<XMP_Uns32>(size - kMinimumDS64ChunkSize - ds64.tableLength * sizeof(ChunkSize64)); + + // Either a table has been correctly parsed or there was no table + ret = (size - kMinimumDS64ChunkSize) >= (ds64.tableLength * sizeof(ChunkSize64)); + } + + return ret; +} + +//----------------------------------------------------------------------------- +// +// WAVEBehavior::serializeRF64Chunk(...) +// +// Purpose: Serializes the internal RF64 data structures into the data part of the given chunk +// +//----------------------------------------------------------------------------- +bool WAVEBehavior::serializeDS64Chunk( const WAVEBehavior::DS64& ds64, Chunk& ds64Chunk ) +{ + if( ds64Chunk.getID() != kChunk_ds64 ) + { + return false; // not a valid ds64 chunk + } + + // Calculate needed size + XMP_Uns32 size = kMinimumDS64ChunkSize + ds64.tableLength * sizeof(ChunkSize64) + ds64.trailingBytes; + // Create tmp buffer + XMP_Uns8* data = new XMP_Uns8[size]; + memset( data, 0, size ); + + // copy fix input data into buffer (except chunk sizes table) + // Safe as fixed size matches size of struct that is #pragma packed(1) + memcpy( data, &ds64, kMinimumDS64ChunkSize ); + + // copy chunk sizes table + if( ds64.tableLength > 0 ) + { + XMP_Uns32 offset = kMinimumDS64ChunkSize; + + for( XMP_Uns32 i = 0 ; i < ds64.tableLength ; i++, offset += sizeof(ChunkSize64) ) + { + mEndian.putUns32( ds64.table.at(i).id, data + offset ); + mEndian.putUns64( ds64.table.at(i).size, data + offset + 4 ); + } + } + + ds64Chunk.setData( data, size ); + + // free tmp buffer + delete []data; + + return true; +} diff --git a/XMPFiles/source/FormatSupport/WAVE/WAVEBehavior.h b/XMPFiles/source/FormatSupport/WAVE/WAVEBehavior.h new file mode 100644 index 0000000..1395cd1 --- /dev/null +++ b/XMPFiles/source/FormatSupport/WAVE/WAVEBehavior.h @@ -0,0 +1,215 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _WAVEBEHAVIOR_h_ +#define _WAVEBEHAVIOR_h_ + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/FormatSupport/IFF/IChunkBehavior.h" +#include "XMPFiles/source/FormatSupport/IFF/ChunkPath.h" +#include "source/Endian.h" + +namespace IFF_RIFF +{ + +/** + WAVE behavior class. + + Implements the IChunkBehavior interface +*/ + + +class WAVEBehavior : public IChunkBehavior +{ +// Internal structure to hold RF64 related data +public: +#pragma pack ( push, 1 ) + struct ChunkSize64 + { + XMP_Uns64 size; + XMP_Uns32 id; + // Ctor + ChunkSize64(): size(0), id(0) {} + }; + + struct DS64 + { + XMP_Uns64 riffSize; + XMP_Uns64 dataSize; + XMP_Uns64 sampleCount; + XMP_Uns32 tableLength; + // fix part ends here + XMP_Uns32 trailingBytes; + std::vector<ChunkSize64> table; + + // ctor + DS64(): riffSize(0), dataSize(0), sampleCount(0), tableLength(0), trailingBytes(0) {} + }; +#pragma pack ( pop ) + + + /** + ctor/dtor + */ + WAVEBehavior() : mChunksAdded(0), mIsRF64(false), mDS64Data(NULL) {} + + virtual ~WAVEBehavior() + { + if( mDS64Data != NULL ) + { + delete mDS64Data; + } + } + + /** + Validate the passed in size value, identify the valid size if the passed in isn't valid + and return the valid size. + throw an exception if the passed in size isn't valid and there's no way to identify a + valid size. + + @param size Size value + @param id Identifier of chunk + @param tree Chunk tree + @param stream Stream handle + + @return Valid size value. + */ + XMP_Uns64 getRealSize( const XMP_Uns64 size, const ChunkIdentifier& id, IChunkContainer& tree, XMP_IO* stream ); + + /** + Return the maximum size of a single chunk, i.e. the maximum size of a top-level chunk. + + @return Maximum size + */ + XMP_Uns64 getMaxChunkSize() const; + + /** + Return true if the passed identifier is valid for top-level chunks of a certain format. + + @param id Chunk identifier + @param chunkNo order number of top-level chunk + @return true, if passed id is a valid top-level chunk + */ + bool isValidTopLevelChunk( const ChunkIdentifier& id, XMP_Uns32 chunkNo ); + + /** + Fix the hierarchy of chunks depending ones based on size changes of one or more chunks + and second based on format specific rules. + Throw an exception if the hierarchy can't be fixed. + + @param tree Vector of root chunks. + */ + void fixHierarchy( IChunkContainer& tree ); + + /** + Insert a new chunk into the hierarchy of chunks. The behavior needs to decide the position + of the new chunk and has to do the insertion. + + @param tree Chunk tree + @param chunk New chunk + */ + void insertChunk( IChunkContainer& tree, Chunk& chunk ) ; + + /** + Remove the chunk described by the passed ChunkPath. + + @param tree Chunk tree + @param path Path to the chunk that needs to be removed + + @return true if the chunk was removed and need to be deleted + */ + bool removeChunk( IChunkContainer& tree, Chunk& chunk ) ; + +protected: + /** + Create a FREE chunk. + If the chunkSize is smaller than the header+type - size then create an annotation chunk. + If the passed size is odd, then add a pad byte. + + @param chunkSize Total size including header + @return New FREE chunk + */ + Chunk* createFREE( XMP_Uns64 chunkSize ); + + /** + Check if the passed chunk is a FREE chunk. + (Could be also a small annotation chunk with zero bytes in its data) + + @param chunk A chunk + + @return true if the passed chunk is a FREE chunk + */ + XMP_Bool isFREEChunk( const Chunk& chunk ) const; + + /** + Return the minimum size of a FREE chunk + */ + XMP_Uns64 getMinFREESize( ) const; + + /** + Is the current file a RF64 file? + + @param tree The whole chunk tree beginning with the root node + + @return true if the current file is in the RF64 format + */ + bool isRF64( const IChunkContainer& tree ); + + /** + Return RF64 structure. + + If the related chunk ('ds64') is not yet parsed then it should be parsed by this method. + + @param tree The chunk tree (probably a subtree) + @param stream File stream + + @return DS64 structure + */ + DS64* getDS64( IChunkContainer& tree, XMP_IO* stream ); + + /** + update the RF64 chunk (if this is RF64) based on the current chunk sizes + */ + void updateRF64( IChunkContainer& tree ); + + /** + * Parses the data block of the given RF64 chunk into the internal data structures + * @param rf64Chunk the RF64 Chunk + * @param rf64 OUT the RF64 data structure + * @return The parsing was successful (true) or not (false) + */ + bool parseDS64Chunk( const Chunk& ds64Chunk, DS64& rf64 ); + + /** + * Serializes the internal RF64 data structures into the data part of the given chunk + * @param rf64 the RF64 data structure + * @param rf64Chunk OUT the RF64 Chunk + * @return The serialization was successful (true) or not (false) + */ + bool serializeDS64Chunk( const WAVEBehavior::DS64& rf64, Chunk& ds64Chunk ); + +private: + void doUpdateRF64( Chunk& chunk ); + +private: + XMP_Uns32 mChunksAdded; + bool mIsRF64; + DS64* mDS64Data; + + static const LittleEndian& mEndian; // WAVE is always Little Endian + static const XMP_Uns32 kNormalRF64ChunkSize = 0xFFFFFFFF; + static const XMP_Uns32 kMinimumDS64ChunkSize = 28; +}; // IFF_RIFF + +} +#endif diff --git a/XMPFiles/source/FormatSupport/WAVE/WAVEReconcile.cpp b/XMPFiles/source/FormatSupport/WAVE/WAVEReconcile.cpp new file mode 100644 index 0000000..3798f3d --- /dev/null +++ b/XMPFiles/source/FormatSupport/WAVE/WAVEReconcile.cpp @@ -0,0 +1,576 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/FormatSupport/WAVE/WAVEReconcile.h" +#include "XMPFiles/source/FormatSupport/WAVE/DISPMetadata.h" +#include "XMPFiles/source/FormatSupport/WAVE/INFOMetadata.h" +#include "XMPFiles/source/FormatSupport/WAVE/BEXTMetadata.h" +#include "XMPFiles/source/FormatSupport/WAVE/CartMetadata.h" + +// cr8r is not yet required for WAVE +//#include "Cr8rMetadata.h" + +#include "XMPFiles/source/NativeMetadataSupport/MetadataSet.h" + +#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp" + +using namespace IFF_RIFF; + +// ************** legacy Mappings ***************** // + +static const MetadataPropertyInfo kBextProperties[] = +{ +// XMP NS XMP Property Name Native Metadata Identifier Native Datatype XMP Datatype Delete Priority ExportPolicy + { kXMP_NS_BWF, "description", BEXTMetadata::kDescription, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, // bext:description <-> BEXT:Description + { kXMP_NS_BWF, "originator", BEXTMetadata::kOriginator, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, // bext:originator <-> BEXT:originator + { kXMP_NS_BWF, "originatorReference", BEXTMetadata::kOriginatorReference, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, // bext:OriginatorReference <-> BEXT:OriginatorReference + { kXMP_NS_BWF, "originationDate", BEXTMetadata::kOriginationDate, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, // bext:originationDate <-> BEXT:originationDate + { kXMP_NS_BWF, "originationTime", BEXTMetadata::kOriginationTime, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, // bext:originationTime <-> BEXT:originationTime + { kXMP_NS_BWF, "timeReference", BEXTMetadata::kTimeReference, kNativeType_Uns64, kXMPType_Simple, true, false, kExport_Always }, // bext:timeReference <-> BEXT:TimeReferenceLow + BEXT:TimeReferenceHigh + // Special case: On export BEXT:version is always written as 1 + { kXMP_NS_BWF, "version", BEXTMetadata::kVersion, kNativeType_Uns16, kXMPType_Simple, true, false, kExport_Never }, // bext:version <-> BEXT:version + // special case: bext:umid <-> BEXT:UMID + { kXMP_NS_BWF, "codingHistory", BEXTMetadata::kCodingHistory, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, // bext:codingHistory <-> BEXT:codingHistory + { NULL } +}; + +static const MetadataPropertyInfo kINFOProperties[] = +{ +// XMP NS XMP Property Name Native Metadata Identifier Native Datatype XMP Datatype Delete Priority ExportPolicy + { kXMP_NS_DM, "artist", INFOMetadata::kArtist, kNativeType_StrUTF8, kXMPType_Simple, false, true, kExport_Always }, // xmpDM:artist <-> IART + { kXMP_NS_DM, "logComment", INFOMetadata::kComments, kNativeType_StrUTF8, kXMPType_Simple, false, true, kExport_Always }, // xmpDM:logComment <-> ICMT + { kXMP_NS_DC, "rights", INFOMetadata::kCopyright, kNativeType_StrUTF8, kXMPType_Localized, false, true, kExport_Always }, // dc:rights <-> ICOP + { kXMP_NS_XMP, "CreateDate", INFOMetadata::kCreationDate, kNativeType_StrUTF8, kXMPType_Simple, false, true, kExport_Always }, // xmp:CreateDate <-> ICRD + { kXMP_NS_DM, "engineer", INFOMetadata::kEngineer, kNativeType_StrUTF8, kXMPType_Simple, false, true, kExport_Always }, // xmpDM:engineer <-> IENG + { kXMP_NS_DM, "genre", INFOMetadata::kGenre, kNativeType_StrUTF8, kXMPType_Simple, false, true, kExport_Always }, // xmpDM:genre <-> IGNR + { kXMP_NS_XMP, "CreatorTool", INFOMetadata::kSoftware, kNativeType_StrUTF8, kXMPType_Simple, false, true, kExport_Always }, // xmp:CreatorTool <-> ISFT + { kXMP_NS_DC, "source", INFOMetadata::kMedium, kNativeType_StrUTF8, kXMPType_Simple, false, false, kExport_Always }, // dc:source <-> IMED, not in old digest + { kXMP_NS_DC, "type", INFOMetadata::kSourceForm, kNativeType_StrUTF8, kXMPType_Array, false, false, kExport_Always }, // dc:type <-> ISRF, not in old digest + + // new mappings + { kXMP_NS_RIFFINFO, "name", INFOMetadata::kName, kNativeType_StrUTF8, kXMPType_Localized, true, false, kExport_Always }, // riffinfo:name <-> INAM + { kXMP_NS_RIFFINFO, "archivalLocation", INFOMetadata::kArchivalLocation,kNativeType_StrUTF8, kXMPType_Simple, true, false, kExport_Always }, // riffinfo:archivalLocation <-> IARL + { kXMP_NS_RIFFINFO, "commissioned", INFOMetadata::kCommissioned, kNativeType_StrUTF8, kXMPType_Simple, true, false, kExport_Always }, // riffinfo:commissioned <-> ICMS + // special case, the native value is a semicolon-separated array + // { kXMP_NS_DC, "subject", INFOMetadata::kKeywords, kNativeType_StrUTF8, kXMPType_Array, false, false, kExport_Always }, // dc:subject <-> IKEY + { kXMP_NS_RIFFINFO, "product", INFOMetadata::kProduct, kNativeType_StrUTF8, kXMPType_Simple, true, false, kExport_Always }, // riffinfo:product <-> IPRD + { kXMP_NS_DC, "description", INFOMetadata::kSubject, kNativeType_StrUTF8, kXMPType_Localized, false, false, kExport_Always }, // dc:description <-> ISBJ + { kXMP_NS_RIFFINFO, "source", INFOMetadata::kSource, kNativeType_StrUTF8, kXMPType_Simple, true, false, kExport_Always }, // riffinfo:source <-> ISRC + { kXMP_NS_RIFFINFO, "technician", INFOMetadata::kTechnican, kNativeType_StrUTF8, kXMPType_Simple, true, false, kExport_Always }, // riffinfo:technician <-> ITCH + + { NULL } +}; + +static const MetadataPropertyInfo kDISPProperties[] = +{ +// XMP NS XMP Property Name Native Metadata Identifier Datatype Datatype Delete Priority ExportPolicy + { kXMP_NS_DC, "title", DISPMetadata::kTitle, kNativeType_StrUTF8, kXMPType_Localized, false, true, kExport_Always }, // dc:title <-> DISP + // Special case: DISP will overwrite LIST/INFO:INAM in dc:title if existing + { NULL } +}; + +static const MetadataPropertyInfo kCartProperties[] = +{ +// XMP NS XMP Property Name Native Metadata Identifier Datatype Datatype Delete Priority ExportPolicy + { kXMP_NS_AEScart, "Version", CartMetadata::kVersion, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "Title", CartMetadata::kTitle, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "Artist", CartMetadata::kArtist, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "CutID", CartMetadata::kCutID, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "ClientID", CartMetadata::kClientID, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "Category", CartMetadata::kCategory, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "Classification", CartMetadata::kClassification, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "OutCue", CartMetadata::kOutCue, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "StartDate", CartMetadata::kStartDate, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "StartTime", CartMetadata::kStartTime, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "EndDate", CartMetadata::kEndDate, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "EndTime", CartMetadata::kEndTime, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "ProducerAppID", CartMetadata::kProducerAppID, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "ProducerAppVersion", CartMetadata::kProducerAppVersion, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "UserDef", CartMetadata::kUserDef, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "URL", CartMetadata::kURL, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "TagText", CartMetadata::kTagText, kNativeType_StrLocal, kXMPType_Simple, true, false, kExport_Always }, + { kXMP_NS_AEScart, "LevelReference", CartMetadata::kLevelReference, kNativeType_Int32, kXMPType_Simple, true, false, kExport_Always }, + // Special case Post Timer + { NULL } +}; + +// cr8r is not yet required for WAVE +// +//static const MetadataPropertyInfo kCr8rProperties[] = +//{ +//// XMP NS XMP Property Name Native Metadata Identifier Native Datatype XMP Datatype Delete Priority ExportPolicy +// { kXMP_NS_CreatorAtom, "macAtom/creatorAtom:applicationCode", Cr8rMetadata::kCreatorCode, kNativeType_Uns32, kXMPType_Simple, false, false, kExport_Always }, // creatorAtom:macAtom/creatorAtom:applicationCode <-> Cr8r.creatorCode +// { kXMP_NS_CreatorAtom, "macAtom/creatorAtom:invocationAppleEvent", Cr8rMetadata::kAppleEvent, kNativeType_Uns32, kXMPType_Simple, false, false, kExport_Always }, // creatorAtom:macAtom/creatorAtom:invocationAppleEvent <-> Cr8r.appleEvent +// { kXMP_NS_CreatorAtom, "windowsAtom/creatorAtom:extension", Cr8rMetadata::kFileExt, kNativeType_Str, kXMPType_Simple, false, false, kExport_Always }, // creatorAtom:windowsAtom/creatorAtom:extension <-> Cr8r.fileExt +// { kXMP_NS_CreatorAtom, "windowsAtom/creatorAtom:invocationFlags", Cr8rMetadata::kAppOptions, kNativeType_Str, kXMPType_Simple, false, false, kExport_Always }, // creatorAtom:windowsAtom/creatorAtom:invocationFlags <-> Cr8r.appOptions +// { kXMP_NS_XMP, "CreatorTool", Cr8rMetadata::kAppName, kNativeType_Str, kXMPType_Simple, false, false, kExport_Always }, // xmp:CreatorTool <-> Cr8r.appName +// { NULL } +//}; + +// ! PrmL atom has all special mappings + +// ************** legacy Mappings end ***************** // + +XMP_Bool WAVEReconcile::importToXMP( SXMPMeta& outXMP, const MetadataSet& inMetaData ) +{ + bool changed = false; + + // the reconciliation is based on the existing outXMP packet + + // + // ! The existence of a digest leads to prefering pre-existing XMP over legacy properties. + // + bool hasDigest = outXMP.GetProperty( kXMP_NS_WAV, "NativeDigest", NULL , NULL ); + + if ( hasDigest ) + { + // remove, as digests are no longer used. + outXMP.DeleteProperty( kXMP_NS_WAV, "NativeDigest" ); + } + + + if ( ! ignoreLocalText ) + { + + // + // Import BEXT + // + BEXTMetadata *bextMeta = inMetaData.get<BEXTMetadata>(); + + if (bextMeta != NULL) + { + changed |= IReconcile::importNativeToXMP( outXMP, *bextMeta, kBextProperties, false ); + + // bext:umid <-> BEXT:UMID + if ( bextMeta->valueExists( BEXTMetadata::kUMID ) ) + { + XMP_Uns32 umidSize = 0; + const XMP_Uns8* const umid = bextMeta->getArray<XMP_Uns8>( BEXTMetadata::kUMID, umidSize ); + + std::string xmpValue; + bool allZero = encodeToHexString( umid, xmpValue ); + + if( ! allZero ) + { + outXMP.SetProperty(kXMP_NS_BWF, "umid", xmpValue.c_str()); + changed = true; + } + } + + } + + + // + // Import cart + // + CartMetadata* cartData = inMetaData.get<CartMetadata>(); + if ( cartData != NULL ) + { + + if (cartData->valueExists( CartMetadata::kPostTimer ) ) + { + // first get Array + XMP_Uns32 size = 0; + const CartMetadata::StoredCartTimer* timerArray = cartData->getArray<CartMetadata::StoredCartTimer> ( CartMetadata::kPostTimer, size ); + XMP_Assert (size == CartMetadata::kPostTimerLength ); + + char usage [5]; + XMP_Uns32 usageBE = 0; + char value [25]; // Unsigned has 10 dezimal digets (buffer has 25 just to be save) + std::string path = ""; + memset ( usage, 0, 5 ); // Fill with zeros + memset ( value, 0, 25 ); // Fill with zeros + + outXMP.DeleteProperty( kXMP_NS_AEScart, "PostTimer"); + outXMP.AppendArrayItem( kXMP_NS_AEScart, "PostTimer", kXMP_PropArrayIsOrdered, NULL, kXMP_PropValueIsStruct ); + + for ( XMP_Uns32 i = 0; i<CartMetadata::kPostTimerLength; i++ ) + { + // Ensure to write as Big Endian + usageBE = MakeUns32BE(timerArray[i].usage); + memcpy( usage, &usageBE, 4 ); + + snprintf ( value, 24, "%u", timerArray[i].value); + + SXMPUtils::ComposeArrayItemPath( kXMP_NS_AEScart, "PostTimer", i+1, &path); + outXMP.SetStructField( kXMP_NS_AEScart, path.c_str(), kXMP_NS_AEScart, "Usage", usage ); + outXMP.SetStructField( kXMP_NS_AEScart, path.c_str(), kXMP_NS_AEScart, "Value", value ); + } + + changed = true; + } + + // import rest if cart properties + changed |= IReconcile::importNativeToXMP ( outXMP, *cartData, kCartProperties, false ); + } + + } + + // cr8r is not yet required for WAVE + + //// + //// import cr8r + //// Special case: If both Cr8r.AppName and LIST/INFO:ISFT are present, ISFT shall win. + //// therefor import Cr8r first. + //// + //Cr8rMetadata* cr8rMeta = inMetaData.get<Cr8rMetadata>(); + + //if( cr8rMeta != NULL ) + //{ + // changed |= IReconcile::importNativeToXMP( outXMP, *cr8rMeta, kCr8rProperties, false ); + //} + + // + // Import LIST/INFO + // + INFOMetadata *infoMeta = inMetaData.get<INFOMetadata>(); + bool hasINAM = false; + std::string actualLang; + bool hasDCTitle = outXMP.GetLocalizedText( kXMP_NS_DC, "title", "" , "x-default" , &actualLang, NULL, NULL ); + + if (infoMeta != NULL) + { + // + // Remember if List/INFO:INAM has been imported + // + hasINAM = infoMeta->valueExists( INFOMetadata::kName ); + + // Keywords are a ;-separated list and is therefore handled manually, + // leveraging the XMPUtils functions + if (infoMeta->valueExists( INFOMetadata::kKeywords ) ) + { + std::string keywordsUTF8; + outXMP.DeleteProperty( kXMP_NS_DC, "subject" ); + ReconcileUtils::NativeToUTF8( infoMeta->getValue<std::string>( INFOMetadata::kKeywords ), keywordsUTF8 ); + SXMPUtils::SeparateArrayItems( &outXMP, kXMP_NS_DC, "subject", kXMP_PropArrayIsUnordered, keywordsUTF8 ); + changed = true; + } + + // + // import properties + // + changed |= IReconcile::importNativeToXMP( outXMP, *infoMeta, kINFOProperties, hasDigest ); + } + + // + // Import DISP + // ! DISP will overwrite dc:title + // + bool hasDISP = false; + + DISPMetadata *dispMeta = inMetaData.get<DISPMetadata>(); + + if( dispMeta != NULL && dispMeta->valueExists( DISPMetadata::kTitle) ) + { + changed |= IReconcile::importNativeToXMP( outXMP, *dispMeta, kDISPProperties, hasDigest ); + hasDISP = true; + } + + if( !hasDISP ) + { + // + // map INAM to dc:title ONLY in the case if: + // * DISP does NOT exists + // * dc:title does NOT exists + // * INAM exists + // + if( !hasDCTitle && hasINAM ) + { + std::string xmpValue; + ReconcileUtils::NativeToUTF8( infoMeta->getValue<std::string>( INFOMetadata::kName ), xmpValue ); + outXMP.SetLocalizedText( kXMP_NS_DC, "title", NULL, "x-default", xmpValue.c_str() ); + } + } + + return changed; +} // importToXMP + + +XMP_Bool WAVEReconcile::exportFromXMP( MetadataSet& outMetaData, SXMPMeta& inXMP ) +{ + // tracks if anything has been exported from the XMP + bool changed = false; + + // + // Export DISP + // + DISPMetadata *dispMeta = outMetaData.get<DISPMetadata>(); + if (dispMeta != NULL) + { + // dc:title <-> DISP + changed |= IReconcile::exportXMPToNative( *dispMeta, inXMP, kDISPProperties ); + } + + + if ( ! ignoreLocalText ) + { + // + // Export BEXT + // + + BEXTMetadata *bextMeta = outMetaData.get<BEXTMetadata>(); + + if (bextMeta != NULL) + { + IReconcile::exportXMPToNative( *bextMeta, inXMP, kBextProperties ); + + std::string xmpValue; + + // bext:umid <-> RIFF:WAVE/bext.UMID + if (inXMP.GetProperty(kXMP_NS_BWF, "umid", &xmpValue, 0)) + { + std::string umid; + + if( this->decodeFromHexString( xmpValue, umid ) ) + { + // + // if the XMP property doesn't contain a valid hex string then + // keep the existing value in the umid BEXT field + // + bextMeta->setArray<XMP_Uns8>(BEXTMetadata::kUMID, reinterpret_cast<const XMP_Uns8*>(umid.data()), umid.length()); + } + } + else + { + bextMeta->deleteValue(BEXTMetadata::kUMID); + } + + // bext:version <-> RIFF:WAVE/bext.version + // Special case: bext.version is always written as 1 + if (inXMP.GetProperty(kXMP_NS_BWF, "version", NULL, 0)) + { + bextMeta->setValue<XMP_Uns16>(BEXTMetadata::kVersion, 1); + } + else + { + bextMeta->deleteValue(BEXTMetadata::kVersion); + } + + // Remove BWF properties from the XMP + SXMPUtils::RemoveProperties(&inXMP, kXMP_NS_BWF, NULL, kXMPUtil_DoAllProperties ); + + changed |= bextMeta->hasChanged(); + } + + + // + // Export cart + // + CartMetadata* cartData = outMetaData.get<CartMetadata>(); + + if ( cartData != NULL ) + { + IReconcile::exportXMPToNative ( *cartData, inXMP, kCartProperties ); + + // Export PostTimer + if ( inXMP.DoesPropertyExist( kXMP_NS_AEScart, "PostTimer" ) ) + { + if( inXMP.CountArrayItems( kXMP_NS_AEScart, "PostTimer" ) == CartMetadata::kPostTimerLength ) + { + + CartMetadata::StoredCartTimer timer[CartMetadata::kPostTimerLength]; + memset ( timer, 0, sizeof(CartMetadata::StoredCartTimer)*CartMetadata::kPostTimerLength ); // Fill with zeros + std::string path = ""; + std::string usageSt = ""; + std::string valueSt = ""; + XMP_Bool invalidArray = false; + XMP_Uns32 usage = 0; + XMP_Uns32 value = 0; + XMP_Int64 tmp = 0; + XMP_OptionBits opts; + + + for ( XMP_Uns32 i = 0; i<CartMetadata::kPostTimerLength && !invalidArray; i++ ) + { + // get options of array item + inXMP.GetArrayItem( kXMP_NS_AEScart, "PostTimer", i+1, NULL, &opts ); + // copose path to array item + SXMPUtils::ComposeArrayItemPath( kXMP_NS_AEScart, "PostTimer", i+1, &path); + + if ( opts == kXMP_PropValueIsStruct && + inXMP.DoesStructFieldExist ( kXMP_NS_AEScart, path.c_str(), kXMP_NS_AEScart, "Usage" ) && + inXMP.DoesStructFieldExist ( kXMP_NS_AEScart, path.c_str(), kXMP_NS_AEScart, "Value" ) ) + { + inXMP.GetStructField( kXMP_NS_AEScart, path.c_str(), kXMP_NS_AEScart, "Usage", &usageSt, NULL); + inXMP.GetStructField( kXMP_NS_AEScart, path.c_str(), kXMP_NS_AEScart, "Value", &valueSt, NULL); + + if ( stringToFOURCC( usageSt,usage ) ) + { + // don't add if the String is not set or not exactly 4 characters + timer[i].usage = usage; + + // now the value + if ( valueSt.length() > 0 ) + { + tmp = SXMPUtils::ConvertToInt64(valueSt); + if ( tmp > 0 && tmp <= 0xffffffff) + { + value = static_cast<XMP_Uns32>(tmp); // save because I checked that the number is positiv. + timer[i].value = value; + } + // else value stays 0 + } + // else value stays 0 + } + } + else + { + invalidArray = true; + } + } + + if (!invalidArray) // if the structure of the array is wrong don't add anything + { + cartData->setArray<CartMetadata::StoredCartTimer> (CartMetadata::kPostTimer, timer, CartMetadata::kPostTimerLength); + } + } // Array length is wrong: don't add anything + } + else + { + cartData->deleteValue( CartMetadata::kPostTimer ); + } + SXMPUtils::RemoveProperties ( &inXMP, kXMP_NS_AEScart, 0, kXMPUtil_DoAllProperties ); + changed |= cartData->hasChanged(); + } + } + + // + // export LIST:INFO + // + INFOMetadata *infoMeta = outMetaData.get<INFOMetadata>(); + + if (infoMeta != NULL) + { + IReconcile::exportXMPToNative( *infoMeta, inXMP, kINFOProperties ); + + if ( inXMP.DoesPropertyExist( kXMP_NS_DC, "subject" ) ) + { + std::string catedStr; + SXMPUtils::CatenateArrayItems( inXMP, kXMP_NS_DC, "subject", NULL, NULL, kXMP_NoOptions, &catedStr ); + infoMeta->setValue<std::string>(INFOMetadata::kKeywords, catedStr); + } + else + { + infoMeta->deleteValue( INFOMetadata::kKeywords ); + } + + // Remove RIFFINFO properties from the XMP + SXMPUtils::RemoveProperties(&inXMP, kXMP_NS_RIFFINFO, NULL, kXMPUtil_DoAllProperties ); + + changed |= infoMeta->hasChanged(); + } + + // cr8r is not yet required for WAVE + + //// + //// export cr8r + //// + //Cr8rMetadata* cr8rMeta = outMetaData.get<Cr8rMetadata>(); + + //if( cr8rMeta != NULL ) + //{ + // changed |= IReconcile::exportXMPToNative( *cr8rMeta, inXMP, kCr8rProperties ); + //} + + // + // remove WAV digest + // + inXMP.DeleteProperty( kXMP_NS_WAV, "NativeDigest" ); + + return changed; +} // exportFromXMP + + +// ************** Helper Functions ***************** // + +bool WAVEReconcile::encodeToHexString ( const XMP_Uns8* input, std::string& output ) +{ + bool allZero = true; // assume for now + XMP_Uns32 kFixedSize = 64; // Only used for UMID Bext field, which is fixed + output.erase(); + + if ( input != 0 ) + { + output.reserve ( kFixedSize * 2 ); + + for( XMP_Uns32 i = 0; i < kFixedSize; i++ ) + { + // first, second nibble + XMP_Uns8 first = input[i] >> 4; + XMP_Uns8 second = input[i] & 0xF; + + if ( allZero && (( first != 0 ) || (second != 0))) + allZero = false; + + output.append( 1, ReconcileUtils::kHexDigits[first] ); + output.append( 1, ReconcileUtils::kHexDigits[second] ); + } + } + return allZero; +} // encodeToHexString + + +bool WAVEReconcile::decodeFromHexString ( std::string input, std::string &output) +{ + if ( (input.length() % 2) != 0 ) + return false; + output.erase(); + output.reserve ( input.length() / 2 ); + + for( XMP_Uns32 i = 0; i < input.length(); ) + { + XMP_Uns8 upperNibble = input[i]; + if ( (upperNibble < 48) || ( (upperNibble > 57 ) && ( upperNibble < 65 ) ) || (upperNibble > 70) ) + { + return false; + } + if ( upperNibble >= 65 ) + { + upperNibble -= 7; // shift A-F area adjacent to 0-9 + } + upperNibble -= 48; // 'shift' to a value [0..15] + upperNibble = ( upperNibble << 4 ); + i++; + + XMP_Uns8 lowerNibble = input[i]; + if ( (lowerNibble < 48) || ( (lowerNibble > 57 ) && ( lowerNibble < 65 ) ) || (lowerNibble > 70) ) + { + return false; + } + if ( lowerNibble >= 65 ) + { + lowerNibble -= 7; // shift A-F area adjacent to 0-9 + } + lowerNibble -= 48; // 'shift' to a value [0..15] + i++; + + output.append ( 1, (upperNibble + lowerNibble) ); + } + return true; +} // decodeFromHexString + +bool WAVEReconcile::stringToFOURCC ( std::string input, XMP_Uns32 &output ) +{ + bool result = false; + std::string asciiST = ""; + + // convert to ACSII + convertToASCII(input, asciiST); + if ( asciiST.length() == 4 ) + { + + output = GetUns32BE( asciiST.c_str() ); + result = true; + } + + return result; +} diff --git a/XMPFiles/source/FormatSupport/WAVE/WAVEReconcile.h b/XMPFiles/source/FormatSupport/WAVE/WAVEReconcile.h new file mode 100644 index 0000000..44a4ef3 --- /dev/null +++ b/XMPFiles/source/FormatSupport/WAVE/WAVEReconcile.h @@ -0,0 +1,66 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _WAVEReconcile_h_ +#define _WAVEReconcile_h_ + +#include "XMPFiles/source/NativeMetadataSupport/IReconcile.h" + +class IMetadata; + +namespace IFF_RIFF +{ + +class INFOMetadata; +class BEXTMetadata; + +class WAVEReconcile : public IReconcile +{ +public: + ~WAVEReconcile() {}; + + /** + * @see IReconcile::importToXMP + * Legacy values are always imported. + * If the values are not UTF-8 they will be converted to UTF-8 except in ServerMode + */ + XMP_Bool importToXMP( SXMPMeta& outXMP, const MetadataSet& inMetaData ); + + /** + * @see IReconcile::exportFromXMP + * XMP values are always exported to Legacy as UTF-8 encoded + */ + XMP_Bool exportFromXMP( MetadataSet& outMetaData, SXMPMeta& inXMP ); + +private: + // Encode a string of raw data bytes into a HexString (w/o spaces, i.e. "DEADBEEF"). + // Only used for UMID Bext field, which has fixed size of 64, so 64 chars are expected in the buffer! + // No insertation/acceptance of whitespace/linefeeds. No output/tolerance of lowercase. + // returns true, if *all* characters returned are zero (or if 0 bytes are returned). + static bool encodeToHexString ( const XMP_Uns8* input, std::string& output ); + + /** + * Decode a hex string to raw data bytes. + * Input must be all uppercase and w/o any whitespace, strictly (0-9A-Z)* (i.e. "DEADBEEF0099AABC") + * No insertation/acceptance of whitespace/linefeeds. + * bNo use/tolerance of lowercase. + * Number of bytes in the encoded String must be even. + * returns true if everything went well, false if illegal (non 0-9A-F) character encountered + */ + static bool decodeFromHexString ( std::string input, std::string &output); + + /** + * convert a 4 character string to XPM_Uns32 (FOURCC) + */ + static bool stringToFOURCC ( std::string input, XMP_Uns32 &output ); +}; + +} + +#endif diff --git a/XMPFiles/source/FormatSupport/XDCAM_Support.cpp b/XMPFiles/source/FormatSupport/XDCAM_Support.cpp new file mode 100644 index 0000000..b9704b9 --- /dev/null +++ b/XMPFiles/source/FormatSupport/XDCAM_Support.cpp @@ -0,0 +1,480 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2008 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/FormatSupport/XDCAM_Support.hpp" + +// ================================================================================================= +/// \file XDCAM_Support.cpp +/// +// ================================================================================================= + +// ================================================================================================= +// CreateChildElement +// ================== + +static XML_Node * CreateChildElement ( XML_Node * parent, + XMP_StringPtr localName, + XMP_StringPtr legacyNS, + int indent /* = 0 */ ) +{ + XML_Node * wsNode; + XML_Node * childNode = parent->GetNamedElement ( legacyNS, localName ); + + if ( childNode == 0 ) { + + // The indenting is a hack, assuming existing 2 spaces per level. + + wsNode = new XML_Node ( parent, "", kCDataNode ); + wsNode->value = " "; // Add 2 spaces to the existing WS before the parent's close tag. + parent->content.push_back ( wsNode ); + + childNode = new XML_Node ( parent, localName, kElemNode ); + childNode->ns = parent->ns; + childNode->nsPrefixLen = parent->nsPrefixLen; + childNode->name.insert ( 0, parent->name, 0, parent->nsPrefixLen ); + parent->content.push_back ( childNode ); + + wsNode = new XML_Node ( parent, "", kCDataNode ); + wsNode->value = '\n'; + for ( ; indent > 1; --indent ) wsNode->value += " "; // Indent less 1, to "outdent" the parent's close. + parent->content.push_back ( wsNode ); + + } + + return childNode; + +} // CreateChildElement + +// ================================================================================================= +// GetTimeScale +// ============ + +static std::string GetTimeScale ( XMP_StringPtr formatFPS ) +{ + std::string timeScale; + + if ( XMP_LitNMatch ( "25p", formatFPS, 3 ) || XMP_LitNMatch ( "50i", formatFPS, 3 ) ) { + timeScale = "1/25"; + } else if ( XMP_LitNMatch ( "50p", formatFPS, 3 ) ) { + timeScale = "1/50"; + } else if ( XMP_LitNMatch ( "23.98p", formatFPS, 6 ) ) { + timeScale = "1001/24000"; + } else if ( XMP_LitNMatch ( "29.97p", formatFPS, 6 ) || XMP_LitNMatch( "59.94i", formatFPS, 6 ) ) { + timeScale = "1001/30000"; + } else if ( XMP_LitNMatch ( "59.94p", formatFPS, 6 ) ) { + timeScale = "1001/60000"; + } + + return timeScale; + +} // GetTimeScale + +// ================================================================================================= +// XDCAM_Support::GetMediaProLegacyMetadata +// ======================================== + +bool XDCAM_Support::GetMediaProLegacyMetadata ( SXMPMeta * xmpObjPtr, + const std::string& clipUMID, + const std::string& mediaProPath, + bool digestFound) +{ + // NOTE: The logic of the form "if ( digestFound || (! XMP-prop-exists) ) Set-XMP-prop" might + // look odd at first, especially the digestFound part. This is OK though. If there is no digest + // then we want to preserve existing XMP. The handlers do not call this routine if the digest is + // present and matched, so here digestFound really means "found and differs". + + bool containsXMP = false; + + Host_IO::FileRef hostRef = Host_IO::Open ( mediaProPath.c_str(), Host_IO::openReadOnly ); + if ( hostRef == Host_IO::noFileRef ) return false; // The open failed. + XMPFiles_IO xmlFile ( hostRef, mediaProPath.c_str(), Host_IO::openReadOnly ); + + ExpatAdapter * expat = XMP_NewExpatAdapter ( ExpatAdapter::kUseLocalNamespaces ); + if ( expat == 0 ) return false; + + #define CleanupAndExit \ + { if ( expat != 0 ) delete expat; return containsXMP; } + + XML_NodePtr mediaproRootElem = 0; + XML_NodePtr contentContext = 0, materialContext = 0; + + XMP_Uns8 buffer [64*1024]; + while ( true ) { + XMP_Int32 ioCount = xmlFile.Read ( buffer, sizeof(buffer) ); + if ( ioCount == 0 ) break; + expat->ParseBuffer ( buffer, ioCount, false /* not the end */ ); + } + expat->ParseBuffer ( 0, 0, true ); // End the parse. + xmlFile.Close(); + + // Get the root node of the XML tree. + + XML_Node & mediaproXMLTree = expat->tree; + for ( size_t i = 0, limit = mediaproXMLTree.content.size(); i < limit; ++i ) { + if ( mediaproXMLTree.content[i]->kind == kElemNode ) { + mediaproRootElem = mediaproXMLTree.content[i]; + } + } + + if ( mediaproRootElem == 0 ) CleanupAndExit + XMP_StringPtr rlName = mediaproRootElem->name.c_str() + mediaproRootElem->nsPrefixLen; + if ( ! XMP_LitMatch ( rlName, "MediaProfile" ) ) CleanupAndExit + + // MediaProfile, Contents + + XMP_StringPtr ns = mediaproRootElem->ns.c_str(); + contentContext = mediaproRootElem->GetNamedElement ( ns, "Contents" ); + + if ( contentContext != 0 ) { + + size_t numMaterialElems = contentContext->CountNamedElements ( ns, "Material" ); + + // Iterate over the Material tags, looking for one that has a matching umid. + for ( size_t i = 0; i < numMaterialElems; ++i ) { + + XML_NodePtr materialElement = contentContext->GetNamedElement ( ns, "Material", i ); + XMP_Assert ( materialElement != 0 ); + + XMP_StringPtr umid = materialElement->GetAttrValue ( "umid" ); + + // Found one that matches the input umid, gather what metadata we can and break. + if ( (umid != 0) && (umid == clipUMID) ) { + + // title + XMP_StringPtr title = materialElement->GetAttrValue ( "title" ); + if ( title != 0 ) { + if ( digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_DC, "title" )) ) { + xmpObjPtr->SetLocalizedText ( kXMP_NS_DC, "title", "", "x-default", title, kXMP_DeleteExisting ); + containsXMP = true; + } + } + + break; + + } + } + + } + + CleanupAndExit + #undef CleanupAndExit + +} // XDCAM_Support::GetMediaProLegacyMetadata + +// ================================================================================================= +// XDCAM_Support::GetLegacyMetadata +// ================================ + +bool XDCAM_Support::GetLegacyMetadata ( SXMPMeta * xmpObjPtr, + XML_NodePtr rootElem, + XMP_StringPtr legacyNS, + bool digestFound, + std::string& umid ) +{ + // NOTE: The logic of the form "if ( digestFound || (! XMP-prop-exists) ) Set-XMP-prop" might + // look odd at first, especially the digestFound part. This is OK though. If there is no digest + // then we want to preserve existing XMP. The handlers do not call this routine if the digest is + // present and matched, so here digestFound really means "found and differs". + + bool containsXMP = false; + + XML_NodePtr legacyContext = 0, legacyProp = 0; + XMP_StringPtr formatFPS; + + // UMID + if ( digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_DC, "identifier" )) ) { + legacyProp = rootElem->GetNamedElement ( legacyNS, "TargetMaterial" ); + if ( (legacyProp != 0) && legacyProp->IsEmptyLeafNode() ) { + XMP_StringPtr legacyValue = legacyProp->GetAttrValue ( "umidRef" ); + if ( legacyValue != 0 ) { + umid = legacyValue; + xmpObjPtr->SetProperty ( kXMP_NS_DC, "identifier", legacyValue, kXMP_DeleteExisting ); + containsXMP = true; + } + } + } + + // Title + if ( digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_DC, "title" )) ) { + legacyProp = rootElem->GetNamedElement ( legacyNS, "Title" ); + if ( legacyProp != 0 ) { + XMP_StringPtr legacyValue = legacyProp->GetAttrValue ( "usAscii" ); + if ( legacyValue != 0 ) { + xmpObjPtr->SetLocalizedText ( kXMP_NS_DC, "title", "", "x-default", legacyValue, kXMP_DeleteExisting ); + containsXMP = true; + } + } + } + + // Creation date + if ( digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_XMP, "CreateDate" )) ) { + legacyProp = rootElem->GetNamedElement ( legacyNS, "CreationDate" ); + if ( (legacyProp != 0) && legacyProp->IsEmptyLeafNode() ) { + XMP_StringPtr legacyValue = legacyProp->GetAttrValue ( "value" ); + if ( legacyValue != 0 ) { + xmpObjPtr->SetProperty ( kXMP_NS_XMP, "CreateDate", legacyValue, kXMP_DeleteExisting ); + containsXMP = true; + } + } + } + + // Modify Date + if ( digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_XMP, "ModifyDate" )) ) { + legacyProp = rootElem->GetNamedElement ( legacyNS, "LastUpdate" ); + if ( (legacyProp != 0) && legacyProp->IsEmptyLeafNode() ) { + XMP_StringPtr legacyValue = legacyProp->GetAttrValue ( "value" ); + if ( legacyValue != 0 ) { + xmpObjPtr->SetProperty ( kXMP_NS_XMP, "ModifyDate", legacyValue, kXMP_DeleteExisting ); + containsXMP = true; + } + } + } + + // Metadata Modify Date + if ( digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_XMP, "MetadataDate" )) ) { + legacyProp = rootElem->GetNamedElement ( legacyNS, "lastUpdate" ); + if ( (legacyProp != 0) && legacyProp->IsEmptyLeafNode() ) { + XMP_StringPtr legacyValue = legacyProp->GetAttrValue ( "value" ); + if ( legacyValue != 0 ) { + xmpObjPtr->SetProperty ( kXMP_NS_XMP, "MetadataDate", legacyValue, kXMP_DeleteExisting ); + containsXMP = true; + } + } + } + + // Description + if ( digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_DC, "description" )) ) { + legacyProp = rootElem->GetNamedElement ( legacyNS, "Description" ); + if ( (legacyProp != 0) && legacyProp->IsLeafContentNode() ) { + XMP_StringPtr legacyValue = legacyProp->GetLeafContentValue(); + if ( legacyValue != 0 ) { + xmpObjPtr->SetLocalizedText ( kXMP_NS_DC, "description", "", "x-default", legacyValue, kXMP_DeleteExisting ); + containsXMP = true; + } + } + } + + legacyContext = rootElem->GetNamedElement ( legacyNS, "VideoFormat" ); + + if ( legacyContext != 0 ) { + + // frame size + if ( digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_DM, "videoFrameSize" )) ) { + legacyProp = legacyContext->GetNamedElement ( legacyNS, "VideoLayout" ); + if ( (legacyProp != 0) && legacyProp->IsEmptyLeafNode() ) { + + XMP_StringPtr widthValue = legacyProp->GetAttrValue ( "pixel" ); + XMP_StringPtr heightValue = legacyProp->GetAttrValue ( "numOfVerticalLine" ); + + if ( (widthValue != 0) && (heightValue != 0) ) { + + xmpObjPtr->DeleteProperty ( kXMP_NS_DM, "videoFrameSize" ); + xmpObjPtr->SetStructField ( kXMP_NS_DM, "videoFrameSize", kXMP_NS_XMP_Dimensions, "w", widthValue ); + xmpObjPtr->SetStructField ( kXMP_NS_DM, "videoFrameSize", kXMP_NS_XMP_Dimensions, "h", heightValue ); + xmpObjPtr->SetStructField ( kXMP_NS_DM, "videoFrameSize", kXMP_NS_XMP_Dimensions, "unit", "pixels" ); + + containsXMP = true; + + } + + } + } + + // Aspect ratio + if ( digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_DM, "videoPixelAspectRatio" )) ) { + legacyProp = legacyContext->GetNamedElement ( legacyNS, "VideoLayout" ); + if ( (legacyProp != 0) && legacyProp->IsEmptyLeafNode() ) { + XMP_StringPtr aspectRatio = legacyProp->GetAttrValue ( "aspectRatio" ); + if ( aspectRatio != 0 ) { + xmpObjPtr->SetProperty ( kXMP_NS_DM, "videoPixelAspectRatio", aspectRatio, kXMP_DeleteExisting ); + containsXMP = true; + } + } + } + + // Frame rate + if ( digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_DM, "videoFrameRate" )) ) { + legacyProp = legacyContext->GetNamedElement ( legacyNS, "VideoFrame" ); + if ( (legacyProp != 0) && legacyProp->IsEmptyLeafNode() ) { + formatFPS = legacyProp->GetAttrValue ( "formatFps" ); + if ( formatFPS != 0 ) { + xmpObjPtr->SetProperty ( kXMP_NS_DM, "videoFrameRate", formatFPS, kXMP_DeleteExisting ); + containsXMP = true; + } + } + } + + // Video codec + if ( digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_DM, "videoCompressor" )) ) { + legacyProp = legacyContext->GetNamedElement ( legacyNS, "VideoFrame" ); + if ( (legacyProp != 0) && legacyProp->IsEmptyLeafNode() ) { + XMP_StringPtr prop = legacyProp->GetAttrValue ( "videoCodec" ); + if ( prop != 0 ) { + xmpObjPtr->SetProperty ( kXMP_NS_DM, "videoCompressor", prop, kXMP_DeleteExisting ); + containsXMP = true; + } + } + } + + } // VideoFormat + + legacyContext = rootElem->GetNamedElement ( legacyNS, "AudioFormat" ); + + if ( legacyContext != 0 ) { + + // Audio codec + if ( digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_DM, "audioCompressor" )) ) { + legacyProp = legacyContext->GetNamedElement ( legacyNS, "AudioRecPort" ); + if ( (legacyProp != 0) && legacyProp->IsEmptyLeafNode() ) { + XMP_StringPtr prop = legacyProp->GetAttrValue ( "audioCodec" ); + if ( prop != 0 ) { + xmpObjPtr->SetProperty ( kXMP_NS_DM, "audioCompressor", prop, kXMP_DeleteExisting ); + containsXMP = true; + } + } + } + + } // AudioFormat + + // Duration + if ( digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_DM, "duration" )) ) { + + std::string durationFrames; + legacyProp = rootElem->GetNamedElement ( legacyNS, "Duration" ); + if ( legacyProp != 0 ) { + XMP_StringPtr durationValue = legacyProp->GetAttrValue ( "value" ); + if ( durationValue != 0 ) durationFrames = durationValue; + } + + std::string timeScale = GetTimeScale ( formatFPS ); + + if ( (! timeScale.empty()) && (! durationFrames.empty()) ) { + xmpObjPtr->DeleteProperty ( kXMP_NS_DM, "duration" ); + xmpObjPtr->SetStructField ( kXMP_NS_DM, "duration", kXMP_NS_DM, "value", durationFrames ); + xmpObjPtr->SetStructField ( kXMP_NS_DM, "duration", kXMP_NS_DM, "scale", timeScale ); + containsXMP = true; + } + + } + + legacyContext = rootElem->GetNamedElement ( legacyNS, "Device" ); + if ( legacyContext != 0 ) { + + std::string model; + + // manufacturer string + XMP_StringPtr manufacturer = legacyContext->GetAttrValue ( "manufacturer" ); + if ( manufacturer != 0 ) { + model += manufacturer; + } + + // model string + XMP_StringPtr modelName = legacyContext->GetAttrValue ( "modelName" ); + if ( modelName != 0 ) { + if ( model.size() > 0 ) { + model += " "; + } + model += modelName; + } + + + // For the dm::cameraModel property, concat the make and model. + if ( digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_DM, "cameraModel" )) ) { + if ( model.size() != 0 ) { + xmpObjPtr->SetProperty ( kXMP_NS_DM, "cameraModel", model, kXMP_DeleteExisting ); + containsXMP = true; + } + } + + // EXIF Model + if ( digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_TIFF, "Model" )) ) { + xmpObjPtr->SetProperty ( kXMP_NS_TIFF, "Model", modelName, kXMP_DeleteExisting ); + } + + // EXIF Make + if ( digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_TIFF, "Make" )) ) { + xmpObjPtr->SetProperty ( kXMP_NS_TIFF, "Make", manufacturer, kXMP_DeleteExisting ); + } + + // EXIF-AUX Serial number + XMP_StringPtr serialNumber = legacyContext->GetAttrValue ( "serialNo" ); + if ( serialNumber != 0 && (digestFound || (! xmpObjPtr->DoesPropertyExist ( kXMP_NS_EXIF_Aux, "SerialNumber" ))) ) { + xmpObjPtr->SetProperty ( kXMP_NS_EXIF_Aux, "SerialNumber", serialNumber, kXMP_DeleteExisting ); + } + + } + + + return containsXMP; + +} // XDCAM_Support::GetLegacyMetadata + +// ================================================================================================= +// XDCAM_Support::SetLegacyMetadata +// ================================ + +bool XDCAM_Support::SetLegacyMetadata ( XML_Node * clipMetadata, + SXMPMeta * xmpObj, + XMP_StringPtr legacyNS ) +{ + bool updateLegacyXML = false; + bool xmpFound = false; + std::string xmpValue; + XML_Node * xmlNode = 0; + + xmpFound = xmpObj->GetProperty ( kXMP_NS_DC, "title", &xmpValue, 0 ); + + if ( xmpFound ) { + + xmlNode = CreateChildElement ( clipMetadata, "Title", legacyNS, 3 ); + if ( xmpValue != xmlNode->GetLeafContentValue() ) { + xmlNode->SetLeafContentValue ( xmpValue.c_str() ); + updateLegacyXML = true; + } + + } + + xmpFound = xmpObj->GetArrayItem ( kXMP_NS_DC, "creator", 1, &xmpValue, 0 ); + + if ( xmpFound ) { + xmlNode = CreateChildElement ( clipMetadata, "Creator", legacyNS, 3 ); + XMP_StringPtr creatorName = xmlNode->GetAttrValue ( "name" ); + if ( creatorName == 0 ) creatorName = ""; + if ( xmpValue != creatorName ) { + xmlNode->SetAttrValue ( "name", xmpValue.c_str() ); + updateLegacyXML = true; + } + } + + xmpFound = xmpObj->GetProperty ( kXMP_NS_DC, "description", &xmpValue, 0 ); + + if ( xmpFound ) { + xmlNode = CreateChildElement ( clipMetadata, "Description", legacyNS, 3 ); + if ( xmpValue != xmlNode->GetLeafContentValue() ) { + // description in non real time metadata is limited to 2047 bytes + if ( xmpValue.size() > 2047 ) xmpValue.resize ( 2047 ); + xmlNode->SetLeafContentValue ( xmpValue.c_str() ); + updateLegacyXML = true; + } + } + + return updateLegacyXML; + +} // XDCAM_Support::SetLegacyMetadata + +// ================================================================================================= diff --git a/XMPFiles/source/FormatSupport/XDCAM_Support.hpp b/XMPFiles/source/FormatSupport/XDCAM_Support.hpp new file mode 100644 index 0000000..cd209e9 --- /dev/null +++ b/XMPFiles/source/FormatSupport/XDCAM_Support.hpp @@ -0,0 +1,50 @@ +#ifndef __XDCAM_Support_hpp__ +#define __XDCAM_Support_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2008 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. +#include "public/include/XMP_Const.h" +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/ExpatAdapter.hpp" + +// ================================================================================================= +/// \file XDCAM_Support.hpp +/// \brief XMPFiles support for XDCAM streams. +/// +// ================================================================================================= + +namespace XDCAM_Support +{ + + // Read XDCAM XML metadata from MEDIAPRO.XML and translate to appropriate XMP. + bool GetMediaProLegacyMetadata ( SXMPMeta * xmpObjPtr, + const std::string& umid, + const std::string& mediaProPath, + bool digestFound); + + // Read XDCAM XML metadata and translate to appropriate XMP. + bool GetLegacyMetadata ( SXMPMeta * xmpObjPtr, + XML_NodePtr rootElem, + XMP_StringPtr legacyNS, + bool digestFound, + std::string& umid ); + + // Write XMP metadata back to XDCAM XML. + bool SetLegacyMetadata ( XML_Node * clipMetadata, + SXMPMeta * xmpObj, + XMP_StringPtr legacyNS ); + + +} // namespace XDCAM_Support + +// ================================================================================================= + +#endif // __XDCAM_Support_hpp__ diff --git a/XMPFiles/source/FormatSupport/XMPScanner.cpp b/XMPFiles/source/FormatSupport/XMPScanner.cpp new file mode 100644 index 0000000..6d8fe82 --- /dev/null +++ b/XMPFiles/source/FormatSupport/XMPScanner.cpp @@ -0,0 +1,1450 @@ +// ================================================================================================= +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved. +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// +// Adobe patent application tracking #P435, entitled 'Unique markers to simplify embedding data of +// one format in a file with a different format', inventors: Sean Parent, Greg Gilley. +// ================================================================================================= + +#if WIN32 + #pragma warning ( disable : 4127 ) // conditional expression is constant + #pragma warning ( disable : 4510 ) // default constructor could not be generated + #pragma warning ( disable : 4610 ) // user defined constructor required + #pragma warning ( disable : 4786 ) // debugger can't handle long symbol names +#endif + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" + +#if TestRunnerBuild + #define EnablePacketScanning 1 +#else + #include "XMPFiles/source/XMPFiles_Impl.hpp" +#endif + +#include "XMPFiles/source/FormatSupport/XMPScanner.hpp" + +#include <cassert> +#include <string> +#include <cstdlib> + +#if DEBUG + #include <iostream> + #include <iomanip> + #include <fstream> +#endif + +#ifndef UseStringPushBack // VC++ 6.x does not provide push_back for strings! + #define UseStringPushBack 0 +#endif + +using namespace std; + +#if EnablePacketScanning + +// ================================================================================================= +// ================================================================================================= +// class PacketMachine +// =================== +// +// This is the packet recognizer state machine. The top of the machine is FindNextPacket, this +// calls the specific state components and handles transitions. The states are described by an +// array of RecognizerInfo records, indexed by the RecognizerKind enumeration. Each RecognizerInfo +// record has a function that does that state's work, the success and failure transition states, +// and a string literal that is passed to the state function. The literal lets a common MatchChar +// or MatchString function be used in several places. +// +// The state functions are responsible for consuming input to recognize their particular state. +// This includes intervening nulls for 16 and 32 bit character forms. For the simplicity, things +// are treated as essentially little endian and the nulls are not actually checked. The opening +// '<' is found with a byte-by-byte search, then the number of bytes per character is determined +// by counting the following nulls. From then on, consuming a character means incrementing the +// buffer pointer by the number of bytes per character. Thus the buffer pointer only points to +// the "real" bytes. This also means that the pointer can go off the end of the buffer by a +// variable amount. The amount of overrun is saved so that the pointer can be positioned at the +// right byte to start the next buffer. +// +// The state functions return a TriState value, eTriYes means the pattern was found, eTriNo means +// the pattern was definitely not found, eTriMaybe means that the end of the buffer was reached +// while working through the pattern. +// +// When eTriYes is returned, the fBufferPtr data member is left pointing to the "real" byte +// following the last actual byte. Which might not be addressable memory! This also means that +// a state function can be entered with nothing available in the buffer. When eTriNo is returned, +// the fBufferPtr data member is left pointing to the byte that caused the failure. The state +// machine starts over from the failure byte. +// +// The state functions must preserve their internal micro-state before returning eTriMaybe, and +// resume processing when called with the next buffer. The fPosition data member is used to denote +// how many actual characters have been consumed. The fNullCount data member is used to denote how +// many nulls are left before the next actual character. + +// ================================================================================================= +// PacketMachine +// ============= + +XMPScanner::PacketMachine::PacketMachine ( XMP_Int64 bufferOffset, const void * bufferOrigin, XMP_Int64 bufferLength ) : + + // Public members + fPacketStart ( 0 ), + fPacketLength ( 0 ), + fBytesAttr ( -1 ), + fCharForm ( eChar8Bit ), + fAccess ( ' ' ), + fBogusPacket ( false ), + + // Private members + fBufferOffset ( bufferOffset ), + fBufferOrigin ( (const char *) bufferOrigin ), + fBufferPtr ( fBufferOrigin ), + fBufferLimit ( fBufferOrigin + bufferLength ), + fRecognizer ( eLeadInRecognizer ), + fPosition ( 0 ), + fBytesPerChar ( 1 ), + fBufferOverrun ( 0 ), + fQuoteChar ( ' ' ) + +{ + /* + REVIEW NOTES : Should the buffer stuff be in a class? + */ + + assert ( bufferOrigin != NULL ); + assert ( bufferLength != 0 ); + +} // PacketMachine + +// ================================================================================================= +// ~PacketMachine +// ============== + +XMPScanner::PacketMachine::~PacketMachine () +{ + + // An empty placeholder. + +} // ~PacketMachine + +// ================================================================================================= +// AssociateBuffer +// =============== + +void +XMPScanner::PacketMachine::AssociateBuffer ( XMP_Int64 bufferOffset, const void * bufferOrigin, XMP_Int64 bufferLength ) +{ + + fBufferOffset = bufferOffset; + fBufferOrigin = (const char *) bufferOrigin; + fBufferPtr = fBufferOrigin + fBufferOverrun; + fBufferLimit = fBufferOrigin + bufferLength; + +} // AssociateBuffer + +// ================================================================================================= +// ResetMachine +// ============ + +void +XMPScanner::PacketMachine::ResetMachine () +{ + + fRecognizer = eLeadInRecognizer; + fPosition = 0; + fBufferOverrun = 0; + fCharForm = eChar8Bit; + fBytesPerChar = 1; + fAccess = ' '; + fBytesAttr = -1; + fBogusPacket = false; + + fAttrName.erase ( fAttrName.begin(), fAttrName.end() ); + fAttrValue.erase ( fAttrValue.begin(), fAttrValue.end() ); + fEncodingAttr.erase ( fEncodingAttr.begin(), fEncodingAttr.end() ); + +} // ResetMachine + +// ================================================================================================= +// FindLessThan +// ============ + +XMPScanner::PacketMachine::TriState +XMPScanner::PacketMachine::FindLessThan ( PacketMachine * ths, const char * which ) +{ + + if ( *which == 'H' ) { + + // -------------------------------------------------------------------------------- + // We're looking for the '<' of the header. If we fail there is no packet in this + // part of the input, so return eTriNo. + + ths->fCharForm = eChar8Bit; // We might have just failed from a bogus 16 or 32 bit case. + ths->fBytesPerChar = 1; + + while ( ths->fBufferPtr < ths->fBufferLimit ) { // Don't skip nulls for the header's '<'! + if ( *ths->fBufferPtr == '<' ) break; + ths->fBufferPtr++; + } + + if ( ths->fBufferPtr >= ths->fBufferLimit ) return eTriNo; + ths->fBufferPtr++; + return eTriYes; + + } else { + + // -------------------------------------------------------------------------------- + // We're looking for the '<' of the trailer. We're already inside the packet body, + // looking for the trailer. So here if we fail we must return eTriMaybe so that we + // keep looking for the trailer in the next buffer. + + const int bytesPerChar = ths->fBytesPerChar; + + while ( ths->fBufferPtr < ths->fBufferLimit ) { + if ( *ths->fBufferPtr == '<' ) break; + ths->fBufferPtr += bytesPerChar; + } + + if ( ths->fBufferPtr >= ths->fBufferLimit ) return eTriMaybe; + ths->fBufferPtr += bytesPerChar; + return eTriYes; + + } + +} // FindLessThan + +// ================================================================================================= +// MatchString +// =========== + +XMPScanner::PacketMachine::TriState +XMPScanner::PacketMachine::MatchString ( PacketMachine * ths, const char * literal ) +{ + const int bytesPerChar = ths->fBytesPerChar; + const char * litPtr = literal + ths->fPosition; + const XMP_Int32 charsToGo = (XMP_Int32) strlen ( literal ) - ths->fPosition; + int charsDone = 0; + + while ( (charsDone < charsToGo) && (ths->fBufferPtr < ths->fBufferLimit) ) { + if ( *litPtr != *ths->fBufferPtr ) return eTriNo; + charsDone++; + litPtr++; + ths->fBufferPtr += bytesPerChar; + } + + if ( charsDone == charsToGo ) return eTriYes; + ths->fPosition += charsDone; + return eTriMaybe; + +} // MatchString + +// ================================================================================================= +// MatchChar +// ========= + +XMPScanner::PacketMachine::TriState +XMPScanner::PacketMachine::MatchChar ( PacketMachine * ths, const char * literal ) +{ + const int bytesPerChar = ths->fBytesPerChar; + + if ( ths->fBufferPtr >= ths->fBufferLimit ) return eTriMaybe; + + const char currChar = *ths->fBufferPtr; + if ( currChar != *literal ) return eTriNo; + ths->fBufferPtr += bytesPerChar; + return eTriYes; + +} // MatchChar + +// ================================================================================================= +// MatchOpenQuote +// ============== + +XMPScanner::PacketMachine::TriState +XMPScanner::PacketMachine::MatchOpenQuote ( PacketMachine * ths, const char * /* unused */ ) +{ + const int bytesPerChar = ths->fBytesPerChar; + + if ( ths->fBufferPtr >= ths->fBufferLimit ) return eTriMaybe; + + const char currChar = *ths->fBufferPtr; + if ( (currChar != '\'') && (currChar != '"') ) return eTriNo; + ths->fQuoteChar = currChar; + ths->fBufferPtr += bytesPerChar; + return eTriYes; + +} // MatchOpenQuote + +// ================================================================================================= +// MatchCloseQuote +// =============== + +XMPScanner::PacketMachine::TriState +XMPScanner::PacketMachine::MatchCloseQuote ( PacketMachine * ths, const char * /* unused */ ) +{ + + return MatchChar ( ths, &ths->fQuoteChar ); + +} // MatchCloseQuote + +// ================================================================================================= +// CaptureAttrName +// =============== + +XMPScanner::PacketMachine::TriState +XMPScanner::PacketMachine::CaptureAttrName ( PacketMachine * ths, const char * /* unused */ ) +{ + const int bytesPerChar = ths->fBytesPerChar; + char currChar; + + if ( ths->fPosition == 0 ) { // Get the first character in the name. + + if ( ths->fBufferPtr >= ths->fBufferLimit ) return eTriMaybe; + + currChar = *ths->fBufferPtr; + if ( ths->fAttrName.size() == 0 ) { + if ( ! ( ( ('a' <= currChar) && (currChar <= 'z') ) || + ( ('A' <= currChar) && (currChar <= 'Z') ) || + (currChar == '_') || (currChar == ':') ) ) { + return eTriNo; + } + } + + ths->fAttrName.erase ( ths->fAttrName.begin(), ths->fAttrName.end() ); + #if UseStringPushBack + ths->fAttrName.push_back ( currChar ); + #else + ths->fAttrName.insert ( ths->fAttrName.end(), currChar ); + #endif + ths->fBufferPtr += bytesPerChar; + + } + + while ( ths->fBufferPtr < ths->fBufferLimit ) { // Get the remainder of the name. + + currChar = *ths->fBufferPtr; + if ( ! ( ( ('a' <= currChar) && (currChar <= 'z') ) || + ( ('A' <= currChar) && (currChar <= 'Z') ) || + ( ('0' <= currChar) && (currChar <= '9') ) || + (currChar == '-') || (currChar == '.') || (currChar == '_') || (currChar == ':') ) ) { + break; + } + + #if UseStringPushBack + ths->fAttrName.push_back ( currChar ); + #else + ths->fAttrName.insert ( ths->fAttrName.end(), currChar ); + #endif + ths->fBufferPtr += bytesPerChar; + + } + + if ( ths->fBufferPtr < ths->fBufferLimit ) return eTriYes; + ths->fPosition = (long) ths->fAttrName.size(); // The name might span into the next buffer. + return eTriMaybe; + +} // CaptureAttrName + +// ================================================================================================= +// CaptureAttrValue +// ================ +// +// Recognize the equal sign and the quoted string value, capture the value along the way. + +XMPScanner::PacketMachine::TriState +XMPScanner::PacketMachine::CaptureAttrValue ( PacketMachine * ths, const char * /* unused */ ) +{ + const int bytesPerChar = ths->fBytesPerChar; + char currChar = 0; + TriState result = eTriMaybe; + + if ( ths->fBufferPtr >= ths->fBufferLimit ) return eTriMaybe; + + switch ( ths->fPosition ) { + + case 0 : // The name should haved ended at the '=', nulls already skipped. + + if ( ths->fBufferPtr >= ths->fBufferLimit ) return eTriMaybe; + if ( *ths->fBufferPtr != '=' ) return eTriNo; + ths->fBufferPtr += bytesPerChar; + ths->fPosition = 1; + // fall through OK because MatchOpenQuote will check the buffer limit and nulls ... + + case 1 : // Look for the open quote. + + result = MatchOpenQuote ( ths, NULL ); + if ( result != eTriYes ) return result; + ths->fPosition = 2; + // fall through OK because the buffer limit and nulls are checked below ... + + default : // Look for the close quote, capturing the value along the way. + + assert ( ths->fPosition == 2 ); + + const char quoteChar = ths->fQuoteChar; + + while ( ths->fBufferPtr < ths->fBufferLimit ) { + currChar = *ths->fBufferPtr; + if ( currChar == quoteChar ) break; + #if UseStringPushBack + ths->fAttrValue.push_back ( currChar ); + #else + ths->fAttrValue.insert ( ths->fAttrValue.end(), currChar ); + #endif + ths->fBufferPtr += bytesPerChar; + } + + if ( ths->fBufferPtr >= ths->fBufferLimit ) return eTriMaybe; + assert ( currChar == quoteChar ); + ths->fBufferPtr += bytesPerChar; // Advance past the closing quote. + return eTriYes; + + } + +} // CaptureAttrValue + +// ================================================================================================= +// RecordStart +// =========== +// +// Note that this routine looks at bytes, not logical characters. It has to figure out how many +// bytes per character there are so that the other recognizers can skip intervening nulls. + +XMPScanner::PacketMachine::TriState +XMPScanner::PacketMachine::RecordStart ( PacketMachine * ths, const char * /* unused */ ) +{ + + while ( true ) { + + if ( ths->fBufferPtr >= ths->fBufferLimit ) return eTriMaybe; + + const char currByte = *ths->fBufferPtr; + + switch ( ths->fPosition ) { + + case 0 : // Record the length. + assert ( ths->fCharForm == eChar8Bit ); + assert ( ths->fBytesPerChar == 1 ); + ths->fPacketStart = ths->fBufferOffset + ((ths->fBufferPtr - 1) - ths->fBufferOrigin); + ths->fPacketLength = 0; + ths->fPosition = 1; + // ! OK to fall through here, we didn't consume a byte in this step. + + case 1 : // Look for the first null byte. + if ( currByte != 0 ) return eTriYes; // No nulls found. + ths->fCharForm = eChar16BitBig; // Assume 16 bit big endian for now. + ths->fBytesPerChar = 2; + ths->fBufferPtr++; + ths->fPosition = 2; + break; // ! Don't fall through, have to check for the end of the buffer between each byte. + + case 2 : // One null was found, look for a second. + if ( currByte != 0 ) return eTriYes; // Just one null found. + ths->fBufferPtr++; + ths->fPosition = 3; + break; + + case 3 : // Two nulls were found, look for a third. + if ( currByte != 0 ) return eTriNo; // Just two nulls is not valid. + ths->fCharForm = eChar32BitBig; // Assume 32 bit big endian for now. + ths->fBytesPerChar = 4; + ths->fBufferPtr++; + return eTriYes; + break; + + } + + } + +} // RecordStart + +// ================================================================================================= +// RecognizeBOM +// ============ +// +// Recognizing the byte order marker is a surprisingly messy thing to do. It can't be done by the +// normal string matcher, there are no intervening nulls. There are 4 transitions after the opening +// quote, the closing quote or one of the three encodings. For the actual BOM there are then 1 or 2 +// following bytes that depend on which of the encodings we're in. Not to mention that the buffer +// might end at any point. +// +// The intervening null count done earlier determined 8, 16, or 32 bits per character, but not the +// big or little endian nature for the 16/32 bit cases. The BOM must be present for the 16 and 32 +// bit cases in order to determine the endian mode. There are six possible byte sequences for the +// quoted BOM string, ignoring the differences for quoting with ''' versus '"'. +// +// Keep in mind that for the 16 and 32 bit cases there will be nulls for the quote. In the table +// below the symbol <quote> means just the one byte containing the ''' or '"'. The nulls for the +// quote character are explicitly shown. +// +// <quote> <quote> - 1: No BOM, this must be an 8 bit case. +// <quote> \xEF \xBB \xBF <quote> - 1.12-13: The 8 bit form. +// +// <quote> \xFE \xFF \x00 <quote> - 1.22-23: The 16 bit, big endian form +// <quote> \x00 \xFF \xFE <quote> - 1.32-33: The 16 bit, little endian form. +// +// <quote> \x00 \x00 \xFE \xFF \x00 \x00 \x00 <quote> - 1.32.43-45.56-57: The 32 bit, big endian form. +// <quote> \x00 \x00 \x00 \xFF \xFE \x00 \x00 <quote> - 1.32.43.54-57: The 32 bit, little endian form. + +enum { + eBOM_8_1 = 0xEF, + eBOM_8_2 = 0xBB, + eBOM_8_3 = 0xBF, + eBOM_Big_1 = 0xFE, + eBOM_Big_2 = 0xFF, + eBOM_Little_1 = eBOM_Big_2, + eBOM_Little_2 = eBOM_Big_1 +}; + +XMPScanner::PacketMachine::TriState +XMPScanner::PacketMachine::RecognizeBOM ( PacketMachine * ths, const char * /* unused */ ) +{ + const int bytesPerChar = ths->fBytesPerChar; + + while ( true ) { // Handle one character at a time, the micro-state (fPosition) changes for each. + + if ( ths->fBufferPtr >= ths->fBufferLimit ) return eTriMaybe; + + const unsigned char currChar = *ths->fBufferPtr; // ! The BOM bytes look like integers bigger than 127. + + switch ( ths->fPosition ) { + + case 0 : // Look for the opening quote. + if ( (currChar != '\'') && (currChar != '"') ) return eTriNo; + ths->fQuoteChar = currChar; + ths->fBufferPtr++; + ths->fPosition = 1; + break; // ! Don't fall through, have to check for the end of the buffer between each byte. + + case 1 : // Look at the byte immediately following the opening quote. + if ( currChar == ths->fQuoteChar ) { // Closing quote, no BOM character, must be 8 bit. + if ( ths->fCharForm != eChar8Bit ) return eTriNo; + ths->fBufferPtr += bytesPerChar; // Skip the nulls after the closing quote. + return eTriYes; + } else if ( currChar == eBOM_8_1 ) { // Start of the 8 bit form. + if ( ths->fCharForm != eChar8Bit ) return eTriNo; + ths->fBufferPtr++; + ths->fPosition = 12; + } else if ( currChar == eBOM_Big_1 ) { // Start of the 16 bit big endian form. + if ( ths->fCharForm != eChar16BitBig ) return eTriNo; + ths->fBufferPtr++; + ths->fPosition = 22; + } else if ( currChar == 0 ) { // Start of the 16 bit little endian or either 32 bit form. + if ( ths->fCharForm == eChar8Bit ) return eTriNo; + ths->fBufferPtr++; + ths->fPosition = 32; + } else { + return eTriNo; + } + break; + + case 12 : // Look for the second byte of the 8 bit form. + if ( currChar != eBOM_8_2 ) return eTriNo; + ths->fPosition = 13; + ths->fBufferPtr++; + break; + + case 13 : // Look for the third byte of the 8 bit form. + if ( currChar != eBOM_8_3 ) return eTriNo; + ths->fPosition = 99; + ths->fBufferPtr++; + break; + + case 22 : // Look for the second byte of the 16 bit big endian form. + if ( currChar != eBOM_Big_2 ) return eTriNo; + ths->fPosition = 23; + ths->fBufferPtr++; + break; + + case 23 : // Look for the null before the closing quote of the 16 bit big endian form. + if ( currChar != 0 ) return eTriNo; + ths->fBufferPtr++; + ths->fPosition = 99; + break; + + case 32 : // Look at the second byte of the 16 bit little endian or either 32 bit form. + if ( currChar == eBOM_Little_1 ) { + ths->fPosition = 33; + } else if ( currChar == 0 ) { + ths->fPosition = 43; + } else { + return eTriNo; + } + ths->fBufferPtr++; + break; + + case 33 : // Look for the third byte of the 16 bit little endian form. + if ( ths->fCharForm != eChar16BitBig ) return eTriNo; // Null count before assumed big endian. + if ( currChar != eBOM_Little_2 ) return eTriNo; + ths->fCharForm = eChar16BitLittle; + ths->fPosition = 99; + ths->fBufferPtr++; + break; + + case 43 : // Look at the third byte of either 32 bit form. + if ( ths->fCharForm != eChar32BitBig ) return eTriNo; // Null count before assumed big endian. + if ( currChar == eBOM_Big_1 ) { + ths->fPosition = 44; + } else if ( currChar == 0 ) { + ths->fPosition = 54; + } else { + return eTriNo; + } + ths->fBufferPtr++; + break; + + case 44 : // Look for the fourth byte of the 32 bit big endian form. + if ( currChar != eBOM_Big_2 ) return eTriNo; + ths->fPosition = 45; + ths->fBufferPtr++; + break; + + case 45 : // Look for the first null before the closing quote of the 32 bit big endian form. + if ( currChar != 0 ) return eTriNo; + ths->fPosition = 56; + ths->fBufferPtr++; + break; + + case 54 : // Look for the fourth byte of the 32 bit little endian form. + ths->fCharForm = eChar32BitLittle; + if ( currChar != eBOM_Little_1 ) return eTriNo; + ths->fPosition = 55; + ths->fBufferPtr++; + break; + + case 55 : // Look for the fifth byte of the 32 bit little endian form. + if ( currChar != eBOM_Little_2 ) return eTriNo; + ths->fPosition = 56; + ths->fBufferPtr++; + break; + + case 56 : // Look for the next to last null before the closing quote of the 32 bit forms. + if ( currChar != 0 ) return eTriNo; + ths->fPosition = 57; + ths->fBufferPtr++; + break; + + case 57 : // Look for the last null before the closing quote of the 32 bit forms. + if ( currChar != 0 ) return eTriNo; + ths->fPosition = 99; + ths->fBufferPtr++; + break; + + default : // Look for the closing quote. + assert ( ths->fPosition == 99 ); + if ( currChar != ths->fQuoteChar ) return eTriNo; + ths->fBufferPtr += bytesPerChar; // Skip the nulls after the closing quote. + return eTriYes; + break; + + } + + } + +} // RecognizeBOM + +// ================================================================================================= +// RecordHeadAttr +// ============== + +XMPScanner::PacketMachine::TriState +XMPScanner::PacketMachine::RecordHeadAttr ( PacketMachine * ths, const char * /* unused */ ) +{ + + if ( ths->fAttrName == "encoding" ) { + + assert ( ths->fEncodingAttr.empty() ); + ths->fEncodingAttr = ths->fAttrValue; + + } else if ( ths->fAttrName == "bytes" ) { + + long value = 0; + int count = (int) ths->fAttrValue.size(); + int i; + + assert ( ths->fBytesAttr == -1 ); + + if ( count > 0 ) { // Allow bytes='' to be the same as no bytes attribute. + + for ( i = 0; i < count; i++ ) { + const char currChar = ths->fAttrValue[i]; + if ( ('0' <= currChar) && (currChar <= '9') ) { + value = (value * 10) + (currChar - '0'); + } else { + ths->fBogusPacket = true; + value = -1; + break; + } + } + ths->fBytesAttr = value; + + if ( CharFormIs16Bit ( ths->fCharForm ) ) { + if ( (ths->fBytesAttr & 1) != 0 ) ths->fBogusPacket = true; + } else if ( CharFormIs32Bit ( ths->fCharForm ) ) { + if ( (ths->fBytesAttr & 3) != 0 ) ths->fBogusPacket = true; + } + + } + + } + + ths->fAttrName.erase ( ths->fAttrName.begin(), ths->fAttrName.end() ); + ths->fAttrValue.erase ( ths->fAttrValue.begin(), ths->fAttrValue.end() ); + + return eTriYes; + +} // RecordHeadAttr + +// ================================================================================================= +// CaptureAccess +// ============= + +XMPScanner::PacketMachine::TriState +XMPScanner::PacketMachine::CaptureAccess ( PacketMachine * ths, const char * /* unused */ ) +{ + const int bytesPerChar = ths->fBytesPerChar; + + while ( true ) { + + if ( ths->fBufferPtr >= ths->fBufferLimit ) return eTriMaybe; + + const char currChar = *ths->fBufferPtr; + + switch ( ths->fPosition ) { + + case 0 : // Look for the opening quote. + if ( (currChar != '\'') && (currChar != '"') ) return eTriNo; + ths->fQuoteChar = currChar; + ths->fBufferPtr += bytesPerChar; + ths->fPosition = 1; + break; // ! Don't fall through, have to check for the end of the buffer between each byte. + + case 1 : // Look for the 'r' or 'w'. + if ( (currChar != 'r') && (currChar != 'w') ) return eTriNo; + ths->fAccess = currChar; + ths->fBufferPtr += bytesPerChar; + ths->fPosition = 2; + break; + + default : // Look for the closing quote. + assert ( ths->fPosition == 2 ); + if ( currChar != ths->fQuoteChar ) return eTriNo; + ths->fBufferPtr += bytesPerChar; + return eTriYes; + break; + + } + + } + +} // CaptureAccess + +// ================================================================================================= +// RecordTailAttr +// ============== + +XMPScanner::PacketMachine::TriState +XMPScanner::PacketMachine::RecordTailAttr ( PacketMachine * ths, const char * /* unused */ ) +{ + + // There are no known "general" attributes for the packet trailer. + + ths->fAttrName.erase ( ths->fAttrName.begin(), ths->fAttrName.end() ); + ths->fAttrValue.erase ( ths->fAttrValue.begin(), ths->fAttrValue.end() ); + + return eTriYes; + +} // RecordTailAttr + +// ================================================================================================= +// CheckPacketEnd +// ============== +// +// Check for trailing padding and record the packet length. We have trailing padding if the bytes +// attribute is present and has a value greater than the current length. + +XMPScanner::PacketMachine::TriState +XMPScanner::PacketMachine::CheckPacketEnd ( PacketMachine * ths, const char * /* unused */ ) +{ + const int bytesPerChar = ths->fBytesPerChar; + + if ( ths->fPosition == 0 ) { // First call, decide if there is trailing padding. + + const XMP_Int64 currLen64 = (ths->fBufferOffset + (ths->fBufferPtr - ths->fBufferOrigin)) - ths->fPacketStart; + if ( currLen64 > 0x7FFFFFFF ) throw std::runtime_error ( "Packet length exceeds 2GB-1" ); + const XMP_Int32 currLength = (XMP_Int32)currLen64; + + if ( (ths->fBytesAttr != -1) && (ths->fBytesAttr != currLength) ) { + if ( ths->fBytesAttr < currLength ) { + ths->fBogusPacket = true; // The bytes attribute value is too small. + } else { + ths->fPosition = ths->fBytesAttr - currLength; + if ( (ths->fPosition % ths->fBytesPerChar) != 0 ) { + ths->fBogusPacket = true; // The padding is not a multiple of the character size. + ths->fPosition = (ths->fPosition / ths->fBytesPerChar) * ths->fBytesPerChar; + } + } + } + + } + + while ( ths->fPosition > 0 ) { + + if ( ths->fBufferPtr >= ths->fBufferLimit ) return eTriMaybe; + + const char currChar = *ths->fBufferPtr; + + if ( (currChar != ' ') && (currChar != '\t') && (currChar != '\n') && (currChar != '\r') ) { + ths->fBogusPacket = true; // The padding is not whitespace. + break; // Stop the packet here. + } + + ths->fPosition -= bytesPerChar; + ths->fBufferPtr += bytesPerChar; + + } + + const XMP_Int64 currLen64 = (ths->fBufferOffset + (ths->fBufferPtr - ths->fBufferOrigin)) - ths->fPacketStart; + if ( currLen64 > 0x7FFFFFFF ) throw std::runtime_error ( "Packet length exceeds 2GB-1" ); + ths->fPacketLength = (XMP_Int32)currLen64; + return eTriYes; + +} // CheckPacketEnd + +// ================================================================================================= +// CheckFinalNulls +// =============== +// +// Do some special case processing for little endian characters. We have to make sure the presumed +// nulls after the last character actually exist, i.e. that the stream does not end too soon. Note +// that the prior character scanning has moved the buffer pointer to the address following the last +// byte of the last character. I.e. we're already past the presumed nulls, so we can't check their +// content. All we can do is verify that the stream does not end too soon. +// +// Doing this check is simple yet subtle. If we're still in the current buffer then the trailing +// bytes obviously exist. If we're exactly at the end of the buffer then the bytes also exist. +// The only question is when we're actually past this buffer, partly into the next buffer. This is +// when "ths->fBufferPtr > ths->fBufferLimit" on entry. For that case we have to wait until we've +// actually seen enough extra bytes of input. +// +// Since the normal buffer processing is already adjusting for this partial character overrun, all +// that needs to be done here is wait until "ths->fBufferPtr <= ths->fBufferLimit" on entry. In +// other words, if we're presently too far, ths->fBufferPtr will be adjusted by the amount of the +// overflow the next time XMPScanner::Scan is called. This might still be too far, so just keep +// waiting for enough data to pass by. +// +// Note that there is a corresponding special case for big endian characters, we must decrement the +// starting offset by the number of leading nulls. But we don't do that here, we leave it to the +// outer code. This is because the leading nulls might have been at the exact end of a previous +// buffer, in which case we have to also decrement the length of that raw data snip. + +XMPScanner::PacketMachine::TriState +XMPScanner::PacketMachine::CheckFinalNulls ( PacketMachine * ths, const char * /* unused */ ) +{ + + if ( (ths->fCharForm != eChar8Bit) && CharFormIsLittleEndian ( ths->fCharForm ) ) { + if ( ths->fBufferPtr > ths->fBufferLimit ) return eTriMaybe; + } + + return eTriYes; + +} // CheckFinalNulls + +// ================================================================================================= +// SetNextRecognizer +// ================= + +void +XMPScanner::PacketMachine::SetNextRecognizer ( RecognizerKind nextRecognizer ) +{ + + fRecognizer = nextRecognizer; + fPosition = 0; + +} // SetNextRecognizer + +// ================================================================================================= +// FindNextPacket +// ============== + +// *** When we start validating intervening nulls for 2 and 4 bytes characters, throw an exception +// *** for errors. Don't return eTriNo, that might skip at an optional point. + +XMPScanner::PacketMachine::TriState +XMPScanner::PacketMachine::FindNextPacket () +{ + + TriState status; + + #define kPacketHead "?xpacket begin=" + #define kPacketID "W5M0MpCehiHzreSzNTczkc9d" + #define kPacketTail "?xpacket end=" + + static const RecognizerInfo recognizerTable [eRecognizerCount] = { // ! Would be safer to assign these explicitly. + + // proc successNext failureNext literal + + { NULL, eFailureRecognizer, eFailureRecognizer, NULL}, // eFailureRecognizer + { NULL, eSuccessRecognizer, eSuccessRecognizer, NULL}, // eSuccessRecognizer + + { FindLessThan, eHeadStartRecorder, eFailureRecognizer, "H" }, // eLeadInRecognizer + { RecordStart, eHeadStartRecognizer, eLeadInRecognizer, NULL }, // eHeadStartRecorder + { MatchString, eBOMRecognizer, eLeadInRecognizer, kPacketHead }, // eHeadStartRecognizer + + { RecognizeBOM, eIDTagRecognizer, eLeadInRecognizer, NULL }, // eBOMRecognizer + + { MatchString, eIDOpenRecognizer, eLeadInRecognizer, " id=" }, // eIDTagRecognizer + { MatchOpenQuote, eIDValueRecognizer, eLeadInRecognizer, NULL }, // eIDOpenRecognizer + { MatchString, eIDCloseRecognizer, eLeadInRecognizer, kPacketID }, // eIDValueRecognizer + { MatchCloseQuote, eAttrSpaceRecognizer_1, eLeadInRecognizer, NULL }, // eIDCloseRecognizer + + { MatchChar, eAttrNameRecognizer_1, eHeadEndRecognizer, " " }, // eAttrSpaceRecognizer_1 + { CaptureAttrName, eAttrValueRecognizer_1, eLeadInRecognizer, NULL }, // eAttrNameRecognizer_1 + { CaptureAttrValue, eAttrValueRecorder_1, eLeadInRecognizer, NULL }, // eAttrValueRecognizer_1 + { RecordHeadAttr, eAttrSpaceRecognizer_1, eLeadInRecognizer, NULL }, // eAttrValueRecorder_1 + + { MatchString, eBodyRecognizer, eLeadInRecognizer, "?>" }, // eHeadEndRecognizer + + { FindLessThan, eTailStartRecognizer, eBodyRecognizer, "T"}, // eBodyRecognizer + + { MatchString, eAccessValueRecognizer, eBodyRecognizer, kPacketTail }, // eTailStartRecognizer + { CaptureAccess, eAttrSpaceRecognizer_2, eBodyRecognizer, NULL }, // eAccessValueRecognizer + + { MatchChar, eAttrNameRecognizer_2, eTailEndRecognizer, " " }, // eAttrSpaceRecognizer_2 + { CaptureAttrName, eAttrValueRecognizer_2, eBodyRecognizer, NULL }, // eAttrNameRecognizer_2 + { CaptureAttrValue, eAttrValueRecorder_2, eBodyRecognizer, NULL }, // eAttrValueRecognizer_2 + { RecordTailAttr, eAttrSpaceRecognizer_2, eBodyRecognizer, NULL }, // eAttrValueRecorder_2 + + { MatchString, ePacketEndRecognizer, eBodyRecognizer, "?>" }, // eTailEndRecognizer + { CheckPacketEnd, eCloseOutRecognizer, eBodyRecognizer, "" }, // ePacketEndRecognizer + { CheckFinalNulls, eSuccessRecognizer, eBodyRecognizer, "" } // eCloseOutRecognizer + + }; + + while ( true ) { + + switch ( fRecognizer ) { + + case eFailureRecognizer : + return eTriNo; + + case eSuccessRecognizer : + return eTriYes; + + default : + + // ------------------------------------------------------------------- + // For everything else, the normal cases, use the state machine table. + + const RecognizerInfo * thisState = &recognizerTable [fRecognizer]; + + status = thisState->proc ( this, thisState->literal ); + + switch ( status ) { + + case eTriNo : + SetNextRecognizer ( thisState->failureNext ); + continue; + + case eTriYes : + SetNextRecognizer ( thisState->successNext ); + continue; + + case eTriMaybe : + fBufferOverrun = (unsigned char)(fBufferPtr - fBufferLimit); + return eTriMaybe; // Keep this recognizer intact, to be resumed later. + + } + + } // switch ( fRecognizer ) { ... + + } // while ( true ) { ... + +} // FindNextPacket + +// ================================================================================================= +// ================================================================================================= +// class InternalSnip +// ================== + +// ================================================================================================= +// InternalSnip +// ============ + +XMPScanner::InternalSnip::InternalSnip ( XMP_Int64 offset, XMP_Int64 length ) +{ + + fInfo.fOffset = offset; + fInfo.fLength = length; + +} // InternalSnip + +// ================================================================================================= +// InternalSnip +// ============ + +XMPScanner::InternalSnip::InternalSnip ( const InternalSnip & rhs ) : + fInfo ( rhs.fInfo ), + fMachine ( NULL ) +{ + + assert ( rhs.fMachine.get() == NULL ); // Don't copy a snip with a machine. + assert ( (rhs.fInfo.fEncodingAttr == 0) || (*rhs.fInfo.fEncodingAttr == 0) ); // Don't copy a snip with an encoding. + +} // InternalSnip + +// ================================================================================================= +// ~InternalSnip +// ============= + +XMPScanner::InternalSnip::~InternalSnip () +{ +} // ~InternalSnip + + +// ================================================================================================= +// ================================================================================================= +// class XMPScanner +// ================ + +// ================================================================================================= +// DumpSnipList +// ============ + +#if DEBUG + +static const char * snipStateName [6] = { "not-seen", "pending", "raw-data", "good-packet", "partial", "bad-packet" }; + +void +XMPScanner::DumpSnipList ( const char * title ) +{ + InternalSnipIterator currPos = fInternalSnips.begin(); + InternalSnipIterator endPos = fInternalSnips.end(); + + cout << endl << title << " snip list: " << fInternalSnips.size() << endl; + + for ( ; currPos != endPos; ++currPos ) { + SnipInfo * currSnip = &currPos->fInfo; + cout << '\t' << currSnip << ' ' << snipStateName[currSnip->fState] << ' ' + << currSnip->fOffset << ".." << (currSnip->fOffset + currSnip->fLength - 1) + << ' ' << currSnip->fLength << ' ' << endl; + } +} // DumpSnipList + +#endif + +// ================================================================================================= +// PrevSnip and NextSnip +// ===================== + +XMPScanner::InternalSnipIterator +XMPScanner::PrevSnip ( InternalSnipIterator snipPos ) +{ + + InternalSnipIterator prev = snipPos; + return --prev; + +} // PrevSnip + +XMPScanner::InternalSnipIterator +XMPScanner::NextSnip ( InternalSnipIterator snipPos ) +{ + + InternalSnipIterator next = snipPos; + return ++next; + +} // NextSnip + +// ================================================================================================= +// XMPScanner +// ========== +// +// Initialize the scanner object with one "not seen" snip covering the whole stream. + +XMPScanner::XMPScanner ( XMP_Int64 streamLength ) : + + fStreamLength ( streamLength ) + +{ + InternalSnip rootSnip ( 0, streamLength ); + + if ( streamLength > 0 ) fInternalSnips.push_front ( rootSnip ); // Be nice for empty files. + // DumpSnipList ( "New XMPScanner" ); + +} // XMPScanner + +// ================================================================================================= +// ~XMPScanner +// =========== + +XMPScanner::~XMPScanner() +{ + +} // ~XMPScanner + +// ================================================================================================= +// GetSnipCount +// ============ + +long +XMPScanner::GetSnipCount () +{ + + return (long)fInternalSnips.size(); + +} // GetSnipCount + +// ================================================================================================= +// StreamAllScanned +// ================ + +bool +XMPScanner::StreamAllScanned () +{ + InternalSnipIterator currPos = fInternalSnips.begin(); + InternalSnipIterator endPos = fInternalSnips.end(); + + for ( ; currPos != endPos; ++currPos ) { + if ( currPos->fInfo.fState == eNotSeenSnip ) return false; + } + return true; + +} // StreamAllScanned + +// ================================================================================================= +// SplitInternalSnip +// ================= +// +// Split the given snip into up to 3 pieces. The new pieces are inserted before and after this one +// in the snip list. The relOffset is the first byte to be kept, it is relative to this snip. If +// the preceeding or following snips have the same state as this one, just shift the boundaries. +// I.e. move the contents from one snip to the other, don't create a new snip. + +// *** To be thread safe we ought to lock the entire list during manipulation. Let data scanning +// *** happen in parallel, serialize all mucking with the list. + +void +XMPScanner::SplitInternalSnip ( InternalSnipIterator snipPos, XMP_Int64 relOffset, XMP_Int64 newLength ) +{ + + assert ( (relOffset + newLength) > relOffset ); // Check for overflow. + assert ( (relOffset + newLength) <= snipPos->fInfo.fLength ); + + // ----------------------------------- + // First deal with the low offset end. + + if ( relOffset > 0 ) { + + InternalSnipIterator prevPos; + if ( snipPos != fInternalSnips.begin() ) prevPos = PrevSnip ( snipPos ); + + if ( (snipPos != fInternalSnips.begin()) && (snipPos->fInfo.fState == prevPos->fInfo.fState) ) { + prevPos->fInfo.fLength += relOffset; // Adjust the preceeding snip. + } else { + InternalSnip headExcess ( snipPos->fInfo.fOffset, relOffset ); + headExcess.fInfo.fState = snipPos->fInfo.fState; + headExcess.fInfo.fOutOfOrder = snipPos->fInfo.fOutOfOrder; + fInternalSnips.insert ( snipPos, headExcess ); // Insert the head piece before the middle piece. + } + + snipPos->fInfo.fOffset += relOffset; // Adjust the remainder of this snip. + snipPos->fInfo.fLength -= relOffset; + + } + + // ---------------------------------- + // Now deal with the high offset end. + + if ( newLength < snipPos->fInfo.fLength ) { + + InternalSnipIterator nextPos = NextSnip ( snipPos ); + const XMP_Int64 tailLength = snipPos->fInfo.fLength - newLength; + + if ( (nextPos != fInternalSnips.end()) && (snipPos->fInfo.fState == nextPos->fInfo.fState) ) { + nextPos->fInfo.fOffset -= tailLength; // Adjust the following snip. + nextPos->fInfo.fLength += tailLength; + } else { + InternalSnip tailExcess ( (snipPos->fInfo.fOffset + newLength), tailLength ); + tailExcess.fInfo.fState = snipPos->fInfo.fState; + tailExcess.fInfo.fOutOfOrder = snipPos->fInfo.fOutOfOrder; + fInternalSnips.insert ( nextPos, tailExcess ); // Insert the tail piece after the middle piece. + } + + snipPos->fInfo.fLength = newLength; + + } + +} // SplitInternalSnip + +// ================================================================================================= +// MergeInternalSnips +// ================== + +XMPScanner::InternalSnipIterator +XMPScanner::MergeInternalSnips ( InternalSnipIterator firstPos, InternalSnipIterator secondPos ) +{ + + firstPos->fInfo.fLength += secondPos->fInfo.fLength; + fInternalSnips.erase ( secondPos ); + return firstPos; + +} // MergeInternalSnips + +// ================================================================================================= +// Scan +// ==== + +void +XMPScanner::Scan ( const void * bufferOrigin, XMP_Int64 bufferOffset, XMP_Int64 bufferLength ) +{ + XMP_Int64 relOffset; + + #if 0 + cout << "Scan: @ " << bufferOrigin << ", " << bufferOffset << ", " << bufferLength << endl; + #endif + + if ( bufferLength == 0 ) return; + + // ---------------------------------------------------------------- + // These comparisons are carefully done to avoid overflow problems. + + if ( (bufferOffset >= fStreamLength) || + (bufferLength > (fStreamLength - bufferOffset)) || + (bufferOrigin == 0) ) { + throw ScanError ( "Bad origin, offset, or length" ); + } + + // ---------------------------------------------------------------------------------------------- + // This buffer must be within a not-seen snip. Find it and split it. The first snip whose whose + // end is beyond the buffer must be the enclosing one. + + // *** It would be friendly for rescans for out of order problems to accept any buffer postion. + + const XMP_Int64 endOffset = bufferOffset + bufferLength - 1; + InternalSnipIterator snipPos = fInternalSnips.begin(); + + while ( endOffset > (snipPos->fInfo.fOffset + snipPos->fInfo.fLength - 1) ) ++ snipPos; + if ( snipPos->fInfo.fState != eNotSeenSnip ) throw ScanError ( "Already seen" ); + + relOffset = bufferOffset - snipPos->fInfo.fOffset; + if ( (relOffset + bufferLength) > snipPos->fInfo.fLength ) throw ScanError ( "Not within existing snip" ); + + SplitInternalSnip ( snipPos, relOffset, bufferLength ); // *** If sequential & prev is partial, just tack on, + + // -------------------------------------------------------- + // Merge this snip with the preceeding snip if appropriate. + + // *** When out of order I/O is supported we have to do something about buffers who's predecessor is not seen. + + if ( snipPos->fInfo.fOffset > 0 ) { + InternalSnipIterator prevPos = PrevSnip ( snipPos ); + if ( prevPos->fInfo.fState == ePartialPacketSnip ) snipPos = MergeInternalSnips ( prevPos, snipPos ); + } + + // ---------------------------------- + // Look for packets within this snip. + + snipPos->fInfo.fState = ePendingSnip; + PacketMachine* thisMachine = snipPos->fMachine.get(); + // DumpSnipList ( "Before scan" ); + + if ( thisMachine != 0 ) { + thisMachine->AssociateBuffer ( bufferOffset, bufferOrigin, bufferLength ); + } else { + // *** snipPos->fMachine.reset ( new PacketMachine ( bufferOffset, bufferOrigin, bufferLength ) ); VC++ lacks reset + #if 0 + snipPos->fMachine = auto_ptr<PacketMachine> ( new PacketMachine ( bufferOffset, bufferOrigin, bufferLength ) ); + #else + { + // Some versions of gcc complain about the assignment operator above. This avoids the gcc bug. + PacketMachine * pm = new PacketMachine ( bufferOffset, bufferOrigin, bufferLength ); + auto_ptr<PacketMachine> ap ( pm ); + snipPos->fMachine = ap; + } + #endif + thisMachine = snipPos->fMachine.get(); + } + + bool bufferDone = false; + while ( ! bufferDone ) { + + PacketMachine::TriState foundPacket = thisMachine->FindNextPacket(); + + if ( foundPacket == PacketMachine::eTriNo ) { + + // ----------------------------------------------------------------------- + // No packet, mark the snip as raw data and get rid of the packet machine. + // We're done with this buffer. + + snipPos->fInfo.fState = eRawInputSnip; + #if 0 + snipPos->fMachine = auto_ptr<PacketMachine>(); // *** snipPos->fMachine.reset(); VC++ lacks reset + #else + { + // Some versions of gcc complain about the assignment operator above. This avoids the gcc bug. + auto_ptr<PacketMachine> ap ( 0 ); + snipPos->fMachine = ap; + } + #endif + bufferDone = true; + + } else { + + // --------------------------------------------------------------------------------------------- + // Either a full or partial packet. First trim any excess off of the front as a raw input snip. + // If this is a partial packet mark the snip and keep the packet machine to be resumed later. + // We're done with this buffer, the partial packet by definition extends to the end. If this is + // a complete packet first extract the additional information from the packet machine. If there + // is leftover data split the snip and transfer the packet machine to the new trailing snip. + + if ( thisMachine->fPacketStart > snipPos->fInfo.fOffset ) { + + // There is data at the front of the current snip that must be trimmed. + SnipState savedState = snipPos->fInfo.fState; + snipPos->fInfo.fState = eRawInputSnip; // ! So it gets propagated to the trimmed front part. + relOffset = thisMachine->fPacketStart - snipPos->fInfo.fOffset; + SplitInternalSnip ( snipPos, relOffset, (snipPos->fInfo.fLength - relOffset) ); + snipPos->fInfo.fState = savedState; + + } + + if ( foundPacket == PacketMachine::eTriMaybe ) { + + // We have only found a partial packet. + snipPos->fInfo.fState = ePartialPacketSnip; + bufferDone = true; + + } else { + + // We have found a complete packet. Extract all the info for it and split any trailing data. + + InternalSnipIterator packetSnip = snipPos; + SnipState packetState = eValidPacketSnip; + + if ( thisMachine->fBogusPacket ) packetState = eBadPacketSnip; + + packetSnip->fInfo.fAccess = thisMachine->fAccess; + packetSnip->fInfo.fCharForm = thisMachine->fCharForm; + packetSnip->fInfo.fBytesAttr = thisMachine->fBytesAttr; + packetSnip->fInfo.fEncodingAttr = thisMachine->fEncodingAttr.c_str(); + thisMachine->fEncodingAttr.erase ( thisMachine->fEncodingAttr.begin(), thisMachine->fEncodingAttr.end() ); + + if ( (thisMachine->fCharForm != eChar8Bit) && CharFormIsBigEndian ( thisMachine->fCharForm ) ) { + + // ------------------------------------------------------------------------------ + // Handle a special case for big endian characters. The packet machine works as + // though things were little endian. The packet starting offset points to the + // byte containing the opening '<', and the length includes presumed nulls that + // follow the last "real" byte. If the characters are big endian we now have to + // decrement the starting offset of the packet, and also decrement the length of + // the previous snip. + // + // Note that we can't do this before the head trimming above in general. The + // nulls might have been exactly at the end of a buffer and already in the + // previous snip. We are doing this before trimming the tail from the raw snip + // containing the packet. We adjust the raw snip's size because it ends with + // the input buffer. We don't adjust the packet's size, it is already correct. + // + // The raw snip (the one before the packet) might entirely disappear. A simple + // example of this is when the packet is at the start of the file. + + assert ( packetSnip != fInternalSnips.begin() ); // Leading nulls were trimmed! + + if ( packetSnip != fInternalSnips.begin() ) { // ... but let's program defensibly. + + InternalSnipIterator prevSnip = PrevSnip ( packetSnip ); + const unsigned int nullsToAdd = ( CharFormIs16Bit ( thisMachine->fCharForm ) ? 1 : 3 ); + + assert ( nullsToAdd <= prevSnip->fInfo.fLength ); + prevSnip->fInfo.fLength -= nullsToAdd; + if ( prevSnip->fInfo.fLength == 0 ) (void) fInternalSnips.erase ( prevSnip ); + + packetSnip->fInfo.fOffset -= nullsToAdd; + packetSnip->fInfo.fLength += nullsToAdd; + thisMachine->fPacketStart -= nullsToAdd; + + } + + } + + if ( thisMachine->fPacketLength == snipPos->fInfo.fLength ) { + + // This packet ends exactly at the end of the current snip. + #if 0 + snipPos->fMachine = auto_ptr<PacketMachine>(); // *** snipPos->fMachine.reset(); VC++ lacks reset + #else + { + // Some versions of gcc complain about the assignment operator above. This avoids the gcc bug. + auto_ptr<PacketMachine> ap ( 0 ); + snipPos->fMachine = ap; + } + #endif + bufferDone = true; + + } else { + + // There is trailing data to split from the just found packet. + SplitInternalSnip ( snipPos, 0, thisMachine->fPacketLength ); + + InternalSnipIterator tailPos = NextSnip ( snipPos ); + + tailPos->fMachine = snipPos->fMachine; // auto_ptr assignment - taking ownership + thisMachine->ResetMachine (); + + snipPos = tailPos; + + } + + packetSnip->fInfo.fState = packetState; // Do this last to avoid messing up the tail split. + // DumpSnipList ( "Found a packet" ); + + } + + } + + } + + // -------------------------------------------------------- + // Merge this snip with the preceeding snip if appropriate. + + // *** When out of order I/O is supported we have to check the following snip too. + + if ( (snipPos->fInfo.fOffset > 0) && (snipPos->fInfo.fState == eRawInputSnip) ) { + InternalSnipIterator prevPos = PrevSnip ( snipPos ); + if ( prevPos->fInfo.fState == eRawInputSnip ) snipPos = MergeInternalSnips ( prevPos, snipPos ); + } + + // DumpSnipList ( "After scan" ); + +} // Scan + +// ================================================================================================= +// Report +// ====== + +void +XMPScanner::Report ( SnipInfoVector& snips ) +{ + const int count = (int)fInternalSnips.size(); + InternalSnipIterator snipPos = fInternalSnips.begin(); + + int s; + + // DumpSnipList ( "Report" ); + + snips.erase ( snips.begin(), snips.end() ); // ! Should use snips.clear, but VC++ doesn't have it. + snips.reserve ( count ); + + for ( s = 0; s < count; s += 1 ) { + snips.push_back ( SnipInfo ( snipPos->fInfo.fState, snipPos->fInfo.fOffset, snipPos->fInfo.fLength ) ); + snips[s] = snipPos->fInfo; // Pick up all of the fields. + ++ snipPos; + } + +} // Report + +// ================================================================================================= + +#endif // EnablePacketScanning diff --git a/XMPFiles/source/FormatSupport/XMPScanner.hpp b/XMPFiles/source/FormatSupport/XMPScanner.hpp new file mode 100644 index 0000000..1648692 --- /dev/null +++ b/XMPFiles/source/FormatSupport/XMPScanner.hpp @@ -0,0 +1,330 @@ +#ifndef __XMPScanner_hpp__ +#define __XMPScanner_hpp__ + +// ================================================================================================= +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved. +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// +// Adobe patent application tracking #P435, entitled 'Unique markers to simplify embedding data of +// one format in a file with a different format', inventors: Sean Parent, Greg Gilley. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. + +#include <list> +#include <vector> +#include <string> +#include <memory> +#include <stdexcept> + +#include "public/include/XMP_Const.h" + +// ================================================================================================= +// The XMPScanner class is used to scan a stream of input for XMP packets. A scanner object is +// constructed then fed the input through a series of calls to Scan. Report may be called at any +// time to get the current knowledge of the input. +// +// A packet starts when a valid header is found and ends when a valid trailer is found. If the +// header contains a "bytes" attribute, additional whitespace must follow. +// +// *** RESTRICTIONS: The current implementation of the scanner has the the following restrictions: +// - The input must be presented in order. +// - Not fully thread safe, don't make concurrent calls to the same XMPScanner object. +// ================================================================================================= + +class XMPScanner { +public: + + // ============================================================================================= + // The entire input stream is represented as a series of snips. Each snip defines one portion + // of the input stream that either has not been seen, has been seen and contains no packets, is + // exactly one packet, or contains the start of an unfinished packet. Adjacent snips with the + // same state are merged, so the number of snips is always minimal. + // + // A newly constructed XMPScanner object has one snip covering the whole input with a state + // of "not seen". A block of input that contains a full XMP packet is split into 3 parts: a + // (possibly empty) raw input snip, the packet, and another (possibly empty) raw input snip. A + // block of input that contains the start of an XMP packet is split into two snips, a (possibly + // empty) raw input snip and the packet start; the following snip must be a "not seen" snip. + // + // It is possible to have ill-formed packets. These have a syntactically valid header and + // trailer, but some semantic error. For example, if the "bytes" attribute length does not span + // to the end of the trailer, or if the following packet begins within trailing padding. + + enum { + eNotSeenSnip, // This snip has not been seen yet. + ePendingSnip, // This snip is an input buffer being processed. + eRawInputSnip, // This snip is raw input, it doesn't contain any part of an XMP packet. + eValidPacketSnip, // This snip is a complete, valid XMP packet. + ePartialPacketSnip, // This snip contains the start of a possible XMP packet. + eBadPacketSnip // This snip contains a complete, but semantically incorrect XMP packet. + }; + typedef XMP_Uns8 SnipState; + + enum { // The values allow easy testing for 16/32 bit and big/little endian. + eChar8Bit = 0, + eChar16BitBig = 2, + eChar16BitLittle = 3, + eChar32BitBig = 4, + eChar32BitLittle = 5 + }; + typedef XMP_Uns8 CharacterForm; + + enum { + eChar16BitMask = 2, // These constant shouldn't be used directly, they are mainly + eChar32BitMask = 4, // for the CharFormIsXyz macros below. + eCharLittleEndianMask = 1 + }; + + #define CharFormIs16Bit(f) ( ((int)(f) & XMPScanner::eChar16BitMask) != 0 ) + #define CharFormIs32Bit(f) ( ((int)(f) & XMPScanner::eChar32BitMask) != 0 ) + + #define CharFormIsBigEndian(f) ( ((int)(f) & XMPScanner::eCharLittleEndianMask) == 0 ) + #define CharFormIsLittleEndian(f) ( ((int)(f) & XMPScanner::eCharLittleEndianMask) != 0 ) + + struct SnipInfo { + + XMP_Int64 fOffset; // The byte offset of this snip within the input stream. + XMP_Int64 fLength; // The length in bytes of this snip. + SnipState fState; // The state of this snip. + bool fOutOfOrder; // If true, this snip was seen before the one in front of it. + char fAccess; // The read-only/read-write access from the end attribute. + CharacterForm fCharForm; // How the packet is divided into characters. + const char * fEncodingAttr; // The value of the encoding attribute, if any, with nulls removed. + XMP_Int64 fBytesAttr; // The value of the bytes attribute, -1 if not present. + + SnipInfo() : + fOffset ( 0 ), + fLength ( 0 ), + fState ( eNotSeenSnip ), + fOutOfOrder ( false ), + fAccess ( ' ' ), + fCharForm ( eChar8Bit ), + fEncodingAttr ( "" ), + fBytesAttr( -1 ) + { } + + SnipInfo ( SnipState state, XMP_Int64 offset, XMP_Int64 length ) : + fOffset ( offset ), + fLength ( length ), + fState ( state ), + fOutOfOrder ( false ), + fAccess ( ' ' ), + fCharForm ( eChar8Bit ), + fEncodingAttr ( "" ), + fBytesAttr( -1 ) + { } + + }; + + typedef std::vector<SnipInfo> SnipInfoVector; + + XMPScanner ( XMP_Int64 streamLength ); + // Constructs a new XMPScanner object for a stream with the given length. + + ~XMPScanner(); + + long GetSnipCount(); + // Returns the number of snips that the stream has been divided into. + + bool StreamAllScanned(); + // Returns true if all of the stream has been seen. + + void Scan ( const void * bufferOrigin, XMP_Int64 bufferOffset, XMP_Int64 bufferLength ); + // Scans the given part of the input, incorporating it in to the known snips. + // The bufferOffset is the offset of this block of input relative to the entire stream. + // The bufferLength is the length in bytes of this block of input. + + void Report ( SnipInfoVector & snips ); + // Produces a report of what is known about the input stream. + + class ScanError : public std::logic_error { + public: + ScanError() throw() : std::logic_error ( "" ) {} + explicit ScanError ( const char * message ) throw() : std::logic_error ( message ) {} + virtual ~ScanError() throw() {} + }; + +private: // XMPScanner + + class PacketMachine; + + class InternalSnip { + public: + + SnipInfo fInfo; // The public info about this snip. + std::auto_ptr<PacketMachine> fMachine; // The state machine for "active" snips. + + InternalSnip ( XMP_Int64 offset, XMP_Int64 length ); + InternalSnip ( const InternalSnip & ); + ~InternalSnip (); + + }; // InternalSnip + + typedef std::list<InternalSnip> InternalSnipList; + typedef InternalSnipList::iterator InternalSnipIterator; + + class PacketMachine { + public: + + XMP_Int64 fPacketStart; // Byte offset relative to the entire stream. + XMP_Int32 fPacketLength; // Length in bytes to the end of the trailer processing instruction. + XMP_Int32 fBytesAttr; // The value of the bytes attribute, -1 if not present. + std::string fEncodingAttr; // The value of the encoding attribute, if any, with nulls removed. + CharacterForm fCharForm; // How the packet is divided into characters. + char fAccess; // The read-only/read-write access from the end attribute. + bool fBogusPacket; // True if the packet has an error such as a bad "bytes" attribute value. + + void ResetMachine(); + + enum TriState { + eTriNo, + eTriMaybe, + eTriYes + }; + + TriState FindNextPacket(); + + void AssociateBuffer ( XMP_Int64 bufferOffset, const void * bufferOrigin, XMP_Int64 bufferLength ); + + PacketMachine ( XMP_Int64 bufferOffset, const void * bufferOrigin, XMP_Int64 bufferLength ); + ~PacketMachine(); + + private: // PacketMachine + + PacketMachine() {}; // ! Hide the default constructor. + + enum RecognizerKind { + + eFailureRecognizer, // Not really recognizers, special states to end one buffer's processing. + eSuccessRecognizer, + + eLeadInRecognizer, // Anything up to the next '<'. + eHeadStartRecorder, // Save the starting offset, count intervening nulls. + eHeadStartRecognizer, // The literal string "?xpacket begin=". + + eBOMRecognizer, // Recognize and record the quoted byte order marker. + + eIDTagRecognizer, // The literal string " id=". + eIDOpenRecognizer, // The opening quote for the ID. + eIDValueRecognizer, // The literal string "W5M0MpCehiHzreSzNTczkc9d". + eIDCloseRecognizer, // The closing quote for the ID. + + eAttrSpaceRecognizer_1, // The space before an attribute. + eAttrNameRecognizer_1, // The name of an attribute. + eAttrValueRecognizer_1, // The equal sign and quoted string value for an attribute. + eAttrValueRecorder_1, // Record the value of an attribute. + + eHeadEndRecognizer, // The string literal "?>". + + eBodyRecognizer, // The packet body, anything up to the next '<'. + + eTailStartRecognizer, // The string literal "?xpacket end=". + eAccessValueRecognizer, // Recognize and record the quoted r/w access mode. + + eAttrSpaceRecognizer_2, // The space before an attribute. + eAttrNameRecognizer_2, // The name of an attribute. + eAttrValueRecognizer_2, // The equal sign and quoted string value for an attribute. + eAttrValueRecorder_2, // Record the value of an attribute. + + eTailEndRecognizer, // The string literal "?>". + ePacketEndRecognizer, // Look for trailing padding, check and record the packet size. + eCloseOutRecognizer, // Look for final nulls for little endian multibyte characters. + + eRecognizerCount + + }; + + XMP_Int64 fBufferOffset; // The offset of the data buffer within the input stream. + const char * fBufferOrigin; // The starting address of the data buffer for this snip. + const char * fBufferPtr; // The current postion in the data buffer. + const char * fBufferLimit; // The address one past the last byte in the data buffer. + + RecognizerKind fRecognizer; // Which recognizer is currently active. + signed long fPosition; // The internal position within a string literal, etc. + unsigned char fBytesPerChar; // The number of bytes per logical character, 1, 2, or 4. + unsigned char fBufferOverrun; // Non-zero if suspended while skipping intervening nulls. + char fQuoteChar; // The kind of quote seen at the start of a quoted value. + std::string fAttrName; // The name for an arbitrary attribute (other than "begin" and "id"). + std::string fAttrValue; // The value for an arbitrary attribute (other than "begin" and "id"). + + void SetNextRecognizer ( RecognizerKind nextRecognizer ); + + typedef TriState (* RecognizerProc) ( PacketMachine *, const char * ); + + static TriState + FindLessThan ( PacketMachine * ths, const char * which ); + + static TriState + MatchString ( PacketMachine * ths, const char * literal ); + + static TriState + MatchChar ( PacketMachine * ths, const char * literal ); + + static TriState + MatchOpenQuote ( PacketMachine * ths, const char * /* unused */ ); + + static TriState + MatchCloseQuote ( PacketMachine * ths, const char * /* unused */ ); + + static TriState + CaptureAttrName ( PacketMachine * ths, const char * /* unused */ ); + + static TriState + CaptureAttrValue ( PacketMachine * ths, const char * /* unused */ ); + + static TriState + RecordStart ( PacketMachine * ths, const char * /* unused */ ); + + static TriState + RecognizeBOM ( PacketMachine * ths, const char * /* unused */ ); + + static TriState + RecordHeadAttr ( PacketMachine * ths, const char * /* unused */ ); + + static TriState + CaptureAccess ( PacketMachine * ths, const char * /* unused */ ); + + static TriState + RecordTailAttr ( PacketMachine * ths, const char * /* unused */ ); + + static TriState + CheckPacketEnd ( PacketMachine * ths, const char * /* unused */ ); + + static TriState + CheckFinalNulls ( PacketMachine * ths, const char * /* unused */ ); + + struct RecognizerInfo { + RecognizerProc proc; + RecognizerKind successNext; + RecognizerKind failureNext; + const char * literal; + }; + + }; // PacketMachine + + XMP_Int64 fStreamLength; + InternalSnipList fInternalSnips; + + void + SplitInternalSnip ( InternalSnipIterator snipPos, XMP_Int64 relOffset, XMP_Int64 newLength ); + + InternalSnipIterator + MergeInternalSnips ( InternalSnipIterator firstPos, InternalSnipIterator secondPos ); + + InternalSnipIterator + PrevSnip ( InternalSnipIterator snipPos ); + + InternalSnipIterator + NextSnip ( InternalSnipIterator snipPos ); + + #if DEBUG + void DumpSnipList ( const char * title ); + #endif + +}; // XMPScanner + +#endif // __XMPScanner_hpp__ diff --git a/XMPFiles/source/HandlerRegistry.cpp b/XMPFiles/source/HandlerRegistry.cpp new file mode 100644 index 0000000..144dbcf --- /dev/null +++ b/XMPFiles/source/HandlerRegistry.cpp @@ -0,0 +1,961 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +#include "XMPFiles/source/HandlerRegistry.h" +#include "XMPFiles/source/PluginHandler/XMPAtoms.h" + +#if EnablePhotoHandlers + #include "XMPFiles/source/FileHandlers/JPEG_Handler.hpp" + #include "XMPFiles/source/FileHandlers/PSD_Handler.hpp" + #include "XMPFiles/source/FileHandlers/TIFF_Handler.hpp" +#endif + +#if EnableDynamicMediaHandlers + #include "XMPFiles/source/FileHandlers/AIFF_Handler.hpp" + #include "XMPFiles/source/FileHandlers/ASF_Handler.hpp" + #include "XMPFiles/source/FileHandlers/FLV_Handler.hpp" + #include "XMPFiles/source/FileHandlers/MP3_Handler.hpp" + #include "XMPFiles/source/FileHandlers/MPEG2_Handler.hpp" + #include "XMPFiles/source/FileHandlers/MPEG4_Handler.hpp" + #include "XMPFiles/source/FileHandlers/P2_Handler.hpp" + #include "XMPFiles/source/FileHandlers/WAVE_Handler.hpp" + #include "XMPFiles/source/FileHandlers/RIFF_Handler.hpp" + #include "XMPFiles/source/FileHandlers/SonyHDV_Handler.hpp" + #include "XMPFiles/source/FileHandlers/SWF_Handler.hpp" + #include "XMPFiles/source/FileHandlers/XDCAM_Handler.hpp" + #include "XMPFiles/source/FileHandlers/XDCAMEX_Handler.hpp" +#endif + +#if EnableMiscHandlers + #include "XMPFiles/source/FileHandlers/InDesign_Handler.hpp" + #include "XMPFiles/source/FileHandlers/PNG_Handler.hpp" + #include "XMPFiles/source/FileHandlers/PostScript_Handler.hpp" + #include "XMPFiles/source/FileHandlers/UCF_Handler.hpp" +#endif + +//#if EnablePacketScanning +//#include "XMPFiles/source/FileHandlers/Scanner_Handler.hpp" +//#endif + +using namespace Common; +using namespace XMP_PLUGIN; + +// ================================================================================================= + +#if EnableDynamicMediaHandlers + +static const char * kP2ContentChildren[] = { "CLIP", "VIDEO", "AUDIO", "ICON", "VOICE", "PROXY", 0 }; + +static inline bool CheckP2ContentChild ( const std::string & folderName ) +{ + for ( int i = 0; kP2ContentChildren[i] != 0; ++i ) { + if ( folderName == kP2ContentChildren[i] ) return true; + } + return false; +} + +#endif + +// ================================================================================================= + +// +// Static init +// +HandlerRegistry* HandlerRegistry::sInstance = 0; + +/*static*/ HandlerRegistry& HandlerRegistry::getInstance() +{ + if ( sInstance == 0 ) sInstance = new HandlerRegistry(); + return *sInstance; +} + +/*static*/ void HandlerRegistry::terminate() +{ + delete sInstance; + sInstance = 0; +} + +HandlerRegistry::HandlerRegistry() +{ + mFolderHandlers = new XMPFileHandlerTable; + mNormalHandlers = new XMPFileHandlerTable; + mOwningHandlers = new XMPFileHandlerTable; + mReplacedHandlers = new XMPFileHandlerTable; +} + +HandlerRegistry::~HandlerRegistry() +{ + delete mFolderHandlers; + delete mNormalHandlers; + delete mOwningHandlers; + delete mReplacedHandlers; +} + +// ================================================================================================= + +void HandlerRegistry::initialize() +{ + + bool allOK = true; // All of the linked-in handler registrations must work, do one test at the end. + + // ----------------------------------------- + // Register the directory-oriented handlers. + +#if EnableDynamicMediaHandlers + allOK &= this->registerFolderHandler ( kXMP_P2File, kP2_HandlerFlags, P2_CheckFormat, P2_MetaHandlerCTor ); + allOK &= this->registerFolderHandler ( kXMP_SonyHDVFile, kSonyHDV_HandlerFlags, SonyHDV_CheckFormat, SonyHDV_MetaHandlerCTor ); + allOK &= this->registerFolderHandler ( kXMP_XDCAM_FAMFile, kXDCAM_HandlerFlags, XDCAM_CheckFormat, XDCAM_MetaHandlerCTor ); + allOK &= this->registerFolderHandler ( kXMP_XDCAM_SAMFile, kXDCAM_HandlerFlags, XDCAM_CheckFormat, XDCAM_MetaHandlerCTor ); + allOK &= this->registerFolderHandler ( kXMP_XDCAM_EXFile, kXDCAMEX_HandlerFlags, XDCAMEX_CheckFormat, XDCAMEX_MetaHandlerCTor ); +#endif + + // ------------------------------------------------------------------------------------------ + // Register the file-oriented handlers that don't want to open and close the file themselves. + +#if EnablePhotoHandlers + allOK &= this->registerNormalHandler ( kXMP_JPEGFile, kJPEG_HandlerFlags, JPEG_CheckFormat, JPEG_MetaHandlerCTor ); + allOK &= this->registerNormalHandler ( kXMP_PhotoshopFile, kPSD_HandlerFlags, PSD_CheckFormat, PSD_MetaHandlerCTor ); + allOK &= this->registerNormalHandler ( kXMP_TIFFFile, kTIFF_HandlerFlags, TIFF_CheckFormat, TIFF_MetaHandlerCTor ); +#endif + +#if EnableDynamicMediaHandlers + allOK &= this->registerNormalHandler ( kXMP_WMAVFile, kASF_HandlerFlags, ASF_CheckFormat, ASF_MetaHandlerCTor ); + allOK &= this->registerNormalHandler ( kXMP_MP3File, kMP3_HandlerFlags, MP3_CheckFormat, MP3_MetaHandlerCTor ); + allOK &= this->registerNormalHandler ( kXMP_WAVFile, kWAVE_HandlerFlags, WAVE_CheckFormat, WAVE_MetaHandlerCTor ); + allOK &= this->registerNormalHandler ( kXMP_AVIFile, kRIFF_HandlerFlags, RIFF_CheckFormat, RIFF_MetaHandlerCTor ); + allOK &= this->registerNormalHandler ( kXMP_SWFFile, kSWF_HandlerFlags, SWF_CheckFormat, SWF_MetaHandlerCTor ); + allOK &= this->registerNormalHandler ( kXMP_MPEG4File, kMPEG4_HandlerFlags, MPEG4_CheckFormat, MPEG4_MetaHandlerCTor ); + allOK &= this->registerNormalHandler ( kXMP_MOVFile, kMPEG4_HandlerFlags, MPEG4_CheckFormat, MPEG4_MetaHandlerCTor ); // ! Yes, MPEG-4 includes MOV. + allOK &= this->registerNormalHandler ( kXMP_FLVFile, kFLV_HandlerFlags, FLV_CheckFormat, FLV_MetaHandlerCTor ); + allOK &= this->registerNormalHandler ( kXMP_AIFFFile, kAIFF_HandlerFlags, AIFF_CheckFormat, AIFF_MetaHandlerCTor ); +#endif + +#if EnableMiscHandlers + allOK &= this->registerNormalHandler ( kXMP_InDesignFile, kInDesign_HandlerFlags, InDesign_CheckFormat, InDesign_MetaHandlerCTor ); + allOK &= this->registerNormalHandler ( kXMP_PNGFile, kPNG_HandlerFlags, PNG_CheckFormat, PNG_MetaHandlerCTor ); + allOK &= this->registerNormalHandler ( kXMP_UCFFile, kUCF_HandlerFlags, UCF_CheckFormat, UCF_MetaHandlerCTor ); + // ! EPS and PostScript have the same handler, EPS is a proper subset of PostScript. + allOK &= this->registerNormalHandler ( kXMP_EPSFile, kPostScript_HandlerFlags, PostScript_CheckFormat, PostScript_MetaHandlerCTor ); + allOK &= this->registerNormalHandler ( kXMP_PostScriptFile, kPostScript_HandlerFlags, PostScript_CheckFormat, PostScript_MetaHandlerCTor ); +#endif + + // ------------------------------------------------------------------------------------ + // Register the file-oriented handlers that need to open and close the file themselves. + +#if EnableDynamicMediaHandlers + allOK &= this->registerOwningHandler ( kXMP_MPEGFile, kMPEG2_HandlerFlags, MPEG2_CheckFormat, MPEG2_MetaHandlerCTor ); + allOK &= this->registerOwningHandler ( kXMP_MPEG2File, kMPEG2_HandlerFlags, MPEG2_CheckFormat, MPEG2_MetaHandlerCTor ); +#endif + + if ( ! allOK ) XMP_Throw ( "Failure initializing linked-in file handlers", kXMPErr_InternalFailure ); + +} // HandlerRegistry::initialize + +// ================================================================================================= + +bool HandlerRegistry::registerFolderHandler( XMP_FileFormat format, + XMP_OptionBits flags, + CheckFolderFormatProc checkProc, + XMPFileHandlerCTor handlerCTor, + bool replaceExisting /*= false*/ ) +{ + XMP_Assert ( format != kXMP_UnknownFile ); + + XMP_Assert ( flags & kXMPFiles_HandlerOwnsFile ); + XMP_Assert ( flags & kXMPFiles_FolderBasedFormat ); + XMP_Assert ( (flags & kXMPFiles_CanInjectXMP) ? (flags & kXMPFiles_CanExpand) : 1 ); + + if ( replaceExisting ) + { + // + // Remember previous file handler for this format. + // Reject if there is already a replacement. + // + if( mReplacedHandlers->find( format ) == mReplacedHandlers->end() ) + { + XMPFileHandlerInfo* standardHandler = this->getHandlerInfo( format ); + + if( standardHandler != NULL ) + { + mReplacedHandlers->insert( mReplacedHandlers->end(), XMPFileHandlerTablePair( format, *standardHandler ) ); + } + else + { + // skip registration if there is nothing to replace + return false; + } + } + else + { + // skip registration if there is already a replacing handler registered for this format + return false; + } + + // remove existing handler + this->removeHandler ( format ); + } + else + { + // skip registration if there is already a handler registered for this format + if ( this->getFormatInfo ( format ) ) return false; + } + + // + // register handler + // + XMPFileHandlerInfo handlerInfo ( format, flags, checkProc, handlerCTor ); + mFolderHandlers->insert ( mFolderHandlers->end(), XMPFileHandlerTablePair ( format, handlerInfo ) ); + + return true; + +} // HandlerRegistry::registerFolderHandler + +// ================================================================================================= + +bool HandlerRegistry::registerNormalHandler( XMP_FileFormat format, + XMP_OptionBits flags, + CheckFileFormatProc checkProc, + XMPFileHandlerCTor handlerCTor, + bool replaceExisting /*= false*/ ) +{ + XMP_Assert ( format != kXMP_UnknownFile ); + + XMP_Assert ( ! (flags & kXMPFiles_HandlerOwnsFile) ); + XMP_Assert ( ! (flags & kXMPFiles_FolderBasedFormat) ); + XMP_Assert ( (flags & kXMPFiles_CanInjectXMP) ? (flags & kXMPFiles_CanExpand) : 1 ); + + if ( replaceExisting ) + { + // + // Remember previous file handler for this format. + // Reject if there is already a replacement. + // + if( mReplacedHandlers->find( format ) == mReplacedHandlers->end() ) + { + XMPFileHandlerInfo* standardHandler = this->getHandlerInfo( format ); + + if( standardHandler != NULL ) + { + mReplacedHandlers->insert( mReplacedHandlers->end(), XMPFileHandlerTablePair( format, *standardHandler ) ); + } + else + { + // skip registration if there is nothing to replace + return false; + } + } + else + { + // skip registration if there is already a replacing handler registered for this format + return false; + } + + // remove existing handler + this->removeHandler ( format ); + } + else + { + // skip registration if there is already a handler registered for this format + if ( this->getFormatInfo ( format ) ) return false; + } + + // + // register handler + // + XMPFileHandlerInfo handlerInfo ( format, flags, checkProc, handlerCTor ); + mNormalHandlers->insert ( mNormalHandlers->end(), XMPFileHandlerTablePair ( format, handlerInfo ) ); + return true; + +} // HandlerRegistry::registerNormalHandler + +// ================================================================================================= + +bool HandlerRegistry::registerOwningHandler( XMP_FileFormat format, + XMP_OptionBits flags, + CheckFileFormatProc checkProc, + XMPFileHandlerCTor handlerCTor, + bool replaceExisting /*= false*/ ) +{ + XMP_Assert ( format != kXMP_UnknownFile ); + + XMP_Assert ( flags & kXMPFiles_HandlerOwnsFile ); + XMP_Assert ( ! (flags & kXMPFiles_FolderBasedFormat) ); + XMP_Assert ( (flags & kXMPFiles_CanInjectXMP) ? (flags & kXMPFiles_CanExpand) : 1 ); + + if ( replaceExisting ) + { + // + // Remember previous file handler for this format. + // Reject if there is already a replacement. + // + if( mReplacedHandlers->find( format ) == mReplacedHandlers->end() ) + { + XMPFileHandlerInfo* standardHandler = this->getHandlerInfo( format ); + + if( standardHandler != NULL ) + { + mReplacedHandlers->insert( mReplacedHandlers->end(), XMPFileHandlerTablePair( format, *standardHandler ) ); + } + else + { + // skip registration if there is nothing to replace + return false; + } + } + else + { + // skip registration if there is already a replacing handler registered for this format + return false; + } + + // remove existing handler + this->removeHandler ( format ); + } + else + { + // skip registration if there is already a handler registered for this format + if ( this->getFormatInfo ( format ) ) return false; + } + + // + // register handler + // + XMPFileHandlerInfo handlerInfo ( format, flags, checkProc, handlerCTor ); + mOwningHandlers->insert ( mOwningHandlers->end(), XMPFileHandlerTablePair ( format, handlerInfo ) ); + return true; + +} // HandlerRegistry::registerOwningHandler + +// ================================================================================================= + +void HandlerRegistry::removeHandler ( XMP_FileFormat format ) { + + XMPFileHandlerTablePos handlerPos; + + handlerPos = mFolderHandlers->find ( format ); + if ( handlerPos != mFolderHandlers->end() ) { + mFolderHandlers->erase ( handlerPos ); + XMP_Assert ( ! this->getFormatInfo ( format ) ); + return; + } + + handlerPos = mNormalHandlers->find ( format ); + if ( handlerPos != mNormalHandlers->end() ) { + mNormalHandlers->erase ( handlerPos ); + XMP_Assert ( ! this->getFormatInfo ( format ) ); + return; + } + + handlerPos = mOwningHandlers->find ( format ); + if ( handlerPos != mOwningHandlers->end() ) { + mOwningHandlers->erase ( handlerPos ); + XMP_Assert ( ! this->getFormatInfo ( format ) ); + return; + } + +} // HandlerRegistry::removeHandler + +// ================================================================================================= + +XMP_FileFormat HandlerRegistry::getFileFormat( const std::string & fileExt, bool addIfNotFound /*= false*/ ) +{ + if ( ! fileExt.empty() ) { + for ( int i=0; kFileExtMap[i].format != 0; ++i ) { + if ( fileExt == kFileExtMap[i].ext ) return kFileExtMap[i].format; + } + } + + return ResourceParser::getPluginFileFormat ( fileExt, addIfNotFound ); +} + +// ================================================================================================= + +XMPFileHandlerInfo* HandlerRegistry::getHandlerInfo( XMP_FileFormat format ) +{ + XMPFileHandlerTablePos handlerPos; + + handlerPos = mFolderHandlers->find( format ); + + if( handlerPos != mFolderHandlers->end() ) + { + return &(handlerPos->second); + } + + handlerPos = mNormalHandlers->find ( format ); + + if( handlerPos != mNormalHandlers->end() ) + { + return &(handlerPos->second); + } + + handlerPos = mOwningHandlers->find ( format ); + + if( handlerPos != mOwningHandlers->end() ) + { + return &(handlerPos->second); + } + + return NULL; +} + +// ================================================================================================= + +XMPFileHandlerInfo* HandlerRegistry::getStandardHandlerInfo( XMP_FileFormat format ) +{ + XMPFileHandlerTablePos handlerPos = mReplacedHandlers->find( format ); + + if( handlerPos != mReplacedHandlers->end() ) + { + return &(handlerPos->second); + } + else + { + return this->getHandlerInfo( format ); + } +} + +// ================================================================================================= + +bool HandlerRegistry::isReplaced( XMP_FileFormat format ) +{ + return ( mReplacedHandlers->find( format ) != mReplacedHandlers->end() ); +} + +// ================================================================================================= + +bool HandlerRegistry::getFormatInfo( XMP_FileFormat format, XMP_OptionBits* flags /*= 0*/ ) +{ + if ( flags == 0 ) flags = &voidOptionBits; + + XMPFileHandlerInfo* handler = this->getHandlerInfo( format ); + + if( handler != NULL ) + { + *flags = handler->flags; + } + + return ( handler != NULL ); +} // HandlerRegistry::getFormatInfo + +// ================================================================================================= + +XMPFileHandlerInfo* HandlerRegistry::pickDefaultHandler ( XMP_FileFormat format, const std::string & fileExt ) +{ + if ( format == kXMP_UnknownFile ) format = this->getFileFormat ( fileExt ); + if ( format == kXMP_UnknownFile ) return 0; + + XMPFileHandlerTablePos handlerPos; + + handlerPos = mNormalHandlers->find ( format ); + if ( handlerPos != mNormalHandlers->end() ) return &handlerPos->second; + + handlerPos = mOwningHandlers->find ( format ); + if ( handlerPos != mOwningHandlers->end() ) return &handlerPos->second; + + handlerPos = mFolderHandlers->find ( format ); + if ( handlerPos != mFolderHandlers->end() ) return &handlerPos->second; + + return 0; +} + +// ================================================================================================= + +XMPFileHandlerInfo* HandlerRegistry::selectSmartHandler( XMPFiles* session, XMP_StringPtr clientPath, XMP_FileFormat format, XMP_OptionBits openFlags ) +{ + + // The normal case for selectSmartHandler is when OpenFile is given a string file path. All of + // the stages described below have slight special cases when OpenFile is given an XMP_IO object + // for client-managed I/O. In that case the only handlers considered are those for embedded XMP + // that do not need to own the file. + // + // There are 4 stages in finding a handler, ending at the first success: + // 1. If the client passes in a format, try that handler. + // 2. Try all of the folder-oriented handlers. + // 3. Try a file-oriented handler based on the file extension. + // 4. Try all of the file-oriented handlers. + // + // The most common case is almost certainly #3, so we want to get there quickly. Most of the + // time the client won't pass in a format, so #1 takes no time. The folder-oriented handler + // checks are preceded by minimal folder checks. These checks are meant to be fast in the + // failure case. The folder-oriented checks have to go before the general file-oriented checks + // because the client path might be to one of the inner files, and we might have a file-oriented + // handler for that kind of file, but we want to recognize the clip. More details are below. + // + // In brief, the folder-oriented formats use shallow trees with specific folder names and + // highly stylized file names. The user thinks of the tree as a collection of clips, each clip + // is stored as multiple files for video, audio, metadata, etc. The folder-oriented stage has + // to be first because there can be files in the structure that are also covered by a + // file-oriented handler. + // + // In the file-oriented case, the CheckProc should do as little as possible to determine the + // format, based on the actual file content. If that is not possible, use the format hint. The + // initial CheckProc calls (steps 1 and 3) has the presumed format in this->format, the later + // calls (step 4) have kXMP_UnknownFile there. + // + // The folder-oriented checks need to be well optimized since the formats are relatively rare, + // but have to go first and could require multiple file system calls to identify. We want to + // get to the first file-oriented guess as quickly as possible, that is the real handler most of + // the time. + // + // The folder-oriented handlers are for things like P2 and XDCAM that use files distributed in a + // well defined folder structure. Using a portion of P2 as an example: + // .../MyMovie + // CONTENTS + // CLIP + // 0001AB.XML + // 0002CD.XML + // VIDEO + // 0001AB.MXF + // 0002CD.MXF + // VOICE + // 0001AB.WAV + // 0002CD.WAV + // + // The user thinks of .../MyMovie as the container of P2 stuff, in this case containing 2 clips + // called 0001AB and 0002CD. The exact folder structure and file layout differs, but the basic + // concepts carry across all of the folder-oriented handlers. + // + // The client path can be a conceptual clip path like .../MyMovie/0001AB, or a full path to any + // of the contained files. For file paths we have to behave the same as the implied conceptual + // path, e.g. we don't want .../MyMovie/CONTENTS/VOICE/0001AB.WAV to invoke the WAV handler. + // There might also be a mapping from user friendly names to clip names (e.g. Intro to 0001AB). + // If so that is private to the handler and does not affect this code. + // + // In order to properly handle the file path input we have to look for the folder-oriented case + // before any of the file-oriented cases. And since these are relatively rare, hence fail most of + // the time, we have to get in and out fast in the not handled case. That is what we do here. + // + // The folder-oriented processing done here is roughly: + // + // 1. Get the state of the client path: does-not-exist, is-file, is-folder, is-other. + // 2. Reject is-folder and is-other, they can't possibly be a valid case. + // 3. For does-not-exist: + // 3a. Split the client path into a leaf component and root path. + // 3b. Make sure the root path names an existing folder. + // 3c. Make sure the root folder has a viable top level child folder (e.g. CONTENTS). + // 4. For is-file: + // 4a. Split the client path into a root path, grandparent folder, parent folder, and leaf name. + // 4b. Make sure the parent or grandparent has a viable name (e.g. CONTENTS). + // 5. Try the registered folder handlers. + // + // For the common case of "regular" files, we should only get as far as 3b. This is just 1 file + // system call to get the client path state and some string processing. + + bool readOnly = XMP_OptionIsClear( openFlags, kXMPFiles_OpenForUpdate ); + + Host_IO::FileMode clientMode; + std::string rootPath; + std::string leafName; + std::string fileExt; + std::string emptyStr; + + XMPFileHandlerInfo* handlerInfo = 0; + bool foundHandler = false; + + if ( openFlags & kXMPFiles_ForceGivenHandler ) { + // We're being told to blindly use the handler for the given format and nothing else. + return this->pickDefaultHandler ( format, emptyStr ); // Picks based on just the format. + } + + if ( session->UsesClientIO() ) { + + XMP_Assert ( session->ioRef != 0 ); + clientMode = Host_IO::kFMode_IsFile; + + } else { + + clientMode = Host_IO::GetFileMode( clientPath ); + if ( (clientMode == Host_IO::kFMode_IsFolder) || (clientMode == Host_IO::kFMode_IsOther) ) return 0; + + rootPath = clientPath; + XIO::SplitLeafName ( &rootPath, &leafName ); + + if ( leafName.empty() ) return 0; + + if ( clientMode == Host_IO::kFMode_IsFile ) { + // Only extract the file extension for existing files. Non-existing files can only be + // logical clip names, and they don't have file extensions. + XIO::SplitFileExtension ( &leafName, &fileExt ); + } + + } + + session->format = kXMP_UnknownFile; // Make sure it is preset for later checks. + session->openFlags = openFlags; + + // If the client passed in a format, try that first. + + if( format != kXMP_UnknownFile ) + { + handlerInfo = this->pickDefaultHandler( format, emptyStr ); // Picks based on just the format. + + if( handlerInfo != 0 ) + { + if( ( session->ioRef == 0 ) && (! ( handlerInfo->flags & kXMPFiles_HandlerOwnsFile ) ) ) + { + session->ioRef = XMPFiles_IO::New_XMPFiles_IO( clientPath, readOnly ); + + if ( session->ioRef == 0 ) return 0; + } + + session->format = format; // ! Hack to tell the CheckProc session is an initial call. + + if( handlerInfo->flags & kXMPFiles_FolderBasedFormat ) + { +#if 0 + std::string gpName, parentName; + if ( clientMode == Host_IO::kFMode_IsFile ) { + XIO::SplitLeafName( &rootPath, &parentName ); + XIO::SplitLeafName( &rootPath, &gpName ); + if ( format != kXMP_XDCAM_FAMFile ) MakeUpperCase( &gpName ); // ! Save the original case for XDCAM-FAM. + MakeUpperCase( &parentName ); + } + CheckFolderFormatProc CheckProc = (CheckFolderFormatProc) (handlerInfo->checkProc); + foundHandler = CheckProc ( handlerInfo->format, rootPath, gpName, parentName, leafName, session ); +#else + // *** Don't try here yet. These are messy, needing existence checking and path processing. + // *** CheckFolderFormatProc CheckProc = (CheckFolderFormatProc) (handlerInfo->checkProc); + // *** foundHandler = CheckProc ( handlerInfo->format, rootPath, gpName, parentName, leafName, session ); + // *** Don't let OpenStrictly cause an early exit: + if( openFlags & kXMPFiles_OpenStrictly ) openFlags ^= kXMPFiles_OpenStrictly; +#endif + } + else + { + bool tryThisHandler = true; + + if( session->UsesClientIO() ) + { + if ( (handlerInfo->flags & kXMPFiles_UsesSidecarXMP) || + (handlerInfo->flags & kXMPFiles_HandlerOwnsFile) ) tryThisHandler = false; + } + + if( tryThisHandler ) + { + CheckFileFormatProc CheckProc = (CheckFileFormatProc) (handlerInfo->checkProc); + foundHandler = CheckProc ( format, clientPath, session->ioRef, session ); + } + } + + XMP_Assert( foundHandler || (session->tempPtr == 0) ); + + if ( foundHandler ) return handlerInfo; + + handlerInfo = 0; // ! Clear again for later use. + } + + if ( openFlags & kXMPFiles_OpenStrictly ) return 0; + + } + +#if EnableDynamicMediaHandlers // All of the folder handlers are for dynamic media. + + // Try the folder handlers if appropriate. + + if( session->UsesLocalIO() ) + { + XMP_Assert ( handlerInfo == 0 ); + XMP_Assert ( (clientMode == Host_IO::kFMode_IsFile) || (clientMode == Host_IO::kFMode_DoesNotExist) ); + + std::string gpName, parentName; + + if( clientMode == Host_IO::kFMode_DoesNotExist ) + { + // 3. For does-not-exist: + // 3a. Split the client path into a leaf component and root path. + // 3b. Make sure the root path names an existing folder. + // 3c. Make sure the root folder has a viable top level child folder. + + // ! This does "return 0" on failure, the file does not exist so a normal file handler can't apply. + + if ( Host_IO::GetFileMode ( rootPath.c_str() ) != Host_IO::kFMode_IsFolder ) return 0; + + session->format = checkTopFolderName ( rootPath ); + + if ( session->format == kXMP_UnknownFile ) return 0; + + handlerInfo = this->tryFolderHandlers( session->format, rootPath, gpName, parentName, leafName, session ); // ! Parent and GP are empty. + + return handlerInfo; // ! Return found handler or 0. + } + + XMP_Assert ( clientMode == Host_IO::kFMode_IsFile ); + + // 4. For is-file: + // 4a. Split the client path into root, grandparent, parent, and leaf. + // 4b. Make sure the parent or grandparent has a viable name. + + // ! Don't "return 0" on failure, this has to fall through to the normal file handlers. + + XIO::SplitLeafName( &rootPath, &parentName ); + XIO::SplitLeafName( &rootPath, &gpName ); + std::string origGPName ( gpName ); // ! Save the original case for XDCAM-FAM. + MakeUpperCase( &parentName ); + MakeUpperCase( &gpName ); + + session->format = checkParentFolderNames( rootPath, gpName, parentName, leafName ); + + if( session->format != kXMP_UnknownFile ) + { + if( (session->format == kXMP_XDCAM_FAMFile) && + ( (parentName == "CLIP") || (parentName == "EDIT") || (parentName == "SUB") ) ) + { + // ! The standard says Clip/Edit/Sub, but we just shifted to upper case. + gpName = origGPName; // ! XDCAM-FAM has just 1 level of inner folder, preserve the "MyMovie" case. + } + + handlerInfo = tryFolderHandlers ( session->format, rootPath, gpName, parentName, leafName, session ); + if ( handlerInfo != 0 ) return handlerInfo; + + } + } + +#endif // EnableDynamicMediaHandlers + + // Try an initial file-oriented handler based on the extension. + + if( session->UsesLocalIO() ) + { + handlerInfo = pickDefaultHandler ( kXMP_UnknownFile, fileExt ); // Picks based on just the extension. + + if( handlerInfo != 0 ) + { + if( (session->ioRef == 0) && (! (handlerInfo->flags & kXMPFiles_HandlerOwnsFile)) ) + { + session->ioRef = XMPFiles_IO::New_XMPFiles_IO ( clientPath, readOnly ); + if ( session->ioRef == 0 ) return 0; + } + else if( (session->ioRef != 0) && (handlerInfo->flags & kXMPFiles_HandlerOwnsFile) ) + { + delete session->ioRef; // Close is implicit in the destructor. + session->ioRef = 0; + } + + session->format = handlerInfo->format; // ! Hack to tell the CheckProc this is an initial call. + CheckFileFormatProc CheckProc = (CheckFileFormatProc) (handlerInfo->checkProc); + foundHandler = CheckProc ( handlerInfo->format, clientPath, session->ioRef, session ); + XMP_Assert ( foundHandler || (session->tempPtr == 0) ); + + if ( foundHandler ) return handlerInfo; + } + } + + // Search the handlers that don't want to open the file themselves. + + if( session->ioRef == 0 ) + { + session->ioRef = XMPFiles_IO::New_XMPFiles_IO ( clientPath, readOnly ); + if ( session->ioRef == 0 ) return 0; + } + + XMPFileHandlerTablePos handlerPos = mNormalHandlers->begin(); + + for( ; handlerPos != mNormalHandlers->end(); ++handlerPos ) + { + session->format = kXMP_UnknownFile; // ! Hack to tell the CheckProc this is not an initial call. + handlerInfo = &handlerPos->second; + CheckFileFormatProc CheckProc = (CheckFileFormatProc) (handlerInfo->checkProc); + foundHandler = CheckProc ( handlerInfo->format, clientPath, session->ioRef, session ); + XMP_Assert ( foundHandler || (session->tempPtr == 0) ); + if ( foundHandler ) return handlerInfo; + } + + // Search the handlers that do want to open the file themselves. + + if( session->UsesLocalIO() ) + { + delete session->ioRef; // Close is implicit in the destructor. + session->ioRef = 0; + handlerPos = mOwningHandlers->begin(); + + for( ; handlerPos != mOwningHandlers->end(); ++handlerPos ) + { + session->format = kXMP_UnknownFile; // ! Hack to tell the CheckProc this is not an initial call. + handlerInfo = &handlerPos->second; + CheckFileFormatProc CheckProc = (CheckFileFormatProc) (handlerInfo->checkProc); + foundHandler = CheckProc ( handlerInfo->format, clientPath, session->ioRef, session ); + XMP_Assert ( foundHandler || (session->tempPtr == 0) ); + if ( foundHandler ) return handlerInfo; + } + } + + // Failed to find a smart handler. + + return 0; + +} // HandlerRegistry::selectSmartHandler + +// ================================================================================================= + +#if EnableDynamicMediaHandlers + +XMPFileHandlerInfo* HandlerRegistry::tryFolderHandlers( XMP_FileFormat format, + const std::string & rootPath, + const std::string & gpName, + const std::string & parentName, + const std::string & leafName, + XMPFiles * parentObj ) +{ + bool foundHandler = false; + XMPFileHandlerInfo * handlerInfo = 0; + XMPFileHandlerTablePos handlerPos; + + // We know we're in a possible context for a folder-oriented handler, so try them. + + if( format != kXMP_UnknownFile ) + { + + // Have an explicit format, pick that or nothing. + handlerPos = mFolderHandlers->find ( format ); + + if( handlerPos != mFolderHandlers->end() ) + { + handlerInfo = &handlerPos->second; + CheckFolderFormatProc CheckProc = (CheckFolderFormatProc) (handlerInfo->checkProc); + foundHandler = CheckProc ( handlerInfo->format, rootPath, gpName, parentName, leafName, parentObj ); + XMP_Assert ( foundHandler || (parentObj->tempPtr == 0) ); + } + } + else + { + // Try all of the folder handlers. + for( handlerPos = mFolderHandlers->begin(); handlerPos != mFolderHandlers->end(); ++handlerPos ) + { + handlerInfo = &handlerPos->second; + CheckFolderFormatProc CheckProc = (CheckFolderFormatProc) (handlerInfo->checkProc); + foundHandler = CheckProc ( handlerInfo->format, rootPath, gpName, parentName, leafName, parentObj ); + XMP_Assert ( foundHandler || (parentObj->tempPtr == 0) ); + if ( foundHandler ) break; // ! Exit before incrementing handlerPos. + } + } + + if ( ! foundHandler ) handlerInfo = 0; + + return handlerInfo; +} + +#endif + +// ================================================================================================= + +#if EnableDynamicMediaHandlers + +/*static*/ XMP_FileFormat HandlerRegistry::checkTopFolderName( const std::string & rootPath ) +{ + // This is called when the input path to XMPFiles::OpenFile does not name an existing file (or + // existing anything). We need to quickly decide if this might be a logical path for a folder + // handler. See if the root contains the top content folder for any of the registered folder + // handlers. This check does not have to be precise, the handler will do that. This does have to + // be fast. + // + // Since we don't have many folder handlers, this is simple hardwired code. + + std::string childPath = rootPath; + childPath += kDirChar; + size_t baseLen = childPath.size(); + + // P2 .../MyMovie/CONTENTS/<group>/... - only check for CONTENTS/CLIP + childPath += "CONTENTS"; + childPath += kDirChar; + childPath += "CLIP"; + if ( Host_IO::GetFileMode ( childPath.c_str() ) == Host_IO::kFMode_IsFolder ) return kXMP_P2File; + childPath.erase ( baseLen ); + + // XDCAM-FAM .../MyMovie/<group>/... - only check for Clip and MEDIAPRO.XML + childPath += "Clip"; // ! Yes, mixed case. + if ( Host_IO::GetFileMode ( childPath.c_str() ) == Host_IO::kFMode_IsFolder ) { + childPath.erase ( baseLen ); + childPath += "MEDIAPRO.XML"; + if ( Host_IO::GetFileMode ( childPath.c_str() ) == Host_IO::kFMode_IsFile ) return kXMP_XDCAM_FAMFile; + } + childPath.erase ( baseLen ); + + // XDCAM-SAM .../MyMovie/PROAV/<group>/... - only check for PROAV/CLPR + childPath += "PROAV"; + childPath += kDirChar; + childPath += "CLPR"; + if ( Host_IO::GetFileMode ( childPath.c_str() ) == Host_IO::kFMode_IsFolder ) return kXMP_XDCAM_SAMFile; + childPath.erase ( baseLen ); + + // XDCAM-EX .../MyMovie/BPAV/<group>/... - check for BPAV/CLPR + childPath += "BPAV"; + childPath += kDirChar; + childPath += "CLPR"; + if ( Host_IO::GetFileMode ( childPath.c_str() ) == Host_IO::kFMode_IsFolder ) return kXMP_XDCAM_EXFile; + childPath.erase ( baseLen ); + + // Sony HDV .../MyMovie/VIDEO/HVR/<file>.<ext> - check for VIDEO/HVR + childPath += "VIDEO"; + childPath += kDirChar; + childPath += "HVR"; + if ( Host_IO::GetFileMode ( childPath.c_str() ) == Host_IO::kFMode_IsFolder ) return kXMP_SonyHDVFile; + childPath.erase ( baseLen ); + + return kXMP_UnknownFile; + +} // CheckTopFolderName + +#endif + +// ================================================================================================= + +#if EnableDynamicMediaHandlers + +/*static*/ XMP_FileFormat HandlerRegistry::checkParentFolderNames( const std::string& rootPath, + const std::string& gpName, + const std::string& parentName, + const std::string& leafName ) +{ + IgnoreParam ( parentName ); + + // This is called when the input path to XMPFiles::OpenFile names an existing file. We need to + // quickly decide if this might be inside a folder-handler's structure. See if the containing + // folders might match any of the registered folder handlers. This check does not have to be + // precise, the handler will do that. This does have to be fast. + // + // Since we don't have many folder handlers, this is simple hardwired code. Note that the caller + // has already shifted the names to upper case. + + // P2 .../MyMovie/CONTENTS/<group>/<file>.<ext> - check CONTENTS and <group> + if ( (gpName == "CONTENTS") && CheckP2ContentChild ( parentName ) ) return kXMP_P2File; + + // XDCAM-EX .../MyMovie/BPAV/CLPR/<clip>/<file>.<ext> - check for BPAV/CLPR + // ! This must be checked before XDCAM-SAM because both have a "CLPR" grandparent. + if ( gpName == "CLPR" ) { + std::string tempPath, greatGP; + tempPath = rootPath; + XIO::SplitLeafName ( &tempPath, &greatGP ); + MakeUpperCase ( &greatGP ); + if ( greatGP == "BPAV" ) return kXMP_XDCAM_EXFile; + } + + // XDCAM-FAM .../MyMovie/<group>/<file>.<ext> - check that <group> is CLIP, or EDIT, or SUB + // ! The standard says Clip/Edit/Sub, but the caller has already shifted to upper case. + if ( (parentName == "CLIP") || (parentName == "EDIT") || (parentName == "SUB") ) return kXMP_XDCAM_FAMFile; + + // XDCAM-SAM .../MyMovie/PROAV/<group>/<clip>/<file>.<ext> - check for PROAV and CLPR or EDTR + if ( (gpName == "CLPR") || (gpName == "EDTR") ) { + std::string tempPath, greatGP; + tempPath = rootPath; + XIO::SplitLeafName ( &tempPath, &greatGP ); + MakeUpperCase ( &greatGP ); + if ( greatGP == "PROAV" ) return kXMP_XDCAM_SAMFile; + } + + // Sony HDV .../MyMovie/VIDEO/HVR/<file>.<ext> - check for VIDEO and HVR + if ( (gpName == "VIDEO") && (parentName == "HVR") ) return kXMP_SonyHDVFile; + + return kXMP_UnknownFile; + +} // CheckParentFolderNames + +#endif diff --git a/XMPFiles/source/HandlerRegistry.h b/XMPFiles/source/HandlerRegistry.h new file mode 100644 index 0000000..a146bfd --- /dev/null +++ b/XMPFiles/source/HandlerRegistry.h @@ -0,0 +1,238 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _HANDLERREGISTRY_h_ +#define _HANDLERREGISTRY_h_ + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XMPFiles_IO.hpp" + +#include "XMPFiles/source/FormatSupport/IFF/IChunkBehavior.h" +#include "XMPFiles/source/FormatSupport/IFF/ChunkPath.h" +#include "source/Endian.h" + +namespace Common +{ + +/** + File handler data + */ +struct XMPFileHandlerInfo +{ + XMP_FileFormat format; + XMP_OptionBits flags; + void* checkProc; + XMPFileHandlerCTor handlerCTor; + + XMPFileHandlerInfo() : format(0), flags(0), checkProc(0), handlerCTor(0) + {}; + + XMPFileHandlerInfo( XMP_FileFormat _format, + XMP_OptionBits _flags, + CheckFileFormatProc _checkProc, + XMPFileHandlerCTor _handlerCTor ) + : format(_format), flags(_flags), checkProc((void*)_checkProc), handlerCTor(_handlerCTor) + {}; + + XMPFileHandlerInfo( XMP_FileFormat _format, + XMP_OptionBits _flags, + CheckFolderFormatProc _checkProc, + XMPFileHandlerCTor _handlerCTor ) + : format(_format), flags(_flags), checkProc((void*)_checkProc), handlerCTor(_handlerCTor) + {}; +}; + +/** + The singleton class HandlerRegistry is responsible to manage all file handler. + It registers file handlers during initialization time and provides functionality + to select a file handler based on a given file format. +*/ + +class HandlerRegistry +{ +public: + static HandlerRegistry& getInstance(); + static void terminate(); + +#if EnableDynamicMediaHandlers + static XMP_FileFormat checkTopFolderName( const std::string & rootPath ); + static XMP_FileFormat checkParentFolderNames( const std::string& rootPath, + const std::string& gpName, + const std::string& parentName, + const std::string& leafName ); +#endif + +public: + /** + * Register all file handler + */ + void initialize(); + + /** + * Register a single folder based file handler. + * + * @param format File format identifier + * @param flags Flags + * @param checkProc Check format function pointer + * @param handlerCTor Factory function pointer + * @param replaceExisting Replace an already existing handler + */ + bool registerFolderHandler( XMP_FileFormat format, + XMP_OptionBits flags, + CheckFolderFormatProc checkProc, + XMPFileHandlerCTor handlerCTor, + bool replaceExisting = false ); + + /** + * Register a single normal file handler. + * + * @param format File format identifier + * @param flags Flags + * @param checkProc Check format function pointer + * @param handlerCTor Factory function pointer + * @param replaceExisting Replace an already existing handler + */ + bool registerNormalHandler( XMP_FileFormat format, + XMP_OptionBits flags, + CheckFileFormatProc checkProc, + XMPFileHandlerCTor handlerCTor, + bool replaceExisting = false ); + + /** + * Register a single owning file handler. + * + * @param format File format identifier + * @param flags Flags + * @param checkProc Check format function pointer + * @param handlerCTor Factory function pointer + * @param replaceExisting Replace an already existing handler + */ + bool registerOwningHandler( XMP_FileFormat format, + XMP_OptionBits flags, + CheckFileFormatProc checkProc, + XMPFileHandlerCTor handlerCTor, + bool replaceExisting = false ); + + // Remove a handler. Does nothing if no such handler exists. + void removeHandler ( XMP_FileFormat format ); + + /** + * Get file format identifier for filename extension. + * + * @param fileExt Filename extension + * @param addIfNotFound If true if handler doesn't exists then add now + */ + XMP_FileFormat getFileFormat( const std::string & fileExt, bool addIfNotFound = false ); + + /** + * Get handler flags for file format. + * + * @param format File format identifier + * @param flags Return handler flag + * @return True on success + */ + bool getFormatInfo( XMP_FileFormat format, XMP_OptionBits* flags = NULL ); + + /** + * Get handler information for passed format. + * The returned file handler is the default handler. I.e. that handler + * that is used when called from outside using the XMPFiles API. + * + * @param format File format identifier + * @return Return handler info if available, otherwise NULL + */ + XMPFileHandlerInfo* getHandlerInfo( XMP_FileFormat format ); + + /** + * Get file handler information of the standard file handler for the + * file format identifier. + * If there is a replacement for this format then the standard handler + * is the replaced handler. Otherwise the standard handler and the + * default handler are the same. + * + * @param format File format identifier + * @return Return handler info if available, otherwise NULL + */ + XMPFileHandlerInfo* getStandardHandlerInfo( XMP_FileFormat format ); + + /** + * Return true if there is a replacement for the file format + */ + bool isReplaced( XMP_FileFormat format ); + + /** + * Select file handler based on passed information and setup XMPFiles instance with related data. + * + * @param session XMPFiles instance + * @param clientPath Path to file + * @param format File format identifier + * @param openFlags Flags + * @return File handler structure + */ + XMPFileHandlerInfo* selectSmartHandler( XMPFiles* session, XMP_StringPtr clientPath, XMP_FileFormat format, XMP_OptionBits openFlags ); + +private: + /** + * Return default file handler for file format identifier or filename extension + * + * @param format File format identifier + * @param fileExt Filename extension + * @return File handler structure for passed format/extension + */ + XMPFileHandlerInfo* pickDefaultHandler ( XMP_FileFormat format, const std::string & fileExt ); + +#if EnableDynamicMediaHandlers + /** + * Try to find folder based file handler. + * + * @param format File format identifier + * @param rootPath Path to root folder + * @param gpName Grand parent folder name + * @param parentName Parent folder name + * @param leafName File name + * @param parentObj XMPFiles instance + * @return File handler structure + */ + XMPFileHandlerInfo* tryFolderHandlers( XMP_FileFormat format, + const std::string& rootPath, + const std::string& gpName, + const std::string& parentName, + const std::string& leafName, + XMPFiles* parentObj ); +#endif + +private: + /** + * ctor/dtor + */ + HandlerRegistry(); + ~HandlerRegistry(); + +private: + typedef std::map <XMP_FileFormat, XMPFileHandlerInfo> XMPFileHandlerTable; + typedef XMPFileHandlerTable::iterator XMPFileHandlerTablePos; + typedef std::pair <XMP_FileFormat, XMPFileHandlerInfo> XMPFileHandlerTablePair; + + XMPFileHandlerTable* mFolderHandlers; // The directory-oriented handlers. + XMPFileHandlerTable* mNormalHandlers; // The normal file-oriented handlers. + XMPFileHandlerTable* mOwningHandlers; // The file-oriented handlers that "own" the file. + + XMPFileHandlerTable* mReplacedHandlers; // All file handler that where replaced by a later one + + static HandlerRegistry* sInstance; // singleton instance +}; + +} // Common + +#endif // _HANDLERREGISTRY_h_ diff --git a/XMPFiles/source/NativeMetadataSupport/IMetadata.cpp b/XMPFiles/source/NativeMetadataSupport/IMetadata.cpp new file mode 100644 index 0000000..40a864e --- /dev/null +++ b/XMPFiles/source/NativeMetadataSupport/IMetadata.cpp @@ -0,0 +1,206 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "XMPFiles/source/NativeMetadataSupport/IMetadata.h" + +//----------------------------------------------------------------------------- +// +// IMetadata::IMetadata(...) +// +// Purpose: ctor/dtor +// +//----------------------------------------------------------------------------- + +IMetadata::IMetadata() +: mDirty(false) +{ +} + +IMetadata::~IMetadata() +{ + for( ValueMap::iterator iter = mValues.begin(); iter != mValues.end(); iter++ ) + { + delete iter->second; + } +} + +//----------------------------------------------------------------------------- +// +// IMetadata::parse(...) +// +// Purpose: Parses the given memory block and creates a data model representation +// We assume that any metadata block is < 4GB so that we can savely use +// 32bit sizes. +// Throws exceptions if parsing is not possible +// +//----------------------------------------------------------------------------- + +void IMetadata::parse( const XMP_Uns8* input, XMP_Uns64 size ) +{ + XMP_Throw ( "Method not implemented", kXMPErr_Unimplemented ); +} + +//----------------------------------------------------------------------------- +// +// IMetadata::parse(...) +// +// Purpose: Parses the given file and creates a data model representation +// Throws exceptions if parsing is not possible +// +//----------------------------------------------------------------------------- + +void IMetadata::parse( XMP_IO* input ) +{ + XMP_Throw ( "Method not implemented", kXMPErr_Unimplemented ); +} + +//----------------------------------------------------------------------------- +// +// IMetadata::serialize(...) +// +// Purpose: Serializes the data model to a memory block. +// The method creates a buffer and pass it to the parameter 'buffer'. +// The callee of the method is responsible to delete the buffer later on. +// We assume that any metadata block is < 4GB so that we can savely use +// 32bit sizes. +// Throws exceptions if serializing is not possible +// +//----------------------------------------------------------------------------- + +XMP_Uns64 IMetadata::serialize( XMP_Uns8** buffer ) +{ + XMP_Throw ( "Method not implemented", kXMPErr_Unimplemented ); +} + +//----------------------------------------------------------------------------- +// +// IMetadata::hasChanged(...) +// +// Purpose: Return true if any values of this container was modified +// +//----------------------------------------------------------------------------- + +bool IMetadata::hasChanged() const +{ + bool isDirty = mDirty; + + for( ValueMap::const_iterator iter=mValues.begin(); ! isDirty && iter != mValues.end(); iter++ ) + { + isDirty = iter->second->hasChanged(); + } + + return isDirty; +} + +//----------------------------------------------------------------------------- +// +// IMetadata::resetChanges(...) +// +// Purpose: Reset dirty flag +// +//----------------------------------------------------------------------------- + +void IMetadata::resetChanges() +{ + mDirty = false; + + for( ValueMap::iterator iter=mValues.begin(); iter != mValues.end(); iter++ ) + { + iter->second->resetChanged(); + } +} + +//----------------------------------------------------------------------------- +// +// IMetadata::isEmpty(...) +// +// Purpose: Return true if the no metadata are available in this container +// +//----------------------------------------------------------------------------- + +bool IMetadata::isEmpty() const +{ + return mValues.empty(); +} + +//----------------------------------------------------------------------------- +// +// IMetadata::deleteValue(...) +// +// Purpose: Remove value for passed identifier +// +//----------------------------------------------------------------------------- + +void IMetadata::deleteValue( XMP_Uns32 id ) +{ + ValueMap::iterator iterator = mValues.find( id ); + + if( iterator != mValues.end() ) + { + delete iterator->second; + mValues.erase( iterator ); + + mDirty = true; + } +} + +//----------------------------------------------------------------------------- +// +// IMetadata::deleteAll(...) +// +// Purpose: Remove all stored values +// +//----------------------------------------------------------------------------- + +void IMetadata::deleteAll() +{ + mDirty = ( mValues.size() > 0 ); + + for( ValueMap::iterator iter = mValues.begin(); iter != mValues.end(); iter++ ) + { + delete iter->second; + } + + mValues.clear(); +} + +//----------------------------------------------------------------------------- +// +// IMetadata::valueExists(...) +// +// Purpose: Return true if an value for the passed identifier exists +// +//----------------------------------------------------------------------------- + +bool IMetadata::valueExists( XMP_Uns32 id ) const +{ + ValueMap::const_iterator iterator = mValues.find( id ); + + return ( iterator != mValues.end() ); +} + +//----------------------------------------------------------------------------- +// +// IMetadata::valueChanged(...) +// +// Purpose: Return true if the value for the passed identifier was changed +// +//----------------------------------------------------------------------------- + +bool IMetadata::valueChanged( XMP_Uns32 id ) const +{ + ValueMap::const_iterator iterator = mValues.find( id ); + + if( iterator != mValues.end() ) + { + return iterator->second->hasChanged(); + } + + return false; +} diff --git a/XMPFiles/source/NativeMetadataSupport/IMetadata.h b/XMPFiles/source/NativeMetadataSupport/IMetadata.h new file mode 100644 index 0000000..1a66c86 --- /dev/null +++ b/XMPFiles/source/NativeMetadataSupport/IMetadata.h @@ -0,0 +1,334 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _IMetadata_h_ +#define _IMetadata_h_ + +#include "public/include/XMP_Environment.h" // ! This must be the first include. +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" +#include "source/XMP_LibUtils.hpp" +#include "XMPFiles/source/NativeMetadataSupport/ValueObject.h" + +#include <map> + +/** + * The class IMetadata is supposed to store any unique metadata. + * It provides a generic interface to store any data types or arrays of any data types. + * The requirements for used datatypes are defined by the class ValueObject and its derived classes. + * For each single value as well as for the container as a whole modification and existing state is provided. + * It also provids methods to parse a byte block to distinct values or to serialize values to a byte block. + * IMetadata is an abstract class due to the method isEmptyValue(...). Derived classes needs to implement this + * method. It has to define when a certain value is "empty", since "empty" values are not stored but removed + * from the container. + * + */ +class IMetadata +{ +public: + /** + * ctor/dtor + */ + IMetadata(); + virtual ~IMetadata(); + + + /** + * Parses the given memory block and creates a data model representation + * We assume that any metadata block is < 4GB so that we can savely use 32bit sizes. + * Throws exceptions if parsing is not possible + * + * @param input The byte buffer to parse + * @param size Size of the given byte buffer + */ + virtual void parse( const XMP_Uns8* input, XMP_Uns64 size ); + + + /** + * Parses the given file and creates a data model representation + * Throws exceptions if parsing is not possible + * + * @param input The file to parse + */ + virtual void parse( XMP_IO* input ); + + + /** + * Serializes the data model to a memory block. + * The method creates a buffer and pass it to the parameter 'buffer'. The callee of + * the method is responsible to delete the buffer later on. + * We assume that any metadata block is < 4GB so that we can savely use 32bit sizes. + * Throws exceptions if serializing is not possible + * + * @param buffer Buffer that gets filled with serialized data + * @param size Size of passed in buffer + * + * @ return Buffer size + */ + virtual XMP_Uns64 serialize( XMP_Uns8** buffer ); + + /** + * Return true if any values of this container was modified + */ + virtual bool hasChanged() const; + + /** + * Reset dirty flag + */ + virtual void resetChanges(); + + /** + * Return true if the no metadata are available in this container + */ + virtual bool isEmpty() const; + + /** + * Set value for passed identifier + * + * @param id Identifier of value + * @param value New value of passed data type + */ + template<class T> void setValue( XMP_Uns32 id, const T& value ); + + /** + * Set array for passed identifier + * + * @param id Identifier of value + * @param value Pointer to the array + * @param bufferSize Number of array elements + */ + template<class T> void setArray( XMP_Uns32 id, const T* buffer, XMP_Uns32 numElements); + + /** + * Return value for passed identifier. + * If the value doesn't exists an exception is thrown! + * + * @param id Identifier of value + * @return Value of passed data type + */ + template<class T> const T& getValue( XMP_Uns32 id ) const; + + /** + * Return array for passed identifier + * If the array doesn't exists an exception is thrown! + * + * @param id Identifier of value + * @param outBuffer Array + * @return Number of array elements + */ + template<class T> const T* const getArray( XMP_Uns32 id, XMP_Uns32& outSize ) const; + + /** + * Remove value for passed identifier + * + * @param id Identifier of value + */ + virtual void deleteValue( XMP_Uns32 id ); + + /** + * Remove all stored values + */ + virtual void deleteAll(); + + /** + * Return true if an value for the passed identifier exists + * + * @param id Identifier of value + */ + virtual bool valueExists( XMP_Uns32 id ) const; + + /** + * Return true if the value for the passed identifier was changed + * + * @param id Identifier of value + */ + virtual bool valueChanged( XMP_Uns32 id ) const; + +protected: + /** + * Is the value of the passed ValueObject that belongs to the given id "empty"? + * Derived classes are required to implement this method and define "empty" + * for its values. + * Needed for setValue and setArray. + * + * @param id Identifier of passed value + * @param valueObj Value to inspect + * + * @return True if the value is "empty" + */ + virtual bool isEmptyValue( XMP_Uns32 id, ValueObject& valueObj ) = 0; + +private: + // Operators hidden on purpose + IMetadata( const IMetadata& ) {}; + IMetadata& operator=( const IMetadata& ) { return *this; }; + +protected: + typedef std::map<XMP_Uns32, ValueObject*> ValueMap; + ValueMap mValues; + bool mDirty; +}; + +template<class T> void IMetadata::setValue( XMP_Uns32 id, const T& value ) +{ + TValueObject<T>* valueObj = NULL; + + // + // find existing value for id + // + ValueMap::iterator iterator = mValues.find( id ); + + if( iterator != mValues.end() ) + { + // + // value exists, set new value + // + valueObj = dynamic_cast<TValueObject<T>*>( iterator->second ); + + if( valueObj != NULL ) + { + valueObj->setValue( value ); + } + else + { + XMP_Throw ( "Invalid identifier", kXMPErr_InternalFailure ); + } + } + else + { + // + // value doesn't exists yet and is not "empty" + // so add a new value to the map + // + valueObj = new TValueObject<T>( value ); + mValues[id] = valueObj; + + mDirty = true; // the new created value isn't dirty, but the container becomes dirty + } + + // + // check if the value is "empty" + // + if( this->isEmptyValue( id, *valueObj ) ) + { + // + // value is "empty", delete it + // + this->deleteValue( id ); + } +} + +template<class T> void IMetadata::setArray( XMP_Uns32 id, const T* buffer, XMP_Uns32 numElements ) +{ + TArrayObject<T>* arrayObj = NULL; + + // + // find existing value for id + // + ValueMap::iterator iterator = mValues.find( id ); + + if( iterator != mValues.end() ) + { + // + // value exists, set new value + // + arrayObj = dynamic_cast<TArrayObject<T>*>( iterator->second ); + + if( arrayObj != NULL ) + { + arrayObj->setArray( buffer, numElements ); + } + else + { + XMP_Throw ( "Invalid identifier", kXMPErr_InternalFailure ); + } + } + else + { + // + // value doesn't exists yet and is not "empty" + // so add a new value to the map + // + arrayObj = new TArrayObject<T>( buffer, numElements ); + mValues[id] = arrayObj; + + mDirty = true; // the new created value isn't dirty, but the container becomes dirty + } + + // + // check if the value is "empty" + // + if( this->isEmptyValue( id, *arrayObj ) ) + { + // + // value is "empty", delete it + // + this->deleteValue( id ); + } +} + +template<class T> const T& IMetadata::getValue( XMP_Uns32 id ) const +{ + ValueMap::const_iterator iterator = mValues.find( id ); + + if( iterator != mValues.end() ) + { + // + // values exists, return value string + // + TValueObject<T>* valueObj = dynamic_cast<TValueObject<T>*>( iterator->second ); + + if( valueObj != NULL ) + { + return valueObj->getValue(); + } + else + { + XMP_Throw ( "Invalid identifier", kXMPErr_InternalFailure ); + } + } + else + { + // + // value for id doesn't exists, throw an exception + // + XMP_Throw ( "Invalid identifier", kXMPErr_InternalFailure ); + } +} + +template<class T> const T* const IMetadata::getArray( XMP_Uns32 id, XMP_Uns32& outSize ) const +{ + ValueMap::const_iterator iterator = mValues.find( id ); + + if( iterator != mValues.end() ) + { + // + // values exists, return value string + // + TArrayObject<T>* arrayObj = dynamic_cast<TArrayObject<T>*>( iterator->second ); + + if( arrayObj != NULL ) + { + return arrayObj->getArray( outSize ); + } + else + { + XMP_Throw ( "Invalid identifier", kXMPErr_InternalFailure ); + } + } + else + { + // + // value for id doesn't exists, throw an exception + // + XMP_Throw ( "Invalid identifier", kXMPErr_InternalFailure ); + } +} + +#endif diff --git a/XMPFiles/source/NativeMetadataSupport/IReconcile.cpp b/XMPFiles/source/NativeMetadataSupport/IReconcile.cpp new file mode 100644 index 0000000..dd34725 --- /dev/null +++ b/XMPFiles/source/NativeMetadataSupport/IReconcile.cpp @@ -0,0 +1,493 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "XMPFiles/source/NativeMetadataSupport/IReconcile.h" + +#include "XMPFiles/source/NativeMetadataSupport/MetadataSet.h" +#include "XMPFiles/source/NativeMetadataSupport/IMetadata.h" + +#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp" + +//----------------------------------------------------------------------------- +// +// WAVEReconcile::importNativeToXMP(...) +// +// Purpose: [static] Import native metadata container into XMP. +// This method is supposed to import all native metadata values +// that are listed in the property table from the IMetadata +// instance to the SXMPMeta instance. +// +//----------------------------------------------------------------------------- + +bool IReconcile::importNativeToXMP( SXMPMeta& outXMP, const IMetadata& nativeMeta, const MetadataPropertyInfo* propertyInfo, bool xmpPriority ) +{ + bool changed = false; + + std::string xmpValue; + XMP_Uns32 index = 0; + + do + { + if( propertyInfo[index].mXMPSchemaNS != NULL ) + { + bool xmpPropertyExists = false; + + // + // need to know if the XMP property exists either + // if a priority needs to be considered or if + // the value isn't set by a native value + // + switch( propertyInfo[index].mXMPType ) + { + case kXMPType_Simple: + { + xmpPropertyExists = outXMP.DoesPropertyExist( propertyInfo[index].mXMPSchemaNS, propertyInfo[index].mXMPPropName ); + } + break; + + case kXMPType_Localized: + { + std::string actualLang; + bool result = outXMP.GetLocalizedText( propertyInfo[index].mXMPSchemaNS, propertyInfo[index].mXMPPropName, "" , "x-default" , &actualLang, NULL, NULL ); + // If existing and the appropriate one + xmpPropertyExists = result && (actualLang == "x-default"); + } + break; + + case kXMPType_Array: + case kXMPType_OrderedArray: + { + xmpPropertyExists = outXMP.DoesArrayItemExist( propertyInfo[index].mXMPSchemaNS, propertyInfo[index].mXMPPropName, 1 ); + } + break; + + default: + { + XMP_Throw( "Unknown XMP data type", kXMPErr_InternalFailure ); + } + break; + } + + // + // import native property if there is no priority or + // the XMP property doesn't exist yet + // + if( ! propertyInfo[index].mConsiderPriority || ! xmpPriority || ! xmpPropertyExists ) + { + // + // if the native property exists + // + if( nativeMeta.valueExists( propertyInfo[index].mMetadataID ) ) + { + // + // prepare native property value for XMP + // depending on the native data type + // + xmpValue.erase(); + + switch( propertyInfo[index].mNativeType ) + { + case kNativeType_Str: + { + xmpValue = nativeMeta.getValue<std::string>( propertyInfo[index].mMetadataID ); + } + break; + + case kNativeType_StrASCII: + { + convertToASCII( nativeMeta.getValue<std::string>( propertyInfo[index].mMetadataID ), xmpValue ); + } + break; + + case kNativeType_StrLocal: + { + ReconcileUtils::NativeToUTF8( nativeMeta.getValue<std::string>( propertyInfo[index].mMetadataID ), xmpValue ); + } + break; + + case kNativeType_StrUTF8: + { + ReconcileUtils::NativeToUTF8( nativeMeta.getValue<std::string>( propertyInfo[index].mMetadataID ), xmpValue ); + } + break; + + case kNativeType_Uns64: + { + SXMPUtils::ConvertFromInt64( nativeMeta.getValue<XMP_Uns64>( propertyInfo[index].mMetadataID ), "%llu", &xmpValue ); + } + break; + + case kNativeType_Uns32: + { + SXMPUtils::ConvertFromInt( nativeMeta.getValue<XMP_Uns32>( propertyInfo[index].mMetadataID ), "%lu", &xmpValue ); + } + break; + + case kNativeType_Int32: + { + SXMPUtils::ConvertFromInt( nativeMeta.getValue<XMP_Int32>( propertyInfo[index].mMetadataID ), 0 /* default format */, &xmpValue ); + } + break; + + case kNativeType_Uns16: + { + SXMPUtils::ConvertFromInt( nativeMeta.getValue<XMP_Uns16>( propertyInfo[index].mMetadataID ), "%lu", &xmpValue ); + } + break; + + default: + { + XMP_Throw( "Unknown native data type", kXMPErr_InternalFailure ); + } + break; + }; + + if( ! xmpValue.empty() ) + { + // + // set the new XMP value depending on the + // XMP data type + // + switch( propertyInfo[index].mXMPType ) + { + case kXMPType_Localized: + { + outXMP.SetLocalizedText( propertyInfo[index].mXMPSchemaNS, propertyInfo[index].mXMPPropName, NULL, "x-default", xmpValue.c_str() ); + } + break; + + case kXMPType_Array: + { + // Overwrite any existing array + outXMP.DeleteProperty( propertyInfo[index].mXMPSchemaNS, propertyInfo[index].mXMPPropName ); + outXMP.AppendArrayItem( propertyInfo[index].mXMPSchemaNS, propertyInfo[index].mXMPPropName, kXMP_PropArrayIsUnordered, xmpValue.c_str(), kXMP_NoOptions ); + } + break; + + case kXMPType_OrderedArray: + { + // Overwrite any existing array + outXMP.DeleteProperty( propertyInfo[index].mXMPSchemaNS, propertyInfo[index].mXMPPropName ); + outXMP.AppendArrayItem( propertyInfo[index].mXMPSchemaNS, propertyInfo[index].mXMPPropName, kXMP_PropArrayIsOrdered, xmpValue.c_str(), kXMP_NoOptions ); + } + break; + + default: + { + outXMP.SetProperty( propertyInfo[index].mXMPSchemaNS, propertyInfo[index].mXMPPropName, xmpValue.c_str() ); + } + break; + } + + changed = true; + } + } + else if( propertyInfo[index].mDeleteXMPIfNoNative && xmpPropertyExists ) + { + // + // native value doesn't exists, delete the XMP property + // + outXMP.DeleteProperty( propertyInfo[index].mXMPSchemaNS, propertyInfo[index].mXMPPropName ); + changed = xmpPropertyExists; + } + } + } + + index++; + + } while( propertyInfo[index].mXMPSchemaNS != NULL ); + + return changed; +} + +//----------------------------------------------------------------------------- +// +// WAVEReconcile::exportXMPToNative(...) +// +// Purpose: [static] Export XMP values to native metadata container. +// This method is supposed to export all native metadata values +// that are listed in the property table from the XMP container +// to the IMetadata instance. +// +//----------------------------------------------------------------------------- + +bool IReconcile::exportXMPToNative( IMetadata& outNativeMeta, SXMPMeta& inXMP, const MetadataPropertyInfo* propertyInfo ) +{ + std::string xmpValue; + XMP_Uns32 index = 0; + + do + { + if( propertyInfo[index].mXMPSchemaNS != NULL && propertyInfo[index].mExportPolicy != kExport_Never ) + { + bool xmpPropertyExists = false; + + // + // get value from XMP depending on + // the XMP data type + // + switch( propertyInfo[index].mXMPType ) + { + case kXMPType_Localized: + { + std::string lang; + xmpPropertyExists = inXMP.GetLocalizedText( propertyInfo[index].mXMPSchemaNS, propertyInfo[index].mXMPPropName, NULL, "x-default", &lang, &xmpValue, 0 ); + } + break; + + case kXMPType_Array: + case kXMPType_OrderedArray: + { + // TOTRACK currently only the first array item is used + if( inXMP.CountArrayItems( propertyInfo[index].mXMPSchemaNS, propertyInfo[index].mXMPPropName ) > 0 ) + { + xmpPropertyExists = inXMP.GetArrayItem( propertyInfo[index].mXMPSchemaNS, propertyInfo[index].mXMPPropName, 1, &xmpValue, 0 ); + } + } + break; + + default: + { + xmpPropertyExists = inXMP.GetProperty( propertyInfo[index].mXMPSchemaNS, propertyInfo[index].mXMPPropName, &xmpValue, 0 ); + } + break; + } + + // + // convert xmp value and set native property + // depending on the native data type and the export policy + // + if( xmpPropertyExists && + ( propertyInfo[index].mExportPolicy != kExport_InjectOnly || + ! outNativeMeta.valueExists( propertyInfo[index].mMetadataID ) ) ) + { + + switch( propertyInfo[index].mNativeType ) + { + case kNativeType_StrASCII: + { + std::string ascii; + convertToASCII( xmpValue, ascii ); + outNativeMeta.setValue<std::string>( propertyInfo[index].mMetadataID, ascii ); + } + break; + + case kNativeType_Str: + case kNativeType_StrUTF8: + { + outNativeMeta.setValue<std::string>( propertyInfo[index].mMetadataID, xmpValue ); + } + break; + + case kNativeType_StrLocal: + { + std::string value; + + try + { + ReconcileUtils::UTF8ToLocal( xmpValue.c_str(), xmpValue.size(), &value ); + outNativeMeta.setValue<std::string>( propertyInfo[index].mMetadataID, value ); + } + catch( XMP_Error& e ) + { + if( e.GetID() != kXMPErr_Unavailable ) + { + // rethrow exception if it wasn't caused + // by missing encoding functionality (UNIX) + throw e; + } + } + } + break; + + case kNativeType_Uns64: + { + XMP_Int64 value = 0; + bool error = false; + + try + { + value = SXMPUtils::ConvertToInt64( xmpValue ); + } + catch( XMP_Error& e ) + { + if ( e.GetID() != kXMPErr_BadParam ) + { + // rethrow exception if it wasn't caused by an + // invalid parameter for the conversion + throw e; + } + error = true; + } + + // Only write the value if it could be converted to a number and has a positive value + if( ! error && value >= 0 ) + { + outNativeMeta.setValue<XMP_Uns64>( propertyInfo[index].mMetadataID, static_cast<XMP_Uns64>(value) ); + } + } + break; + + case kNativeType_Uns32: + { + XMP_Int32 value = 0; + bool error = false; + + try + { + value = SXMPUtils::ConvertToInt( xmpValue ); + } + catch( XMP_Error& e ) + { + if ( e.GetID() != kXMPErr_BadParam ) + { + // rethrow exception if it wasn't caused by an + // invalid parameter for the conversion + throw e; + } + error = true; + } + + // Only write the value if it could be converted to a number and has a positive value + if( ! error && value >= 0 ) + { + outNativeMeta.setValue<XMP_Uns32>( propertyInfo[index].mMetadataID, static_cast<XMP_Uns32>(value) ); + } + } + break; + + case kNativeType_Int32: + { + XMP_Int32 value = 0; + bool error = false; + + try + { + value = SXMPUtils::ConvertToInt( xmpValue ); + } + catch( XMP_Error& e ) + { + if ( e.GetID() != kXMPErr_BadParam ) + { + // rethrow exception if it wasn't caused by an + // invalid parameter for the conversion + throw e; + } + error = true; + } + + // Only write the value if it could be converted to a number + if( ! error ) + { + outNativeMeta.setValue<XMP_Int32>( propertyInfo[index].mMetadataID, static_cast<XMP_Int32>(value) ); + } + } + break; + + case kNativeType_Uns16: + { + XMP_Int32 value = 0; + bool error = false; + + try + { + value = SXMPUtils::ConvertToInt( xmpValue ); + } + catch( XMP_Error& e ) + { + if ( e.GetID() != kXMPErr_BadParam ) + { + // rethrow exception if it wasn't caused by an + // invalid parameter for the conversion + throw e; + } + error = true; + } + + // Only write the value if it could be converted to a number and has a positive value + if( ! error && value >= 0 ) + { + outNativeMeta.setValue<XMP_Uns16>( propertyInfo[index].mMetadataID, static_cast<XMP_Uns16>(value) ); + } + } + break; + + default: + { + XMP_Throw( "Unknown native data type", kXMPErr_InternalFailure ); + } + break; + } + } + else + { + if( propertyInfo[index].mExportPolicy == kExport_Always ) + { + // + // delete native value if the corresponding XMP value doesn't exists + // + outNativeMeta.deleteValue( propertyInfo[index].mMetadataID ); + } + } + } + + index++; + + } while( propertyInfo[index].mXMPSchemaNS != NULL ); + + return outNativeMeta.hasChanged(); +} + +//----------------------------------------------------------------------------- +// +// IReconcile::convertToASCII(...) +// +// Purpose: Converts input string to an ascii output string +// - terminates at first 0 +// - replaces all non ascii with 0x3F ('?') +// +//----------------------------------------------------------------------------- + +void IReconcile::convertToASCII( const std::string& input, std::string& output ) +{ + output.erase(); + output.reserve( input.length() ); + + bool isUTF8 = ReconcileUtils::IsUTF8( input.c_str(), input.length() ); + + XMP_StringPtr iter = input.c_str(); + + for ( XMP_Uns32 i=0; i < input.length(); i++ ) + { + XMP_Uns8 c = (XMP_Uns8) iter[i]; + if ( c == 0 ) // early 0 termination, leave. + break; + if ( c > 127 ) // uft-8 multi-byte sequence. + { + if ( isUTF8 ) // skip all high bytes + { + // how many bytes in this ? + if ( c >= 0xC2 && c <= 0xDF ) + i+=1; // 2-byte sequence + else if ( c >= 0xE0 && c <= 0xEF ) + i+=2; // 3-byte sequence + else if ( c >= 0xF0 && c <= 0xF4 ) + i+=3; // 4-byte sequence + else + continue; //invalid sequence, look for next 'low' byte .. + } // thereafter and 'else': just append a question mark: + output.append( 1, '?' ); + } + else // regular valid ascii. 1 byte. + { + output.append( 1, iter[i] ); + } + } +} // convertToASCII + diff --git a/XMPFiles/source/NativeMetadataSupport/IReconcile.h b/XMPFiles/source/NativeMetadataSupport/IReconcile.h new file mode 100644 index 0000000..af8646d --- /dev/null +++ b/XMPFiles/source/NativeMetadataSupport/IReconcile.h @@ -0,0 +1,139 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _IReconcile_h_ +#define _IReconcile_h_ + +#include <string> + +#ifndef TXMP_STRING_TYPE + #define TXMP_STRING_TYPE std::string +#endif + +#include "XMP.hpp" + +class MetadataSet; +class IMetadata; + +enum XMPPropertyType +{ + kXMPType_Simple, + // Structs can be treated as Simple if the whole path is given to the API + kXMPType_Localized, + // Unordered array (bag) + kXMPType_Array, + // Ordered array (seq) + kXMPType_OrderedArray +}; + +/** Types that describe how the native property is interpreted */ +enum MetadataPropertyType +{ + kNativeType_Str, // Take the value as is + kNativeType_StrASCII, // Treat it as ASCII, convert if necessary + kNativeType_StrUTF8, // Treat it as UTF-8, convert if necessary + kNativeType_StrLocal, // Use local encoding + kNativeType_Uns64, + kNativeType_Uns32, + kNativeType_Int32, + kNativeType_Uns16 +}; + +/** Types that describe how an XMP property is exported to native Metadata */ +enum ExportPolicy +{ + kExport_Never = 0, // Never export. + kExport_Always = 1, // Add, modify, or delete. + kExport_NoDelete = 2, // Add or modify, do not delete if no XMP. + kExport_InjectOnly = 3 // Add tag if new, never modify or delete existing values. +}; + +struct MetadataPropertyInfo +{ + XMP_StringPtr mXMPSchemaNS; + XMP_StringPtr mXMPPropName; + XMP_Uns32 mMetadataID; + MetadataPropertyType mNativeType; + XMPPropertyType mXMPType; + // If true, delete the XMP property if the native one does not exist on import + bool mDeleteXMPIfNoNative; + // If true, any existing XMP has higher priority on import + bool mConsiderPriority; + ExportPolicy mExportPolicy; +}; + +class IReconcile +{ +public: + virtual ~IReconcile() {}; + /** + * Reconciles metadata from legacy formats into XMP. + * + * @param outXMP the reconciliated XMP packet contains all XMP and legacy metadata, + * it is created and owned by the the caller. + * @param inMetaData contains all legacy containers that are relevant for the processed file format AND + * which actually contain data. + * If a container is not included in the set, it is omitted by the reconciliation method. + * Note: inMetaData#getXMP() and outXMP can be the same object + * @return XMP has been changed (true) or not (false) + * + * Throws exception if reconciliation is not possible. + */ + virtual XMP_Bool importToXMP( SXMPMeta& outXMP, const MetadataSet& inMetaData ) = 0; + + /** + * Dissolves metadata from the XMP object to legacy formats. + * + * @param outMetaData contains all legacy containers that are relevant for the processed file format + * (and the file handler is interested in). + * If a container is not included in the set, it is omitted by the dissolve method. + * Note: outMetaData#getXMP() and inXMP can be the same object + * @param inXMP the XMP packet that contains all XMP and legacy metadata, + * it is created and owned by the the caller. The legacy data is distributed into the legacy containers. + * @return legacy has been changed (true) or not (false) + * + * Throws exception if dissolving is not possible. + */ + virtual XMP_Bool exportFromXMP( MetadataSet& outMetaData, SXMPMeta& inXMP ) = 0; + +protected: + /** + Import native metadata container into XMP. + This method is supposed to import all native metadata values that are listed in the property table + from the IMetadata instance to the SXMPMeta instance. + + @param outXMP Target XMP container + @param nativeMeta Native metadata container + @param propertyInfo Property table that lists all values that are supposed to be imported + @param xmpPriority Pass true, if an existing XMP value has higher priority than the native metadata + + @return true if any XMP properties were changed + */ + static bool importNativeToXMP( SXMPMeta& outXMP, const IMetadata& nativeMeta, const MetadataPropertyInfo* propertyInfo, bool xmpPriority ); + + /** + Export XMP values to native metadata container. + This method is supposed to export all native metadata values that are listed in the property table + from the XMP container to the IMetadata instance. + + @param outNativeMeta Target native metadata container + @param inXMP XMP container + @param propertyInfo Property table that lists all values that are supposed to be exported + + @return true if any native metadata value were changed + */ + static bool exportXMPToNative( IMetadata& outNativeMeta, SXMPMeta& inXMP, const MetadataPropertyInfo* propertyInfo ); + + // Converts input string to an ascii output string + // - terminates at first 0 + // - replaces all non ascii with 0x3F ('?') + static void convertToASCII( const std::string& input, std::string& output ); +}; + +#endif diff --git a/XMPFiles/source/NativeMetadataSupport/MetadataSet.cpp b/XMPFiles/source/NativeMetadataSupport/MetadataSet.cpp new file mode 100644 index 0000000..f6389f4 --- /dev/null +++ b/XMPFiles/source/NativeMetadataSupport/MetadataSet.cpp @@ -0,0 +1,127 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "XMPFiles/source/NativeMetadataSupport/MetadataSet.h" +#include "source/XMP_LibUtils.hpp" + +//----------------------------------------------------------------------------- +// +// MetadataSet::MetadataSet(...) +// +// Purpose: ctor/dtor +// +//----------------------------------------------------------------------------- + +MetadataSet::MetadataSet() +: mMeta ( NULL ) +{ +} + +MetadataSet::~MetadataSet() +{ + delete mMeta; +} + +//----------------------------------------------------------------------------- +// +// MetadataSet::append(...) +// +// Purpose: Append metadata container +// +//----------------------------------------------------------------------------- + +void MetadataSet::append( IMetadata* meta ) +{ + if( mMeta == NULL ) + { + mMeta = new std::vector<IMetadata*>; + } + + mMeta->push_back( meta ); +} + +//----------------------------------------------------------------------------- +// +// MetadataSet::removeAt(...) +// +// Purpose: Remove metadata container at passed position +// +//----------------------------------------------------------------------------- + +void MetadataSet::removeAt( XMP_Uns32 pos ) +{ + if( mMeta != NULL && pos < mMeta->size() ) + { + mMeta->erase( mMeta->begin() + pos ); + } + else + { + XMP_Throw( "Index out of range.", kXMPErr_BadIndex ); + } +} + +//----------------------------------------------------------------------------- +// +// MetadataSet::remove(...) +// +// Purpose: Remove the last metadata container inside the vector +// +//----------------------------------------------------------------------------- + +void MetadataSet::remove() +{ + if( mMeta != NULL ) + { + mMeta->pop_back(); + } +} + +//----------------------------------------------------------------------------- +// +// MetadataSet::length(...) +// +// Purpose: Return the number of stored metadata container +// +//----------------------------------------------------------------------------- + +XMP_Uns32 MetadataSet::length() const +{ + XMP_Uns32 ret = 0; + + if( mMeta != NULL ) + { + ret = (XMP_Uns32)mMeta->size(); + } + + return ret; +} + +//----------------------------------------------------------------------------- +// +// MetadataSet::getAt(...) +// +// Purpose: Return metadata container of passed position. +// +//----------------------------------------------------------------------------- + +IMetadata* MetadataSet::getAt( XMP_Uns32 pos ) const +{ + IMetadata* ret = NULL; + + if( mMeta != NULL && pos < mMeta->size() ) + { + ret = mMeta->at(pos); + } + else + { + XMP_Throw( "Index out of range.", kXMPErr_BadIndex ); + } + + return ret; +} diff --git a/XMPFiles/source/NativeMetadataSupport/MetadataSet.h b/XMPFiles/source/NativeMetadataSupport/MetadataSet.h new file mode 100644 index 0000000..62bb8b5 --- /dev/null +++ b/XMPFiles/source/NativeMetadataSupport/MetadataSet.h @@ -0,0 +1,100 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _MetadataSet_h_ +#define _MetadataSet_h_ + +#include "public/include/XMP_Environment.h" // ! This must be the first include. +#include "public/include/XMP_Const.h" +#include <vector> + +/** + The class MetadataSet acts as a container for any metadata that base on + the interface IMeta. +*/ + +class IMetadata; + +class MetadataSet +{ + public: + /** + ctor/dtor + */ + MetadataSet (); + ~MetadataSet (); + + /** + Append metadata container + + @param meta Metadata container + */ + void append ( IMetadata* meta ); + + /** + Remove metadata container at passed position + + @param pos Position inside the vector of metadata + */ + void removeAt ( XMP_Uns32 pos ); + + /** + Remove the last metadata container inside the vector + */ + void remove (); + + /** + Return the number of stored metadata container + + @return length of vector + */ + XMP_Uns32 length () const; + + /** + Return metadata container of passed position. + + @param pos Position inside of the vector of metadata + */ + IMetadata* getAt ( XMP_Uns32 pos ) const; + + /** + Return metadata container of passed type + + @return stored metadata container of type T + */ + template<class T> T* get () const; + + private: + std::vector<IMetadata*>* mMeta; + typedef std::vector<IMetadata*>::iterator MetaIterator; + +}; // MetadataSet + +template<class T> T* MetadataSet::get() const +{ + T* ret = NULL; + + if( mMeta != NULL ) + { + for( MetaIterator iter = mMeta->begin(); iter != mMeta->end(); iter++ ) + { + T* tmp = dynamic_cast<T*>( *iter ); + + if( tmp != NULL ) + { + ret = tmp; + break; + } + } + } + + return ret; +} + +#endif diff --git a/XMPFiles/source/NativeMetadataSupport/ValueObject.h b/XMPFiles/source/NativeMetadataSupport/ValueObject.h new file mode 100644 index 0000000..cc8b444 --- /dev/null +++ b/XMPFiles/source/NativeMetadataSupport/ValueObject.h @@ -0,0 +1,143 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2010 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _ValueObject_h_ +#define _ValueObject_h_ + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. +#include "public/include/XMP_Const.h" + +/** + * The class ValueObject acts as a base class for the two generic classes + * TValueObject and TValueArray. + * The overall purpose of these classes is to store a single value (or array) + * of any data types and a modification state. The modification state is + * always set if the existing value has changed. It is not set if a new value + * is set that is equal to the existing value. + * If a ValueObject based instance is created or a new value is passed to an + * instance the actual value gets copied. + * + * So there're some requirements of possible data types: + * - data type needs to provide a relational operator + * - data type needs to provide an assign operator + */ + +/** + * The class ValueObject is the base class for the following generic classes. + * It provids common functionality for handle the modification state. + * This class can't be used directly! + */ +class ValueObject +{ +public: + virtual ~ValueObject() {} + + inline bool hasChanged() const { return mDirty; } + inline void resetChanged() { mDirty = false; } + +protected: + ValueObject() : mDirty(false) {} + +protected: + bool mDirty; +}; + +/** + * The generic class TValueObject is supposed to store values of any data types. + * It is based on the class ValueObject and support modification state functionality. + * See above for the requirements of possible data types. + */ +template <class T> class TValueObject : public ValueObject +{ +public: + TValueObject( const T& value ) : mValue(value) {} + ~TValueObject() {} + + inline const T& getValue() const { return mValue; } + inline void setValue( const T& value ) { mDirty = !( mValue == value ); mValue = value; } + +private: + T mValue; +}; + +/** + * The generic class TArrayObject is supposed to store arrays of any data types. + * It is based on the class ValueObject and supports modification state functionality. + * See above for the requirements of possible data types. + */ +template <class T> class TArrayObject : public ValueObject +{ +public: + TArrayObject( const T* buffer, XMP_Uns32 bufferSize ); + ~TArrayObject(); + + inline const T* const getArray( XMP_Uns32& outSize ) const { outSize = mSize; return mArray; } + void setArray( const T* buffer, XMP_Uns32 numElements); + +private: + T* mArray; + XMP_Uns32 mSize; +}; + +template<class T> inline TArrayObject<T>::TArrayObject( const T* buffer, XMP_Uns32 bufferSize ) +: mArray(NULL), mSize(0) +{ + this->setArray( buffer, bufferSize ); + mDirty = false; +} + +template<class T> inline TArrayObject<T>::~TArrayObject() +{ + if( mArray != NULL ) + { + delete[] mArray; + } +} + +template<class T> inline void TArrayObject<T>::setArray( const T* buffer, XMP_Uns32 numElements ) +{ + if( buffer != NULL && numElements > 0 ) + { + bool doSet = true; + + if( mArray != NULL && mSize == numElements ) + { + doSet = ( memcmp( mArray, buffer, numElements*sizeof(T) ) != 0 ); + } + + if( doSet ) + { + if( mArray != NULL ) + { + delete[] mArray; + } + + mArray = new T[numElements]; + mSize = numElements; + + memcpy( mArray, buffer, numElements*sizeof(T) ); + + mDirty = true; + } + } + else + { + mDirty = ( mArray != NULL ); + + if( mArray != NULL ) + { + delete[] mArray; + } + + mArray = NULL; + mSize = 0; + } +} + +#endif diff --git a/XMPFiles/source/PluginHandler/FileHandler.h b/XMPFiles/source/PluginHandler/FileHandler.h new file mode 100644 index 0000000..16e4c6b --- /dev/null +++ b/XMPFiles/source/PluginHandler/FileHandler.h @@ -0,0 +1,101 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef PLUGINHANDLER_H +#define PLUGINHANDLER_H +#include "Module.h" + +namespace XMP_PLUGIN +{ + +/** @struct CheckFormat + * @brief Record of byte sequences. + * + * It is a static information of the file handler provided in the resource file, if the format + * can be identified by one or more sequences of fixed bytes at a fixed location within the format. + */ +struct CheckFormat +{ + XMP_Int64 mOffset; + XMP_Uns32 mLength; + std::string mByteSeq; + + CheckFormat(): mOffset( 0 ), mLength( 0 ) { } + + inline void clear() + { + mOffset = 0; mLength = 0; mByteSeq.clear(); + } + + inline bool empty() + { + return ( mLength == 0 || mByteSeq.empty() ); + } +}; + +/** @class FileHandler + * @brief File handler exposed through plugin architecture. + * + * At the initialization time of XMPFiles only static information from all the avalbale plugins + * is populated by creating instance of this class FileHandler. It's loaded later when it's actually + * required to get information from the format. + */ +class FileHandler +{ +public: + + FileHandler(std::string & uid, XMP_OptionBits handlerFlags, FileHandlerType type, ModuleSharedPtr module): + mVersion(0), mUID(uid), mHandlerFlags(handlerFlags), mType(type), mModule(module), mOverwrite(false) {} + + virtual ~FileHandler(){} + + inline XMP_Int64 getVersion() const { return mVersion; } + inline void setVersion( XMP_Uns32 version ) { mVersion = version; } + + inline const std::string& getUID() const { return mUID; } + inline XMP_OptionBits getHandlerFlags() const { return mHandlerFlags; } + inline void setHandlerFlags( XMP_OptionBits flags ) { mHandlerFlags = flags; } + + inline XMP_OptionBits getSerializeOption() const { return mSerializeOption; } + inline void setSerializeOption( XMP_OptionBits option ) { mSerializeOption = option; } + + inline bool getOverwriteHandler() const { return mOverwrite; } + inline void setOverwriteHandler( bool overwrite ) { mOverwrite = overwrite; } + + inline FileHandlerType getHandlerType() const { return mType; } + inline void setHandlerType( FileHandlerType type ) { mType = type; } + + inline bool load() { return mModule->load(); } + inline ModuleSharedPtr getModule() const { return mModule; } + + inline void addCheckFormat( const CheckFormat & checkFormat ) { mCheckFormatVec.push_back( checkFormat ); } + inline XMP_Uns32 getCheckFormatSize() const { return (XMP_Uns32) mCheckFormatVec.size(); } + inline CheckFormat getCheckFormat( XMP_Uns32 index ) const + { + CheckFormat checkFormat; + if( index < mCheckFormatVec.size() ) + checkFormat = mCheckFormatVec[index]; + return checkFormat; + } + +private: + typedef std::vector<CheckFormat> CheckFormatVec; + + CheckFormatVec mCheckFormatVec; + XMP_Uns32 mVersion; + std::string mUID; + XMP_OptionBits mHandlerFlags; + XMP_OptionBits mSerializeOption; + bool mOverwrite; + FileHandlerType mType; + ModuleSharedPtr mModule; +}; + +} //namespace XMP_PLUGIN +#endif //PLUGINHANDLER_H diff --git a/XMPFiles/source/PluginHandler/FileHandlerInstance.cpp b/XMPFiles/source/PluginHandler/FileHandlerInstance.cpp new file mode 100644 index 0000000..42de1a4 --- /dev/null +++ b/XMPFiles/source/PluginHandler/FileHandlerInstance.cpp @@ -0,0 +1,106 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "FileHandlerInstance.h" + +namespace XMP_PLUGIN +{ + +FileHandlerInstance::FileHandlerInstance ( SessionRef object, FileHandlerSharedPtr handler, XMPFiles * _parent ): +XMPFileHandler( _parent ), mObject( object ), mHandler( handler ) +{ + this->handlerFlags = mHandler->getHandlerFlags(); + this->stdCharForm = kXMP_Char8Bit; + PluginManager::addHandlerInstance( this->mObject, this ); +} + +FileHandlerInstance::~FileHandlerInstance() +{ + WXMP_Error error; + mHandler->getModule()->getPluginAPIs()->mTerminateSessionProc( this->mObject, &error ); + PluginManager::removeHandlerInstance( this->mObject ); + CheckError( error ); +} + +bool FileHandlerInstance::GetFileModDate ( XMP_DateTime * modDate ) +{ + bool ok; + WXMP_Error error; + GetSessionFileModDateProc wGetFileModDate = mHandler->getModule()->getPluginAPIs()->mGetFileModDateProc; + wGetFileModDate ( this->mObject, &ok, modDate, &error ); + CheckError ( error ); + return ok; +} + +void FileHandlerInstance::CacheFileData() +{ + if( this->containsXMP ) return; + + WXMP_Error error; + XMP_StringPtr xmpStr = NULL; + mHandler->getModule()->getPluginAPIs()->mCacheFileDataProc( this->mObject, this->parent->ioRef, &xmpStr, &error ); + + if( error.mErrorID != kXMPErr_NoError ) + { + if ( xmpStr != 0 ) free( (void*) xmpStr ); + throw XMP_Error( kXMPErr_InternalFailure, error.mErrorMsg ); + } + + if( xmpStr != NULL ) + { + this->xmpPacket.assign( xmpStr ); + free( (void*) xmpStr ); // It should be freed as documentation of mCacheFileDataProc says so. + } + this->containsXMP = true; +} + +void FileHandlerInstance::ProcessXMP() +{ + if( !this->containsXMP || this->processedXMP ) return; + this->processedXMP = true; + + SXMPUtils::RemoveProperties ( &this->xmpObj, 0, 0, kXMPUtil_DoAllProperties ); + this->xmpObj.ParseFromBuffer ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + + WXMP_Error error; + if( mHandler->getModule()->getPluginAPIs()->mImportToXMPProc ) + mHandler->getModule()->getPluginAPIs()->mImportToXMPProc( this->mObject, this->xmpObj.GetInternalRef(), &error ); + CheckError( error ); +} + +void FileHandlerInstance::UpdateFile ( bool doSafeUpdate ) +{ + if ( !this->needsUpdate || this->xmpPacket.size() == 0 ) return; + + WXMP_Error error; + if( mHandler->getModule()->getPluginAPIs()->mExportFromXMPProc ) + mHandler->getModule()->getPluginAPIs()->mExportFromXMPProc( this->mObject, this->xmpObj.GetInternalRef(), &error ); + CheckError( error ); + + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, mHandler->getSerializeOption() ); + + mHandler->getModule()->getPluginAPIs()->mUpdateFileProc( this->mObject, this->parent->ioRef, doSafeUpdate, this->xmpPacket.c_str(), &error ); + CheckError( error ); + this->needsUpdate = false; +} + +void FileHandlerInstance::WriteTempFile( XMP_IO* tempRef ) +{ + WXMP_Error error; + if( mHandler->getModule()->getPluginAPIs()->mExportFromXMPProc ) + mHandler->getModule()->getPluginAPIs()->mExportFromXMPProc( this->mObject, this->xmpObj.GetInternalRef(), &error ); + CheckError( error ); + + this->xmpObj.SerializeToBuffer ( &this->xmpPacket, mHandler->getSerializeOption() ); + + mHandler->getModule()->getPluginAPIs()->mWriteTempFileProc( this->mObject, this->parent->ioRef, tempRef, this->xmpPacket.c_str(), &error ); + CheckError( error ); +} + +} //namespace XMP_PLUGIN diff --git a/XMPFiles/source/PluginHandler/FileHandlerInstance.h b/XMPFiles/source/PluginHandler/FileHandlerInstance.h new file mode 100644 index 0000000..5cf2420 --- /dev/null +++ b/XMPFiles/source/PluginHandler/FileHandlerInstance.h @@ -0,0 +1,47 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef PLUGINHANDLERINSTANCE_H +#define PLUGINHANDLERINSTANCE_H +#include "FileHandler.h" + +namespace XMP_PLUGIN +{ + +/** @class FileHandlerInstance + * @brief This class is equivalent to the native file handlers like JPEG_MetaHandler or the simialr one. + * + * This class is equivalent to the native file handler. This class is supposed to support all the + * function which a native file handler support. + * As of now, it support only the functions required for OwningFileHandler. + */ +class FileHandlerInstance : public XMPFileHandler +{ +public: + FileHandlerInstance ( SessionRef object, FileHandlerSharedPtr handler, XMPFiles * parent ); + virtual ~FileHandlerInstance(); + + virtual bool GetFileModDate ( XMP_DateTime * modDate ); + + virtual void CacheFileData(); + virtual void ProcessXMP(); + //virtual XMP_OptionBits GetSerializeOptions(); //It should not be needed as its required only inside updateFile. + virtual void UpdateFile ( bool doSafeUpdate ); + virtual void WriteTempFile ( XMP_IO* tempRef ); + + inline SessionRef GetSession() const { return mObject; } + inline FileHandlerSharedPtr GetHandlerInfo() const { return mHandler; } + +private: + SessionRef mObject; + FileHandlerSharedPtr mHandler; +}; + +} //namespace XMP_PLUGIN +#endif diff --git a/XMPFiles/source/PluginHandler/HostAPIImpl.cpp b/XMPFiles/source/PluginHandler/HostAPIImpl.cpp new file mode 100644 index 0000000..04dcdd0 --- /dev/null +++ b/XMPFiles/source/PluginHandler/HostAPIImpl.cpp @@ -0,0 +1,637 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "HostAPI.h" +#include "PluginManager.h" +#include "FileHandlerInstance.h" +#include "source/XIO.hpp" +#include "XMPFiles/source/HandlerRegistry.h" + +using namespace Common; + +namespace XMP_PLUGIN +{ + +/////////////////////////////////////////////////////////////////////////////// +// +// Exception handler +// + +static void HandleException( WXMP_Error* wError ) +{ + try + { + throw; + } + catch( XMP_Error& xmpErr ) + { + wError->mErrorMsg = xmpErr.GetErrMsg(); + wError->mErrorID = xmpErr.GetID(); + } + catch( std::exception& stdErr ) + { + wError->mErrorMsg = stdErr.what(); + } + catch( ... ) + { + wError->mErrorMsg = "Caught unknown exception"; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// +// FileIO_API +// + +static XMPErrorID FileSysRead( XMP_IORef io, void* buffer, XMP_Uns32 count, bool readAll, XMP_Uns32& byteRead, WXMP_Error * wError ) +{ + if( wError == NULL ) return kXMPErr_BadParam; + + wError->mErrorID = kXMPErr_InternalFailure; + + try + { + if( io != NULL ) + { + ::XMP_IO * thiz = (::XMP_IO*)io; + byteRead = thiz->Read( buffer, count, readAll ); + wError->mErrorID = kXMPErr_NoError; + } + } + catch( ... ) + { + HandleException( wError ); + } + + return wError->mErrorID; +} + +static XMPErrorID FileSysWrite( XMP_IORef io, void* buffer, XMP_Uns32 count, WXMP_Error * wError ) +{ + if( wError == NULL ) return kXMPErr_BadParam; + + wError->mErrorID = kXMPErr_InternalFailure; + + try + { + if( io != NULL ) + { + ::XMP_IO * thiz = (::XMP_IO*)io; + thiz->Write( buffer, count ); + wError->mErrorID = kXMPErr_NoError; + } + } + catch( ... ) + { + HandleException( wError ); + } + + return wError->mErrorID; +} + +static XMPErrorID FileSysSeek( XMP_IORef io, XMP_Int64& offset, SeekMode mode, WXMP_Error * wError ) +{ + if( wError == NULL ) return kXMPErr_BadParam; + + wError->mErrorID = kXMPErr_InternalFailure; + + try + { + if( io != NULL ) + { + ::XMP_IO * thiz = (::XMP_IO*)io; + thiz->Seek( offset, mode ); + wError->mErrorID = kXMPErr_NoError; + } + } + catch( ... ) + { + HandleException( wError ); + } + + return wError->mErrorID; +} + +static XMPErrorID FileSysLength( XMP_IORef io, XMP_Int64& length, WXMP_Error * wError ) +{ + if( wError == NULL ) return kXMPErr_BadParam; + + wError->mErrorID = kXMPErr_InternalFailure; + + try + { + if( io != NULL ) + { + ::XMP_IO * thiz = (::XMP_IO*)io; + length = thiz->Length(); + wError->mErrorID = kXMPErr_NoError; + } + } + catch( ... ) + { + HandleException( wError ); + } + + return wError->mErrorID; +} + +static XMPErrorID FileSysTruncate( XMP_IORef io, XMP_Int64 length, WXMP_Error * wError ) +{ + if( wError == NULL ) return kXMPErr_BadParam; + + wError->mErrorID = kXMPErr_InternalFailure; + + try + { + if( io != NULL ) + { + ::XMP_IO * thiz = (::XMP_IO*)io; + thiz->Truncate( length ); + wError->mErrorID = kXMPErr_NoError; + } + } + catch( ... ) + { + HandleException( wError ); + } + + return wError->mErrorID; +} + +static XMPErrorID FileSysDeriveTemp( XMP_IORef io, XMP_IORef& tempIO, WXMP_Error * wError ) +{ + if( wError == NULL ) return kXMPErr_BadParam; + + wError->mErrorID = kXMPErr_InternalFailure; + + try + { + if( io != NULL ) + { + ::XMP_IO * thiz = (::XMP_IO*)io; + tempIO = thiz->DeriveTemp(); + wError->mErrorID = kXMPErr_NoError; + } + } + catch( ... ) + { + HandleException( wError ); + } + + return wError->mErrorID; +} + +static XMPErrorID FileSysAbsorbTemp( XMP_IORef io, WXMP_Error * wError ) +{ + if( wError == NULL ) return kXMPErr_BadParam; + + wError->mErrorID = kXMPErr_InternalFailure; + + try + { + if( io != NULL ) + { + ::XMP_IO * thiz = (::XMP_IO*)io; + thiz->AbsorbTemp(); + wError->mErrorID = kXMPErr_NoError; + } + } + catch( ... ) + { + HandleException( wError ); + } + + return wError->mErrorID; +} + +static XMPErrorID FileSysDeleteTemp( XMP_IORef io, WXMP_Error * wError ) +{ + if( wError == NULL ) return kXMPErr_BadParam; + + wError->mErrorID = kXMPErr_InternalFailure; + + try + { + if( io != NULL ) + { + ::XMP_IO * thiz = (::XMP_IO*)io; + thiz->DeleteTemp(); + wError->mErrorID = kXMPErr_NoError; + } + } + catch( ... ) + { + HandleException( wError ); + } + + return wError->mErrorID; +} + +static void GetFileSysAPI( FileIO_API* fileSys ) +{ + if( fileSys ) + { + fileSys->mReadProc = FileSysRead; + fileSys->mWriteProc = FileSysWrite; + fileSys->mSeekProc = FileSysSeek; + fileSys->mLengthProc = FileSysLength; + fileSys->mTruncateProc = FileSysTruncate; + fileSys->mDeriveTempProc = FileSysDeriveTemp; + fileSys->mAbsorbTempProc = FileSysAbsorbTemp; + fileSys->mDeleteTempProc = FileSysDeleteTemp; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// +// String_API +// + +static XMPErrorID CreateBuffer( StringPtr* buffer, XMP_Uns32 size, WXMP_Error * wError ) +{ + if( wError == NULL ) return kXMPErr_BadParam; + + wError->mErrorID = kXMPErr_InternalFailure; + + try + { + if( buffer != NULL ) + { + *buffer = (StringPtr) malloc( size ); + if( *buffer != NULL ) + { + wError->mErrorID = kXMPErr_NoError; + } + else + { + wError->mErrorMsg = "Allocation failed"; + } + } + } + catch( ... ) + { + HandleException( wError ); + } + + return wError->mErrorID; +} + +static XMPErrorID ReleaseBuffer( StringPtr buffer, WXMP_Error * wError ) +{ + if( wError == NULL ) return kXMPErr_BadParam; + + wError->mErrorID = kXMPErr_InternalFailure; + + try + { + if( buffer ) + { + free( buffer ); + wError->mErrorID = kXMPErr_NoError; + } + } + catch( ... ) + { + HandleException( wError ); + } + + return wError->mErrorID; +} + +static void GetStringAPI( String_API* strAPI ) +{ + if( strAPI ) + { + strAPI->mCreateBufferProc = CreateBuffer; + strAPI->mReleaseBufferProc = ReleaseBuffer; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// +// Abort_API +// + +static XMPErrorID CheckAbort( SessionRef session, bool* aborted, WXMP_Error* wError ) +{ + if( wError == NULL ) return kXMPErr_BadParam; + + wError->mErrorID = kXMPErr_InternalFailure; + + if( aborted ) + { + *aborted = false; + + // + // find FileHandlerInstance associated to session reference + // + FileHandlerInstancePtr instance = PluginManager::getHandlerInstance( session ); + + if( instance != NULL ) + { + // + // execute abort procedure if available + // + wError->mErrorID = kXMPErr_NoError; + XMP_AbortProc proc = instance->parent->abortProc; + void* arg = instance->parent->abortArg; + + if( proc ) + { + try + { + *aborted = proc( arg ); + } + catch( ... ) + { + HandleException( wError ); + } + } + } + } + else + { + wError->mErrorMsg = "Invalid parameter"; + } + + return wError->mErrorID; +} + +static void GetAbortAPI( Abort_API* abortAPI ) +{ + if( abortAPI ) + { + abortAPI->mCheckAbort = CheckAbort; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// +// StandardHandler_API +// + +static XMPErrorID CheckFormatStandardHandler( SessionRef session, XMP_FileFormat format, StringPtr path, bool& checkOK, WXMP_Error* wError ) +{ + if( wError == NULL ) return kXMPErr_BadParam; + + wError->mErrorID = kXMPErr_InternalFailure; + wError->mErrorMsg = NULL; + checkOK = false; + + // + // find FileHandlerInstance associated to session reference + // + FileHandlerInstancePtr instance = PluginManager::getHandlerInstance( session ); + + if( instance != NULL && PluginManager::getHandlerPriority( instance ) == PluginManager::kReplacementHandler ) + { + // + // find previous file handler for file format identifier + // + XMPFileHandlerInfo* hdlInfo = HandlerRegistry::getInstance().getStandardHandlerInfo( format ); + + if( hdlInfo != NULL && HandlerRegistry::getInstance().isReplaced( format ) ) + { + if( hdlInfo->checkProc != NULL ) + { + try + { + // + // setup temporary XMPFiles instance + // + XMPFiles standardClient; + standardClient.format = format; + standardClient.filePath = std::string( path ); + + if( hdlInfo->flags & kXMPFiles_FolderBasedFormat ) + { + // + // process folder based handler + // + if( path != NULL ) + { + // The following code corresponds to the one found in HandlerRegistry::selectSmartHandler, + // in the folder based handler selection section of the function + // In this case the format is already known, therefor no folder checks are needed here, + // but the path must be split into the meaningful parts to call checkFolderFormat correctly + + std::string rootPath = path; + std::string leafName; + std::string fileExt; + std::string gpName; + std::string parentName; + + XIO::SplitLeafName ( &rootPath, &leafName ); + + if( !leafName.empty() ) + { + size_t extPos = leafName.size(); + + for ( --extPos; extPos > 0; --extPos ) if ( leafName[extPos] == '.' ) break; + + if( leafName[extPos] == '.' ) + { + fileExt.assign( &leafName[extPos+1] ); + MakeLowerCase( &fileExt ); + leafName.erase( extPos ); + } + + CheckFolderFormatProc CheckProc = (CheckFolderFormatProc) (hdlInfo->checkProc); + + // The case that a logical path to a clip has been passed, which does not point to a real file + if( Host_IO::GetFileMode( path ) == Host_IO::kFMode_DoesNotExist ) + { + checkOK = CheckProc( hdlInfo->format, rootPath, gpName, parentName, leafName, &standardClient ); + } + else + { + XIO::SplitLeafName( &rootPath, &parentName ); + XIO::SplitLeafName( &rootPath, &gpName ); + std::string origGPName ( gpName ); // ! Save the original case for XDCAM-FAM. + MakeUpperCase( &parentName ); + MakeUpperCase( &gpName ); + + if( format == kXMP_XDCAM_FAMFile && ( (parentName == "CLIP") || (parentName == "EDIT") || (parentName == "SUB") ) ) + { + // ! The standard says Clip/Edit/Sub, but we just shifted to upper case. + gpName = origGPName; // ! XDCAM-FAM has just 1 level of inner folder, preserve the "MyMovie" case. + } + + checkOK = CheckProc( hdlInfo->format, rootPath, gpName, parentName, leafName, &standardClient ); + } + } + else + { + wError->mErrorID = kXMPErr_BadParam; + } + } + else + { + wError->mErrorID = kXMPErr_BadParam; + } + } + else + { + // + // file based handler (requires XMP_IO object) + // + CheckFileFormatProc CheckProc = (CheckFileFormatProc) (hdlInfo->checkProc); + XMPFiles_IO* io = XMPFiles_IO::New_XMPFiles_IO ( path, true ); + checkOK = CheckProc( hdlInfo->format, path, io, &standardClient ); + delete io; + } + + wError->mErrorID = kXMPErr_NoError; + } + catch( ... ) + { + HandleException( wError ); + } + } + } + else + { + wError->mErrorMsg = "No standard handler available"; + } + } + else + { + wError->mErrorMsg = "Standard file handler can't call prior handler"; + } + + return wError->mErrorID; +} + +static XMPErrorID GetXMPStandardHandler( SessionRef session, XMP_FileFormat format, StringPtr path, XMPMetaRef xmpRef, bool* xmpExists, WXMP_Error* wError ) +{ + if( wError == NULL ) return kXMPErr_BadParam; + + wError->mErrorID = kXMPErr_InternalFailure; + wError->mErrorMsg = NULL; + + // + // find FileHandlerInstance associated to session reference + // + FileHandlerInstancePtr instance = PluginManager::getHandlerInstance( session ); + + if( instance != NULL && PluginManager::getHandlerPriority( instance ) == PluginManager::kReplacementHandler ) + { + // + // find previous file handler for file format identifier + // + XMPFileHandlerInfo* hdlInfo = HandlerRegistry::getInstance().getStandardHandlerInfo( format ); + + if( hdlInfo != NULL && HandlerRegistry::getInstance().isReplaced( format ) ) + { + // + // check format first + // + bool suc = false; + + XMPErrorID errorID = CheckFormatStandardHandler( session, format, path, suc, wError ); + + if( errorID == kXMPErr_NoError && suc ) + { + // + // setup temporary XMPFiles instance + // + XMPFiles standardClient; + standardClient.format = format; + standardClient.filePath = std::string( path ); + + SXMPMeta meta( xmpRef ); + + try + { + // + // open with passed handler info + // + suc = standardClient.OpenFile( *hdlInfo, path, kXMPFiles_OpenForRead ); + + if( suc ) + { + // + // read meta data + // + suc = standardClient.GetXMP( &meta ); + + if( xmpExists != NULL ) *xmpExists = suc; + } + } + catch( ... ) + { + HandleException( wError ); + } + + // + // close and cleanup + // + try + { + standardClient.CloseFile(); + } + catch( ... ) + { + HandleException( wError ); + } + } + else if( errorID == kXMPErr_NoError ) + { + wError->mErrorID = kXMPErr_BadFileFormat; + wError->mErrorMsg = "Standard handler can't process file format"; + } + } + else + { + wError->mErrorID = kXMPErr_NoFileHandler; + wError->mErrorMsg = "No standard handler available"; + } + } + else + { + wError->mErrorMsg = "Standard file handler can't call prior handler"; + } + + return wError->mErrorID; +} + +static void GetStandardHandlerAPI( StandardHandler_API* standardHandlerAPI ) +{ + if( standardHandlerAPI ) + { + standardHandlerAPI->mCheckFormatStandardHandler = CheckFormatStandardHandler; + standardHandlerAPI->mGetXMPStandardHandler = GetXMPStandardHandler; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// +// Init/Term Host APIs +// + +void PluginManager::SetupHostAPI_V1( HostAPIRef hostAPI ) +{ + // Get XMP_IO APIs + hostAPI->mFileIOAPI = new FileIO_API(); + GetFileSysAPI( hostAPI->mFileIOAPI ); + + // Get String APIs + hostAPI->mStrAPI = new String_API(); + GetStringAPI( hostAPI->mStrAPI ); + + // Get Abort API + hostAPI->mAbortAPI = new Abort_API(); + GetAbortAPI( hostAPI->mAbortAPI ); + + // Get standard handler APIs + hostAPI->mStandardHandlerAPI = new StandardHandler_API(); + GetStandardHandlerAPI( hostAPI->mStandardHandlerAPI ); +} + +} //namespace XMP_PLUGIN diff --git a/XMPFiles/source/PluginHandler/Module.cpp b/XMPFiles/source/PluginHandler/Module.cpp new file mode 100644 index 0000000..91b8ea8 --- /dev/null +++ b/XMPFiles/source/PluginHandler/Module.cpp @@ -0,0 +1,169 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "Module.h" + +namespace XMP_PLUGIN +{ + +PluginAPIRef Module::getPluginAPIs() +{ + // + // return ref. to Plugin API, load module if not yet loaded + // + if( !mPluginAPIs && !load() ) + { + XMP_Throw ( "Plugin API not available.", kXMPErr_Unavailable ); + } + + return mPluginAPIs; +} + +bool Module::load() +{ + if( mLoaded == kModuleNotLoaded ) + { + std::string errorMsg; + + // + // load module + // + mLoaded = kModuleErrorOnLoad; + mHandle = LoadModule(mPath, false); + + if( mHandle != NULL ) + { + // + // get entry point function pointer + // + InitializePluginProc InitializePlugin = reinterpret_cast<InitializePluginProc>( + GetFunctionPointerFromModuleImpl(mHandle, "InitializePlugin") ); + + if( InitializePlugin != NULL ) + { + // + // get module ID from plugin resource + // + std::string moduleID; + GetResourceDataFromModule(mHandle, "MODULE_IDENTIFIER", "txt", moduleID); + mPluginAPIs = new PluginAPI(); + + // + // initialize plugin by calling entry point function + // + WXMP_Error error; + InitializePlugin( moduleID.c_str(), mPluginAPIs, &error ); + + if( error.mErrorID == kXMPErr_NoError + && mPluginAPIs->mTerminatePluginProc + && mPluginAPIs->mSetHostAPIProc + && mPluginAPIs->mInitializeSessionProc + && mPluginAPIs->mTerminateSessionProc + && mPluginAPIs->mCacheFileDataProc + && mPluginAPIs->mUpdateFileProc + && mPluginAPIs->mWriteTempFileProc + ) + { + // + // set host API at plugin + // + HostAPIRef hostAPI = PluginManager::getHostAPI( mPluginAPIs->mVersion ); + mPluginAPIs->mSetHostAPIProc( hostAPI, &error ); + + if( error.mErrorID == kXMPErr_NoError ) + { + mLoaded = kModuleLoaded; + } + else + { + errorMsg = "Incompatible plugin API version. (" + moduleID + ")"; + } + } + else + { + if( error.mErrorID != kXMPErr_NoError ) + { + errorMsg = "Plugin initialization failed. (" + moduleID + ")"; + } + else + { + errorMsg = "Plugin API incomplete. (" + moduleID + ")"; + } + } + } + else + { + errorMsg = "Missing plugin entry point \"InitializePlugin\" in plugin " + mPath; + } + + if( mLoaded != kModuleLoaded ) + { + // + // plugin wasn't loaded and initialized successfully, + // so unload the module + // + this->unload(); + } + } + else + { + errorMsg = "Can't load module " + mPath; + } + + if( mLoaded != kModuleLoaded ) + { + // + // error occurred + // + throw XMP_Error( kXMPErr_InternalFailure, errorMsg.c_str() ); + } + } + + return ( mLoaded == kModuleLoaded ); +} + +void Module::unload() +{ + WXMP_Error error; + + // + // terminate plugin + // + if( mPluginAPIs != NULL ) + { + if( mPluginAPIs->mTerminatePluginProc ) + { + mPluginAPIs->mTerminatePluginProc( &error ); + } + delete mPluginAPIs; + mPluginAPIs = NULL; + } + + // + // unload plugin module + // + if( mLoaded != kModuleNotLoaded ) + { + UnloadModule(mHandle, false); + mHandle = NULL; + if( mLoaded == kModuleLoaded ) + { + // + // Reset mLoaded to kModuleNotLoaded, if the module was loaded successfully. + // Otherwise let it remain kModuleErrorOnLoad so that we won't try to load + // it again if some other handler ask to do so. + // + mLoaded = kModuleNotLoaded; + } + } + + CheckError( error ); +} + +} //namespace XMP_PLUGIN diff --git a/XMPFiles/source/PluginHandler/Module.h b/XMPFiles/source/PluginHandler/Module.h new file mode 100644 index 0000000..0e308a0 --- /dev/null +++ b/XMPFiles/source/PluginHandler/Module.h @@ -0,0 +1,65 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef MODULE_H +#define MODULE_H +#include "ModuleUtils.h" +#include "PluginManager.h" + +namespace XMP_PLUGIN +{ + +/** @class Module + * @brief Manages module's loading and unloading. + */ +class Module +{ +public: + Module( std::string & path ): + mPath( path ), mHandle( NULL ), mPluginAPIs( NULL ), mLoaded( kModuleNotLoaded ) { } + + ~Module() { unload(); } + + inline OS_ModuleRef getHandle() const { return mHandle; } + inline const std::string & getPath() const { return mPath; } + + /** + * Returns pluginAPI. It loads the module if not already loaded. + * @return pluginAPI. + */ + PluginAPIRef getPluginAPIs(); + + /** + * It loads the module if not already loaded. + * @return true if module is loaded successfully otherwise returns false. + */ + bool load(); + + /** + * Unloads the module if it is loaded. + * @return Void. + */ + void unload(); + +private: + typedef enum + { + kModuleNotLoaded = 0, + kModuleLoaded, + kModuleErrorOnLoad + } LoadStatus; + + std::string mPath; + OS_ModuleRef mHandle; + PluginAPIRef mPluginAPIs; + LoadStatus mLoaded; +}; + +} //namespace XMP_PLUGIN +#endif //MODULE_H diff --git a/XMPFiles/source/PluginHandler/ModuleUtils.h b/XMPFiles/source/PluginHandler/ModuleUtils.h new file mode 100644 index 0000000..eedf6bf --- /dev/null +++ b/XMPFiles/source/PluginHandler/ModuleUtils.h @@ -0,0 +1,77 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef MODULEUTILS_H +#define MODULEUTILS_H +#include "XMPFiles/source/XMPFiles_Impl.hpp" + +#if XMP_WinBuild +#include <Windows.h> +typedef HMODULE OS_ModuleRef; +#elif XMP_MacBuild +#include <CoreFoundation/CFBundle.h> +#include <tr1/memory> +typedef CFBundleRef OS_ModuleRef; +#elif XMP_UNIXBuild +#include <tr1/memory> +typedef void* OS_ModuleRef; +#else +#error Unsupported operating system +#endif + +namespace XMP_PLUGIN +{ + +/** + * Platform implementation to retrieve a function pointer of the name \param inSymbol from a module \param inOSModule + */ +void* GetFunctionPointerFromModuleImpl( OS_ModuleRef inOSModule, const char* inSymbol ); + +/** + * @return true if @param inModulePath points to a valid shared library +*/ +#if XMP_MacBuild +bool IsValidLibrary( const std::string & inModulePath ); +#endif + +/** + * Load module specified by absolute path \param inModulePath + * + * Win: + * If \param inOnlyResourceAccess = true, only the image is loaded, no referenced dlls are loaded nor initialization code is executed. + * If the module is already loaded and executable, it behaves as \param inOnlyResourceAccess = false. + * The reference count is increased, so don't forget to call UnloadModule. + * + * Mac: + * If \param inOnlyResourceAccess = true, only the CFBundleRef is created. No code is loaded and executed. + */ +OS_ModuleRef LoadModule( const std::string & inModulePath, bool inOnlyResourceAccess = false ); + +/** + * Unload module + * @param inModule + * @param inOnlyResourceAccess = true, close resource file (only relevant for Linux !!). + */ +void UnloadModule( OS_ModuleRef inModule, bool inOnlyResourceAccess = false ); + +/** @brief Read resource file and fill the data in outBuffer + * @param inOSModule Handle of the module. + * @param inResourceName Name of the resource file which needs to be read. + * @param inResourceType Type/Extension of the resource file. + * @param outBuffer Output buffer where data read from the resource file will be stored. + * @return true on success otherwise false + */ +bool GetResourceDataFromModule( + OS_ModuleRef inOSModule, + const std::string & inResourceName, + const std::string & inResourceType, + std::string & outBuffer); + +} //namespace XMP_PLUGIN +#endif diff --git a/XMPFiles/source/PluginHandler/OS_Utils_Linux.cpp b/XMPFiles/source/PluginHandler/OS_Utils_Linux.cpp new file mode 100644 index 0000000..1fe1f9f --- /dev/null +++ b/XMPFiles/source/PluginHandler/OS_Utils_Linux.cpp @@ -0,0 +1,230 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "ModuleUtils.h" +#include "source/UnicodeConversions.hpp" +#include "source/XIO.hpp" + +#include <iostream> +#include <map> +#include <limits> +#include <dlfcn.h> +#include <errno.h> + +namespace XMP_PLUGIN +{ + +// global map of loaded modules (handle, full path) +typedef std::map<OS_ModuleRef, std::string> ModuleRefToPathMap; +static ModuleRefToPathMap sMapModuleRefToPath; +//typedef std::map<void*, std::string> ResourceFileToPathMap; +typedef std::map<OS_ModuleRef, std::string> ResourceFileToPathMap; +static ResourceFileToPathMap sMapResourceFileToPath; + +typedef std::tr1::shared_ptr<int> FilePtr; + +static std::string GetModulePath( OS_ModuleRef inOSModule ); +/** ************************************************************************************************************************ +** CloseFile() +*/ +void CloseFile( + int* inFilePtr) +{ + close(*inFilePtr); + delete inFilePtr; +} + +/** ************************************************************************************************************************ +** OpenResourceFile() +*/ +static FilePtr OpenResourceFile( + OS_ModuleRef inOSModule, + const std::string& inResourceName, + const std::string& inResourceType) +{ + // It is assumed, that all resources reside in a folder with + // the same name as the shared object plus '.resources' extension + std::string path( GetModulePath(inOSModule) ); + + XMP_StringPtr extPos = path.c_str() + path.size(); + for ( ; (extPos != path.c_str()) && (*extPos != '.'); --extPos ) {} + path.erase( extPos - path.c_str() ); // Remove extension + + path += ".resources"; + path += kDirChar; + path += inResourceName + "." + inResourceType; + + FilePtr file; + if( Host_IO::GetFileMode(path.c_str()) == Host_IO::kFMode_IsFile ) + { + int fileRef = ::open( path.c_str(), O_RDONLY ); + if (fileRef != -1) + { + file.reset(new int(fileRef), CloseFile); + } + } + return file; +} + +OS_ModuleRef LoadModule( const std::string & inModulePath, bool inOnlyResourceAccess) +{ + OS_ModuleRef result = NULL; + if( inOnlyResourceAccess ) + { + int fileHandle = open(reinterpret_cast<const char*>(inModulePath.c_str()), O_RDONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + if( !fileHandle ) + { + std::cerr << "Cannot open library for resource access: " << strerror(errno) << std::endl; + } + else + { // success ! + result = (void*) fileHandle; + ResourceFileToPathMap::const_iterator iter = sMapResourceFileToPath.find(result); + if (iter == sMapResourceFileToPath.end()) + { + // not found, so insert + sMapResourceFileToPath.insert(std::make_pair(result, inModulePath)); + } + } + + } + else + { + result = dlopen(reinterpret_cast<const char*>(inModulePath.c_str()), RTLD_LAZY/*RTLD_NOW*/); + + if( !result ) + { + std::cerr << "Cannot open library: " << dlerror() << std::endl; + } + else + { // success ! + ModuleRefToPathMap::const_iterator iter = sMapModuleRefToPath.find(result); + if( iter == sMapModuleRefToPath.end() ) + { + // not found, so insert + sMapModuleRefToPath.insert(std::make_pair(result, inModulePath)); + } + } + } + + return result; +} + +void UnloadModule( OS_ModuleRef inModule, bool inOnlyResourceAccess ) +{ + if( inModule != NULL ) + { + // we bluntly assume, that only one instance of the same library is loaded ad therefore added to the global map ! + if( inOnlyResourceAccess ) + { + ResourceFileToPathMap::iterator iter = sMapResourceFileToPath.find(inModule); + if( iter != sMapResourceFileToPath.end() ) + { + close((long) inModule); + sMapResourceFileToPath.erase(iter); + } + else + { + XMP_Throw("OS_Utils_Linux::UnloadModule called with invalid module handle", kXMPErr_InternalFailure); + } + } + else + { + ModuleRefToPathMap::iterator iter = sMapModuleRefToPath.find(inModule); + if( iter != sMapModuleRefToPath.end() ) + { + dlclose(inModule); + sMapModuleRefToPath.erase(iter); + } + else + { + XMP_Throw("OS_Utils_Linux::UnloadModule called with invalid module handle", kXMPErr_InternalFailure); + } + } + } +} + +/** ************************************************************************************************************************ +** GetModulePath() +*/ +static std::string GetModulePath( + OS_ModuleRef inOSModule) +{ + std::string result; + + if( inOSModule != NULL ) + { + ModuleRefToPathMap::const_iterator iter = sMapModuleRefToPath.find(inOSModule); + ResourceFileToPathMap::const_iterator iter2 = sMapResourceFileToPath.find(inOSModule); + if( (iter != sMapModuleRefToPath.end()) && (iter2 != sMapResourceFileToPath.end()) ) + { + XMP_Throw("OS_Utils_Linux::GetModulePath: Module handle is present in both global maps", kXMPErr_InternalFailure); + } + if (iter != sMapModuleRefToPath.end()) + { + result = iter->second; + } + else if (iter2 != sMapResourceFileToPath.end()) + { + result = iter2->second; + } + else + { + XMP_Throw("OS_Utils_Linux::GetModulePath: Failed to find inOSModule in global map !", kXMPErr_InternalFailure); + } + } + + return result; +} + +void* GetFunctionPointerFromModuleImpl( OS_ModuleRef inOSModule, const char* inSymbol ) +{ + void* proc = NULL; + if( inOSModule != NULL ) + { + proc = dlsym(inOSModule, inSymbol); + if( !proc ) + { + std::cerr << "Cannot get function " << inSymbol << " : " << dlerror() << std::endl; + } + } + return proc; +} + +bool GetResourceDataFromModule( + OS_ModuleRef inOSModule, + const std::string & inResourceName, + const std::string & inResourceType, + std::string & outBuffer) +{ + bool success = false; + FilePtr file = OpenResourceFile( inOSModule, inResourceName, inResourceType ); + + if( file ) + { + ssize_t size = 0; + ssize_t file_size = ::lseek(*file.get(), 0, SEEK_END); + // presumingly we don't want to load more than 2GByte at once (!) + if( file_size < std::numeric_limits<XMP_Int32>::max() ) + { + size = file_size; + if( size > 0 ) + { + outBuffer.resize(size); + + ::lseek(*file.get(), 0, SEEK_SET); + ssize_t bytesRead = ::read( *file.get(), (unsigned char*)&outBuffer[0], size ); + success = bytesRead == size; + } + } + } + return success; +} + +} //namespace XMP_PLUGIN diff --git a/XMPFiles/source/PluginHandler/OS_Utils_Mac.cpp b/XMPFiles/source/PluginHandler/OS_Utils_Mac.cpp new file mode 100644 index 0000000..c042eb7 --- /dev/null +++ b/XMPFiles/source/PluginHandler/OS_Utils_Mac.cpp @@ -0,0 +1,366 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "ModuleUtils.h" +#include "source/UnicodeConversions.hpp" +#include "source/XIO.hpp" + +#include <CoreFoundation/CFBundle.h> +#include <CoreFoundation/CFDate.h> +#include <CoreFoundation/CFNumber.h> +#include <CoreFoundation/CFError.h> +#include <string> + + +namespace XMP_PLUGIN +{ + +/** ************************************************************************************************************************ +** Auto-releasing wrapper for Core Foundation objects +** AutoCFRef is an auto-releasing wrapper for any Core Foundation data type that can be passed +** to CFRetain and CFRelease. When constructed with a Core Foundation object, it is assumed it +** has just been created; that constructor does not call CFRetain. This supports reference counting +** through Core Foundation's own mechanism. +*/ +template <class T> +class AutoCFRef +{ + +public: + /// Default constructor creates NULL reference + AutoCFRef() + { + mCFTypeRef = NULL; + } + + /// Construct with any CFXXXRef type. + explicit AutoCFRef(const T& value) + { + mCFTypeRef = value; + } + + /// Copy constructor + AutoCFRef(const AutoCFRef<T>& rhs) + { + mCFTypeRef = rhs.mCFTypeRef; + if (mCFTypeRef) + CFRetain(mCFTypeRef); + } + + /// Destructor + ~AutoCFRef() + { + if (mCFTypeRef) + CFRelease(mCFTypeRef); + } + + /// Assignment operator + void operator=(const AutoCFRef<T>& rhs) + { + if (!Same(rhs)) + { + if (mCFTypeRef) + CFRelease(mCFTypeRef); + mCFTypeRef = rhs.mCFTypeRef; + if (mCFTypeRef) + CFRetain(mCFTypeRef); + } + } + + /// Equivalence operator + /** operator== returns logical equivalence, the meaning of which varies from class to class + in Core Foundation. See also AutoCFRef::Same. + @param rhs The AutoCFRef<T> to compare to + @return true if objects are logically equivalent. + */ + bool operator==(const AutoCFRef<T>& rhs) const + { + if (mCFTypeRef && rhs.mCFTypeRef) + return CFEqual(mCFTypeRef, rhs.mCFTypeRef); + else + return mCFTypeRef == rhs.mCFTypeRef; + } + + /// Non-equivalence operator + /** operator!= returns logical equivalence, the meaning of which varies from class to class + in Core Foundation. See also AutoCFRef::Same. + @param rhs The AutoCFRef<T> to compare to + @return true if objects are not logically equivalent. + */ + bool operator!=(const AutoCFRef<T>& rhs) const + { + return !operator==(rhs); + } + + /// References same CF object + /** Same returns true if both objects reference the same CF object (or both are NULL). + For logical equivalence (CFEqual) use == operator. + @param rhs The AutoCFRef<T> to compare to + @return true if AutoRefs reference the same object. + */ + bool Same(const AutoCFRef<T>& rhs) const + { + return mCFTypeRef == rhs.mCFTypeRef; + } + + /// Change the object referenced + /** Reset is used to put a new CF object into a preexisting AutoCFRef. Does NOT call CFRetain, + so if its ref count is 1 on entry, it will delete on destruction, barring other influences. + @param value (optional) the new CFXXXRef to set, default is NULL. + */ + void Reset(T value = NULL) + { + if (value != mCFTypeRef) + { + if (mCFTypeRef) + CFRelease(mCFTypeRef); + mCFTypeRef = value; + } + } + + /// Return true if no object referenced. + bool IsNull() const + { + return mCFTypeRef == NULL; + } + + /// Return retain count. + /** Returns retain count of referenced object from Core Foundation. Can be used to track down + failures in reference management, but be aware that some objects might be returned to your + code by the operating system with a retain count already greater than one. + @return Referenced object's retain count. + */ + CFIndex RetainCount() + { + return mCFTypeRef ? CFGetRetainCount(mCFTypeRef) : 0; + } + + /// Const dereference operator + /** operator* returns a reference to the contained CFTypeRef. Use this to pass the object into + Core Foundation functions. DO NOT use this to create a new AutoCFRef; copy construct instead. + */ + const T& operator*() const + { + return mCFTypeRef; + } + + /// Nonconst dereference operator + /** operator* returns a reference to the contained CFTypeRef. Use this to pass the object into + Core Foundation functions. DO NOT use this to create a new AutoCFRef; copy construct instead. + */ + T& operator*() + { + return mCFTypeRef; + } + +private: + T mCFTypeRef; +}; + +typedef AutoCFRef<CFURLRef> AutoCFURL; +typedef AutoCFRef<CFStringRef> AutoCFString; + +typedef std::tr1::shared_ptr<FSIORefNum> FilePtr; + + +/** ************************************************************************************************************************ +** Convert string into CFString +*/ +static inline CFStringRef MakeCFString(const std::string& inString, CFStringEncoding inEncoding = kCFStringEncodingUTF8) +{ + CFStringRef str = ::CFStringCreateWithCString( NULL, inString.c_str(), inEncoding ); + return str; +} + +/** ************************************************************************************************************************ +** Convert CFString into std::string +*/ +static std::string MacToSTDString(CFStringRef inCFString) +{ + std::string result; + + if (inCFString == NULL) + { + return result; + } + + // The CFStringGetLength returns the length of the string in UTF-16 encoding units. + ::CFIndex const stringUtf16Length = ::CFStringGetLength(inCFString); + if (stringUtf16Length == 0) + { + return result; + } + + // Check if the CFStringRef can allow fast-return. + char const* ptr = ::CFStringGetCStringPtr(inCFString, kCFStringEncodingUTF8); + if (ptr != NULL) + { + result = std::string( ptr ); // This kind of assign expects '\0' termination. + return result; + } + + // Since this is an encoding conversion, the converted string may not be the same length in bytes + // as the CFStringRef in UTF-16 encoding units. + + ::UInt8 const noLossByte = 0; + ::Boolean const internalRepresentation = FALSE; // TRUE is external representation, with a BOM. FALSE means no BOM. + ::CFRange const range = ::CFRangeMake(0, stringUtf16Length); // [NOTE] length is in UTF-16 encoding units, not bytes. + ::CFIndex const maxBufferLength = ::CFStringGetMaximumSizeForEncoding(stringUtf16Length, kCFStringEncodingUTF8); // Convert from UTF-16 encoding units to UTF-8 worst-case-scenario encoding units, in bytes. + ::CFIndex usedBufferLength = 0; // In byte count. + char buffer[1024]; + memset( buffer, 0, 1024 ); + ::CFIndex numberOfUtf16EncodingUnitsConverted = ::CFStringGetBytes(inCFString, range, ::kCFStringEncodingUTF8, noLossByte, internalRepresentation, (UInt8*)buffer, maxBufferLength, &usedBufferLength); + result = std::string( buffer ); + + return result; +} + + +static void CloseFile( FSIORefNum* inFilePtr ) +{ + FSCloseFork(*inFilePtr); + delete inFilePtr; +} + +static FilePtr OpenResourceFile( OS_ModuleRef inOSModule, const std::string& inResourceName, const std::string& inResourceType, bool inSearchInSubFolderWithNameOfResourceType) +{ + FilePtr file; + if (inOSModule != nil) + { + AutoCFString resourceName( MakeCFString( inResourceName ) ); + AutoCFString resourceType( MakeCFString( inResourceType ) ); + AutoCFString subfolderName(inSearchInSubFolderWithNameOfResourceType ? MakeCFString( inResourceType ) : nil); + + AutoCFURL url( + ::CFBundleCopyResourceURL(inOSModule, *resourceName, *resourceType, *subfolderName)); + + FSRef fileReference; + if (!url.IsNull() && ::CFURLGetFSRef(*url, &fileReference)) + { + HFSUniStr255 str; + if (::FSGetDataForkName(&str) == noErr) + { + FSIORefNum forkRef; + if (::FSOpenFork(&fileReference, str.length, str.unicode, fsRdPerm, &forkRef) == noErr) + { + file.reset(new FSIORefNum(forkRef), CloseFile); + } + } + } + } + return file; +} + +bool IsValidLibrary( const std::string & inModulePath ) +{ + bool result = false; + AutoCFURL bundleURL(::CFURLCreateFromFileSystemRepresentation( + kCFAllocatorDefault, + (const UInt8*) inModulePath.c_str(), + inModulePath.size(), + false)); + if (*bundleURL != NULL) + { + CFBundleRef bundle = ::CFBundleCreate(kCFAllocatorDefault, *bundleURL); + if (bundle != NULL) + { + CFArrayRef arrayRef = ::CFBundleCopyExecutableArchitectures(bundle); + if (arrayRef != NULL) + { + result = true; + ::CFRelease(arrayRef); + } + + ::CFRelease(bundle); + } + } + return result; +} + +OS_ModuleRef LoadModule( const std::string & inModulePath, bool inOnlyResourceAccess ) +{ + OS_ModuleRef result = NULL; + AutoCFURL bundleURL(::CFURLCreateFromFileSystemRepresentation( + kCFAllocatorDefault, + (const UInt8*) inModulePath.c_str(), + inModulePath.size(), + false)); + if (*bundleURL != NULL) + { + result = ::CFBundleCreate(kCFAllocatorDefault, *bundleURL); + if (!inOnlyResourceAccess && (result != NULL)) + { + ::CFErrorRef errorRef = NULL; + Boolean loaded = ::CFBundleIsExecutableLoaded(result); + if (!loaded) + { + loaded = ::CFBundleLoadExecutableAndReturnError(result, &errorRef); + if(!loaded || errorRef != NULL) + { + AutoCFString errorDescr(::CFErrorCopyDescription(errorRef)); + throw XMP_Error( kXMPErr_InternalFailure, MacToSTDString(*errorDescr).c_str() ); + ::CFRelease(errorRef); + // release bundle and return NULL + ::CFRelease(result); + result = NULL; + } + } + } + } + return result; +} + +void UnloadModule( OS_ModuleRef inModule, bool inOnlyResourceAccess ) +{ + if (inModule != NULL) + { + ::CFRelease(inModule); + } +} + +void* GetFunctionPointerFromModuleImpl( OS_ModuleRef inOSModule, const char* inSymbol ) +{ + void* proc = NULL; + if( inOSModule != NULL) + { + proc = ::CFBundleGetFunctionPointerForName( inOSModule, *AutoCFString(MakeCFString(inSymbol)) ); + } + return proc; +} + +bool GetResourceDataFromModule( + OS_ModuleRef inOSModule, + const std::string & inResourceName, + const std::string & inResourceType, + std::string & outBuffer) +{ + bool success = false; + + if (FilePtr file = OpenResourceFile(inOSModule, inResourceName, inResourceType, false)) + { + ByteCount size = 0; + SInt64 fork_size = 0; + ::FSGetForkSize(*file.get(), &fork_size); + // presumingly we don't want to load more than 2GByte at once (!) + if( fork_size < std::numeric_limits<XMP_Int32>::max() ) + { + size = static_cast<ByteCount>(fork_size); + if (size > 0) + { + outBuffer.resize(size); + ::FSReadFork(*file.get(), fsFromStart, 0, size, (unsigned char*)&outBuffer[0], (ByteCount*)&size); + } + success = true; + } + } + + return success; +} + +} //namespace XMP_PLUGIN diff --git a/XMPFiles/source/PluginHandler/OS_Utils_WIN.cpp b/XMPFiles/source/PluginHandler/OS_Utils_WIN.cpp new file mode 100644 index 0000000..66df45a --- /dev/null +++ b/XMPFiles/source/PluginHandler/OS_Utils_WIN.cpp @@ -0,0 +1,66 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "ModuleUtils.h" +#include "source/UnicodeConversions.hpp" + +namespace XMP_PLUGIN +{ + +OS_ModuleRef LoadModule( const std::string & inModulePath, bool inOnlyResourceAccess) +{ + std::string wideString; + ToUTF16 ( (UTF8Unit*)inModulePath.c_str(), inModulePath.size() + 1, &wideString, false ); // need +1 character otherwise \0 won't be converted into UTF16 + + DWORD flags = inOnlyResourceAccess ? LOAD_LIBRARY_AS_IMAGE_RESOURCE : 0; + OS_ModuleRef result = ::LoadLibraryEx((WCHAR*) wideString.c_str(), NULL, flags); + + // anything below indicates error in LoadLibrary + if((result != NULL) && (result < OS_ModuleRef(32))) + { + result = NULL; + } + return result; +} + +void UnloadModule( OS_ModuleRef inModule, bool inOnlyResourceAccess ) +{ + ::FreeLibrary(inModule); +} + +void* GetFunctionPointerFromModuleImpl( OS_ModuleRef inOSModule, const char* inSymbol ) +{ + return reinterpret_cast<void*>( ::GetProcAddress( inOSModule, inSymbol ) ); +} + +bool GetResourceDataFromModule( + OS_ModuleRef inOSModule, + const std::string & inResourceName, + const std::string & inResourceType, + std::string & outBuffer) +{ + HRSRC src = ::FindResourceA(inOSModule, reinterpret_cast<LPCSTR>(inResourceName.c_str()), reinterpret_cast<LPCSTR>(inResourceType.c_str())); + if( src != NULL ) + { + HGLOBAL resource = (::LoadResource(inOSModule, src)); + HGLOBAL data = (::LockResource(resource)); + std::size_t size = (std::size_t)(::SizeofResource(inOSModule, src)); + if (size) + { + outBuffer.assign((const char*)data, size); + } + UnlockResource(data); + ::FreeResource(resource); + + return true; + } + return false; +} + +} //namespace XMP_PLUGIN
\ No newline at end of file diff --git a/XMPFiles/source/PluginHandler/PluginManager.cpp b/XMPFiles/source/PluginHandler/PluginManager.cpp new file mode 100644 index 0000000..af60388 --- /dev/null +++ b/XMPFiles/source/PluginHandler/PluginManager.cpp @@ -0,0 +1,779 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "PluginManager.h" +#include "FileHandler.h" +#include <algorithm> +#include "XMPAtoms.h" +#include "XMPFiles/source/HandlerRegistry.h" +#include "FileHandlerInstance.h" +#include "HostAPI.h" + +using namespace Common; +using namespace std; + +// ================================================================================================= + +namespace XMP_PLUGIN +{ + +const char* kResourceName_UIDs = "XMPPLUGINUIDS"; +const char* kLibraryExtensions[] = { "xpi" }; + +struct FileHandlerPair +{ + FileHandlerSharedPtr mStandardHandler; + FileHandlerSharedPtr mReplacementHandler; +}; + +// ================================================================================================= + +static XMP_FileFormat GetXMPFileFormatFromFilePath( XMP_StringPtr filePath ) +{ + XMP_StringPtr pathName = filePath + strlen(filePath); + for ( ; pathName > filePath; --pathName ) { + if ( *pathName == '.' ) break; + } + + XMP_StringPtr fileExt = pathName + 1; + return HandlerRegistry::getInstance().getFileFormat ( fileExt ); +} + +// ================================================================================================= + +static XMPFileHandler* Plugin_MetaHandlerCTor ( FileHandlerSharedPtr handler, XMPFiles* parent ) +{ + SessionRef object; + WXMP_Error error; + + if( (handler == 0) || (! handler->load()) ) + { + XMP_Throw ( "Plugin not loaded", kXMPErr_InternalFailure ); + } + + handler->getModule()->getPluginAPIs()->mInitializeSessionProc ( handler->getUID().c_str(), parent->filePath.c_str(), (XMP_Uns32)parent->format, (XMP_Uns32)handler->getHandlerFlags(), (XMP_Uns32)parent->openFlags, &object, &error ); + CheckError ( error ); + + FileHandlerInstance* instance = new FileHandlerInstance ( object, handler, parent ); + return instance; +} + +static XMPFileHandler* Plugin_MetaHandlerCTor_Standard( XMPFiles * parent ) +{ + FileHandlerSharedPtr handler = PluginManager::getFileHandler( parent->format, PluginManager::kStandardHandler ); + + return Plugin_MetaHandlerCTor( handler, parent ); +} + +static XMPFileHandler* Plugin_MetaHandlerCTor_Replacement( XMPFiles * parent ) +{ + FileHandlerSharedPtr handler = PluginManager::getFileHandler( parent->format, PluginManager::kReplacementHandler ); + + return Plugin_MetaHandlerCTor( handler, parent ); +} + +// ================================================================================================= + +static bool Plugin_CheckFileFormat ( FileHandlerSharedPtr handler, XMP_StringPtr filePath, XMP_IO * fileRef, XMPFiles * parent ) +{ + if ( handler != 0 ) { + + // call into plugin if owning handler or if manifest has no CheckFormat entry + if ( fileRef == 0 || handler->getCheckFormatSize() == 0) { + + bool ok; + WXMP_Error error; + CheckSessionFileFormatProc checkProc = handler->getModule()->getPluginAPIs()->mCheckFileFormatProc; + checkProc ( handler->getUID().c_str(), filePath, fileRef, &ok, &error ); + CheckError ( error ); + return ok; + + } else { + + // all CheckFormat manifest entries must match + for ( XMP_Uns32 i=0; i < handler->getCheckFormatSize(); i++ ) { + + CheckFormat checkFormat = handler->getCheckFormat ( i ); + + if ( checkFormat.empty() ) return false; + + XMP_Uns8 buffer[1024]; + + if ( checkFormat.mLength > 1024 ) { + //Ideally check format string should not be that long. + //The check is here to handle only malicious data. + checkFormat.mLength = 1024; + } + + fileRef->Seek ( checkFormat.mOffset, kXMP_SeekFromStart ); + XMP_Uns32 len = fileRef->Read ( buffer, checkFormat.mLength ); + + if ( len != checkFormat.mLength ) { + + // Not enough byte read from the file. + return false; + + } else { + + // Check if byteSeq is hexadecimal byte sequence, e.g 0x03045100 + + bool isHex = ( (checkFormat.mLength > 2) && + (checkFormat.mByteSeq[0] == '0') && + (checkFormat.mByteSeq[1] == 'x') && + (checkFormat.mByteSeq.size() == (2 + 2*checkFormat.mLength)) ); + + if ( ! isHex ) { + + if ( memcmp ( buffer, checkFormat.mByteSeq.c_str(), checkFormat.mLength ) != 0 ) return false; + + } else { + + for ( XMP_Uns32 current = 0; current < checkFormat.mLength; current++ ) { + + char oneByteBuffer[3]; + oneByteBuffer[0] = checkFormat.mByteSeq [ 2 + 2*current ]; + oneByteBuffer[1] = checkFormat.mByteSeq [ 2 + 2*current + 1 ]; + oneByteBuffer[2] = '\0'; + + XMP_Uns8 oneByte = (XMP_Uns8) strtoul ( oneByteBuffer, 0, 16 ); + if ( oneByte != buffer[current] ) return false; + + } + + } + + } + + } + + return true; // The checkFormat string comparison passed. + + } + + } + + return false; // Should never get here. +} // Plugin_CheckFileFormat + +static bool Plugin_CheckFileFormat_Standard( XMP_FileFormat format, XMP_StringPtr filePath, XMP_IO* fileRef, XMPFiles* parent ) +{ + FileHandlerSharedPtr handler = PluginManager::getFileHandler( format, PluginManager::kStandardHandler ); + + return Plugin_CheckFileFormat( handler, filePath, fileRef, parent ); +} + +static bool Plugin_CheckFileFormat_Replacement( XMP_FileFormat format, XMP_StringPtr filePath, XMP_IO* fileRef, XMPFiles* parent ) +{ + FileHandlerSharedPtr handler = PluginManager::getFileHandler( format, PluginManager::kReplacementHandler ); + + return Plugin_CheckFileFormat( handler, filePath, fileRef, parent ); +} + +// ================================================================================================= + +static bool Plugin_CheckFolderFormat( FileHandlerSharedPtr handler, + const std::string & rootPath, + const std::string & gpName, + const std::string & parentName, + const std::string & leafName, + XMPFiles * parent ) +{ + bool result = false; + + if ( handler != 0 ) + { + WXMP_Error error; + CheckSessionFolderFormatProc checkProc = handler->getModule()->getPluginAPIs()->mCheckFolderFormatProc; + checkProc ( handler->getUID().c_str(), rootPath.c_str(), gpName.c_str(), parentName.c_str(), leafName.c_str(), &result, &error ); + CheckError( error ); + } + + return result; + +} + +static bool Plugin_CheckFolderFormat_Standard( XMP_FileFormat format, + const std::string & rootPath, + const std::string & gpName, + const std::string & parentName, + const std::string & leafName, + XMPFiles * parent ) +{ + FileHandlerSharedPtr handler = PluginManager::getFileHandler( format, PluginManager::kStandardHandler ); + + return Plugin_CheckFolderFormat( handler, rootPath, gpName, parentName, leafName, parent ); +} + +static bool Plugin_CheckFolderFormat_Replacement( XMP_FileFormat format, + const std::string & rootPath, + const std::string & gpName, + const std::string & parentName, + const std::string & leafName, + XMPFiles * parent ) +{ + FileHandlerSharedPtr handler = PluginManager::getFileHandler( format, PluginManager::kReplacementHandler ); + + return Plugin_CheckFolderFormat( handler, rootPath, gpName, parentName, leafName, parent ); +} + +// ================================================================================================= + +PluginManager* PluginManager::msPluginManager = 0; + +PluginManager::PluginManager( const std::string& pluginDir, const std::string& plugins ) : mPluginDir ( pluginDir ) +{ + + const std::size_t count = sizeof(kLibraryExtensions) / sizeof(kLibraryExtensions[0]); + + for ( std::size_t i = 0; i<count; ++i ) { + mExtensions.push_back ( std::string ( kLibraryExtensions[i] ) ); + } + + size_t pos = std::string::npos; + + #if XMP_WinBuild + // convert to Win kDirChar + while ( (pos = mPluginDir.find ('/')) != string::npos ) { + mPluginDir.replace (pos, 1, "\\"); + } + #else + while ( (pos = mPluginDir.find ('\\')) != string::npos ) { + mPluginDir.replace (pos, 1, "/"); + } + #endif + + if ( ! mPluginDir.empty() && Host_IO::Exists( mPluginDir.c_str() ) ) { + + XMP_StringPtr strPtr = plugins.c_str(); + size_t pos = 0; + size_t length = 0; + + for ( ; ; ++strPtr, ++length ) { + + if ( (*strPtr == ',') || (*strPtr == '\0') ) { + + if ( length != 0 ) { + + //Remove white spaces from front + while ( plugins[pos] == ' ' ) { + ++pos; + --length; + } + + std::string pluginName; + pluginName.assign ( plugins, pos, length ); + + //Remove extension from the plugin name + size_t found = pluginName.find ( '.' ); + if ( found != string::npos ) pluginName.erase ( found ); + + //Remove white spaces from the back + found = pluginName.find ( ' ' ); + if ( found != string::npos ) pluginName.erase ( found ); + + MakeLowerCase ( &pluginName ); + mPluginsNeeded.push_back ( pluginName ); + + //Reset for next plugin + pos = pos + length + 1; + length = 0; + + } + + if ( *strPtr == '\0' ) break; + + } + + } + + } + +} // PluginManager::PluginManager + +// ================================================================================================= + +PluginManager::~PluginManager() +{ + mPluginDir.clear(); + mExtensions.clear(); + mPluginsNeeded.clear(); + mModules.clear(); + mHandlers.clear(); + mSessions.clear(); + + terminateHostAPI(); +} + +// ================================================================================================= + +static bool registerHandler( XMP_FileFormat format, FileHandlerSharedPtr handler ) +{ + bool ret = false; + + HandlerRegistry& hdlrReg = HandlerRegistry::getInstance(); + FileHandlerType type = handler->getHandlerType(); + CheckFileFormatProc chkFileFormat = NULL; + CheckFolderFormatProc chkFolderFormat = NULL; + XMPFileHandlerCTor hdlCtor = NULL; + + if( handler->getOverwriteHandler() ) + { + // + // ctor, checkformat function pointers for replacement handler + // + hdlCtor = Plugin_MetaHandlerCTor_Replacement; + chkFileFormat = Plugin_CheckFileFormat_Replacement; + chkFolderFormat = Plugin_CheckFolderFormat_Replacement; + } + else + { + // + // ctor, checkformat function pointers for standard handler + // + hdlCtor = Plugin_MetaHandlerCTor_Standard; + chkFileFormat = Plugin_CheckFileFormat_Standard; + chkFolderFormat = Plugin_CheckFolderFormat_Standard; + } + + // + // register handler according to its type + // + switch( handler->getHandlerType() ) + { + case NormalHandler_K: + ret = hdlrReg.registerNormalHandler( format, handler->getHandlerFlags(), chkFileFormat, + hdlCtor, handler->getOverwriteHandler() ); + break; + + case OwningHandler_K: + ret = hdlrReg.registerOwningHandler( format, handler->getHandlerFlags(), chkFileFormat, + hdlCtor, handler->getOverwriteHandler() ); + break; + + case FolderHandler_K: + ret = hdlrReg.registerFolderHandler( format, handler->getHandlerFlags(), chkFolderFormat, + hdlCtor, handler->getOverwriteHandler() ); + break; + + default: + break; + } + + return ret; +} + +void PluginManager::initialize( const std::string& pluginDir, const std::string& plugins ) +{ + try + { + HandlerRegistry & hdlrReg = HandlerRegistry::getInstance(); + if( msPluginManager == 0 ) msPluginManager = new PluginManager( pluginDir, plugins ); + + msPluginManager->doScan( 2 ); + + // + // Register all the found plugin based file handler + // + for( PluginHandlerMap::iterator it = msPluginManager->mHandlers.begin(); it != msPluginManager->mHandlers.end(); ++it ) + { + XMP_FileFormat format = it->first; + FileHandlerPair handlers = it->second; + + if( handlers.mStandardHandler != NULL ) + { + registerHandler( format, handlers.mStandardHandler ); + } + + if( handlers.mReplacementHandler != NULL ) + { + registerHandler( format, handlers.mReplacementHandler ); + } + } + } + catch( ... ) + { + // Absorb exceptions. This is the plugin-architecture entry point. + } + +} // PluginManager::initialize + +// ================================================================================================= + +void PluginManager::terminate() +{ + delete msPluginManager; + msPluginManager = 0; + ResourceParser::terminate(); +} + +// ================================================================================================= + +void PluginManager::addFileHandler( XMP_FileFormat format, FileHandlerSharedPtr handler ) +{ + if ( msPluginManager != 0 ) + { + PluginHandlerMap & handlerMap = msPluginManager->mHandlers; + + // + // Create placeholder in map for format + // + if ( handlerMap.find(format) == handlerMap.end() ) + { + FileHandlerPair pair; + handlerMap.insert( handlerMap.end(), std::pair<XMP_FileFormat, FileHandlerPair>( format, pair) ); + } + + // + // if there is already a standard handler or a replacement handler for the file format + // then just ignore it. The first one wins. + // + if( handler->getOverwriteHandler() ) + { + if( handlerMap[format].mReplacementHandler.get() == NULL ) handlerMap[format].mReplacementHandler = handler; + } + else + { + if( handlerMap[format].mStandardHandler.get() == NULL ) handlerMap[format].mStandardHandler = handler; + } + } +} + +// ================================================================================================= + +FileHandlerSharedPtr PluginManager::getFileHandler( XMP_FileFormat format, HandlerPriority priority /*= kStandardHandler*/ ) +{ + if ( msPluginManager != 0 ) + { + PluginHandlerMap::iterator it = msPluginManager->mHandlers.find( format ); + + if( it != msPluginManager->mHandlers.end() ) + { + if( priority == kStandardHandler ) + { + return it->second.mStandardHandler; + } + else if( priority == kReplacementHandler ) + { + return it->second.mReplacementHandler; + } + } + } + + return FileHandlerSharedPtr(); +} + +// ================================================================================================= + +static XMP_ReadWriteLock sPluginManagerRWLock; + +void PluginManager::addHandlerInstance( SessionRef session, FileHandlerInstancePtr handler ) +{ + if ( msPluginManager != 0 ) { + XMP_AutoLock(&sPluginManagerRWLock, kXMP_WriteLock); + SessionMap & sessionMap = msPluginManager->mSessions; + if ( sessionMap.find(session) == sessionMap.end() ) { + sessionMap[session] = handler; + } + } +} + +// ================================================================================================= + +void PluginManager::removeHandlerInstance( SessionRef session ) +{ + if ( msPluginManager != 0 ) { + XMP_AutoLock(&sPluginManagerRWLock, kXMP_WriteLock); + SessionMap & sessionMap = msPluginManager->mSessions; + sessionMap.erase ( session ); + } +} + +// ================================================================================================= + +FileHandlerInstancePtr PluginManager::getHandlerInstance( SessionRef session ) +{ + FileHandlerInstancePtr ret = 0; + if ( msPluginManager != 0 ) { + XMP_AutoLock(&sPluginManagerRWLock, kXMP_ReadLock); + ret = msPluginManager->mSessions[session]; + } + return ret; +} + +// ================================================================================================= + +PluginManager::HandlerPriority PluginManager::getHandlerPriority( FileHandlerInstancePtr handler ) +{ + if( handler != NULL ) + { + for( PluginHandlerMap::iterator it=msPluginManager->mHandlers.begin(); + it != msPluginManager->mHandlers.end(); it++ ) + { + if( it->second.mStandardHandler == handler->GetHandlerInfo() ) return kStandardHandler; + if( it->second.mReplacementHandler == handler->GetHandlerInfo() ) return kReplacementHandler; + } + } + + return kUnknown; +} + +// ================================================================================================= + +static bool CheckPluginArchitecture ( XMLParserAdapter * xmlParser ) { + + #if XMP_MacBuild + bool okArchitecture = true; // Missing Architecture attribute means load on Mac. + #else + bool okArchitecture = false; // Missing Architecture attribute means do not load elsewhere. + #endif + + #if XMP_64 + const char * nativeArchitecture = "x64"; + #else + const char * nativeArchitecture = "x86"; + #endif + + size_t i, limit; + XML_Node & xmlTree = xmlParser->tree; + XML_NodePtr rootElem = 0; + + // Find the outermost XML element and see if it is PluginResource. + for ( i = 0, limit = xmlTree.content.size(); i < limit; ++i ) { + if ( xmlTree.content[i]->kind == kElemNode ) { + rootElem = xmlTree.content[i]; + break; + } + } + + if ( (rootElem == 0) || (rootElem->name != "PluginResource") ) return okArchitecture; + + // Look for the Architecture attribute and see if it matches. + + XML_NodePtr archAttr = 0; + for ( i = 0, limit = rootElem->attrs.size(); i < limit; ++i ) { + if ( rootElem->attrs[i]->name == "Architecture" ) { + archAttr = rootElem->attrs[i]; + break; + } + } + + if ( archAttr != 0 ) okArchitecture = (archAttr->value == nativeArchitecture); + + return okArchitecture; + +} // CheckPluginArchitecture + +// ================================================================================================= + +void PluginManager::loadResourceFile( ModuleSharedPtr module ) +{ + + OS_ModuleRef moduleRef = LoadModule ( module->getPath(), true ); + + if ( moduleRef != 0 ) { + + XMLParserAdapter* parser = 0; + + try { + + std::string buffer; + if ( GetResourceDataFromModule ( moduleRef, kResourceName_UIDs, "txt", buffer ) ) { + + ResourceParser::initialize(); // Initialize XMPAtoms before processing resource file. + + parser = XMP_NewExpatAdapter ( ExpatAdapter::kUseGlobalNamespaces ); + parser->ParseBuffer ( (XMP_Uns8*)buffer.c_str(), buffer.size(), true ); + + if ( CheckPluginArchitecture ( parser ) ) { + ResourceParser resource ( module ); + resource.parseElementList ( &parser->tree, true ); + } + + delete parser; + + } + + } catch ( ... ) { + + if ( parser != 0 ) delete parser; + // Otherwise ignore errors. + + } + + UnloadModule ( moduleRef, true ); + + } + +} // PluginManager::loadResourceFile + +// ================================================================================================= + +void PluginManager::scanRecursive( const std::string & tempPath, std::vector<std::string>& ioFoundLibs, XMP_Int32 inLevel, XMP_Int32 inMaxNestingLevel ) +{ + ++inLevel; + Host_IO::AutoFolder aFolder; + if ( Host_IO::GetFileMode( tempPath.c_str() ) != Host_IO::kFMode_IsFolder ) return; + + aFolder.folder = Host_IO::OpenFolder( tempPath.c_str() ); + std::string childPath, childName; + + while ( Host_IO::GetNextChild ( aFolder.folder, &childName ) ) { + + // Make sure the children of CONTENTS are legit. + childPath = tempPath; + childPath += kDirChar; + childPath += childName; + Host_IO::FileMode clientMode = Host_IO::GetFileMode ( childPath.c_str() ); + + bool okFolder = (clientMode == Host_IO::kFMode_IsFolder); + #if XMP_MacBuild + if ( okFolder ) okFolder = ( ! IsValidLibrary ( childPath ) ); + #endif + + // only step into non-packages (neither bundle nor framework) on Mac + if ( okFolder ) { + + if ( inLevel < inMaxNestingLevel ) { + scanRecursive ( childPath + kDirChar, ioFoundLibs, inLevel, inMaxNestingLevel ); + } + + } else { + + if ( childName[0] == '~' ) continue; // ignore plug-ins like "~PDFL.xpi" + + std::string fileExt; + XMP_StringPtr extPos = childName.c_str() + childName.size(); + for ( ; (extPos != childName.c_str()) && (*extPos != '.'); --extPos ) {} + if ( *extPos == '.' ) { + fileExt.assign ( extPos+1 ); + MakeLowerCase ( &fileExt ); + } + + StringVec::const_iterator iterFound = + std::find_if ( mExtensions.begin(), mExtensions.end(), + std::bind2nd ( std::equal_to<std::string>(), fileExt ) ); + + if ( iterFound != mExtensions.end() ) { + + //Check if the found plugin is present in the user's demanding plugin list. + childName.erase ( extPos - childName.c_str() ); + MakeLowerCase ( &childName ); + + StringVec::const_iterator pluginNeeded = + std::find_if ( mPluginsNeeded.begin(), mPluginsNeeded.end(), + std::bind2nd ( std::equal_to<std::string>(), childName ) ); + + if ( (pluginNeeded != mPluginsNeeded.end()) || mPluginsNeeded.empty() ) { + ioFoundLibs.push_back ( childPath ); + } + + } + + } + + } + + aFolder.Close(); + +} // PluginManager::scanRecursive + +// ================================================================================================= + +void PluginManager::doScan( const XMP_Int32 inMaxNumOfNestedFolder ) +{ + XMP_Assert(inMaxNumOfNestedFolder > 0); + if ( inMaxNumOfNestedFolder < 1 ) return; // noop, wrong parameter + + // scan directory + std::vector<std::string> foundLibs; + XMP_Int32 iteration = 0; + scanRecursive ( mPluginDir, foundLibs, iteration, inMaxNumOfNestedFolder ); + + // add found modules + std::vector<std::string>::const_iterator iter = foundLibs.begin(); + std::vector<std::string>::const_iterator iterEnd = foundLibs.end(); + for ( ; iter != iterEnd; ++iter ) { + std::string path ( *iter ); + ModuleSharedPtr module ( new Module ( path ) ); + loadResourceFile ( module ); + } + +} // PluginManager::doScan + +// ================================================================================================= + +HostAPIRef PluginManager::getHostAPI( XMP_Uns32 version ) +{ + HostAPIRef hostAPI = NULL; + + if( msPluginManager == NULL ) return NULL; + if( version < 1 ) return NULL; + + HostAPIMap::iterator iter = msPluginManager->mHostAPIs.find( version ); + + if( iter != msPluginManager->mHostAPIs.end() ) + { + hostAPI = iter->second; + } + else + { + hostAPI = new HostAPI(); + hostAPI->mSize = sizeof( HostAPI ); + hostAPI->mVersion = version; + + switch( version ) + { + case 1: SetupHostAPI_V1( hostAPI ); break; + + default: + { + delete hostAPI; + hostAPI = NULL; + } + } + + if( hostAPI != NULL ) + { + msPluginManager->mHostAPIs[ version ] = hostAPI; + } + } + + return hostAPI; +} + +// ================================================================================================= + +void PluginManager::terminateHostAPI() +{ + for( HostAPIMap::iterator it = msPluginManager->mHostAPIs.begin(); it != msPluginManager->mHostAPIs.end(); ++it ) + { + XMP_Uns32 version = it->first; + HostAPIRef hostAPI = it->second; + + switch( version ) + { + case 1: + { + delete hostAPI->mFileIOAPI; + delete hostAPI->mStrAPI; + delete hostAPI->mAbortAPI; + delete hostAPI->mStandardHandlerAPI; + delete hostAPI; + } + break; + + default: + { + delete hostAPI; + } + } + } +} + +} // namespace XMP_PLUGIN diff --git a/XMPFiles/source/PluginHandler/PluginManager.h b/XMPFiles/source/PluginHandler/PluginManager.h new file mode 100644 index 0000000..9b2240e --- /dev/null +++ b/XMPFiles/source/PluginHandler/PluginManager.h @@ -0,0 +1,190 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef PLUGINMANAGER_H +#define PLUGINMANAGER_H +#include "PluginHandler.h" +#include "ModuleUtils.h" + +namespace XMP_PLUGIN +{ + +typedef XMP_Uns32 XMPAtom; +typedef XMPAtom FileHandlerType; + +class Module; +typedef std::tr1::shared_ptr<Module> ModuleSharedPtr; + +class FileHandler; +typedef std::tr1::shared_ptr<FileHandler> FileHandlerSharedPtr; + +class FileHandlerInstance; +typedef FileHandlerInstance* FileHandlerInstancePtr; + +typedef std::vector<std::string> StringVec; +typedef std::vector<ModuleSharedPtr> ModuleVec; +typedef std::vector<XMP_FileFormat> XMP_FileFormatVec; + +struct FileHandlerPair; + +inline void CheckError( WXMP_Error & error ) +{ + if( error.mErrorID != kXMPErr_NoError ) + { + if( error.mErrorID >= 500 /* kXMPErr_PluginInternal */ + || error.mErrorID <= 512 /* kXMPErr_SetHostAPI */ ) + { + throw XMP_Error( kXMPErr_InternalFailure, error.mErrorMsg ); + } + else + { + throw XMP_Error( error.mErrorID, error.mErrorMsg ); + } + } +} + +/** @class PluginManager + * @brief Register all file handlers from all the plugins available in Plugin directory. + * + * At the initialization time of XMPFiles, PluginManager loads all the avalbale plugins + */ +class PluginManager +{ +public: + enum HandlerPriority + { + kStandardHandler, + kReplacementHandler, + kUnknown + }; + + /** + * Initialize the plugin manager. It's a singleton class which manages the plugins. + * @param pluginDir The directory where to search for the plugins. + * @param plugins Comma separated list of the plugins which should be loaded from the plugin directory. + * By default, all plug-ins available in the pluginDir will be loaded. + */ + static void initialize( const std::string& pluginDir, const std::string& plugins = std::string() ); + + /** + * Terminate the plugin manager. + */ + static void terminate(); + + /** + * Add file handler corresponding to the given format + * @param format FileFormat supported by the file handler + * @param handler shared pointer to the file handler which is to be added. + * @return Void. + */ + static void addFileHandler( XMP_FileFormat format, FileHandlerSharedPtr handler ); + + /** + * Returns file handler corresponding to the given format and priority + * + * @param format FileFormat supported by the file handler + * @param priority The priority of the handler (there could be one standard and + * one replacing handler, use the enums kStandardHandler or kReplacementHandler) + * @return shared pointer to the file handler. It does not need to be freed. + */ + static FileHandlerSharedPtr getFileHandler( XMP_FileFormat format, HandlerPriority priority = kStandardHandler ); + + /** + * Store mapping between session reference (comes from plugin) and + * FileHandlerInstance. + * @param session Session reference from plugin + * @param handler FileHandlerInstance related to the session + */ + static void addHandlerInstance( SessionRef session, FileHandlerInstancePtr handler ); + + /** + * Remove mapping between session reference (comes from plugin) and + * FilehandlerInstance + * @param session Session reference from plugin + */ + static void removeHandlerInstance( SessionRef session ); + + /** + * Return FileHandlerInstance that is associated to the session reference + * @param session Session reference from plugin + * @return FileHandlerInstance + */ + static FileHandlerInstancePtr getHandlerInstance( SessionRef session ); + + /** + * Return the priority of the handler + * @param handler Instance of file handler + * @return Return kStandardHandler or kReplacementHandler + */ + static HandlerPriority getHandlerPriority( FileHandlerInstancePtr handler ); + + /** + * Return Host API + */ + static HostAPIRef getHostAPI( XMP_Uns32 version ); + +private: + PluginManager( const std::string& pluginDir, const std::string& plugins ); + ~PluginManager(); + + /** + * Terminate host API + */ + void terminateHostAPI(); + + /** + * Load resource file of the given module. + * @param module + * @return Void. + */ + void loadResourceFile( ModuleSharedPtr module ); + + /** + * Scan mPluginDir for the plugins. It also scans nested folder upto level inMaxNumOfNestedFolder. + * @param inMaxNumOfNestedFolder Nested level where to scan. + * @return Void. + */ + void doScan( const XMP_Int32 inMaxNumOfNestedFolder = 1 ); + + /** + * Scan recursively the directory /a inPath and insert the found plug-in in ioFoundLibs. + * @param inPath Full path of the directory name to be scanned. + * @param ioFoundLibs Vector of string. Found plug-in will be inserted in this vector. + * @param inLevel The current level + * @param inMaxNumOfNestedFolder Nested level where to scan upto. + * @return Void. + */ + void scanRecursive( + const std::string& inPath, + std::vector<std::string>& ioFoundLibs, + XMP_Int32 inLevel, + XMP_Int32 inMaxNestingLevel ); + + /** + * Setup passed in HostAPI structure for the host API v1 + */ + static void SetupHostAPI_V1( HostAPIRef hostAPI ); + + typedef std::map<XMP_FileFormat, FileHandlerPair> PluginHandlerMap; + typedef std::map<XMP_Uns32, HostAPIRef> HostAPIMap; + typedef std::map<SessionRef, FileHandlerInstancePtr> SessionMap; + + std::string mPluginDir; + StringVec mExtensions; + StringVec mPluginsNeeded; + ModuleVec mModules; + PluginHandlerMap mHandlers; + SessionMap mSessions; + HostAPIMap mHostAPIs; + + static PluginManager* msPluginManager; +}; + +} //namespace XMP_PLUGIN +#endif //PLUGINMANAGER_H diff --git a/XMPFiles/source/PluginHandler/XMPAtoms.cpp b/XMPFiles/source/PluginHandler/XMPAtoms.cpp new file mode 100644 index 0000000..ed01027 --- /dev/null +++ b/XMPFiles/source/PluginHandler/XMPAtoms.cpp @@ -0,0 +1,408 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "XMPAtoms.h" +#include "XMPFiles/source/HandlerRegistry.h" + +using namespace Common; + +namespace XMP_PLUGIN +{ + +struct XMPAtomMapping +{ + XMP_StringPtr name; + XMPAtom atom; +}; + +const XMPAtomMapping kXMPAtomVec[] = +{ + { "", emptyStr_K }, + { "Handler", Handler_K }, + { "Extensions", Extensions_K }, + { "Extension", Extension_K }, + { "FormatIDs", FormatIDs_K }, + { "FormatID", FormatID_K }, + { "HandlerType", HandlerType_K }, + { "Priority", OverwriteHdl_K }, + { "HandlerFlags", HandlerFlags_K }, + { "HandlerFlag", HandlerFlag_K }, + { "SerializeOptions", SerializeOptions_K }, + { "SerializeOption", SerializeOption_K }, + { "Version", Version_K }, + { "CheckFormat", CheckFormat_K }, + { "Name", Name_K }, + { "Offset", Offset_K }, + { "Length", Length_K }, + { "ByteSeq", ByteSeq_K }, + + // Handler types + { "NormalHandler", NormalHandler_K }, + { "OwningHandler", OwningHandler_K }, + { "FolderHandler", FolderHandler_K }, + + // Handler flags + { "kXMPFiles_CanInjectXMP", kXMPFiles_CanInjectXMP_K }, + { "kXMPFiles_CanExpand", kXMPFiles_CanExpand_K }, + { "kXMPFiles_CanRewrite", kXMPFiles_CanRewrite_K }, + { "kXMPFiles_PrefersInPlace", kXMPFiles_PrefersInPlace_K }, + { "kXMPFiles_CanReconcile", kXMPFiles_CanReconcile_K }, + { "kXMPFiles_AllowsOnlyXMP", kXMPFiles_AllowsOnlyXMP_K }, + { "kXMPFiles_ReturnsRawPacket", kXMPFiles_ReturnsRawPacket_K }, + { "kXMPFiles_HandlerOwnsFile", kXMPFiles_HandlerOwnsFile_K }, + { "kXMPFiles_AllowsSafeUpdate", kXMPFiles_AllowsSafeUpdate_K }, + { "kXMPFiles_NeedsReadOnlyPacket", kXMPFiles_NeedsReadOnlyPacket_K }, + { "kXMPFiles_UsesSidecarXMP", kXMPFiles_UsesSidecarXMP_K }, + { "kXMPFiles_FolderBasedFormat", kXMPFiles_FolderBasedFormat_K }, + + // Serialize option + { "kXMP_OmitPacketWrapper", kXMP_OmitPacketWrapper_K }, + { "kXMP_ReadOnlyPacket", kXMP_ReadOnlyPacket_K }, + { "kXMP_UseCompactFormat", kXMP_UseCompactFormat_K }, + { "kXMP_UseCanonicalFormat", kXMP_UseCanonicalFormat_K }, + { "kXMP_IncludeThumbnailPad", kXMP_IncludeThumbnailPad_K }, + { "kXMP_ExactPacketLength", kXMP_ExactPacketLength_K }, + { "kXMP_OmitAllFormatting", kXMP_OmitAllFormatting_K }, + { "kXMP_OmitXMPMetaElement", kXMP_OmitXMPMetaElement_K }, + { "kXMP_EncodingMask", kXMP_EncodingMask_K }, + { "kXMP_EncodeUTF8", kXMP_EncodeUTF8_K }, + { "kXMP_EncodeUTF16Big", kXMP_EncodeUTF16Big_K }, + { "kXMP_EncodeUTF16Little", kXMP_EncodeUTF16Little_K }, + { "kXMP_EncodeUTF32Big", kXMP_EncodeUTF32Big_K }, + { "kXMP_EncodeUTF32Little", kXMP_EncodeUTF32Little_K } + +}; + + +XMPAtomsMap* ResourceParser::msXMPAtoms = NULL; +void ResourceParser::clear() +{ + mUID.clear(); + mFileExtensions.clear(); + mFormatIDs.clear(); + mCheckFormat.clear(); + mHandler = FileHandlerSharedPtr(); + mFlags = mSerializeOption = mType = mVersion = 0; +} + +void ResourceParser::addHandler() +{ + if( mUID.empty() || ( mFileExtensions.empty() && mFormatIDs.empty() ) || !isValidHandlerType( mType ) || mFlags == 0 ) + { + XMP_Throw( "Atleast one of uid, format, ext, typeStr, flags non-valid ...", kXMPErr_Unavailable ); + } + else + { + mHandler->setHandlerFlags( mFlags ); + mHandler->setHandlerType( mType ); + mHandler->setSerializeOption( mSerializeOption ); + mHandler->setOverwriteHandler( mOverwriteHandler ); + if( mVersion != 0) mHandler->setVersion( mVersion ); + + // A plugin could define the XMP_FileFormat value in manifest file through keyword "FormatID" and + // file extensions for NormalHandler and OwningHandler through keyword "Extension". + // If Both are defined then give priority to FormatID. + + std::set<XMP_FileFormat> formatIDs = mFormatIDs.empty() ? mFileExtensions : mFormatIDs; + for( std::set<XMP_FileFormat>::const_iterator it = formatIDs.begin(); it != formatIDs.end(); ++it ) + { + PluginManager::addFileHandler(*it, mHandler); + } + } +} + +bool ResourceParser::parseElementAttrs( const XML_Node * xmlNode, bool isTopLevel ) +{ + XMP_OptionBits exclusiveAttrs = 0; // Used to detect attributes that are mutually exclusive. + + XMPAtom nodeAtom = getXMPAtomFromString( xmlNode->name ); + if( nodeAtom == Handler_K ) + this->clear(); + + XML_cNodePos currAttr = xmlNode->attrs.begin(); + XML_cNodePos endAttr = xmlNode->attrs.end(); + + for( ; currAttr != endAttr; ++currAttr ) + { + XMP_OptionBits oneflag=0; + XMP_FileFormat formatID = kXMP_UnknownFile; + std::string formatStr; + XMPAtom attrAtom = getXMPAtomFromString( (*currAttr)->name ); + switch(nodeAtom) + { + case Handler_K: + switch(attrAtom) + { + case Name_K: + this->mUID = (*currAttr)->value; + mHandler = FileHandlerSharedPtr( new FileHandler( this->mUID, 0, 0, mModule ) ); + break; + case Version_K: + this->mVersion = atoi( (*currAttr)->value.c_str() ); + break; + case HandlerType_K: + this->mType = getXMPAtomFromString( (*currAttr)->value ); + break; + case OverwriteHdl_K: + this->mOverwriteHandler = ( (*currAttr)->value == "true" ); + break; + default: + XMP_Throw( "Invalid Attr in Handler encountered in resource file", kXMPErr_Unavailable ); + } + break; + case CheckFormat_K: + switch(attrAtom) + { + case Offset_K: + this->mCheckFormat.mOffset = atoi( (*currAttr)->value.c_str() ); + break; + case Length_K: + this->mCheckFormat.mLength = atoi( (*currAttr)->value.c_str() ); + break; + case ByteSeq_K: + this->mCheckFormat.mByteSeq = (*currAttr)->value; + break; + default: + XMP_Throw( "Invalid Attr in CheckFormat encountered in resource file", kXMPErr_Unavailable ); + } + break; + case Extension_K: + switch(attrAtom) + { + case Name_K: + this->mFileExtensions.insert( HandlerRegistry::getInstance().getFileFormat( (*currAttr)->value, true) ); + break; + default: + XMP_Throw( "Invalid Attr in Extension encountered in resource file", kXMPErr_Unavailable ); + } + break; + case FormatID_K: + switch(attrAtom) + { + case Name_K: + formatStr.assign( (*currAttr)->value ); + + // Convert string into 4-byte string by appending space '0x20' character. + for( size_t j=formatStr.size(); j<4; j++ ) + formatStr.push_back(' '); + + formatID = GetUns32BE( formatStr.c_str() ); + this->mFormatIDs.insert( formatID ); + break; + default: + XMP_Throw( "Invalid Attr in FormatID encountered in resource file", kXMPErr_Unavailable ); + } + break; + case HandlerFlag_K: + switch(attrAtom) + { + case Name_K: + oneflag = getHandlerFlag( (*currAttr)->value ); + if( 0 == oneflag ) + XMP_Throw( "Invalid handler flag found in resource file...", kXMPErr_Unavailable ); + this->mFlags |= oneflag; + break; + default: + XMP_Throw( "Invalid handler flag found in resource file...", kXMPErr_Unavailable ); + } + break; + case SerializeOption_K: + switch(attrAtom) + { + case Name_K: + oneflag = getSerializeOption( (*currAttr)->value ); + if( 0 == oneflag ) + XMP_Throw( "Invalid serialize option found in resource file...", kXMPErr_Unavailable ); + this->mSerializeOption |= oneflag; + break; + default: + XMP_Throw( "Invalid handler flag found in resource file...", kXMPErr_Unavailable ); + } + break; + default: + break; + } + } + + if( nodeAtom == CheckFormat_K ) + mHandler->addCheckFormat( mCheckFormat ); + + return (nodeAtom == Handler_K) ? true : false; +} // parseElementAttrs + +void ResourceParser::parseElement( const XML_Node * xmlNode, bool isTopLevel ) +{ + bool HandlerFound = this->parseElementAttrs( xmlNode, isTopLevel ); + this->parseElementList ( xmlNode, false ); + + if(HandlerFound) + { + this->addHandler(); + } +} + +void ResourceParser::parseElementList( const XML_Node * xmlParent, bool isTopLevel ) +{ + initialize(); //Make sure XMPAtoms are initialized. + + XML_cNodePos currChild = xmlParent->content.begin(); + XML_cNodePos endChild = xmlParent->content.end(); + + for( ; currChild != endChild; ++currChild ) + { + if( (*currChild)->IsWhitespaceNode() ) continue; + this->parseElement( *currChild, isTopLevel ); + } +} + +bool ResourceParser::initialize() +{ + //Check if already populated. + if( msXMPAtoms == NULL ) + { + msXMPAtoms = new XMPAtomsMap(); + + //Create XMPAtomMap from XMPAtomVec, as we need to do a lot of searching while processing the resource file. + int count = sizeof(kXMPAtomVec)/sizeof(XMPAtomMapping); + for( int i=0; i<count; i++ ) + { + XMP_StringPtr name = kXMPAtomVec[i].name; + XMPAtom atom = kXMPAtomVec[i].atom; + (*msXMPAtoms)[name] = atom; + } + } + return true; +} + +void ResourceParser::terminate() +{ + delete msXMPAtoms; + msXMPAtoms = NULL; +} + +XMPAtom ResourceParser::getXMPAtomFromString( const std::string & stringAtom ) +{ + XMPAtomsMap::const_iterator it = msXMPAtoms->find(stringAtom); + + if( it != msXMPAtoms->end() ) + return it->second; + + return XMPAtomNull; +} + +//static +XMP_OptionBits ResourceParser::getHandlerFlag( const std::string & stringAtom ) +{ + XMPAtom atom = getXMPAtomFromString( stringAtom ); + + if( !isValidXMPAtom(atom) ) + return 0; + + switch( atom ) + { + case kXMPFiles_CanInjectXMP_K: + return kXMPFiles_CanInjectXMP; + case kXMPFiles_CanExpand_K: + return kXMPFiles_CanExpand; + case kXMPFiles_CanRewrite_K: + return kXMPFiles_CanRewrite; + case kXMPFiles_PrefersInPlace_K: + return kXMPFiles_PrefersInPlace; + case kXMPFiles_CanReconcile_K: + return kXMPFiles_CanReconcile; + case kXMPFiles_AllowsOnlyXMP_K: + return kXMPFiles_AllowsOnlyXMP; + case kXMPFiles_ReturnsRawPacket_K: + return kXMPFiles_ReturnsRawPacket; + case kXMPFiles_HandlerOwnsFile_K: + return kXMPFiles_HandlerOwnsFile; + case kXMPFiles_AllowsSafeUpdate_K: + return kXMPFiles_AllowsSafeUpdate; + case kXMPFiles_NeedsReadOnlyPacket_K: + return kXMPFiles_NeedsReadOnlyPacket; + case kXMPFiles_UsesSidecarXMP_K: + return kXMPFiles_UsesSidecarXMP; + case kXMPFiles_FolderBasedFormat_K: + return kXMPFiles_FolderBasedFormat; + default: + XMP_Throw( "Invalid PluginhandlerFlag ...", kXMPErr_Unavailable ); + return 0; + } +} + +//static +XMP_OptionBits ResourceParser::getSerializeOption( const std::string & stringAtom ) +{ + XMPAtom atom = getXMPAtomFromString( stringAtom ); + + if( !isValidXMPAtom(atom) ) + return 0; + + switch( atom ) + { + case kXMP_OmitPacketWrapper_K: + return kXMP_OmitPacketWrapper; + case kXMP_ReadOnlyPacket_K: + return kXMP_ReadOnlyPacket; + case kXMP_UseCompactFormat_K: + return kXMP_UseCompactFormat; + case kXMP_UseCanonicalFormat_K: + return kXMP_UseCanonicalFormat; + case kXMP_IncludeThumbnailPad_K: + return kXMP_IncludeThumbnailPad; + case kXMP_ExactPacketLength_K: + return kXMP_ExactPacketLength; + case kXMP_OmitAllFormatting_K: + return kXMP_OmitAllFormatting; + case kXMP_OmitXMPMetaElement_K: + return kXMP_OmitXMPMetaElement; + case kXMP_EncodingMask_K: + return kXMP_EncodingMask; + case kXMP_EncodeUTF8_K: + return kXMP_EncodeUTF8; + case kXMP_EncodeUTF16Big_K: + return kXMP_EncodeUTF16Big; + case kXMP_EncodeUTF16Little_K: + return kXMP_EncodeUTF16Little; + case kXMP_EncodeUTF32Big_K: + return kXMP_EncodeUTF32Big; + case kXMP_EncodeUTF32Little_K: + return kXMP_EncodeUTF32Little; + default: + XMP_Throw( "Invalid Serialize Option ...", kXMPErr_Unavailable ); + return 0; + } +} + +XMP_FileFormat ResourceParser::getPluginFileFormat( const std::string & fileExt, bool AddIfNotFound ) +{ + XMP_FileFormat format = kXMP_UnknownFile; + + if( msXMPAtoms ) + { + XMPAtomsMap::const_iterator iter = msXMPAtoms->find( fileExt ); + if( iter != msXMPAtoms->end() ) + { + format = iter->second; + } + else if( AddIfNotFound ) + { + std::string formatStr(fileExt); + MakeUpperCase(&formatStr); + for( size_t j=formatStr.size(); j<4; j++ ) // Convert "pdf" to "PDF " + formatStr.push_back(' '); + format = GetUns32BE( formatStr.c_str() ); //Convert "PDF " into kXMP_PDFFile + (*msXMPAtoms)[ fileExt ] = format; + } + } + + return format; +} + +} //namespace XMP_PLUGIN diff --git a/XMPFiles/source/PluginHandler/XMPAtoms.h b/XMPFiles/source/PluginHandler/XMPAtoms.h new file mode 100644 index 0000000..5004008 --- /dev/null +++ b/XMPFiles/source/PluginHandler/XMPAtoms.h @@ -0,0 +1,200 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2011 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#ifndef _H_XMPATOMS +#define _H_XMPATOMS +#include "FileHandler.h" +#include "source/ExpatAdapter.hpp" +#include <set> + +namespace XMP_PLUGIN +{ + +/** @brief An enum required to parse the resource file of plugin. + */ +enum +{ + emptyStr_K = 0, //Empty string + + // Mandatory keys in the resource file + Handler_K, + Extensions_K, + Extension_K, + FormatIDs_K, + FormatID_K, + HandlerType_K, + OverwriteHdl_K, + HandlerFlags_K, + HandlerFlag_K, + SerializeOptions_K, + SerializeOption_K, + Version_K, + CheckFormat_K, + Name_K, + Offset_K, + Length_K, + ByteSeq_K, + + // Handler types + NormalHandler_K, + OwningHandler_K, + FolderHandler_K, + + // Handler flags + kXMPFiles_CanInjectXMP_K, + kXMPFiles_CanExpand_K, + kXMPFiles_CanRewrite_K, + kXMPFiles_PrefersInPlace_K, + kXMPFiles_CanReconcile_K, + kXMPFiles_AllowsOnlyXMP_K, + kXMPFiles_ReturnsRawPacket_K, + kXMPFiles_HandlerOwnsFile_K, + kXMPFiles_AllowsSafeUpdate_K, + kXMPFiles_NeedsReadOnlyPacket_K, + kXMPFiles_UsesSidecarXMP_K, + kXMPFiles_FolderBasedFormat_K, + + // Serialize option + kXMP_OmitPacketWrapper_K, + kXMP_ReadOnlyPacket_K, + kXMP_UseCompactFormat_K, + kXMP_UseCanonicalFormat_K, + kXMP_IncludeThumbnailPad_K, + kXMP_ExactPacketLength_K, + kXMP_OmitAllFormatting_K, + kXMP_OmitXMPMetaElement_K, + kXMP_EncodingMask_K, + kXMP_EncodeUTF8_K, + kXMP_EncodeUTF16Big_K, + kXMP_EncodeUTF16Little_K, + kXMP_EncodeUTF32Big_K, + kXMP_EncodeUTF32Little_K, + + //Last element + lastfinal_K +}; + +#define XMPAtomNull emptyStr_K + +struct StringCompare : std::binary_function<const std::string &, const std::string &, bool> +{ + bool operator() (const std::string & a, const std::string & b) const + { + return ( a.compare(b) < 0 ); + } +}; +typedef std::map<std::string, XMPAtom, StringCompare> XMPAtomsMap; + + +/** @class ResourceParser + * @brief Class to parse resource file of the plugin. + */ +class ResourceParser +{ +public: + ResourceParser(ModuleSharedPtr module) + : mModule(module), mFlags(0), mSerializeOption(0), mType(0), mVersion(0), mOverwriteHandler(false) {} + + /** + * Initialize the XMPAtoms which will be used in parsing resource files. + * @return true on success. + */ + static bool initialize(); + static void terminate(); + + /** @brief Return file format corresponding to file extension \a fileExt. + * + * It is similar to GetXMPFileFormat except that it also searches in PluginManager's + * private file formats. If the extension is not a public file format and \a AddIfNotFound is true + * then it also adds the private fileformat. The priavte 4-byte file format is created by converting + * extension to upper case and appending space '0x20' to make it 4-byte. For i.e + * "pdf" -> 'PDF ' (0x50444620) + * "tmp" -> 'TMP ' (0x54415020) + * "temp" -> 'TEMP' (0x54454150) + * + * @param fileExt Extension string of the file. For i.e "pdf" + * @param AddIfNotFound If AddIfNotFound is true and public format is not found then + * priavte format is added. + * @return File format correspoding to file extension \a fileExt. + */ + static XMP_FileFormat getPluginFileFormat( const std::string & fileExt, bool AddIfNotFound ); + + /** + * Parse the XML node's children recursively. + * @param xmlParent XMLNode which will be parsed. + * @return Void. + */ + void parseElementList( const XML_Node * xmlParent, bool isTopLevel ); + +private: + void clear(); + void addHandler(); + + /** + * Parse the XML node's attribute. + * @param xmlNode XMLNode which will be parsed. + * @return true if xmlNode's name is Handler_K. + */ + bool parseElementAttrs( const XML_Node * xmlNode, bool isTopLevel ); + + /** + * Parse the XML node. + * @param xmlNode XMLNode which will be parsed. + * @return Void. + */ + void parseElement( const XML_Node * xmlNode, bool isTopLevel ); + + /** + * Return XMPAtom corresponding to string. + * @param stringAtom std::string whose XMPAtom will be return. + * @return XMPAtom corresponding to string. + */ + static XMPAtom getXMPAtomFromString( const std::string & stringAtom ); + + /** + * Return Handler flag corresponding to XMPAtom string. + * @param stringAtom Handler flag string + * @return Handler flag. + */ + static XMP_OptionBits getHandlerFlag( const std::string & stringAtom ); + + /** + * Return Serialize option corresponding to XMPAtom string. + * @param stringAtom Handler flag string + * @return Serialize option. + */ + static XMP_OptionBits getSerializeOption( const std::string & stringAtom ); + + static inline bool isValidXMPAtom( XMPAtom atom ) + { + return ( atom > emptyStr_K && atom < lastfinal_K ); + } + + static inline bool isValidHandlerType(XMPAtom atom) + { + return ( atom >= NormalHandler_K && atom <= FolderHandler_K ); + } + + ModuleSharedPtr mModule; + std::string mUID; + FileHandlerType mType; + XMP_OptionBits mFlags; + XMP_OptionBits mSerializeOption; + XMP_Uns32 mVersion; + bool mOverwriteHandler; + CheckFormat mCheckFormat; + std::set<XMP_FileFormat> mFileExtensions; + std::set<XMP_FileFormat> mFormatIDs; + FileHandlerSharedPtr mHandler; + + static XMPAtomsMap * msXMPAtoms; +}; + +} //namespace XMP_PLUGIN +#endif // _H_XMPATOMS diff --git a/XMPFiles/source/WXMPFiles.cpp b/XMPFiles/source/WXMPFiles.cpp new file mode 100644 index 0000000..f0f7b52 --- /dev/null +++ b/XMPFiles/source/WXMPFiles.cpp @@ -0,0 +1,345 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" +#include "public/include/XMP_Const.h" + +#include "public/include/client-glue/WXMPFiles.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "XMPFiles/source/XMPFiles.hpp" + +#if XMP_WinBuild + #if XMP_DebugBuild + #pragma warning ( disable : 4297 ) // function assumed not to throw an exception but does + #endif +#endif + +#if __cplusplus +extern "C" { +#endif + +// ================================================================================================= + +static WXMP_Result voidResult; // Used for functions that don't use the normal result mechanism. + +// ================================================================================================= + +void WXMPFiles_GetVersionInfo_1 ( XMP_VersionInfo * versionInfo ) +{ + WXMP_Result * wResult = &voidResult; // ! Needed to "fool" the EnterWrapper macro. + XMP_ENTER_NoLock ( "WXMPFiles_GetVersionInfo_1" ) + + XMPFiles::GetVersionInfo ( versionInfo ); + + XMP_EXIT_NoThrow +} + +// ------------------------------------------------------------------------------------------------- + +void WXMPFiles_Initialize_1 ( XMP_OptionBits options, + WXMP_Result * wResult ) +{ + XMP_ENTER_NoLock ( "WXMPFiles_Initialize_1" ) + + wResult->int32Result = XMPFiles::Initialize ( options, NULL, NULL ); + + XMP_EXIT +} + +// ------------------------------------------------------------------------------------------------- + +// ------------------------------------------------------------------------------------------------- + +void WXMPFiles_Initialize_2 ( XMP_OptionBits options, + const char * pluginFolder, + const char * plugins, + WXMP_Result * wResult ) +{ + XMP_ENTER_NoLock ( "WXMPFiles_Initialize_1" ) + + wResult->int32Result = XMPFiles::Initialize ( options, pluginFolder, plugins ); + + XMP_EXIT +} + +// ------------------------------------------------------------------------------------------------- + +void WXMPFiles_Terminate_1() +{ + WXMP_Result * wResult = &voidResult; // ! Needed to "fool" the EnterWrapper macro. + XMP_ENTER_NoLock ( "WXMPFiles_Terminate_1" ) + + XMPFiles::Terminate(); + + XMP_EXIT_NoThrow +} + +// ================================================================================================= + +void WXMPFiles_CTor_1 ( WXMP_Result * wResult ) +{ + XMP_ENTER_Static ( "WXMPFiles_CTor_1" ) // No lib object yet, use the static entry. + + XMPFiles * newObj = new XMPFiles(); + ++newObj->clientRefs; + XMP_Assert ( newObj->clientRefs == 1 ); + wResult->ptrResult = newObj; + + XMP_EXIT +} + +// ------------------------------------------------------------------------------------------------- + +void WXMPFiles_IncrementRefCount_1 ( XMPFilesRef xmpObjRef ) +{ + WXMP_Result * wResult = &voidResult; // ! Needed to "fool" the EnterWrapper macro. + XMP_ENTER_ObjWrite ( XMPFiles, "WXMPFiles_IncrementRefCount_1" ) + + ++thiz->clientRefs; + XMP_Assert ( thiz->clientRefs > 0 ); + + XMP_EXIT_NoThrow +} + +// ------------------------------------------------------------------------------------------------- + +void WXMPFiles_DecrementRefCount_1 ( XMPFilesRef xmpObjRef ) +{ + WXMP_Result * wResult = &voidResult; // ! Needed to "fool" the EnterWrapper macro. + XMP_ENTER_ObjWrite ( XMPFiles, "WXMPFiles_DecrementRefCount_1" ) + + XMP_Assert ( thiz->clientRefs > 0 ); + --thiz->clientRefs; + if ( thiz->clientRefs <= 0 ) { + objLock.Release(); + delete ( thiz ); + } + + XMP_EXIT_NoThrow +} + +// ================================================================================================= + +void WXMPFiles_GetFormatInfo_1 ( XMP_FileFormat format, + XMP_OptionBits * flags, + WXMP_Result * wResult ) +{ + XMP_ENTER_Static ( "WXMPFiles_GetFormatInfo_1" ) + + wResult->int32Result = XMPFiles::GetFormatInfo ( format, flags ); + + XMP_EXIT +} + +// ================================================================================================= + +void WXMPFiles_CheckFileFormat_1 ( XMP_StringPtr filePath, + WXMP_Result * wResult ) +{ + XMP_ENTER_Static ( "WXMPFiles_CheckFileFormat_1" ) + + wResult->int32Result = XMPFiles::CheckFileFormat ( filePath ); + + XMP_EXIT +} + +// ================================================================================================= + +void WXMPFiles_CheckPackageFormat_1 ( XMP_StringPtr folderPath, + WXMP_Result * wResult ) +{ + XMP_ENTER_Static ( "WXMPFiles_CheckPackageFormat_1" ) + + wResult->int32Result = XMPFiles::CheckPackageFormat ( folderPath ); + + XMP_EXIT +} + +// ================================================================================================= + +void WXMPFiles_GetFileModDate_1 ( XMP_StringPtr filePath, + XMP_DateTime * modDate, + XMP_FileFormat * format, + XMP_OptionBits options, + WXMP_Result * wResult ) +{ + XMP_ENTER_Static ( "WXMPFiles_GetFileModDate_1" ) + + wResult->int32Result = XMPFiles::GetFileModDate ( filePath, modDate, format, options ); + + XMP_EXIT +} + +// ================================================================================================= + +void WXMPFiles_OpenFile_1 ( XMPFilesRef xmpObjRef, + XMP_StringPtr filePath, + XMP_FileFormat format, + XMP_OptionBits openFlags, + WXMP_Result * wResult ) +{ + XMP_ENTER_ObjWrite ( XMPFiles, "WXMPFiles_OpenFile_1" ) + StartPerfCheck ( kAPIPerf_OpenFile, filePath ); + + bool ok = thiz->OpenFile ( filePath, format, openFlags ); + wResult->int32Result = ok; + + EndPerfCheck ( kAPIPerf_OpenFile ); + XMP_EXIT +} + +// ------------------------------------------------------------------------------------------------- + +#if XMP_StaticBuild // ! Client XMP_IO objects can only be used in static builds. +void WXMPFiles_OpenFile_2 ( XMPFilesRef xmpObjRef, + XMP_IO* clientIO, + XMP_FileFormat format, + XMP_OptionBits openFlags, + WXMP_Result * wResult ) +{ + XMP_ENTER_ObjWrite ( XMPFiles, "WXMPFiles_OpenFile_2" ) + StartPerfCheck ( kAPIPerf_OpenFile, "<client-managed>" ); + + bool ok = thiz->OpenFile ( clientIO, format, openFlags ); + wResult->int32Result = ok; + + EndPerfCheck ( kAPIPerf_OpenFile ); + XMP_EXIT +} +#endif + +// ------------------------------------------------------------------------------------------------- + +void WXMPFiles_CloseFile_1 ( XMPFilesRef xmpObjRef, + XMP_OptionBits closeFlags, + WXMP_Result * wResult ) +{ + XMP_ENTER_ObjWrite ( XMPFiles, "WXMPFiles_CloseFile_1" ) + StartPerfCheck ( kAPIPerf_CloseFile, "" ); + + thiz->CloseFile ( closeFlags ); + + EndPerfCheck ( kAPIPerf_CloseFile ); + XMP_EXIT +} + +// ------------------------------------------------------------------------------------------------- + +void WXMPFiles_GetFileInfo_1 ( XMPFilesRef xmpObjRef, + void * clientPath, + XMP_OptionBits * openFlags, + XMP_FileFormat * format, + XMP_OptionBits * handlerFlags, + SetClientStringProc SetClientString, + WXMP_Result * wResult ) +{ + XMP_ENTER_ObjRead ( XMPFiles, "WXMPFiles_GetFileInfo_1" ) + + XMP_StringPtr pathStr; + XMP_StringLen pathLen; + + bool isOpen = thiz.GetFileInfo ( &pathStr, &pathLen, openFlags, format, handlerFlags ); + if ( isOpen && (clientPath != 0) ) (*SetClientString) ( clientPath, pathStr, pathLen ); + wResult->int32Result = isOpen; + + XMP_EXIT +} + +// ------------------------------------------------------------------------------------------------- + +void WXMPFiles_SetAbortProc_1 ( XMPFilesRef xmpObjRef, + XMP_AbortProc abortProc, + void * abortArg, + WXMP_Result * wResult ) +{ + XMP_ENTER_ObjWrite ( XMPFiles, "WXMPFiles_SetAbortProc_1" ) + + thiz->SetAbortProc ( abortProc, abortArg ); + + XMP_EXIT +} + +// ------------------------------------------------------------------------------------------------- + +void WXMPFiles_GetXMP_1 ( XMPFilesRef xmpObjRef, + XMPMetaRef xmpRef, + void * clientPacket, + XMP_PacketInfo * packetInfo, + SetClientStringProc SetClientString, + WXMP_Result * wResult ) +{ + XMP_ENTER_ObjWrite ( XMPFiles, "WXMPFiles_GetXMP_1" ) + StartPerfCheck ( kAPIPerf_GetXMP, "" ); + + bool hasXMP = false; + XMP_StringPtr packetStr; + XMP_StringLen packetLen; + + if ( xmpRef == 0 ) { + hasXMP = thiz->GetXMP ( 0, &packetStr, &packetLen, packetInfo ); + } else { + SXMPMeta xmpObj ( xmpRef ); + hasXMP = thiz->GetXMP ( &xmpObj, &packetStr, &packetLen, packetInfo ); + } + + if ( hasXMP && (clientPacket != 0) ) (*SetClientString) ( clientPacket, packetStr, packetLen ); + wResult->int32Result = hasXMP; + + EndPerfCheck ( kAPIPerf_GetXMP ); + XMP_EXIT +} + +// ------------------------------------------------------------------------------------------------- + +void WXMPFiles_PutXMP_1 ( XMPFilesRef xmpObjRef, + XMPMetaRef xmpRef, // ! Only one of the XMP object or packet are passed. + XMP_StringPtr xmpPacket, + XMP_StringLen xmpPacketLen, + WXMP_Result * wResult ) +{ + XMP_ENTER_ObjWrite ( XMPFiles, "WXMPFiles_PutXMP_1" ) + StartPerfCheck ( kAPIPerf_PutXMP, "" ); + + if ( xmpRef != 0 ) { + thiz->PutXMP ( xmpRef ); + } else { + thiz->PutXMP ( xmpPacket, xmpPacketLen ); + } + + EndPerfCheck ( kAPIPerf_PutXMP ); + XMP_EXIT +} + +// ------------------------------------------------------------------------------------------------- + +void WXMPFiles_CanPutXMP_1 ( XMPFilesRef xmpObjRef, + XMPMetaRef xmpRef, // ! Only one of the XMP object or packet are passed. + XMP_StringPtr xmpPacket, + XMP_StringLen xmpPacketLen, + WXMP_Result * wResult ) +{ + XMP_ENTER_ObjWrite ( XMPFiles, "WXMPFiles_CanPutXMP_1" ) + StartPerfCheck ( kAPIPerf_CanPutXMP, "" ); + + if ( xmpRef != 0 ) { + wResult->int32Result = thiz->CanPutXMP ( xmpRef ); + } else { + wResult->int32Result = thiz->CanPutXMP ( xmpPacket, xmpPacketLen ); + } + + EndPerfCheck ( kAPIPerf_CanPutXMP ); + XMP_EXIT +} + +// ================================================================================================= + +#if __cplusplus +} +#endif diff --git a/XMPFiles/source/XMPFiles.cpp b/XMPFiles/source/XMPFiles.cpp new file mode 100644 index 0000000..b720725 --- /dev/null +++ b/XMPFiles/source/XMPFiles.cpp @@ -0,0 +1,1084 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! Must be the first #include! +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include <vector> +#include <string.h> + +#include "source/UnicodeConversions.hpp" +#include "source/XMPFiles_IO.hpp" +#include "source/XIO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "XMPFiles/source/HandlerRegistry.h" +#include "XMPFiles/source/PluginHandler/PluginManager.h" + +#include "XMPFiles/source/FormatSupport/ID3_Support.hpp" + +#if EnablePacketScanning + #include "XMPFiles/source/FileHandlers/Scanner_Handler.hpp" +#endif + +// ================================================================================================= +/// \file XMPFiles.cpp +/// \brief High level support to access metadata in files of interest to Adobe applications. +/// +/// This header ... +/// +// ================================================================================================= + +using namespace Common; +using namespace XMP_PLUGIN; + +// ================================================================================================= + +XMP_Int32 sXMPFilesInitCount = 0; + +#if GatherPerformanceData + APIPerfCollection* sAPIPerf = 0; +#endif + +// These are embedded version strings. + +#if XMP_DebugBuild + #define kXMPFiles_DebugFlag 1 +#else + #define kXMPFiles_DebugFlag 0 +#endif + +#define kXMPFiles_VersionNumber ( (kXMPFiles_DebugFlag << 31) | \ + (XMP_API_VERSION_MAJOR << 24) | \ + (XMP_API_VERSION_MINOR << 16) | \ + (XMP_API_VERSION_MICRO << 8) ) + + #define kXMPFilesName "XMP Files" + #define kXMPFiles_VersionMessage kXMPFilesName " " XMPFILES_API_VERSION_STRING +const char * kXMPFiles_EmbeddedVersion = kXMPFiles_VersionMessage; +const char * kXMPFiles_EmbeddedCopyright = kXMPFilesName " " kXMP_CopyrightStr; + +// ================================================================================================= + +#if EnablePacketScanning + static XMPFileHandlerInfo kScannerHandlerInfo ( kXMP_UnknownFile, kScanner_HandlerFlags, + (CheckFileFormatProc)0, Scanner_MetaHandlerCTor ); +#endif + +// ================================================================================================= + +/* class-static */ +void +XMPFiles::GetVersionInfo ( XMP_VersionInfo * info ) +{ + + memset ( info, 0, sizeof(XMP_VersionInfo) ); + + info->major = XMPFILES_API_VERSION_MAJOR; + info->minor = XMPFILES_API_VERSION_MINOR; + info->micro = 0; //no longer used + info->isDebug = kXMPFiles_DebugFlag; + info->flags = 0; // ! None defined yet. + info->message = kXMPFiles_VersionMessage; + +} // XMPFiles::GetVersionInfo + +// ================================================================================================= + +#if XMP_TraceFilesCalls + FILE * xmpFilesLog = stderr; +#endif + +#if UseGlobalLibraryLock & (! XMP_StaticBuild ) + XMP_BasicMutex sLibraryLock; // ! Handled in XMPMeta for static builds. +#endif + +/* class static */ +bool +XMPFiles::Initialize( XMP_OptionBits options, const char* pluginFolder, const char* plugins /* = NULL */ ) +{ + ++sXMPFilesInitCount; + if ( sXMPFilesInitCount > 1 ) return true; + + #if XMP_TraceFilesCallsToFile + xmpFilesLog = fopen ( "XMPFilesLog.txt", "w" ); + if ( xmpFilesLog == 0 ) xmpFilesLog = stderr; + #endif + + #if UseGlobalLibraryLock & (! XMP_StaticBuild ) + InitializeBasicMutex ( sLibraryLock ); // ! Handled in XMPMeta for static builds. + #endif + + SXMPMeta::Initialize(); // Just in case the client does not. + + if ( ! Initialize_LibUtils() ) return false; + if ( ! ID3_Support::InitializeGlobals() ) return false; + + #if GatherPerformanceData + sAPIPerf = new APIPerfCollection; + #endif + + XMP_Uns16 endianInt = 0x00FF; + XMP_Uns8 endianByte = *((XMP_Uns8*)&endianInt); + if ( kBigEndianHost ) { + if ( endianByte != 0 ) XMP_Throw ( "Big endian flag mismatch", kXMPErr_InternalFailure ); + } else { + if ( endianByte != 0xFF ) XMP_Throw ( "Little endian flag mismatch", kXMPErr_InternalFailure ); + } + + XMP_Assert ( kUTF8_PacketHeaderLen == strlen ( "<?xpacket begin='xxx' id='W5M0MpCehiHzreSzNTczkc9d'" ) ); + XMP_Assert ( kUTF8_PacketTrailerLen == strlen ( (const char *) kUTF8_PacketTrailer ) ); + + HandlerRegistry::getInstance().initialize(); + + InitializeUnicodeConversions(); + + ignoreLocalText = XMP_OptionIsSet ( options, kXMPFiles_IgnoreLocalText ); + #if XMP_UNIXBuild + if ( ! ignoreLocalText ) XMP_Throw ( "Generic UNIX clients must pass kXMPFiles_IgnoreLocalText", kXMPErr_EnforceFailure ); + #endif + + if ( pluginFolder != 0 ) { + std::string pluginList; + if ( plugins != 0 ) pluginList.assign ( plugins ); + PluginManager::initialize ( pluginFolder, pluginList ); // Load file handler plugins. + } + + // Make sure the embedded info strings are referenced and kept. + if ( (kXMPFiles_EmbeddedVersion[0] == 0) || (kXMPFiles_EmbeddedCopyright[0] == 0) ) return false; + // Verify critical type sizes. + XMP_Assert ( sizeof(XMP_Int8) == 1 ); + XMP_Assert ( sizeof(XMP_Int16) == 2 ); + XMP_Assert ( sizeof(XMP_Int32) == 4 ); + XMP_Assert ( sizeof(XMP_Int64) == 8 ); + XMP_Assert ( sizeof(XMP_Uns8) == 1 ); + XMP_Assert ( sizeof(XMP_Uns16) == 2 ); + XMP_Assert ( sizeof(XMP_Uns32) == 4 ); + XMP_Assert ( sizeof(XMP_Uns64) == 8 ); + + return true; + +} // XMPFiles::Initialize + +// ================================================================================================= + +#if GatherPerformanceData + +#if XMP_WinBuild + #pragma warning ( disable : 4996 ) // '...' was declared deprecated +#endif + +#include "PerfUtils.cpp" + +static void ReportPerformanceData() +{ + struct SummaryInfo { + size_t callCount; + double totalTime; + SummaryInfo() : callCount(0), totalTime(0.0) {}; + }; + + SummaryInfo perfSummary [kAPIPerfProcCount]; + + XMP_DateTime now; + SXMPUtils::CurrentDateTime ( &now ); + std::string nowStr; + SXMPUtils::ConvertFromDate ( now, &nowStr ); + + #if XMP_WinBuild + #define kPerfLogPath "C:\\XMPFilesPerformanceLog.txt" + #else + #define kPerfLogPath "/XMPFilesPerformanceLog.txt" + #endif + FILE * perfLog = fopen ( kPerfLogPath, "ab" ); + if ( perfLog == 0 ) return; + + fprintf ( perfLog, "\n\n// =================================================================================================\n\n" ); + fprintf ( perfLog, "XMPFiles performance data\n" ); + fprintf ( perfLog, " %s\n", kXMPFiles_VersionMessage ); + fprintf ( perfLog, " Reported at %s\n", nowStr.c_str() ); + fprintf ( perfLog, " %s\n", PerfUtils::GetTimerInfo() ); + + // Gather and report the summary info. + + for ( size_t i = 0; i < sAPIPerf->size(); ++i ) { + SummaryInfo& summaryItem = perfSummary [(*sAPIPerf)[i].whichProc]; + ++summaryItem.callCount; + summaryItem.totalTime += (*sAPIPerf)[i].elapsedTime; + } + + fprintf ( perfLog, "\nSummary data:\n" ); + + for ( size_t i = 0; i < kAPIPerfProcCount; ++i ) { + long averageTime = 0; + if ( perfSummary[i].callCount != 0 ) { + averageTime = (long) (((perfSummary[i].totalTime/perfSummary[i].callCount) * 1000.0*1000.0) + 0.5); + } + fprintf ( perfLog, " %s, %d total calls, %d average microseconds per call\n", + kAPIPerfNames[i], perfSummary[i].callCount, averageTime ); + } + + fprintf ( perfLog, "\nPer-call data:\n" ); + + // Report the info for each call. + + for ( size_t i = 0; i < sAPIPerf->size(); ++i ) { + long time = (long) (((*sAPIPerf)[i].elapsedTime * 1000.0*1000.0) + 0.5); + fprintf ( perfLog, " %s, %d microSec, ref %.8X, \"%s\"\n", + kAPIPerfNames[(*sAPIPerf)[i].whichProc], time, + (*sAPIPerf)[i].xmpFilesRef, (*sAPIPerf)[i].extraInfo.c_str() ); + } + + fclose ( perfLog ); + +} // ReportAReportPerformanceDataPIPerformance + +#endif + +// ================================================================================================= + +/* class static */ +void +XMPFiles::Terminate() +{ + --sXMPFilesInitCount; + if ( sXMPFilesInitCount != 0 ) return; // Not ready to terminate, or already terminated. + + #if GatherPerformanceData + ReportPerformanceData(); + EliminateGlobal ( sAPIPerf ); + #endif + + PluginManager::terminate(); + HandlerRegistry::terminate(); + + SXMPMeta::Terminate(); // Just in case the client does not. + + ID3_Support::TerminateGlobals(); + Terminate_LibUtils(); + + #if UseGlobalLibraryLock & (! XMP_StaticBuild ) + TerminateBasicMutex ( sLibraryLock ); // ! Handled in XMPMeta for static builds. + #endif + + #if XMP_TraceFilesCallsToFile + if ( xmpFilesLog != stderr ) fclose ( xmpFilesLog ); + xmpFilesLog = stderr; + #endif + +} // XMPFiles::Terminate + +// ================================================================================================= + +XMPFiles::XMPFiles() : + clientRefs(0), + format(kXMP_UnknownFile), + ioRef(0), + openFlags(0), + handler(0), + tempPtr(0), + tempUI32(0), + abortProc(0), + abortArg(0) +{ + // Nothing more to do, clientRefs is incremented in wrapper. + +} // XMPFiles::XMPFiles + +// ================================================================================================= + +static inline void CloseLocalFile ( XMPFiles* thiz ) +{ + if ( thiz->UsesLocalIO() ) { + + XMPFiles_IO* localFile = (XMPFiles_IO*)thiz->ioRef; + + if ( localFile != 0 ) { + localFile->Close(); + delete localFile; + thiz->ioRef = 0; + } + + } + +} // CloseLocalFile + +// ================================================================================================= + +XMPFiles::~XMPFiles() +{ + XMP_Assert ( this->clientRefs <= 0 ); + + if ( this->handler != 0 ) { + delete this->handler; + this->handler = 0; + } + + CloseLocalFile ( this ); + + if ( this->tempPtr != 0 ) free ( this->tempPtr ); // ! Must have been malloc-ed! + +} // XMPFiles::~XMPFiles + +// ================================================================================================= + +/* class static */ +bool +XMPFiles::GetFormatInfo ( XMP_FileFormat format, + XMP_OptionBits * flags /* = 0 */ ) +{ + + return HandlerRegistry::getInstance().getFormatInfo ( format, flags ); + +} // XMPFiles::GetFormatInfo + +// ================================================================================================= + +/* class static */ +XMP_FileFormat +XMPFiles::CheckFileFormat ( XMP_StringPtr clientPath ) +{ + if ( (clientPath == 0) || (*clientPath == 0) ) return kXMP_UnknownFile; + + XMPFiles bogus; // Needed to provide context to SelectSmartHandler. + bogus.filePath = clientPath; // So that XMPFiles destructor cleans up the XMPFiles_IO object. + XMPFileHandlerInfo * handlerInfo = HandlerRegistry::getInstance().selectSmartHandler( &bogus, clientPath, kXMP_UnknownFile, kXMPFiles_OpenForRead ); + + if ( handlerInfo == 0 ) return kXMP_UnknownFile; + return handlerInfo->format; + +} // XMPFiles::CheckFileFormat + +// ================================================================================================= + +/* class static */ +XMP_FileFormat +XMPFiles::CheckPackageFormat ( XMP_StringPtr folderPath ) +{ + // This is called with a path to a folder, and checks to see if that folder is the top level of + // a "package" that should be recognized by one of the folder-oriented handlers. The checks here + // are not overly extensive, but hopefully enough to weed out false positives. + // + // Since we don't have many folder handlers, this is simple hardwired code. + + #if ! EnableDynamicMediaHandlers + return kXMP_UnknownFile; + #else + Host_IO::FileMode folderMode = Host_IO::GetFileMode ( folderPath ); + if ( folderMode != Host_IO::kFMode_IsFolder ) return kXMP_UnknownFile; + return HandlerRegistry::checkTopFolderName ( std::string ( folderPath ) ); + #endif + +} // XMPFiles::CheckPackageFormat + +// ================================================================================================= + +static bool FileIsExcluded ( XMP_StringPtr clientPath, std::string * fileExt, Host_IO::FileMode * clientMode ) +{ + // ! Return true for excluded files, false for OK files. + + *clientMode = Host_IO::GetFileMode ( clientPath ); + if ( (*clientMode == Host_IO::kFMode_IsFolder) || (*clientMode == Host_IO::kFMode_IsOther) ) return true; + XMP_Assert ( (*clientMode == Host_IO::kFMode_IsFile) || (*clientMode == Host_IO::kFMode_DoesNotExist) ); + + if ( *clientMode == Host_IO::kFMode_IsFile ) { + + // Find the file extension. OK to be "wrong" for something like "C:\My.dir\file". Any + // filtering looks for matches with real extensions, "dir\file" won't match any of these. + XMP_StringPtr extPos = clientPath + strlen ( clientPath ); + for ( ; (extPos != clientPath) && (*extPos != '.'); --extPos ) {} + if ( *extPos == '.' ) { + fileExt->assign ( extPos+1 ); + MakeLowerCase ( fileExt ); + } + + // See if this file is one that XMPFiles should never process. + for ( size_t i = 0; kKnownRejectedFiles[i] != 0; ++i ) { + if ( *fileExt == kKnownRejectedFiles[i] ) return true; + } + + } + + return false; + +} // FileIsExcluded + +// ================================================================================================= + +/* class static */ +bool +XMPFiles::GetFileModDate ( XMP_StringPtr clientPath, + XMP_DateTime * modDate, + XMP_FileFormat * format, /* = 0 */ + XMP_OptionBits options /* = 0 */ ) +{ + + // --------------------------------------------------------------- + // First try to select a smart handler. Return false if not found. + + XMPFiles dummyParent; // GetFileModDate is static, but the handler needs a parent. + dummyParent.filePath = clientPath; + + Host_IO::FileMode clientMode; + std::string fileExt; // Used to check for excluded files. + bool excluded = FileIsExcluded ( clientPath, &fileExt, &clientMode ); // ! Fills in fileExt and clientMode. + if ( excluded ) return false; + + XMPFileHandlerInfo * handlerInfo = 0; + XMPFileHandlerCTor handlerCTor = 0; + XMP_OptionBits handlerFlags = 0; + + XMP_FileFormat dummyFormat = kXMP_UnknownFile; + if ( format == 0 ) format = &dummyFormat; + + options |= kXMPFiles_OpenForRead; + handlerInfo = HandlerRegistry::getInstance().selectSmartHandler ( &dummyParent, clientPath, *format, options ); + if ( handlerInfo == 0 ) return false; + + // ------------------------------------------------------------------------- + // Fill in the format output. Call the handler to get the modification date. + + dummyParent.format = handlerInfo->format; + *format = handlerInfo->format; + + dummyParent.handler = handlerInfo->handlerCTor ( &dummyParent ); + + bool ok = dummyParent.handler->GetFileModDate ( modDate ); + + delete dummyParent.handler; + dummyParent.handler = 0; + + return ok; + +} // XMPFiles::GetFileModDate + +// ================================================================================================= + +static bool +DoOpenFile ( XMPFiles * thiz, + XMP_IO * clientIO, + XMP_StringPtr clientPath, + XMP_FileFormat format /* = kXMP_UnknownFile */, + XMP_OptionBits openFlags /* = 0 */ ) +{ + XMP_Assert ( (clientIO == 0) ? (clientPath[0] != 0) : (clientPath[0] == 0) ); + + openFlags &= ~kXMPFiles_ForceGivenHandler; // Don't allow this flag for OpenFile. + + if ( thiz->handler != 0 ) XMP_Throw ( "File already open", kXMPErr_BadParam ); + CloseLocalFile ( thiz ); // Sanity checks if prior call failed. + + thiz->ioRef = clientIO; + thiz->filePath = clientPath; + + thiz->format = kXMP_UnknownFile; // Make sure it is preset for later check. + thiz->openFlags = openFlags; + + bool readOnly = XMP_OptionIsClear ( openFlags, kXMPFiles_OpenForUpdate ); + + Host_IO::FileMode clientMode; + std::string fileExt; // Used to check for camera raw files and OK to scan files. + + if ( thiz->UsesClientIO() ) { + clientMode = Host_IO::kFMode_IsFile; + } else { + bool excluded = FileIsExcluded ( clientPath, &fileExt, &clientMode ); // ! Fills in fileExt and clientMode. + if ( excluded ) return false; + } + + // Find the handler, fill in the XMPFiles member variables, cache the desired file data. + + XMPFileHandlerInfo * handlerInfo = 0; + XMPFileHandlerCTor handlerCTor = 0; + XMP_OptionBits handlerFlags = 0; + + if ( ! (openFlags & kXMPFiles_OpenUsePacketScanning) ) { + handlerInfo = HandlerRegistry::getInstance().selectSmartHandler( thiz, clientPath, format, openFlags ); + } + + #if ! EnablePacketScanning + + if ( handlerInfo == 0 ) return false; + + #else + + if ( handlerInfo == 0 ) { + + // No smart handler, packet scan if appropriate. + + if ( clientMode != Host_IO::kFMode_IsFile ) return false; + if ( openFlags & kXMPFiles_OpenUseSmartHandler ) return false; + + if ( openFlags & kXMPFiles_OpenLimitedScanning ) { + bool scanningOK = false; + for ( size_t i = 0; kKnownScannedFiles[i] != 0; ++i ) { + if ( fileExt == kKnownScannedFiles[i] ) { scanningOK = true; break; } + } + if ( ! scanningOK ) return false; + } + + handlerInfo = &kScannerHandlerInfo; + if ( thiz->ioRef == 0 ) { // Normally opened in SelectSmartHandler, but might not be open yet. + thiz->ioRef = XMPFiles_IO::New_XMPFiles_IO ( clientPath, readOnly ); + if ( thiz->ioRef == 0 ) return false; + } + + } + + #endif + + XMP_Assert ( handlerInfo != 0 ); + handlerCTor = handlerInfo->handlerCTor; + handlerFlags = handlerInfo->flags; + + XMP_Assert ( (thiz->ioRef != 0) || + (handlerFlags & kXMPFiles_UsesSidecarXMP) || + (handlerFlags & kXMPFiles_HandlerOwnsFile) || + (handlerFlags & kXMPFiles_FolderBasedFormat) ); + + if ( thiz->format == kXMP_UnknownFile ) thiz->format = handlerInfo->format; // ! The CheckProc might have set it. + XMPFileHandler* handler = (*handlerCTor) ( thiz ); + XMP_Assert ( handlerFlags == handler->handlerFlags ); + + thiz->handler = handler; + + try { + handler->CacheFileData(); + } catch ( ... ) { + delete thiz->handler; + thiz->handler = 0; + if ( ! (handlerFlags & kXMPFiles_HandlerOwnsFile) ) CloseLocalFile ( thiz ); + throw; + } + + if ( handler->containsXMP ) FillPacketInfo ( handler->xmpPacket, &handler->packetInfo ); + + if ( (! (openFlags & kXMPFiles_OpenForUpdate)) && (! (handlerFlags & kXMPFiles_HandlerOwnsFile)) ) { + // Close the disk file now if opened for read-only access. + CloseLocalFile ( thiz ); + } + + return true; + +} // DoOpenFile + +// ================================================================================================= + +static bool DoOpenFile( XMPFiles* thiz, + const Common::XMPFileHandlerInfo& hdlInfo, + XMP_IO* clientIO, + XMP_StringPtr clientPath, + XMP_OptionBits openFlags ) +{ + XMP_Assert ( (clientIO == 0) ? (clientPath[0] != 0) : (clientPath[0] == 0) ); + + openFlags &= ~kXMPFiles_ForceGivenHandler; // Don't allow this flag for OpenFile. + + + if ( thiz->handler != 0 ) XMP_Throw ( "File already open", kXMPErr_BadParam ); + + // + // setup members + // + thiz->ioRef = clientIO; + thiz->filePath = std::string( clientPath ); + thiz->format = hdlInfo.format; + thiz->openFlags = openFlags; + + // + // create file handler instance + // + XMPFileHandlerCTor handlerCTor = hdlInfo.handlerCTor; + XMP_OptionBits handlerFlags = hdlInfo.flags; + + XMPFileHandler* handler = (*handlerCTor) ( thiz ); + XMP_Assert ( handlerFlags == handler->handlerFlags ); + + thiz->handler = handler; + + // + // try to read metadata + // + try + { + handler->CacheFileData(); + + if( handler->containsXMP ) + { + FillPacketInfo( handler->xmpPacket, &handler->packetInfo ); + } + + if( (! (openFlags & kXMPFiles_OpenForUpdate)) && (! (handlerFlags & kXMPFiles_HandlerOwnsFile)) ) + { + // Close the disk file now if opened for read-only access. + CloseLocalFile ( thiz ); + } + } + catch( ... ) + { + delete thiz->handler; + thiz->handler = NULL; + + if( ! (handlerFlags & kXMPFiles_HandlerOwnsFile) ) + { + CloseLocalFile( thiz ); + } + + throw; + } + + return true; +} + +// ================================================================================================= + +bool +XMPFiles::OpenFile ( XMP_StringPtr clientPath, + XMP_FileFormat format /* = kXMP_UnknownFile */, + XMP_OptionBits openFlags /* = 0 */ ) +{ + + return DoOpenFile ( this, 0, clientPath, format, openFlags ); + +} + +// ================================================================================================= + +#if XMP_StaticBuild // ! Client XMP_IO objects can only be used in static builds. + +bool +XMPFiles::OpenFile ( XMP_IO* clientIO, + XMP_FileFormat format /* = kXMP_UnknownFile */, + XMP_OptionBits openFlags /* = 0 */ ) +{ + + return DoOpenFile ( this, clientIO, "", format, openFlags ); + +} // XMPFiles::OpenFile + +#endif // XMP_StaticBuild + +// ================================================================================================= + +#if XMP_StaticBuild // ! Client XMP_IO objects can only be used in static builds. + +bool XMPFiles::OpenFile( const Common::XMPFileHandlerInfo& hdlInfo, + XMP_IO* clientIO, + XMP_OptionBits openFlags /*= 0*/ ) +{ + + return DoOpenFile( this, hdlInfo, clientIO, NULL, openFlags ); +} + +#endif // XMP_StaticBuild + +// ================================================================================================= + +bool XMPFiles::OpenFile( const Common::XMPFileHandlerInfo& hdlInfo, + XMP_StringPtr filePath, + XMP_OptionBits openFlags /*= 0*/ ) +{ + + return DoOpenFile( this, hdlInfo, NULL, filePath, openFlags ); +} + +// ================================================================================================= + +void +XMPFiles::CloseFile ( XMP_OptionBits closeFlags /* = 0 */ ) +{ + if ( this->handler == 0 ) return; // Return if there is no open file (not an error). + + bool needsUpdate = this->handler->needsUpdate; + XMP_OptionBits handlerFlags = this->handler->handlerFlags; + + // Decide if we're doing a safe update. If so, make sure the handler supports it. All handlers + // that don't own the file tolerate safe update using common code below. + + bool doSafeUpdate = XMP_OptionIsSet ( closeFlags, kXMPFiles_UpdateSafely ); + #if GatherPerformanceData + if ( doSafeUpdate ) sAPIPerf->back().extraInfo += ", safe update"; // Client wants safe update. + #endif + + if ( ! (this->openFlags & kXMPFiles_OpenForUpdate) ) doSafeUpdate = false; + if ( ! needsUpdate ) doSafeUpdate = false; + + bool safeUpdateOK = ( (handlerFlags & kXMPFiles_AllowsSafeUpdate) || + (! (handlerFlags & kXMPFiles_HandlerOwnsFile)) ); + if ( doSafeUpdate && (! safeUpdateOK) ) { + XMP_Throw ( "XMPFiles::CloseFile - Safe update not supported", kXMPErr_Unavailable ); + } + + // Try really hard to make sure the file is closed and the handler is deleted. + + try { + + if ( (! doSafeUpdate) || (handlerFlags & kXMPFiles_HandlerOwnsFile) ) { // ! Includes no update case. + + // Close the file without doing common crash-safe writing. The handler might do it. + + if ( needsUpdate ) { + #if GatherPerformanceData + sAPIPerf->back().extraInfo += ", direct update"; + #endif + this->handler->UpdateFile ( doSafeUpdate ); + } + + delete this->handler; + this->handler = 0; + CloseLocalFile ( this ); + + } else { + + // Update the file in a crash-safe manner using common control of a temp file. + + XMP_IO* tempFileRef = this->ioRef->DeriveTemp(); + if ( tempFileRef == 0 ) XMP_Throw ( "XMPFiles::CloseFile, cannot create temp", kXMPErr_InternalFailure ); + + if ( handlerFlags & kXMPFiles_CanRewrite ) { + + // The handler can rewrite an entire file based on the original. + + #if GatherPerformanceData + sAPIPerf->back().extraInfo += ", file rewrite"; + #endif + + this->handler->WriteTempFile ( tempFileRef ); + + } else { + + // The handler can only update an existing file. Copy to the temp then update. + + + #if GatherPerformanceData + sAPIPerf->back().extraInfo += ", copy update"; + #endif + + + XMP_IO* origFileRef = this->ioRef; + + origFileRef->Rewind(); + XIO::Copy ( origFileRef, tempFileRef, origFileRef->Length(), abortProc, abortArg ); + + try { + + this->ioRef = tempFileRef; + this->handler->UpdateFile ( false ); // We're doing the safe update, not the handler. + this->ioRef = origFileRef; + + } catch ( ... ) { + + this->ioRef->DeleteTemp(); + this->ioRef = origFileRef; + throw; + + } + + } + + this->ioRef->AbsorbTemp(); + CloseLocalFile ( this ); + + delete this->handler; + this->handler = 0; + + } + + } catch ( ... ) { + + // *** Don't delete the temp or copy files, not sure which is best. + + try { + if ( this->handler != 0 ) delete this->handler; + } catch ( ... ) { /* Do nothing, throw the outer exception later. */ } + + if( this->ioRef ) this->ioRef->DeleteTemp(); + CloseLocalFile ( this ); + this->filePath.clear(); + + this->handler = 0; + this->format = kXMP_UnknownFile; + this->ioRef = 0; + this->openFlags = 0; + + if ( this->tempPtr != 0 ) free ( this->tempPtr ); // ! Must have been malloc-ed! + this->tempPtr = 0; + this->tempUI32 = 0; + + throw; + + } + + // Clear the XMPFiles member variables. + + CloseLocalFile ( this ); + this->filePath.clear(); + + this->handler = 0; + this->format = kXMP_UnknownFile; + this->ioRef = 0; + this->openFlags = 0; + + if ( this->tempPtr != 0 ) free ( this->tempPtr ); // ! Must have been malloc-ed! + this->tempPtr = 0; + this->tempUI32 = 0; + +} // XMPFiles::CloseFile + +// ================================================================================================= + +bool +XMPFiles::GetFileInfo ( XMP_StringPtr * filePath /* = 0 */, + XMP_StringLen * pathLen /* = 0 */, + XMP_OptionBits * openFlags /* = 0 */, + XMP_FileFormat * format /* = 0 */, + XMP_OptionBits * handlerFlags /* = 0 */ ) const +{ + if ( this->handler == 0 ) return false; + XMPFileHandler * handler = this->handler; + + if ( filePath == 0 ) filePath = &voidStringPtr; + if ( pathLen == 0 ) pathLen = &voidStringLen; + if ( openFlags == 0 ) openFlags = &voidOptionBits; + if ( format == 0 ) format = &voidFileFormat; + if ( handlerFlags == 0 ) handlerFlags = &voidOptionBits; + + *filePath = this->filePath.c_str(); + *pathLen = (XMP_StringLen) this->filePath.size(); + *openFlags = this->openFlags; + *format = this->format; + *handlerFlags = this->handler->handlerFlags; + + return true; + +} // XMPFiles::GetFileInfo + +// ================================================================================================= + +void +XMPFiles::SetAbortProc ( XMP_AbortProc abortProc, + void * abortArg ) +{ + this->abortProc = abortProc; + this->abortArg = abortArg; + + XMP_Assert ( (abortProc != (XMP_AbortProc)0) || (abortArg != (void*)(unsigned long long)0xDEADBEEFULL) ); // Hack to test the assert callback. +} // XMPFiles::SetAbortProc + +// ================================================================================================= +// SetClientPacketInfo +// =================== +// +// Set the packet info returned to the client. This is the internal packet info at first, which +// tells what is in the file. But once the file needs update (PutXMP has been called), we return +// info about the latest XMP. The internal packet info is left unchanged since it is needed when +// the file is updated to locate the old packet in the file. + +static void +SetClientPacketInfo ( XMP_PacketInfo * clientInfo, const XMP_PacketInfo & handlerInfo, + const std::string & xmpPacket, bool needsUpdate ) +{ + + if ( clientInfo == 0 ) return; + + if ( ! needsUpdate ) { + *clientInfo = handlerInfo; + } else { + clientInfo->offset = kXMPFiles_UnknownOffset; + clientInfo->length = (XMP_Int32) xmpPacket.size(); + FillPacketInfo ( xmpPacket, clientInfo ); + } + +} // SetClientPacketInfo + +// ================================================================================================= + +bool +XMPFiles::GetXMP ( SXMPMeta * xmpObj /* = 0 */, + XMP_StringPtr * xmpPacket /* = 0 */, + XMP_StringLen * xmpPacketLen /* = 0 */, + XMP_PacketInfo * packetInfo /* = 0 */ ) +{ + if ( this->handler == 0 ) XMP_Throw ( "XMPFiles::GetXMP - No open file", kXMPErr_BadObject ); + + XMP_OptionBits applyTemplateFlags = kXMPTemplate_AddNewProperties | kXMPTemplate_IncludeInternalProperties; + + if ( ! this->handler->processedXMP ) { + try { + this->handler->ProcessXMP(); + } catch ( ... ) { + // Return the outputs then rethrow the exception. + if ( xmpObj != 0 ) { + // ! Don't use Clone, that replaces the internal ref in the local xmpObj, leaving the client unchanged! + xmpObj->Erase(); + SXMPUtils::ApplyTemplate ( xmpObj, this->handler->xmpObj, applyTemplateFlags ); + } + if ( xmpPacket != 0 ) *xmpPacket = this->handler->xmpPacket.c_str(); + if ( xmpPacketLen != 0 ) *xmpPacketLen = (XMP_StringLen) this->handler->xmpPacket.size(); + SetClientPacketInfo ( packetInfo, this->handler->packetInfo, + this->handler->xmpPacket, this->handler->needsUpdate ); + throw; + } + } + + if ( ! this->handler->containsXMP ) return false; + + #if 0 // *** See bug 1131815. A better way might be to pass the ref up from here. + if ( xmpObj != 0 ) *xmpObj = this->handler->xmpObj.Clone(); + #else + if ( xmpObj != 0 ) { + // ! Don't use Clone, that replaces the internal ref in the local xmpObj, leaving the client unchanged! + xmpObj->Erase(); + SXMPUtils::ApplyTemplate ( xmpObj, this->handler->xmpObj, applyTemplateFlags ); + } + #endif + + if ( xmpPacket != 0 ) *xmpPacket = this->handler->xmpPacket.c_str(); + if ( xmpPacketLen != 0 ) *xmpPacketLen = (XMP_StringLen) this->handler->xmpPacket.size(); + SetClientPacketInfo ( packetInfo, this->handler->packetInfo, + this->handler->xmpPacket, this->handler->needsUpdate ); + + return true; + +} // XMPFiles::GetXMP + +// ================================================================================================= + +static bool +DoPutXMP ( XMPFiles * thiz, const SXMPMeta & xmpObj, const bool doIt ) +{ + // Check some basic conditions to see if the Put should be attempted. + + if ( thiz->handler == 0 ) XMP_Throw ( "XMPFiles::PutXMP - No open file", kXMPErr_BadObject ); + if ( ! (thiz->openFlags & kXMPFiles_OpenForUpdate) ) { + XMP_Throw ( "XMPFiles::PutXMP - Not open for update", kXMPErr_BadObject ); + } + + XMPFileHandler * handler = thiz->handler; + XMP_OptionBits handlerFlags = handler->handlerFlags; + XMP_PacketInfo & packetInfo = handler->packetInfo; + std::string & xmpPacket = handler->xmpPacket; + + if ( ! handler->processedXMP ) handler->ProcessXMP(); // Might have Open/Put with no GetXMP. + + size_t oldPacketOffset = (size_t)packetInfo.offset; + size_t oldPacketLength = packetInfo.length; + + if ( oldPacketOffset == (size_t)kXMPFiles_UnknownOffset ) oldPacketOffset = 0; // ! Simplify checks. + if ( oldPacketLength == (size_t)kXMPFiles_UnknownLength ) oldPacketLength = 0; + + bool fileHasPacket = (oldPacketOffset != 0) && (oldPacketLength != 0); + + if ( ! fileHasPacket ) { + if ( ! (handlerFlags & kXMPFiles_CanInjectXMP) ) { + XMP_Throw ( "XMPFiles::PutXMP - Can't inject XMP", kXMPErr_Unavailable ); + } + if ( handler->stdCharForm == kXMP_CharUnknown ) { + XMP_Throw ( "XMPFiles::PutXMP - No standard character form", kXMPErr_InternalFailure ); + } + } + + // Serialize the XMP and update the handler's info. + + XMP_Uns8 charForm = handler->stdCharForm; + if ( charForm == kXMP_CharUnknown ) charForm = packetInfo.charForm; + + XMP_OptionBits options = handler->GetSerializeOptions() | XMP_CharToSerializeForm ( charForm ); + if ( handlerFlags & kXMPFiles_NeedsReadOnlyPacket ) options |= kXMP_ReadOnlyPacket; + if ( fileHasPacket && (thiz->format == kXMP_UnknownFile) && (! packetInfo.writeable) ) options |= kXMP_ReadOnlyPacket; + + bool preferInPlace = ((handlerFlags & kXMPFiles_PrefersInPlace) != 0); + bool tryInPlace = (fileHasPacket & preferInPlace) || (! (handlerFlags & kXMPFiles_CanExpand)); + + if ( handlerFlags & kXMPFiles_UsesSidecarXMP ) tryInPlace = false; + + if ( tryInPlace ) { + try { + xmpObj.SerializeToBuffer ( &xmpPacket, (options | kXMP_ExactPacketLength), (XMP_StringLen) oldPacketLength ); + XMP_Assert ( xmpPacket.size() == oldPacketLength ); + } catch ( ... ) { + if ( preferInPlace ) { + tryInPlace = false; // ! Try again, out of place this time. + } else { + if ( ! doIt ) return false; + throw; + } + } + } + + if ( ! tryInPlace ) { + try { + xmpObj.SerializeToBuffer ( &xmpPacket, options ); + } catch ( ... ) { + if ( ! doIt ) return false; + throw; + } + } + + if ( doIt ) { + handler->xmpObj = xmpObj.Clone(); + handler->containsXMP = true; + handler->processedXMP = true; + handler->needsUpdate = true; + } + + return true; + +} // DoPutXMP + +// ================================================================================================= + +void +XMPFiles::PutXMP ( const SXMPMeta & xmpObj ) +{ + (void) DoPutXMP ( this, xmpObj, true ); + +} // XMPFiles::PutXMP + +// ================================================================================================= + +void +XMPFiles::PutXMP ( XMP_StringPtr xmpPacket, + XMP_StringLen xmpPacketLen /* = kXMP_UseNullTermination */ ) +{ + SXMPMeta xmpObj ( xmpPacket, xmpPacketLen ); + this->PutXMP ( xmpObj ); + +} // XMPFiles::PutXMP + +// ================================================================================================= + +bool +XMPFiles::CanPutXMP ( const SXMPMeta & xmpObj ) +{ + if ( this->handler == 0 ) XMP_Throw ( "XMPFiles::CanPutXMP - No open file", kXMPErr_BadObject ); + + if ( ! (this->openFlags & kXMPFiles_OpenForUpdate) ) return false; + + if ( this->handler->handlerFlags & kXMPFiles_CanInjectXMP ) return true; + if ( ! this->handler->containsXMP ) return false; + if ( this->handler->handlerFlags & kXMPFiles_CanExpand ) return true; + + return DoPutXMP ( this, xmpObj, false ); + +} // XMPFiles::CanPutXMP + +// ================================================================================================= + +bool +XMPFiles::CanPutXMP ( XMP_StringPtr xmpPacket, + XMP_StringLen xmpPacketLen /* = kXMP_UseNullTermination */ ) +{ + SXMPMeta xmpObj ( xmpPacket, xmpPacketLen ); + return this->CanPutXMP ( xmpObj ); + +} // XMPFiles::CanPutXMP + +// ================================================================================================= diff --git a/XMPFiles/source/XMPFiles.hpp b/XMPFiles/source/XMPFiles.hpp new file mode 100644 index 0000000..f4f2e96 --- /dev/null +++ b/XMPFiles/source/XMPFiles.hpp @@ -0,0 +1,252 @@ +#ifndef __XMPFiles_hpp__ +#define __XMPFiles_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! This must be the first include. + +#include <string> +#define TXMP_STRING_TYPE std::string +#include "public/include/XMP.hpp" + +#include "public/include/XMP_IO.hpp" + +class XMPFileHandler; +namespace Common{ struct XMPFileHandlerInfo; } + +// ================================================================================================= +/// \file XMPFiles.hpp +/// \brief High level support to access metadata in files of interest to Adobe applications. +/// +/// This header ... +/// +// ================================================================================================= + +// ================================================================================================= +// *** Usage Notes (eventually to become Doxygen comments) *** +// =========================================================== +// +// This is the main part of the internal (DLL side) implementation of XMPFiles. Other parts are +// the entry point wrappers and the file format handlers. The XMPFiles class distills the client +// API from TXMPFiles.hpp, removing convenience overloads and substituting a pointer/length pair +// for output strings. +// +// The wrapper functions provide a stable binary interface and perform minor impedance correction +// between the client template API from TDocMeta.hpp and the DLL's XMPFiles class. The details of +// the wrappers should be considered private. +// +// File handlers are registered during DLL initialization with hard coded calls in Init_XMPFiles. +// Each file handler provides 2 standalone functions, CheckFormatProc and DocMetaHandlerCTor, plus a +// class derived from DocMetaHandler. The format and capability flags are passed when registering. +// This allows the same physical handler to be registered for multiple formats. +// +// ------------------------------------------------------------------------------------------------- +// +// Basic outlines of the processing by the XMPFiles methods: +// +// Constructor: +// - Minimal work to create an empty XMPFiles object, set the ref count to 1. +// +// Destructor: +// - Decrement the ref count, return if greater than zero. +// - Call LFA_Close if necessary. +// +// UnlockLib & UnlockObj: +// - Release the thread lock. Same for now, no per-object lock. +// +// GetFormatInfo: +// - Return the flags for the registered handler. +// +// OpenFile: +// - The physical file is opened via LFA_OpenFile. +// - A handler is selected by calling the registered format checkers. +// - The handler object is created by calling the registered constructor proc. +// +// CloseFile: +// - Return if there is no open file (not an error). +// - If not a crash-safe update (includes read-only or no update), or the handler owns the file: +// - Throw an exception if the handler owns the file but does not support safe update. +// - If the file needs updating, call the handler's UpdateFile method. +// - else: +// - If the handler supports file rewrite: +// - *** This might not preserve ownership and permissions. +// - Create an empty temp file. +// - Call the handler's WriteFile method, writing to the temp file. +// - else +// - *** This preserves ownership, permissions, and Mac resources. +// - Copy the original file to a temp name (Mac data fork only). +// - Rename the original file to a different temp name. +// - Rename the copy file back to the original name. +// - Call the handler's UpdateFile method for the "original as temp" file. +// - Close both the original and temp files. +// - Delete the file with the original name. +// - Rename the temp file to the original name. +// - Delete the handler object. +// - Call LFA_Close if necessary. +// +// GetFileInfo: +// - Return the file info from the XMPFiles member variables. +// +// GetXMP: +// - Throw an exception if there is no open file. +// - Call the handler's GetXMP method. +// +// PutXMP: +// - Throw an exception if there is no open file. +// - Call the handler's PutXMP method. +// +// CanPutXMP: +// - Implement roughly as shown in TXMPFiles.hpp, there is no handler CanPutXMP method. +// +// ------------------------------------------------------------------------------------------------- +// +// The format checker should do nothing but the minimal work to identify the overall file format. +// In particular it should not look for XMP or other metadata. Note that the format checker has no +// means to carry state forward, it just returns a yes/no answer about a particular file format. +// +// The format checker and file handler should use the LFA_* functions for all I/O. They should not +// open or close the file themselves unless the handler sets the "handler-owns-file" flag. +// +// The format checker is passed the format being checked, allowing one checker to handle multiple +// formats. It is passed the LFA file ref so that it can do additional reads if necessary. The +// buffer is from the start of the file, the file will be positioned to the byte following the +// buffer. The buffer length will be at least 4K, unless the file is smaller in which case it will +// be the length of the file. This buffer may be reused for additional reads. +// +// Identifying some file formats can require checking variable length strings. Doing seeks and reads +// for each is suboptimal. There are utilities to maintain a rolling buffer and ensure that a given +// amount of data is available. See the template file handler code for details. +// +// ------------------------------------------------------------------------------------------------- +// +// The file handler has no explicit open and close methods. These are implicit in the handler's +// constructor and destructor. The file handler should use the XMPFiles member variables for the +// active file ref (and path if necessary), unless it owns the file. Note that these might change +// between the open and close in the case of crash-safe updates. Don't copy the XMPFiles member +// variables in the handler's constructor, save the pointer to the XMPFiles object and access +// directly as needed. +// +// The handler should have an UpdateFile method. This is called from XMPFiles::CloseFile if the +// file needs to be updated. The handler's destructor must only close the file, not update it. +// The handler can optionally have a WriteFile method, if it can rewrite the entire file. +// +// The handler is free to use its best judgement about caching parts of the file in memory. Overall +// speed of a single open/get/put/close cycle is probably the best goal, assuming a modern processor +// with a reasonable (significant but not enormous) amount of RAM. +// +// The handler methods will be called in a per-object thread safe manner. Concurrent access might +// occur for different objects, but not for the same object. The handler's constructor and destructor +// will always be globally serialized, so they can safely modify global data structures. +// +// (Testing issue: What about separate XMPFiles objects accessing the same file?) +// +// Handler's must not have any global objects that are heap allocated. Use pointers to objects that +// are allocated and deleted during the XMPFiles initialization and termination process. Some +// client apps are very picky about what they detect as memory leaks. +// +// static char gSomeBuffer [10*1000]; // OK, not from the heap. +// static std::string gSomeString; // Not OK, content from the heap. +// static std::vector<int> gSomeVector; // Not OK, content from the heap. +// static std::string * gSomeString = 0; // OK, alloc at init, delete at term. +// static std::vector<int> * gSomeVector = 0; // OK, alloc at init, delete at term. +// +// ================================================================================================= + +class XMPFiles { +public: + + static void GetVersionInfo ( XMP_VersionInfo * info ); + + static bool Initialize ( XMP_OptionBits options, const char* pluginFolder, const char* plugins = NULL ); + static void Terminate(); + + XMPFiles(); + virtual ~XMPFiles(); + + static bool GetFormatInfo ( XMP_FileFormat format, + XMP_OptionBits * flags = 0 ); + + static XMP_FileFormat CheckFileFormat ( XMP_StringPtr filePath ); + static XMP_FileFormat CheckPackageFormat ( XMP_StringPtr folderPath ); + + static bool GetFileModDate ( XMP_StringPtr filePath, + XMP_DateTime * modDate, + XMP_FileFormat * format = 0, // ! Can be null. + XMP_OptionBits options = 0 ); + + bool OpenFile ( XMP_StringPtr filePath, + XMP_FileFormat format = kXMP_UnknownFile, + XMP_OptionBits openFlags = 0 ); + + #if XMP_StaticBuild // ! Client XMP_IO objects can only be used in static builds. + bool OpenFile ( XMP_IO * clientIO, + XMP_FileFormat format = kXMP_UnknownFile, + XMP_OptionBits openFlags = 0 ); + #endif + + bool OpenFile( const Common::XMPFileHandlerInfo& hdlInfo, + XMP_StringPtr filePath, + XMP_OptionBits openFlags = 0 ); + +#if XMP_StaticBuild // ! Client XMP_IO objects can only be used in static builds. + bool OpenFile( const Common::XMPFileHandlerInfo& hdlInfo, + XMP_IO* clientIO, + XMP_OptionBits openFlags = 0 ); +#endif + + void CloseFile ( XMP_OptionBits closeFlags = 0 ); + + bool GetFileInfo ( XMP_StringPtr * filePath = 0, + XMP_StringLen * filePathLen = 0, + XMP_OptionBits * openFlags = 0, + XMP_FileFormat * format = 0, + XMP_OptionBits * handlerFlags = 0 ) const; + + void SetAbortProc ( XMP_AbortProc abortProc, + void * abortArg ); + + bool GetXMP ( SXMPMeta * xmpObj = 0, + XMP_StringPtr * xmpPacket = 0, + XMP_StringLen * xmpPacketLen = 0, + XMP_PacketInfo * packetInfo = 0 ); + + void PutXMP ( const SXMPMeta & xmpObj ); + + void PutXMP ( XMP_StringPtr xmpPacket, + XMP_StringLen xmpPacketLen = kXMP_UseNullTermination ); + + bool CanPutXMP ( const SXMPMeta & xmpObj ); + + bool CanPutXMP ( XMP_StringPtr xmpPacket, + XMP_StringLen xmpPacketLen = kXMP_UseNullTermination ); + + inline bool UsesClientIO() { return this->filePath.empty(); }; + inline bool UsesLocalIO() { return ( ! this->UsesClientIO() ); }; + + // Leave this data public so file handlers can see it. + + XMP_Int32 clientRefs; // ! Must be signed to allow decrement from zero. + XMP_ReadWriteLock lock; + + XMP_FileFormat format; + XMP_IO * ioRef; // Non-zero if a file is open. + std::string filePath; // Empty for client-managed I/O. + XMP_OptionBits openFlags; + XMPFileHandler * handler; // Non-null if a file is open. + + void * tempPtr; // For use between the CheckProc and handler creation. + XMP_Uns32 tempUI32; + + XMP_AbortProc abortProc; + void * abortArg; + +}; // XMPFiles + +#endif /* __XMPFiles_hpp__ */ diff --git a/XMPFiles/source/XMPFiles_Impl.cpp b/XMPFiles/source/XMPFiles_Impl.cpp new file mode 100644 index 0000000..92b2ea3 --- /dev/null +++ b/XMPFiles/source/XMPFiles_Impl.cpp @@ -0,0 +1,416 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! XMP_Environment.h must be the first included header. + +#include "public/include/XMP_Const.h" +#include "public/include/XMP_IO.hpp" + +#include "XMPFiles/source/XMPFiles_Impl.hpp" +#include "source/XIO.hpp" + +#include "source/UnicodeConversions.hpp" + +using namespace std; + +// Internal code should be using #if with XMP_MacBuild, XMP_WinBuild, or XMP_UNIXBuild. +// This is a sanity check in case of accidental use of *_ENV. Some clients use the poor +// practice of defining the *_ENV macro with an empty value. +#if defined ( MAC_ENV ) + #if ! MAC_ENV + #error "MAC_ENV must be defined so that \"#if MAC_ENV\" is true" + #endif +#elif defined ( WIN_ENV ) + #if ! WIN_ENV + #error "WIN_ENV must be defined so that \"#if WIN_ENV\" is true" + #endif +#elif defined ( UNIX_ENV ) + #if ! UNIX_ENV + #error "UNIX_ENV must be defined so that \"#if UNIX_ENV\" is true" + #endif +#endif + +// ================================================================================================= +/// \file XMPFiles_Impl.cpp +/// \brief ... +/// +/// This file ... +/// +// ================================================================================================= + +#if XMP_WinBuild + #pragma warning ( disable : 4290 ) // C++ exception specification ignored except to indicate a function is not __declspec(nothrow) + #pragma warning ( disable : 4800 ) // forcing value to bool 'true' or 'false' (performance warning) +#endif + +bool ignoreLocalText = false; + +XMP_FileFormat voidFileFormat = 0; // Used as sink for unwanted output parameters. +// ================================================================================================= + +// Add all known mappings, multiple mappings (tif, tiff) are OK. +const FileExtMapping kFileExtMap[] = + { { "pdf", kXMP_PDFFile }, + { "ps", kXMP_PostScriptFile }, + { "eps", kXMP_EPSFile }, + + { "jpg", kXMP_JPEGFile }, + { "jpeg", kXMP_JPEGFile }, + { "jpx", kXMP_JPEG2KFile }, + { "tif", kXMP_TIFFFile }, + { "tiff", kXMP_TIFFFile }, + { "dng", kXMP_TIFFFile }, // DNG files are well behaved TIFF. + { "gif", kXMP_GIFFile }, + { "giff", kXMP_GIFFile }, + { "png", kXMP_PNGFile }, + + { "swf", kXMP_SWFFile }, + { "flv", kXMP_FLVFile }, + + { "aif", kXMP_AIFFFile }, + + { "mov", kXMP_MOVFile }, + { "avi", kXMP_AVIFile }, + { "cin", kXMP_CINFile }, + { "wav", kXMP_WAVFile }, + { "mp3", kXMP_MP3File }, + { "mp4", kXMP_MPEG4File }, + { "m4v", kXMP_MPEG4File }, + { "m4a", kXMP_MPEG4File }, + { "f4v", kXMP_MPEG4File }, + { "ses", kXMP_SESFile }, + { "cel", kXMP_CELFile }, + { "wma", kXMP_WMAVFile }, + { "wmv", kXMP_WMAVFile }, + { "mxf", kXMP_MXFFile }, + + { "mpg", kXMP_MPEGFile }, + { "mpeg", kXMP_MPEGFile }, + { "mp2", kXMP_MPEGFile }, + { "mod", kXMP_MPEGFile }, + { "m2v", kXMP_MPEGFile }, + { "mpa", kXMP_MPEGFile }, + { "mpv", kXMP_MPEGFile }, + { "m2p", kXMP_MPEGFile }, + { "m2a", kXMP_MPEGFile }, + { "m2t", kXMP_MPEGFile }, + { "mpe", kXMP_MPEGFile }, + { "vob", kXMP_MPEGFile }, + { "ms-pvr", kXMP_MPEGFile }, + { "dvr-ms", kXMP_MPEGFile }, + + { "html", kXMP_HTMLFile }, + { "xml", kXMP_XMLFile }, + { "txt", kXMP_TextFile }, + { "text", kXMP_TextFile }, + + { "psd", kXMP_PhotoshopFile }, + { "ai", kXMP_IllustratorFile }, + { "indd", kXMP_InDesignFile }, + { "indt", kXMP_InDesignFile }, + { "aep", kXMP_AEProjectFile }, + { "aepx", kXMP_AEProjectFile }, + { "aet", kXMP_AEProjTemplateFile }, + { "ffx", kXMP_AEFilterPresetFile }, + { "ncor", kXMP_EncoreProjectFile }, + { "prproj", kXMP_PremiereProjectFile }, + { "prtl", kXMP_PremiereTitleFile }, + { "ucf", kXMP_UCFFile }, + { "xfl", kXMP_UCFFile }, + { "pdfxml", kXMP_UCFFile }, + { "mars", kXMP_UCFFile }, + { "idml", kXMP_UCFFile }, + { "idap", kXMP_UCFFile }, + { "icap", kXMP_UCFFile }, + { "", 0 } }; // ! Must be last as a sentinel. + +// Files known to contain XMP but have no smart handling, here or elsewhere. +const char * kKnownScannedFiles[] = + { "gif", // GIF, public format but no smart handler. + "ai", // Illustrator, actually a PDF file. + "ait", // Illustrator template, actually a PDF file. + "svg", // SVG, an XML file. + "aet", // After Effects template project file. + "ffx", // After Effects filter preset file. + "aep", // After Effects project file in proprietary format + "aepx", // After Effects project file in XML format + "inx", // InDesign interchange, an XML file. + "inds", // InDesign snippet, an XML file. + "inpk", // InDesign package for GoLive, a text file (not XML). + "incd", // InCopy story, an XML file. + "inct", // InCopy template, an XML file. + "incx", // InCopy interchange, an XML file. + "fm", // FrameMaker file, proprietary format. + "book", // FrameMaker book, proprietary format. + "icml", // an inCopy (inDesign) format + "icmt", // an inCopy (inDesign) format + "idms", // an inCopy (inDesign) format + 0 }; // ! Keep a 0 sentinel at the end. + + +// Extensions that XMPFiles never handles. +const char * kKnownRejectedFiles[] = + { + // RAW files + "cr2", "erf", "fff", "dcr", "kdc", "mos", "mfw", "mef", + "raw", "nef", "orf", "pef", "arw", "sr2", "srf", "sti", + "3fr", "rwl", "crw", "sraw", "mos", "mrw", "nrw", "rw2", + "c3f", + // UCF subformats + "air", + // Others + "r3d", + 0 }; // ! Keep a 0 sentinel at the end. + +// ================================================================================================= + +// ================================================================================================= + +// ================================================================================================= +// GetPacketCharForm +// ================= +// +// The first character must be U+FEFF or ASCII, typically '<' for an outermost element, initial +// processing instruction, or XML declaration. The second character can't be U+0000. +// The possible input sequences are: +// Cases with U+FEFF +// EF BB BF -- - UTF-8 +// FE FF -- -- - Big endian UTF-16 +// 00 00 FE FF - Big endian UTF 32 +// FF FE 00 00 - Little endian UTF-32 +// FF FE -- -- - Little endian UTF-16 +// Cases with ASCII +// nn mm -- -- - UTF-8 - +// 00 00 00 nn - Big endian UTF-32 +// 00 nn -- -- - Big endian UTF-16 +// nn 00 00 00 - Little endian UTF-32 +// nn 00 -- -- - Little endian UTF-16 + +static XMP_Uns8 GetPacketCharForm ( XMP_StringPtr packetStr, XMP_StringLen packetLen ) +{ + XMP_Uns8 charForm = kXMP_CharUnknown; + XMP_Uns8 * unsBytes = (XMP_Uns8*)packetStr; // ! Make sure comparisons are unsigned. + + if ( packetLen < 2 ) return kXMP_Char8Bit; + + if ( packetLen < 4 ) { + + // These cases are based on the first 2 bytes: + // 00 nn Big endian UTF-16 + // nn 00 Little endian UTF-16 + // FE FF Big endian UTF-16 + // FF FE Little endian UTF-16 + // Otherwise UTF-8 + + if ( packetStr[0] == 0 ) return kXMP_Char16BitBig; + if ( packetStr[1] == 0 ) return kXMP_Char16BitLittle; + if ( CheckBytes ( packetStr, "\xFE\xFF", 2 ) ) return kXMP_Char16BitBig; + if ( CheckBytes ( packetStr, "\xFF\xFE", 2 ) ) return kXMP_Char16BitLittle; + return kXMP_Char8Bit; + + } + + // If we get here the packet is at least 4 bytes, could be any form. + + if ( unsBytes[0] == 0 ) { + + // These cases are: + // 00 nn -- -- - Big endian UTF-16 + // 00 00 00 nn - Big endian UTF-32 + // 00 00 FE FF - Big endian UTF 32 + + if ( unsBytes[1] != 0 ) { + charForm = kXMP_Char16BitBig; // 00 nn + } else { + if ( (unsBytes[2] == 0) && (unsBytes[3] != 0) ) { + charForm = kXMP_Char32BitBig; // 00 00 00 nn + } else if ( (unsBytes[2] == 0xFE) && (unsBytes[3] == 0xFF) ) { + charForm = kXMP_Char32BitBig; // 00 00 FE FF + } + } + + } else { + + // These cases are: + // FE FF -- -- - Big endian UTF-16, FE isn't valid UTF-8 + // FF FE 00 00 - Little endian UTF-32, FF isn't valid UTF-8 + // FF FE -- -- - Little endian UTF-16 + // nn mm -- -- - UTF-8, includes EF BB BF case + // nn 00 00 00 - Little endian UTF-32 + // nn 00 -- -- - Little endian UTF-16 + + if ( unsBytes[0] == 0xFE ) { + if ( unsBytes[1] == 0xFF ) charForm = kXMP_Char16BitBig; // FE FF + } else if ( unsBytes[0] == 0xFF ) { + if ( unsBytes[1] == 0xFE ) { + if ( (unsBytes[2] == 0) && (unsBytes[3] == 0) ) { + charForm = kXMP_Char32BitLittle; // FF FE 00 00 + } else { + charForm = kXMP_Char16BitLittle; // FF FE + } + } + } else if ( unsBytes[1] != 0 ) { + charForm = kXMP_Char8Bit; // nn mm + } else { + if ( (unsBytes[2] == 0) && (unsBytes[3] == 0) ) { + charForm = kXMP_Char32BitLittle; // nn 00 00 00 + } else { + charForm = kXMP_Char16BitLittle; // nn 00 + } + } + + } + + // XMP_Assert ( charForm != kXMP_CharUnknown ); + return charForm; + +} // GetPacketCharForm + +// ================================================================================================= +// FillPacketInfo +// ============== +// +// If a packet wrapper is present, the the packet string is roughly: +// <?xpacket begin= ...?> +// <outer-XML-element> +// ... more XML ... +// </outer-XML-element> +// ... whitespace padding ... +// <?xpacket end='.'?> + +// The 8-bit form is 14 bytes, the 16-bit form is 28 bytes, the 32-bit form is 56 bytes. +#define k8BitTrailer "<?xpacket end=" +#define k16BitTrailer "<\0?\0x\0p\0a\0c\0k\0e\0t\0 \0e\0n\0d\0=\0" +#define k32BitTrailer "<\0\0\0?\0\0\0x\0\0\0p\0\0\0a\0\0\0c\0\0\0k\0\0\0e\0\0\0t\0\0\0 \0\0\0e\0\0\0n\0\0\0d\0\0\0=\0\0\0" +static XMP_StringPtr kPacketTrailiers[3] = { k8BitTrailer, k16BitTrailer, k32BitTrailer }; + +void FillPacketInfo ( const std::string & packet, XMP_PacketInfo * info ) +{ + XMP_StringPtr packetStr = packet.c_str(); + XMP_StringLen packetLen = (XMP_StringLen) packet.size(); + if ( packetLen == 0 ) return; + + info->charForm = GetPacketCharForm ( packetStr, packetLen ); + XMP_StringLen charSize = XMP_GetCharSize ( info->charForm ); + + // Look for a packet wrapper. For our purposes, we can be lazy and just look for the trailer PI. + // If that is present we'll assume that a recognizable header is present. First do a bytewise + // search for '<', then a char sized comparison for the start of the trailer. We don't really + // care about big or little endian here. We're looking for ASCII bytes with zeroes between. + // Shorten the range comparisons (n*charSize) by 1 to easily tolerate both big and little endian. + + XMP_StringLen padStart, padEnd; + XMP_StringPtr packetTrailer = kPacketTrailiers [ charSize>>1 ]; + + padEnd = packetLen - 1; + for ( ; padEnd > 0; --padEnd ) if ( packetStr[padEnd] == '<' ) break; + if ( (packetStr[padEnd] != '<') || ((packetLen - padEnd) < (18*charSize)) ) return; + if ( ! CheckBytes ( &packetStr[padEnd], packetTrailer, (13*charSize) ) ) return; + + info->hasWrapper = true; + + char rwFlag = packetStr [padEnd + 15*charSize]; + if ( rwFlag == 'w' ) info->writeable = true; + + // Look for the start of the padding, right after the last XML end tag. + + padStart = padEnd; // Don't do the -charSize here, might wrap below zero. + for ( ; padStart >= charSize; padStart -= charSize ) if ( packetStr[padStart] == '>' ) break; + if ( padStart < charSize ) return; + padStart += charSize; // The padding starts after the '>'. + + info->padSize = padEnd - padStart; // We want bytes of padding, not character units. + +} // FillPacketInfo + +// ================================================================================================= +// ReadXMPPacket +// ============= + +void ReadXMPPacket ( XMPFileHandler * handler ) +{ + XMP_IO* fileRef = handler->parent->ioRef; + std::string & xmpPacket = handler->xmpPacket; + XMP_StringLen packetLen = handler->packetInfo.length; + + if ( packetLen == 0 ) XMP_Throw ( "ReadXMPPacket - No XMP packet", kXMPErr_BadXMP ); + + xmpPacket.erase(); + xmpPacket.reserve ( packetLen ); + xmpPacket.append ( packetLen, ' ' ); + + XMP_StringPtr packetStr = XMP_StringPtr ( xmpPacket.c_str() ); // Don't set until after reserving the space! + + fileRef->Seek ( handler->packetInfo.offset, kXMP_SeekFromStart ); + fileRef->ReadAll ( (char*)packetStr, packetLen ); + +} // ReadXMPPacket + +// ================================================================================================= +// XMPFileHandler::GetFileModDate +// ============================== +// +// The base implementation is only for typical embedding handlers. It returns the modification date +// of the named file. + +bool XMPFileHandler::GetFileModDate ( XMP_DateTime * modDate ) +{ + + XMP_OptionBits flags = this->handlerFlags; + if ( (flags & kXMPFiles_HandlerOwnsFile) || + (flags & kXMPFiles_UsesSidecarXMP) || + (flags & kXMPFiles_FolderBasedFormat) ) { + XMP_Throw ( "Base implementation of GetFileModDate only for typical embedding handlers", kXMPErr_InternalFailure ); + } + + if ( this->parent->filePath.empty() ) { + XMP_Throw ( "GetFileModDate cannot be used with client-provided I/O", kXMPErr_InternalFailure ); + } + + return Host_IO::GetModifyDate ( this->parent->filePath.c_str(), modDate ); + +} // XMPFileHandler::GetFileModDate + +// ================================================================================================= +// XMPFileHandler::ProcessXMP +// ========================== +// +// This base implementation just parses the XMP. If the derived handler does reconciliation then it +// must have its own implementation of ProcessXMP. + +void XMPFileHandler::ProcessXMP() +{ + + if ( (!this->containsXMP) || this->processedXMP ) return; + + if ( this->handlerFlags & kXMPFiles_CanReconcile ) { + XMP_Throw ( "Reconciling file handlers must implement ProcessXMP", kXMPErr_InternalFailure ); + } + + SXMPUtils::RemoveProperties ( &this->xmpObj, 0, 0, kXMPUtil_DoAllProperties ); + this->xmpObj.ParseFromBuffer ( this->xmpPacket.c_str(), (XMP_StringLen)this->xmpPacket.size() ); + this->processedXMP = true; + +} // XMPFileHandler::ProcessXMP + +// ================================================================================================= +// XMPFileHandler::GetSerializeOptions +// =================================== +// +// This base implementation just selects compact serialization. The character form and padding/in-place +// settings are added in the common code before calling SerializeToBuffer. + +XMP_OptionBits XMPFileHandler::GetSerializeOptions() +{ + + return kXMP_UseCompactFormat; + +} // XMPFileHandler::GetSerializeOptions + +// ================================================================================================= diff --git a/XMPFiles/source/XMPFiles_Impl.hpp b/XMPFiles/source/XMPFiles_Impl.hpp new file mode 100644 index 0000000..689aa43 --- /dev/null +++ b/XMPFiles/source/XMPFiles_Impl.hpp @@ -0,0 +1,329 @@ +#ifndef __XMPFiles_Impl_hpp__ +#define __XMPFiles_Impl_hpp__ 1 + +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2004 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +#include "public/include/XMP_Environment.h" // ! Must be the first #include! +#include "public/include/XMP_Const.h" +#include "build/XMP_BuildInfo.h" +#include "source/XMP_LibUtils.hpp" +#include "source/EndianUtils.hpp" + +#include <string> +#define TXMP_STRING_TYPE std::string +#define XMP_INCLUDE_XMPFILES 1 +#include "public/include/XMP.hpp" + +#include "XMPFiles/source/XMPFiles.hpp" +#include "public/include/XMP_IO.hpp" +#include "source/Host_IO.hpp" + +#include <vector> +#include <map> +#include <cassert> +#include <cstring> +#include <cstdlib> +#include <cstdio> + +#if XMP_WinBuild + + #define snprintf _snprintf + +#else + + #if XMP_MacBuild + #include <CoreServices/CoreServices.h> + #endif + + // POSIX headers for both Mac and generic UNIX. + #include <fcntl.h> + #include <unistd.h> + #include <dirent.h> + #include <sys/stat.h> + #include <sys/types.h> + +#endif + +// ================================================================================================= +// General global variables and macros +// =================================== + +typedef std::vector<XMP_Uns8> RawDataBlock; + +extern bool ignoreLocalText; + +#ifndef EnablePhotoHandlers + #define EnablePhotoHandlers 1 +#endif + +#ifndef EnableDynamicMediaHandlers + #define EnableDynamicMediaHandlers 1 +#endif + +#ifndef EnableMiscHandlers + #define EnableMiscHandlers 1 +#endif + +#ifndef EnablePacketScanning + #define EnablePacketScanning 1 +#endif + +extern XMP_Int32 sXMPFilesInitCount; + +#ifndef GatherPerformanceData + #define GatherPerformanceData 0 +#endif + +#if ! GatherPerformanceData + + #define StartPerfCheck(proc,info) /* do nothing */ + #define EndPerfCheck(proc) /* do nothing */ + +#else + + #include "PerfUtils.hpp" + + enum { + kAPIPerf_OpenFile, + kAPIPerf_CloseFile, + kAPIPerf_GetXMP, + kAPIPerf_PutXMP, + kAPIPerf_CanPutXMP, + kAPIPerfProcCount // Last, count of the procs. + }; + + static const char* kAPIPerfNames[] = + { "OpenFile", "CloseFile", "GetXMP", "PutXMP", "CanPutXMP", 0 }; + + struct APIPerfItem { + XMP_Uns8 whichProc; + double elapsedTime; + XMPFilesRef xmpFilesRef; + std::string extraInfo; + APIPerfItem ( XMP_Uns8 proc, double time, XMPFilesRef ref, const char * info ) + : whichProc(proc), elapsedTime(time), xmpFilesRef(ref), extraInfo(info) {}; + }; + + typedef std::vector<APIPerfItem> APIPerfCollection; + + extern APIPerfCollection* sAPIPerf; + + #define StartPerfCheck(proc,info) \ + sAPIPerf->push_back ( APIPerfItem ( proc, 0.0, xmpFilesRef, info ) ); \ + APIPerfItem & thisPerf = sAPIPerf->back(); \ + PerfUtils::MomentValue startTime, endTime; \ + try { \ + startTime = PerfUtils::NoteThisMoment(); + + #define EndPerfCheck(proc) \ + endTime = PerfUtils::NoteThisMoment(); \ + thisPerf.elapsedTime = PerfUtils::GetElapsedSeconds ( startTime, endTime ); \ + } catch ( ... ) { \ + endTime = PerfUtils::NoteThisMoment(); \ + thisPerf.elapsedTime = PerfUtils::GetElapsedSeconds ( startTime, endTime ); \ + thisPerf.extraInfo += " ** THROW **"; \ + throw; \ + } + +#endif + +extern XMP_FileFormat voidFileFormat; // Used as sink for unwanted output parameters. +extern XMP_PacketInfo voidPacketInfo; +extern void * voidVoidPtr; +extern XMP_StringPtr voidStringPtr; +extern XMP_StringLen voidStringLen; +extern XMP_OptionBits voidOptionBits; + +static const XMP_Uns8 * kUTF8_PacketStart = (const XMP_Uns8 *) "<?xpacket begin="; +static const XMP_Uns8 * kUTF8_PacketID = (const XMP_Uns8 *) "W5M0MpCehiHzreSzNTczkc9d"; +static const size_t kUTF8_PacketHeaderLen = 51; // ! strlen ( "<?xpacket begin='xxx' id='W5M0MpCehiHzreSzNTczkc9d'" ) + +static const XMP_Uns8 * kUTF8_PacketTrailer = (const XMP_Uns8 *) "<?xpacket end=\"w\"?>"; +static const size_t kUTF8_PacketTrailerLen = 19; // ! strlen ( kUTF8_PacketTrailer ) + +struct FileExtMapping { + XMP_StringPtr ext; + XMP_FileFormat format; +}; + +extern const FileExtMapping kFileExtMap[]; +extern const char * kKnownScannedFiles[]; +extern const char * kKnownRejectedFiles[]; + +typedef std::map < const char *, const char * > ID3GenreMap; +extern ID3GenreMap* kMapID3GenreCodeToName; // Storage defined in ID3_Support.cpp. +extern ID3GenreMap* kMapID3GenreNameToCode; + +#define Uns8Ptr(p) ((XMP_Uns8 *) (p)) + +#define IsNewline( ch ) ( ((ch) == kLF) || ((ch) == kCR) ) +#define IsSpaceOrTab( ch ) ( ((ch) == ' ') || ((ch) == kTab) ) +#define IsWhitespace( ch ) ( IsSpaceOrTab ( ch ) || IsNewline ( ch ) ) + +static inline void MakeLowerCase ( std::string * str ) +{ + for ( size_t i = 0, limit = str->size(); i < limit; ++i ) { + char ch = (*str)[i]; + if ( ('A' <= ch) && (ch <= 'Z') ) (*str)[i] += 0x20; + } +} + +static inline void MakeUpperCase ( std::string * str ) +{ + for ( size_t i = 0, limit = str->size(); i < limit; ++i ) { + char ch = (*str)[i]; + if ( ('a' <= ch) && (ch <= 'z') ) (*str)[i] -= 0x20; + } +} + +#define XMP_LitMatch(s,l) (strcmp((s),(l)) == 0) +#define XMP_LitNMatch(s,l,n) (strncmp((s),(l),(n)) == 0) + +// ================================================================================================= +// Support for call tracing +// ======================== + +#ifndef XMP_TraceFilesCalls + #define XMP_TraceFilesCalls 0 + #define XMP_TraceFilesCallsToFile 0 +#endif + +#if XMP_TraceFilesCalls + + #undef AnnounceThrow + #undef AnnounceCatch + + #undef AnnounceEntry + #undef AnnounceNoLock + #undef AnnounceExit + + extern FILE * xmpFilesLog; + + #define AnnounceThrow(msg) \ + fprintf ( xmpFilesLog, "XMP_Throw: %s\n", msg ); fflush ( xmpFilesLog ) + #define AnnounceCatch(msg) \ + fprintf ( xmpFilesLog, "Catch in %s: %s\n", procName, msg ); fflush ( xmpFilesLog ) + + #define AnnounceEntry(proc) \ + const char * procName = proc; \ + fprintf ( xmpFilesLog, "Entering %s\n", procName ); fflush ( xmpFilesLog ) + #define AnnounceNoLock(proc) \ + const char * procName = proc; \ + fprintf ( xmpFilesLog, "Entering %s (no lock)\n", procName ); fflush ( xmpFilesLog ) + #define AnnounceExit() \ + fprintf ( xmpFilesLog, "Exiting %s\n", procName ); fflush ( xmpFilesLog ) + +#endif + +// ================================================================================================= +// Support for memory leak tracking +// ================================ + +#ifndef TrackMallocAndFree + #define TrackMallocAndFree 0 +#endif + +#if TrackMallocAndFree + + static void* ChattyMalloc ( size_t size ) + { + void* ptr = malloc ( size ); + fprintf ( stderr, "Malloc %d bytes @ %.8X\n", size, ptr ); + return ptr; + } + + static void ChattyFree ( void* ptr ) + { + fprintf ( stderr, "Free @ %.8X\n", ptr ); + free ( ptr ); + } + + #define malloc(s) ChattyMalloc ( s ) + #define free(p) ChattyFree ( p ) + +#endif + +// ================================================================================================= +// FileHandler declarations +// ======================== + +extern void ReadXMPPacket ( XMPFileHandler * handler ); + +extern void FillPacketInfo ( const XMP_VarString & packet, XMP_PacketInfo * info ); + +class XMPFileHandler { // See XMPFiles.hpp for usage notes. +public: + +#define DefaultCTorPresets \ + handlerFlags(0), stdCharForm(kXMP_CharUnknown), \ + containsXMP(false), processedXMP(false), needsUpdate(false) + + XMPFileHandler() : parent(0), DefaultCTorPresets {}; + XMPFileHandler (XMPFiles * _parent) : parent(_parent), DefaultCTorPresets {}; + + virtual ~XMPFileHandler() {}; // ! The specific handler is responsible for tnailInfo.tnailImage. + + virtual bool GetFileModDate ( XMP_DateTime * modDate ); // The default implementation is for embedding handlers. + + virtual void CacheFileData() = 0; + virtual void ProcessXMP(); // The default implementation just parses the XMP. + + virtual XMP_OptionBits GetSerializeOptions(); // The default is compact. + + virtual void UpdateFile ( bool doSafeUpdate ) = 0; + virtual void WriteTempFile ( XMP_IO* tempRef ) = 0; + + // ! Leave the data members public so common code can see them. + + XMPFiles * parent; // Let's the handler see the file info. + XMP_OptionBits handlerFlags; // Capabilities of this handler. + XMP_Uns8 stdCharForm; // The standard character form for output. + + bool containsXMP; // True if the file has XMP or PutXMP has been called. + bool processedXMP; // True if the XMP is parsed and reconciled. + bool needsUpdate; // True if the file needs to be updated. + + XMP_PacketInfo packetInfo; // ! This is always info about the packet in the file, if any! + std::string xmpPacket; // ! This is the current XMP, updated by XMPFiles::PutXMP. + SXMPMeta xmpObj; + +}; // XMPFileHandler + +typedef XMPFileHandler * (* XMPFileHandlerCTor) ( XMPFiles * parent ); + +typedef bool (* CheckFileFormatProc ) ( XMP_FileFormat format, + XMP_StringPtr filePath, + XMP_IO * fileRef, + XMPFiles * parent ); + +typedef bool (*CheckFolderFormatProc ) ( XMP_FileFormat format, + const std::string & rootPath, + const std::string & gpName, + const std::string & parentName, + const std::string & leafName, + XMPFiles * parent ); + +// ================================================================================================= + +// ------------------------------------------------------------------------------------------------- + +static inline bool CheckBytes ( const void * left, const void * right, size_t length ) +{ + return (memcmp ( left, right, length ) == 0); +} + +// ------------------------------------------------------------------------------------------------- + +static inline bool CheckCString ( const void * left, const void * right ) +{ + return (strcmp ( (char*)left, (char*)right ) == 0); +} + +#endif /* __XMPFiles_Impl_hpp__ */ |