diff --git a/XMLDB/FileReader.cpp b/XMLDB/FileReader.cpp index 70492371..228c801d 100644 --- a/XMLDB/FileReader.cpp +++ b/XMLDB/FileReader.cpp @@ -1,544 +1,547 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Local includes #include "CompressFileInfo.h" #include "Database.h" #include "FileReader.h" #include "Logging.h" #include "XMLCategory.h" #include #include #include #include // KDE includes #include #include // Qt includes #include #include #include #include #include #include void XMLDB::FileReader::read( const QString& configFile ) { static QString versionString = QString::fromUtf8("version"); static QString compressedString = QString::fromUtf8("compressed"); ReaderPtr reader = readConfigFile( configFile ); ElementInfo info = reader->readNextStartOrStopElement(QString::fromUtf8("KPhotoAlbum")); if (!info.isStartToken) reader->complainStartElementExpected(QString::fromUtf8("KPhotoAlbum")); m_fileVersion = reader->attribute( versionString, QString::fromLatin1( "1" ) ).toInt(); if ( m_fileVersion > Database::fileVersion() ) { int ret = KMessageBox::warningContinueCancel( messageParent(), i18n("

The database file (index.xml) is from a newer version of KPhotoAlbum!

" "

Chances are you will be able to read this file, but when writing it back, " "information saved in the newer version will be lost

"), i18n("index.xml version mismatch"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QString::fromLatin1( "checkDatabaseFileVersion" ) ); if (ret == KStandardGuiItem::Cancel) exit(-1); } setUseCompressedFileFormat( reader->attribute(compressedString).toInt() ); m_db->m_members.setLoading( true ); loadCategories( reader ); loadImages( reader ); loadBlockList( reader ); loadMemberGroups( reader ); //loadSettings(reader); m_db->m_members.setLoading( false ); checkIfImagesAreSorted(); checkIfAllImagesHaveSizeAttributes(); } void XMLDB::FileReader::createSpecialCategories() { // Setup the "Folder" category m_folderCategory = new XMLCategory(i18n("Folder"), QString::fromLatin1("folder"), DB::Category::TreeView, 32, false ); m_folderCategory->setType( DB::Category::FolderCategory ); // The folder category is not stored in the index.xml file, // but older versions of KPhotoAlbum stored a stub entry, which we need to remove first: if ( m_db->m_categoryCollection.categoryForName(m_folderCategory->name()) ) m_db->m_categoryCollection.removeCategory(m_folderCategory->name()); m_db->m_categoryCollection.addCategory( m_folderCategory ); dynamic_cast( m_folderCategory.data() )->setShouldSave( false ); // Setup the "Tokens" category DB::CategoryPtr tokenCat; if (m_fileVersion >= 7) { tokenCat = m_db->m_categoryCollection.categoryForSpecial( DB::Category::TokensCategory ); } else { // Before version 7, the "Tokens" category name wasn't stored to the settings. So ... // look for a literal "Tokens" category ... tokenCat = m_db->m_categoryCollection.categoryForName(QString::fromUtf8("Tokens")); if (!tokenCat) { // ... and a translated "Tokens" category if we don't have the literal one. tokenCat = m_db->m_categoryCollection.categoryForName(i18n("Tokens")); } if (tokenCat) { // in this case we need to give the tokens category its special meaning: m_db->m_categoryCollection.removeCategory(tokenCat->name()); tokenCat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory(tokenCat); } } if (! tokenCat) { // Create a new "Tokens" category tokenCat = new XMLCategory(i18n("Tokens"), QString::fromUtf8("tag"), DB::Category::TreeView, 32, true); tokenCat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory(tokenCat); } // KPhotoAlbum 2.2 did not write the tokens to the category section, // so unless we do this small trick they will not show up when importing. for (char ch = 'A'; ch <= 'Z'; ++ch) { tokenCat->addItem(QString::fromUtf8("%1").arg(QChar::fromLatin1(ch))); } // Setup the "Media Type" category DB::CategoryPtr mediaCat; mediaCat = new XMLCategory(i18n("Media Type"), QString::fromLatin1("view-categories"), DB::Category::TreeView, 32, false); mediaCat->addItem( i18n( "Image" ) ); mediaCat->addItem( i18n( "Video" ) ); mediaCat->setType( DB::Category::MediaTypeCategory ); dynamic_cast( mediaCat.data() )->setShouldSave( false ); // The media type is not stored in the media category, // but older versions of KPhotoAlbum stored a stub entry, which we need to remove first: if ( m_db->m_categoryCollection.categoryForName(mediaCat->name()) ) m_db->m_categoryCollection.removeCategory( mediaCat->name() ); m_db->m_categoryCollection.addCategory( mediaCat ); } void XMLDB::FileReader::loadCategories( ReaderPtr reader ) { static QString nameString = QString::fromUtf8("name"); static QString iconString = QString::fromUtf8("icon"); static QString viewTypeString = QString::fromUtf8("viewtype"); static QString showString = QString::fromUtf8("show"); static QString thumbnailSizeString = QString::fromUtf8("thumbnailsize"); static QString positionableString = QString::fromUtf8("positionable"); static QString metaString = QString::fromUtf8("meta"); static QString tokensString = QString::fromUtf8("tokens"); static QString valueString = QString::fromUtf8("value"); static QString idString = QString::fromUtf8("id"); static QString birthDateString = QString::fromUtf8("birthDate"); static QString categoriesString = QString::fromUtf8("Categories"); static QString categoryString = QString::fromUtf8("Category"); ElementInfo info = reader->readNextStartOrStopElement(categoriesString); if (!info.isStartToken) reader->complainStartElementExpected(categoriesString); while ( reader->readNextStartOrStopElement(categoryString).isStartToken) { const QString categoryName = unescape(reader->attribute(nameString)); if ( !categoryName.isNull() ) { // Read Category info QString icon = reader->attribute(iconString); DB::Category::ViewType type = (DB::Category::ViewType) reader->attribute( viewTypeString, QString::fromLatin1( "0" ) ).toInt(); int thumbnailSize = reader->attribute( thumbnailSizeString, QString::fromLatin1( "32" ) ).toInt(); bool show = (bool) reader->attribute( showString, QString::fromLatin1( "1" ) ).toInt(); bool positionable = (bool) reader->attribute( positionableString, QString::fromLatin1( "0" ) ).toInt(); bool tokensCat = reader->attribute(metaString) == tokensString; DB::CategoryPtr cat = m_db->m_categoryCollection.categoryForName( categoryName ); bool repairMode = false; if (cat) { int choice = KMessageBox::warningContinueCancel( messageParent(), i18n( "

Line %1, column %2: duplicate category '%3'

" "

Choose continue to ignore the duplicate category and try an automatic repair, " "or choose cancel to quit.

", reader->lineNumber(), reader->columnNumber(), categoryName ), i18n("Error in database file")); if ( choice == KMessageBox::Continue ) repairMode = true; else exit(-1); } else { cat = new XMLCategory( categoryName, icon, type, thumbnailSize, show, positionable ); if (tokensCat) cat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory( cat ); } // Read values QStringList items; while( reader->readNextStartOrStopElement(valueString).isStartToken) { QString value = reader->attribute(valueString); if ( reader->hasAttribute(idString) ) { int id = reader->attribute(idString).toInt(); static_cast(cat.data())->setIdMapping( value, id ); } if (reader->hasAttribute(birthDateString)) cat->setBirthDate(value,QDate::fromString(reader->attribute(birthDateString), Qt::ISODate)); items.append( value ); reader->readEndElement(); } if ( repairMode ) { // merge with duplicate category qCInfo(XMLDBLog) << "Repairing category " << categoryName << ": merging items " << cat->items() << " with " << items; items.append(cat->items()); items.removeDuplicates(); } cat->setItems( items ); } } createSpecialCategories(); if (m_fileVersion < 7) { KMessageBox::information( messageParent(), i18nc("Leave \"Folder\" and \"Media Type\" untranslated below, those will show up with " "these exact names. Thanks :-)", "

This version of KPhotoAlbum does not translate \"standard\" categories " "any more.

" "

This may mean that – if you use a locale other than English – some of your " "categories are now displayed in English.

" "

You can manually rename your categories any time and then save your database." "

" "

In some cases, you may get two additional empty categories, \"Folder\" and " "\"Media Type\". You can delete those.

"), i18n("Changed standard category names") ); } } void XMLDB::FileReader::loadImages( ReaderPtr reader ) { static QString fileString = QString::fromUtf8("file"); static QString imagesString = QString::fromUtf8("images"); static QString imageString = QString::fromUtf8("image"); ElementInfo info = reader->readNextStartOrStopElement(imagesString); if (!info.isStartToken) reader->complainStartElementExpected(imagesString); while (reader->readNextStartOrStopElement(imageString).isStartToken) { const QString fileNameStr = reader->attribute(fileString); if ( fileNameStr.isNull() ) { qCWarning(XMLDBLog, "Element did not contain a file attribute" ); return; } const DB::FileName dbFileName = DB::FileName::fromRelativePath(fileNameStr); DB::ImageInfoPtr info = load( dbFileName, reader ); m_db->m_images.append(info); m_db->m_md5map.insert( info->MD5Sum(), dbFileName ); } } void XMLDB::FileReader::loadBlockList( ReaderPtr reader ) { static QString fileString = QString::fromUtf8("file"); static QString blockListString = QString::fromUtf8("blocklist"); static QString blockString = QString::fromUtf8("block"); ElementInfo info = reader->peekNext(); if ( info.isStartToken && info.tokenName == blockListString ) { reader->readNextStartOrStopElement(blockListString); while (reader->readNextStartOrStopElement(blockString).isStartToken) { QString fileName = reader->attribute(fileString); if ( !fileName.isEmpty() ) m_db->m_blockList.insert(DB::FileName::fromRelativePath(fileName)); reader->readEndElement(); } } } void XMLDB::FileReader::loadMemberGroups( ReaderPtr reader ) { static QString categoryString = QString::fromUtf8("category"); static QString groupNameString = QString::fromUtf8("group-name"); static QString memberString = QString::fromUtf8("member"); static QString membersString = QString::fromUtf8("members"); static QString memberGroupsString = QString::fromUtf8("member-groups"); ElementInfo info = reader->peekNext(); if ( info.isStartToken && info.tokenName == memberGroupsString) { reader->readNextStartOrStopElement(memberGroupsString); while(reader->readNextStartOrStopElement(memberString).isStartToken) { QString category = reader->attribute(categoryString); QString group = reader->attribute(groupNameString); if ( reader->hasAttribute(memberString) ) { QString member = reader->attribute(memberString); m_db->m_members.addMemberToGroup( category, group, member ); } else { QStringList members = reader->attribute(membersString).split( QString::fromLatin1( "," ), QString::SkipEmptyParts ); Q_FOREACH( const QString &memberItem, members ) { DB::CategoryPtr catPtr = m_db->m_categoryCollection.categoryForName( category ); if (!catPtr) { // category was not declared in "Categories" qCWarning(XMLDBLog) << "File corruption in index.xml. Inserting missing category: " << category; catPtr = new XMLCategory(category, QString::fromUtf8("dialog-warning"), DB::Category::TreeView, 32, false); m_db->m_categoryCollection.addCategory( catPtr ); } XMLCategory* cat = static_cast( catPtr.data() ); QString member = cat->nameForId( memberItem.toInt() ); if (member.isNull()) continue; m_db->m_members.addMemberToGroup( category, group, member ); } if(members.size() == 0) { // Groups are stored even if they are empty, so we also have to read them. // With no members, the above for loop will not be executed. m_db->m_members.addGroup(category, group); } } reader->readEndElement(); } } } /* void XMLDB::FileReader::loadSettings(ReaderPtr reader) { static QString settingsString = QString::fromUtf8("settings"); static QString settingString = QString::fromUtf8("setting"); static QString keyString = QString::fromUtf8("key"); static QString valueString = QString::fromUtf8("value"); ElementInfo info = reader->peekNext(); if (info.isStartToken && info.tokenName == settingsString) { reader->readNextStartOrStopElement(settingString); while(reader->readNextStartOrStopElement(settingString).isStartToken) { if (reader->hasAttribute(keyString) && reader->hasAttribute(valueString)) { m_db->m_settings.insert(unescape(reader->attribute(keyString)), unescape(reader->attribute(valueString))); } else { qWarning() << "File corruption in index.xml. Setting either lacking a key or a " << "value attribute. Ignoring this entry."; } reader->readEndElement(); } } } */ void XMLDB::FileReader::checkIfImagesAreSorted() { if ( !KMessageBox::shouldBeShownContinue( QString::fromLatin1( "checkWhetherImagesAreSorted" ) ) ) return; QDateTime last( QDate( 1900, 1, 1 ) ); bool wrongOrder = false; for( DB::ImageInfoListIterator it = m_db->m_images.begin(); !wrongOrder && it != m_db->m_images.end(); ++it ) { if ( last > (*it)->date().start() && (*it)->date().start().isValid() ) wrongOrder = true; last = (*it)->date().start(); } if ( wrongOrder ) { KMessageBox::information( messageParent(), i18n("

Your images/videos are not sorted, which means that navigating using the date bar " "will only work suboptimally.

" "

In the Maintenance menu, you can find Display Images with Incomplete Dates " "which you can use to find the images that are missing date information.

" "

You can then select the images that you have reason to believe have a correct date " "in either their Exif data or on the file, and execute Maintenance->Read Exif Info " "to reread the information.

" "

Finally, once all images have their dates set, you can execute " "Maintenance->Sort All by Date & Time to sort them in the database.

"), i18n("Images/Videos Are Not Sorted"), QString::fromLatin1( "checkWhetherImagesAreSorted" ) ); } } void XMLDB::FileReader::checkIfAllImagesHaveSizeAttributes() { QTime time; time.start(); if ( !KMessageBox::shouldBeShownContinue( QString::fromLatin1( "checkWhetherAllImagesIncludesSize" ) ) ) return; if ( m_db->s_anyImageWithEmptySize ) { KMessageBox::information( messageParent(), i18n("

Not all the images in the database have information about image sizes; this is needed to " "get the best result in the thumbnail view. To fix this, simply go to the Maintenance menu, " "and first choose Remove All Thumbnails, and after that choose Build Thumbnails.

" "

Not doing so will result in extra space around images in the thumbnail view - that is all - so " "there is no urgency in doing it.

"), i18n("Not All Images Have Size Information"), QString::fromLatin1( "checkWhetherAllImagesIncludesSize" ) ); } } DB::ImageInfoPtr XMLDB::FileReader::load( const DB::FileName& fileName, ReaderPtr reader ) { DB::ImageInfoPtr info = XMLDB::Database::createImageInfo(fileName, reader, m_db); m_nextStackId = qMax( m_nextStackId, info->stackId() + 1 ); info->createFolderCategoryItem( m_folderCategory, m_db->m_members ); return info; } XMLDB::ReaderPtr XMLDB::FileReader::readConfigFile( const QString& configFile ) { ReaderPtr reader = ReaderPtr(new XmlReader); QFile file( configFile ); if ( !file.exists() ) { // Load a default setup QFile file(Utilities::locateDataFile(QString::fromLatin1("default-setup"))); if ( !file.open( QIODevice::ReadOnly ) ) { KMessageBox::information( messageParent(), i18n( "

KPhotoAlbum was unable to load a default setup, which indicates an installation error

" "

If you have installed KPhotoAlbum yourself, then you must remember to set the environment variable " "KDEDIRS, to point to the topmost installation directory.

" "

If you for example ran cmake with -DCMAKE_INSTALL_PREFIX=/usr/local/kde, then you must use the following " "environment variable setup (this example is for Bash and compatible shells):

" "

export KDEDIRS=/usr/local/kde

" "

In case you already have KDEDIRS set, simply append the string as if you where setting the PATH " "environment variable

"), i18n("No default setup file found") ); } else { QTextStream stream( &file ); stream.setCodec( QTextCodec::codecForName("UTF-8") ); QString str = stream.readAll(); // Replace the default setup's category and tag names with localized ones str = str.replace(QString::fromUtf8("People"), i18n("People")); str = str.replace(QString::fromUtf8("Places"), i18n("Places")); str = str.replace(QString::fromUtf8("Events"), i18n("Events")); str = str.replace(QString::fromUtf8("untagged"), i18n("untagged")); str = str.replace( QRegExp( QString::fromLatin1("imageDirectory=\"[^\"]*\"")), QString::fromLatin1("") ); str = str.replace( QRegExp( QString::fromLatin1("htmlBaseDir=\"[^\"]*\"")), QString::fromLatin1("") ); str = str.replace( QRegExp( QString::fromLatin1("htmlBaseURL=\"[^\"]*\"")), QString::fromLatin1("") ); reader->addData(str); } } else { if ( !file.open( QIODevice::ReadOnly ) ) { KMessageBox::error( messageParent(), i18n("Unable to open '%1' for reading", configFile ), i18n("Error Running Demo") ); exit(-1); } reader->addData(file.readAll()); #if 0 QString errMsg; int errLine; int errCol; if ( !doc.setContent( &file, false, &errMsg, &errLine, &errCol )) { file.close(); // If parsing index.xml fails let's see if we could use a backup instead Utilities::checkForBackupFile( configFile, i18n( "line %1 column %2 in file %3: %4", errLine , errCol , configFile , errMsg ) ); if ( !file.open( QIODevice::ReadOnly ) || ( !doc.setContent( &file, false, &errMsg, &errLine, &errCol ) ) ) { KMessageBox::error( messageParent(), i18n( "Failed to recover the backup: %1", errMsg ) ); exit(-1); } } #endif } // Now read the content of the file. #if 0 QDomElement top = doc.documentElement(); if ( top.isNull() ) { KMessageBox::error( messageParent(), i18n("Error in file %1: No elements found", configFile ) ); exit(-1); } if ( top.tagName().toLower() != QString::fromLatin1( "kphotoalbum" ) && top.tagName().toLower() != QString::fromLatin1( "kimdaba" ) ) { // KimDaBa compatibility KMessageBox::error( messageParent(), i18n("Error in file %1: expected 'KPhotoAlbum' as top element but found '%2'", configFile , top.tagName() ) ); exit(-1); } #endif file.close(); return reader; } /** * @brief Unescape a string used as an XML attribute name. * * @see XMLDB::FileWriter::escape * * @param str the string to be unescaped * @return the unescaped string */ QString XMLDB::FileReader::unescape( const QString& str ) { - static QHash cache; - if ( cache.contains(str) ) - return cache[str]; + static bool hashUsesCompressedFormat = useCompressedFileFormat(); + static QHash s_cache; + if (hashUsesCompressedFormat != useCompressedFileFormat()) + s_cache.clear(); + if ( s_cache.contains(str) ) + return s_cache[str]; QString tmp( str ); // Matches encoded characters in attribute names QRegExp rx( QString::fromLatin1( "(_.)([0-9A-F]{2})" ) ); int pos = 0; // Unencoding special characters if compressed XML is selected if ( useCompressedFileFormat() ) { while ( ( pos = rx.indexIn( tmp, pos ) ) != -1 ) { QString before = rx.cap( 1 ) + rx.cap( 2 ); QString after = QString::fromLatin1( QByteArray::fromHex( rx.cap( 2 ).toLocal8Bit() ) ); tmp.replace( pos, before.length(), after ); pos += after.length(); } } else tmp.replace( QString::fromLatin1( "_" ), QString::fromLatin1( " " ) ); - cache.insert(str,tmp); + s_cache.insert(str,tmp); return tmp; } // TODO(hzeller): DEPENDENCY This pulls in the whole MainWindow dependency into the database backend. QWidget *XMLDB::FileReader::messageParent() { return MainWindow::Window::theMainWindow(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/FileWriter.cpp b/XMLDB/FileWriter.cpp index f0a876f4..04155576 100644 --- a/XMLDB/FileWriter.cpp +++ b/XMLDB/FileWriter.cpp @@ -1,532 +1,536 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "FileWriter.h" #include "CompressFileInfo.h" #include "Database.h" #include "ElementWriter.h" #include "Logging.h" #include "NumberedBackup.h" #include "XMLCategory.h" #include #include #include #include #include #include #include #include #include // I've added this to provide anyone interested // with a quick and easy means to benchmark performance differences // between old and new save behaviour. // Note: in Qt4, saving files was deterministic "by accident"; with Qt5, we have to work for it // (mostly because QSet is randomized now) // FIXME(ZaJ): this should be removed "soon" (at latest by KPA 5.2.0) #define DETERMINISTIC_DBSAVE // print saving time: //#define BENCHMARK_FILEWRITER // // // // +++++++++++++++++++++++++++++++ REMEMBER ++++++++++++++++++++++++++++++++ // // // // // Update XMLDB::Database::fileVersion every time you update the file format! // // // // // // // // // (sorry for the noise, but it is really important :-) using Utilities::StringSet; void XMLDB::FileWriter::save( const QString& fileName, bool isAutoSave ) { setUseCompressedFileFormat( Settings::SettingsData::instance()->useCompressedIndexXML() ); if ( !isAutoSave ) NumberedBackup().makeNumberedBackup(); // prepare XML document for saving: m_db->m_categoryCollection.initIdMap(); QFile out(fileName + QString::fromLatin1(".tmp")); if ( !out.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::sorry( messageParent(), i18n("

Could not save the image database to XML.

" "File %1 could not be opened because of the following error: %2" , out.fileName(), out.errorString() ) ); return; } QTime t; if (TimingLog().isDebugEnabled()) t.start(); QXmlStreamWriter writer(&out); writer.setAutoFormatting(true); writer.writeStartDocument(); { ElementWriter dummy(writer, QString::fromLatin1("KPhotoAlbum")); writer.writeAttribute( QString::fromLatin1( "version" ), QString::number(Database::fileVersion())); writer.writeAttribute( QString::fromLatin1( "compressed" ), QString::number(useCompressedFileFormat())); saveCategories( writer ); saveImages( writer ); saveBlockList( writer ); saveMemberGroups( writer ); //saveSettings(writer); } writer.writeEndDocument(); qCDebug(TimingLog) << "XMLDB::FileWriter::save(): Saving took" << t.elapsed() <<"ms"; // State: index.xml has previous DB version, index.xml.tmp has the current version. // original file can be safely deleted if ( ( ! QFile::remove( fileName ) ) && QFile::exists( fileName ) ) { KMessageBox::sorry( messageParent(), i18n("

Failed to remove old version of image database.

" "

Please try again or replace the file %1 with file %2 manually!

", fileName, out.fileName() ) ); return; } // State: index.xml doesn't exist, index.xml.tmp has the current version. if ( ! out.rename( fileName ) ) { KMessageBox::sorry( messageParent(), i18n("

Failed to move temporary XML file to permanent location.

" "

Please try again or rename file %1 to %2 manually!

", out.fileName(), fileName ) ); // State: index.xml.tmp has the current version. return; } // State: index.xml has the current version. } void XMLDB::FileWriter::saveCategories( QXmlStreamWriter& writer ) { QStringList categories = DB::ImageDB::instance()->categoryCollection()->categoryNames(); ElementWriter dummy(writer, QString::fromLatin1("Categories") ); DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial( DB::Category::TokensCategory ); for (QString name : categories) { DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(name); if (! shouldSaveCategory(name)) { continue; } ElementWriter dummy(writer, QString::fromUtf8("Category")); writer.writeAttribute(QString::fromUtf8("name"), name); writer.writeAttribute(QString::fromUtf8("icon"), category->iconName()); writer.writeAttribute(QString::fromUtf8("show"), QString::number(category->doShow())); writer.writeAttribute(QString::fromUtf8("viewtype"), QString::number(category->viewType())); writer.writeAttribute(QString::fromUtf8("thumbnailsize"), QString::number(category->thumbnailSize())); writer.writeAttribute(QString::fromUtf8("positionable"), QString::number(category->positionable())); if (category == tokensCategory) { writer.writeAttribute(QString::fromUtf8("meta"),QString::fromUtf8("tokens")); } // FIXME (l3u): // Correct me if I'm wrong, but we don't need this, as the tags used as groups are // added to the respective category anyway when they're created, so there's no need to // re-add them here. Apart from this, adding an empty group (one without members) does // add an empty tag ("") doing so. /* QStringList list = Utilities::mergeListsUniqly(category->items(), m_db->_members.groups(name)); */ Q_FOREACH(const QString &tagName, category->items()) { ElementWriter dummy( writer, QString::fromLatin1("value") ); writer.writeAttribute( QString::fromLatin1("value"), tagName ); writer.writeAttribute( QString::fromLatin1( "id" ), QString::number(static_cast( category.data() )->idForName( tagName ) )); QDate birthDate = category->birthDate(tagName); if (!birthDate.isNull()) writer.writeAttribute( QString::fromUtf8("birthDate"), birthDate.toString(Qt::ISODate) ); } } } void XMLDB::FileWriter::saveImages( QXmlStreamWriter& writer ) { DB::ImageInfoList list = m_db->m_images; // Copy files from clipboard to end of overview, so we don't loose them Q_FOREACH(const DB::ImageInfoPtr &infoPtr, m_db->m_clipboard) { list.append(infoPtr); } { ElementWriter dummy(writer, QString::fromLatin1( "images" ) ); Q_FOREACH(const DB::ImageInfoPtr &infoPtr, list) { save( writer, infoPtr ); } } } void XMLDB::FileWriter::saveBlockList( QXmlStreamWriter& writer ) { ElementWriter dummy( writer, QString::fromLatin1( "blocklist" ) ); #ifdef DETERMINISTIC_DBSAVE QList blockList = m_db->m_blockList.toList(); // sort blocklist to get diffable files std::sort(blockList.begin(), blockList.end()); #else QSet blockList = m_db->m_blockList; #endif Q_FOREACH(const DB::FileName &block, blockList) { ElementWriter dummy( writer, QString::fromLatin1( "block" ) ); writer.writeAttribute( QString::fromLatin1( "file" ), block.relative() ); } } void XMLDB::FileWriter::saveMemberGroups( QXmlStreamWriter& writer ) { if ( m_db->m_members.isEmpty() ) return; ElementWriter dummy( writer, QString::fromLatin1( "member-groups" ) ); for( QMap< QString,QMap >::ConstIterator memberMapIt= m_db->m_members.memberMap().constBegin(); memberMapIt != m_db->m_members.memberMap().constEnd(); ++memberMapIt ) { const QString categoryName = memberMapIt.key(); // FIXME (l3u): This can happen when an empty sub-category (group) is present. // Would be fine to fix the reason why this happens in the first place. if (categoryName.isEmpty()) { continue; } if ( !shouldSaveCategory( categoryName ) ) continue; QMap groupMap = memberMapIt.value(); for( QMap::ConstIterator groupMapIt= groupMap.constBegin(); groupMapIt != groupMap.constEnd(); ++groupMapIt ) { // FIXME (l3u): This can happen when an empty sub-category (group) is present. // Would be fine to fix the reason why this happens in the first place. if (groupMapIt.key().isEmpty()) { continue; } if ( useCompressedFileFormat() ) { StringSet members = groupMapIt.value(); ElementWriter dummy( writer, QString::fromLatin1( "member" ) ); writer.writeAttribute( QString::fromLatin1( "category" ), categoryName ); writer.writeAttribute( QString::fromLatin1( "group-name" ), groupMapIt.key() ); QStringList idList; Q_FOREACH(const QString& member, members) { DB::CategoryPtr catPtr = m_db->m_categoryCollection.categoryForName( categoryName ); XMLCategory* category = static_cast( catPtr.data() ); if (category->idForName(member)==0) qCWarning(XMLDBLog) << "Member" << member << "in group" << categoryName << "->" << groupMapIt.key() << "has no id!"; idList.append( QString::number( category->idForName( member ) ) ); } #ifdef DETERMINISTIC_DBSAVE std::sort(idList.begin(), idList.end()); #endif writer.writeAttribute( QString::fromLatin1( "members" ), idList.join( QString::fromLatin1( "," ) ) ); } else { #ifdef DETERMINISTIC_DBSAVE QStringList members = groupMapIt.value().toList(); std::sort(members.begin(), members.end()); #else QSet members = groupMapIt.value(); #endif Q_FOREACH(const QString& member, members) { ElementWriter dummy( writer, QString::fromLatin1( "member" ) ); writer.writeAttribute( QString::fromLatin1( "category" ), memberMapIt.key() ); writer.writeAttribute( QString::fromLatin1( "group-name" ), groupMapIt.key() ); writer.writeAttribute( QString::fromLatin1( "member" ), member ); } // Add an entry even if the group is empty // (this is not necessary for the compressed format) if (members.size() == 0) { ElementWriter dummy(writer, QString::fromLatin1("member")); writer.writeAttribute(QString::fromLatin1("category"), memberMapIt.key()); writer.writeAttribute(QString::fromLatin1("group-name"), groupMapIt.key()); } } } } } /* Perhaps, we may need this later ;-) void XMLDB::FileWriter::saveSettings(QXmlStreamWriter& writer) { static QString settingsString = QString::fromUtf8("settings"); static QString settingString = QString::fromUtf8("setting"); static QString keyString = QString::fromUtf8("key"); static QString valueString = QString::fromUtf8("value"); ElementWriter dummy(writer, settingsString); QMap settings; // For testing settings.insert(QString::fromUtf8("tokensCategory"), QString::fromUtf8("Tokens")); settings.insert(QString::fromUtf8("untaggedCategory"), QString::fromUtf8("Events")); settings.insert(QString::fromUtf8("untaggedTag"), QString::fromUtf8("untagged")); QMapIterator settingsIterator(settings); while (settingsIterator.hasNext()) { ElementWriter dummy(writer, settingString); settingsIterator.next(); writer.writeAttribute(keyString, escape(settingsIterator.key())); writer.writeAttribute(valueString, escape(settingsIterator.value())); } } */ void XMLDB::FileWriter::save( QXmlStreamWriter& writer, const DB::ImageInfoPtr& info ) { ElementWriter dummy( writer, QString::fromLatin1("image") ); writer.writeAttribute( QString::fromLatin1("file"), info->fileName().relative() ); if ( info->label() != QFileInfo(info->fileName().relative()).completeBaseName() ) writer.writeAttribute( QString::fromLatin1("label"), info->label() ); if ( !info->description().isEmpty() ) writer.writeAttribute( QString::fromLatin1("description"), info->description() ); DB::ImageDate date = info->date(); QDateTime start = date.start(); QDateTime end = date.end(); writer.writeAttribute( QString::fromLatin1( "startDate" ), start.toString(Qt::ISODate) ); if ( start != end ) writer.writeAttribute( QString::fromLatin1( "endDate" ), end.toString(Qt::ISODate) ); if ( info->angle() != 0 ) writer.writeAttribute( QString::fromLatin1("angle"), QString::number(info->angle())); writer.writeAttribute( QString::fromLatin1( "md5sum" ), info->MD5Sum().toHexString() ); writer.writeAttribute( QString::fromLatin1( "width" ), QString::number(info->size().width())); writer.writeAttribute( QString::fromLatin1( "height" ), QString::number(info->size().height())); if ( info->rating() != -1 ) { writer.writeAttribute( QString::fromLatin1("rating"), QString::number(info->rating())); } if ( info->stackId() ) { writer.writeAttribute( QString::fromLatin1("stackId"), QString::number(info->stackId())); writer.writeAttribute( QString::fromLatin1("stackOrder"), QString::number(info->stackOrder())); } if ( info->isVideo() ) writer.writeAttribute( QLatin1String("videoLength"), QString::number(info->videoLength())); if ( useCompressedFileFormat() ) writeCategoriesCompressed( writer, info ); else writeCategories( writer, info ); } QString XMLDB::FileWriter::areaToString(QRect area) const { QStringList areaString; areaString.append( QString::number(area.x()) ); areaString.append( QString::number(area.y()) ); areaString.append( QString::number(area.width()) ); areaString.append( QString::number(area.height()) ); return areaString.join( QString::fromLatin1(" ") ); } void XMLDB::FileWriter::writeCategories( QXmlStreamWriter& writer, const DB::ImageInfoPtr& info ) { ElementWriter topElm(writer, QString::fromLatin1("options"), false ); QStringList grps = info->availableCategories(); Q_FOREACH(const QString &name, grps) { if ( !shouldSaveCategory( name ) ) continue; ElementWriter categoryElm(writer, QString::fromLatin1("option"), false ); #ifdef DETERMINISTIC_DBSAVE QStringList items = info->itemsOfCategory(name).toList(); std::sort(items.begin(), items.end()); #else QSet items = info->itemsOfCategory(name); #endif if ( !items.isEmpty() ) { topElm.writeStartElement(); categoryElm.writeStartElement(); writer.writeAttribute( QString::fromLatin1("name"), name ); } Q_FOREACH(const QString& itemValue, items) { ElementWriter dummy( writer, QString::fromLatin1("value") ); writer.writeAttribute( QString::fromLatin1("value"), itemValue ); QRect area = info->areaForTag(name, itemValue); if ( ! area.isNull() ) { writer.writeAttribute(QString::fromLatin1("area"), areaToString(area)); } } } } void XMLDB::FileWriter::writeCategoriesCompressed( QXmlStreamWriter& writer, const DB::ImageInfoPtr& info ) { QMap>> positionedTags; QList categoryList = DB::ImageDB::instance()->categoryCollection()->categories(); Q_FOREACH(const DB::CategoryPtr &category, categoryList) { QString categoryName = category->name(); if ( !shouldSaveCategory( categoryName ) ) continue; StringSet items = info->itemsOfCategory(categoryName); if ( !items.empty() ) { QStringList idList; Q_FOREACH(const QString &itemValue, items) { QRect area = info->areaForTag(categoryName, itemValue); if ( area.isValid() ) { // Positioned tags can't be stored in the "fast" format // so we have to handle them separately positionedTags[categoryName] << QPair(itemValue, area); } else { int id = static_cast(category.data())->idForName(itemValue); idList.append( QString::number( id ) ); } } // Possibly all ids of a category have area information, so only // write the category attribute if there are actually ids to write if ( !idList.isEmpty() ) { #ifdef DETERMINISTIC_DBSAVE std::sort(idList.begin(), idList.end()); #endif writer.writeAttribute( escape( categoryName ), idList.join( QString::fromLatin1( "," ) ) ); } } } // Add a "readable" sub-element for the positioned tags // FIXME: can this be merged with the code in writeCategories()? if ( ! positionedTags.isEmpty() ) { ElementWriter topElm( writer, QString::fromLatin1("options"), false ); topElm.writeStartElement(); QMapIterator>> categoryWithAreas(positionedTags); while (categoryWithAreas.hasNext()) { categoryWithAreas.next(); ElementWriter categoryElm( writer, QString::fromLatin1("option"), false ); categoryElm.writeStartElement(); writer.writeAttribute( QString::fromLatin1("name"), categoryWithAreas.key() ); QList> areas = categoryWithAreas.value(); std::sort(areas.begin(),areas.end(), [](QPair a, QPair b) { return a.first < b.first; } ); Q_FOREACH( const auto &positionedTag, areas) { ElementWriter dummy( writer, QString::fromLatin1("value") ); writer.writeAttribute( QString::fromLatin1("value"), positionedTag.first ); writer.writeAttribute( QString::fromLatin1("area"), areaToString(positionedTag.second) ); } } } } bool XMLDB::FileWriter::shouldSaveCategory( const QString& categoryName ) const { // Profiling indicated that this function was a hotspot, so this cache improved saving speed with 25% static QHash cache; if ( cache.contains(categoryName)) return cache[categoryName]; // A few bugs has shown up, where an invalid category name has crashed KPA. It therefore checks for such invalid names here. if ( !m_db->m_categoryCollection.categoryForName( categoryName ) ) { qCWarning(XMLDBLog,"Invalid category name: %s", qPrintable(categoryName)); cache.insert(categoryName,false); return false; } const bool shouldSave = dynamic_cast( m_db->m_categoryCollection.categoryForName( categoryName ).data() )->shouldSave(); cache.insert(categoryName,shouldSave); return shouldSave; } /** * @brief Escape problematic characters in a string that forms an XML attribute name. * * N.B.: Attribute values do not need to be escaped! * @see XMLDB::FileReader::unescape * * @param str the string to be escaped * @return the escaped string */ QString XMLDB::FileWriter::escape( const QString& str ) { - static QHash cache; - if ( cache.contains(str) ) - return cache[str]; + static bool hashUsesCompressedFormat = useCompressedFileFormat(); + static QHash s_cache; + if (hashUsesCompressedFormat != useCompressedFileFormat()) + s_cache.clear(); + + if ( s_cache.contains(str) ) + return s_cache[str]; QString tmp( str ); // Regex to match characters that are not allowed to start XML attribute names const QRegExp rx( QString::fromLatin1( "([^a-zA-Z0-9:_])" ) ); int pos = 0; // Encoding special characters if compressed XML is selected if ( useCompressedFileFormat() ) { while ( ( pos = rx.indexIn( tmp, pos ) ) != -1 ) { QString before = rx.cap( 1 ); QString after; after.sprintf( "_.%0X", rx.cap( 1 ).data()->toLatin1()); tmp.replace( pos, before.length(), after); pos += after.length(); } } else tmp.replace( QString::fromLatin1( " " ), QString::fromLatin1( "_" ) ); - cache.insert(str,tmp); + s_cache.insert(str,tmp); return tmp; } // TODO(hzeller): DEPENDENCY This pulls in the whole MainWindow dependency into the database backend. QWidget *XMLDB::FileWriter::messageParent() { return MainWindow::Window::theMainWindow(); } // vi:expandtab:tabstop=4 shiftwidth=4: