diff --git a/DB/MemberMap.cpp b/DB/MemberMap.cpp index ad9c3738..b80e43ae 100644 --- a/DB/MemberMap.cpp +++ b/DB/MemberMap.cpp @@ -1,375 +1,389 @@ /* Copyright (C) 2003-2020 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 "MemberMap.h" #include "Category.h" #include "Logging.h" using namespace DB; MemberMap::MemberMap() : QObject(nullptr) , m_dirty(true) , m_loading(false) { } /** returns the groups directly available from category (non closure that is) */ QStringList MemberMap::groups(const QString &category) const { return QStringList(m_members[category].keys()); } bool MemberMap::contains(const QString &category, const QString &item) const { return m_flatMembers[category].contains(item); } void MemberMap::markDirty(const QString &category) { if (m_loading) regenerateFlatList(category); else emit dirty(); } void MemberMap::deleteGroup(const QString &category, const QString &name) { m_members[category].remove(name); m_dirty = true; markDirty(category); } /** return all the members of memberGroup */ QStringList MemberMap::members(const QString &category, const QString &memberGroup, bool closure) const { if (closure) { if (m_dirty) { calculate(); } #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) const auto &members = m_closureMembers[category][memberGroup]; return QStringList(members.begin(), members.end()); #else return m_closureMembers[category][memberGroup].toList(); #endif } else { #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) const auto &members = m_members[category][memberGroup]; return QStringList(members.begin(), members.end()); #else return m_members[category][memberGroup].toList(); #endif } } void MemberMap::setMembers(const QString &category, const QString &memberGroup, const QStringList &members) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + StringSet allowedMembers(members.begin(), members.end()); +#else StringSet allowedMembers = members.toSet(); +#endif for (QStringList::const_iterator i = members.begin(); i != members.end(); ++i) if (!canAddMemberToGroup(category, memberGroup, *i)) allowedMembers.remove(*i); m_members[category][memberGroup] = allowedMembers; m_dirty = true; markDirty(category); } bool MemberMap::isEmpty() const { return m_members.empty(); } /** returns true if item is a group for category. */ bool MemberMap::isGroup(const QString &category, const QString &item) const { return m_members[category].find(item) != m_members[category].end(); } /** return a map from groupName to list of items for category example: { USA |-> [Chicago, Grand Canyon, Santa Clara], Denmark |-> [Esbjerg, Odense] } */ QMap MemberMap::groupMap(const QString &category) const { if (m_dirty) calculate(); return m_closureMembers[category]; } /** Calculates the closure for group, that is finds all members for group. Imagine there is a group called USA, and that this groups has a group inside it called Califonia, Califonia consists of members San Fransisco and Los Angeless. This function then maps USA to include Califonia, San Fransisco and Los Angeless. */ QStringList MemberMap::calculateClosure(QMap &resultSoFar, const QString &category, const QString &group) const { resultSoFar[group] = StringSet(); // Prevent against cykles. StringSet members = m_members[category][group]; StringSet result = members; for (StringSet::const_iterator it = members.begin(); it != members.end(); ++it) { if (resultSoFar.contains(*it)) { result += resultSoFar[*it]; } else if (isGroup(category, *it)) { - result += calculateClosure(resultSoFar, category, *it).toSet(); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + const auto closure = calculateClosure(resultSoFar, category, *it); + const StringSet closureSet(closure.begin(), closure.end()); +#else + const StringSet closureSet = calculateClosure(resultSoFar, category, *it).toSet(); +#endif + result += closureSet; } } resultSoFar[group] = result; +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + return QStringList(result.begin(), result.end()); +#else return result.toList(); +#endif } /** This methods create the map _closureMembers from _members This is simply to avoid finding the closure each and every time it is needed. */ void MemberMap::calculate() const { m_closureMembers.clear(); // run through all categories for (QMap>::ConstIterator categoryIt = m_members.begin(); categoryIt != m_members.end(); ++categoryIt) { QString category = categoryIt.key(); QMap groupMap = categoryIt.value(); // Run through each of the groups for the given categories for (QMap::const_iterator groupIt = groupMap.constBegin(); groupIt != groupMap.constEnd(); ++groupIt) { QString group = groupIt.key(); if (m_closureMembers[category].find(group) == m_closureMembers[category].end()) { (void)calculateClosure(m_closureMembers[category], category, group); } } } m_dirty = false; } void MemberMap::renameGroup(const QString &category, const QString &oldName, const QString &newName) { // Don't allow overwriting to avoid creating cycles if (m_members[category].contains(newName)) return; m_dirty = true; markDirty(category); QMap &groupMap = m_members[category]; groupMap.insert(newName, m_members[category][oldName]); groupMap.remove(oldName); for (StringSet &set : groupMap) { if (set.contains(oldName)) { set.remove(oldName); set.insert(newName); } } } MemberMap::MemberMap(const MemberMap &other) : QObject(nullptr) , m_members(other.memberMap()) , m_dirty(true) , m_loading(false) { } void MemberMap::deleteItem(DB::Category *category, const QString &name) { QMap &groupMap = m_members[category->name()]; for (StringSet &items : groupMap) { items.remove(name); } m_members[category->name()].remove(name); m_dirty = true; markDirty(category->name()); } void MemberMap::renameItem(DB::Category *category, const QString &oldName, const QString &newName) { if (oldName == newName) return; QMap &groupMap = m_members[category->name()]; for (StringSet &items : groupMap) { if (items.contains(oldName)) { items.remove(oldName); items.insert(newName); } } if (groupMap.contains(oldName)) { groupMap[newName] = groupMap[oldName]; groupMap.remove(oldName); } m_dirty = true; markDirty(category->name()); } MemberMap &MemberMap::operator=(const MemberMap &other) { if (this != &other) { m_members = other.memberMap(); m_dirty = true; } return *this; } void MemberMap::regenerateFlatList(const QString &category) { m_flatMembers[category].clear(); for (QMap::const_iterator i = m_members[category].constBegin(); i != m_members[category].constEnd(); i++) { for (StringSet::const_iterator j = i.value().constBegin(); j != i.value().constEnd(); j++) { m_flatMembers[category].insert(*j); } } } void MemberMap::addMemberToGroup(const QString &category, const QString &group, const QString &item) { // Only test for cycles after database is already loaded if (!m_loading && !canAddMemberToGroup(category, group, item)) { qCWarning(DBLog, "Inserting item %s into group %s/%s would create a cycle. Ignoring...", qPrintable(item), qPrintable(category), qPrintable(group)); return; } if (item.isEmpty()) { qCWarning(DBLog, "Tried to insert null item into group %s/%s. Ignoring...", qPrintable(category), qPrintable(group)); return; } m_members[category][group].insert(item); m_flatMembers[category].insert(item); if (m_loading) { m_dirty = true; } else if (!m_dirty) { // Update _closureMembers to avoid marking it dirty QMap &categoryClosure = m_closureMembers[category]; categoryClosure[group].insert(item); QMap::const_iterator closureOfItem = categoryClosure.constFind(item); const StringSet *closureOfItemPtr(nullptr); if (closureOfItem != categoryClosure.constEnd()) { closureOfItemPtr = &(*closureOfItem); categoryClosure[group] += *closureOfItem; } for (QMap::iterator i = categoryClosure.begin(); i != categoryClosure.end(); ++i) if ((*i).contains(group)) { (*i).insert(item); if (closureOfItemPtr) (*i) += *closureOfItemPtr; } } // If we are loading, we do *not* want to regenerate the list! if (!m_loading) emit dirty(); } void MemberMap::removeMemberFromGroup(const QString &category, const QString &group, const QString &item) { Q_ASSERT(m_members.contains(category)); if (m_members[category].contains(group)) { m_members[category][group].remove(item); // We shouldn't be doing this very often, so just regenerate // the flat list regenerateFlatList(category); emit dirty(); } } void MemberMap::addGroup(const QString &category, const QString &group) { if (!m_members[category].contains(group)) { m_members[category].insert(group, StringSet()); } markDirty(category); } void MemberMap::renameCategory(const QString &oldName, const QString &newName) { if (oldName == newName) return; m_members[newName] = m_members[oldName]; m_members.remove(oldName); m_closureMembers[newName] = m_closureMembers[oldName]; m_closureMembers.remove(oldName); if (!m_loading) emit dirty(); } void MemberMap::deleteCategory(const QString &category) { m_members.remove(category); m_closureMembers.remove(category); markDirty(category); } QMap DB::MemberMap::inverseMap(const QString &category) const { QMap res; const QMap &map = m_members[category]; for (QMap::ConstIterator mapIt = map.begin(); mapIt != map.end(); ++mapIt) { QString group = mapIt.key(); StringSet members = mapIt.value(); for (StringSet::const_iterator memberIt = members.begin(); memberIt != members.end(); ++memberIt) { res[*memberIt].insert(group); } } return res; } bool DB::MemberMap::hasPath(const QString &category, const QString &from, const QString &to) const { if (from == to) return true; else if (!m_members[category].contains(from)) // Try to avoid calculate(), which is quite time consuming. return false; else { // return members(category, from, true).contains(to); if (m_dirty) calculate(); return m_closureMembers[category][from].contains(to); } } void DB::MemberMap::setLoading(bool b) { if (m_loading && !b) { // TODO: Remove possible loaded cycles. } m_loading = b; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/HTMLGenerator/Generator.cpp b/HTMLGenerator/Generator.cpp index c0a832cb..9bd971a0 100644 --- a/HTMLGenerator/Generator.cpp +++ b/HTMLGenerator/Generator.cpp @@ -1,805 +1,811 @@ /* Copyright (C) 2003-2020 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 "Generator.h" #include "ImageSizeCheckBox.h" #include "Logging.h" #include "Setup.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { QString readFile(const QString &fileName) { if (fileName.isEmpty()) { KMessageBox::error(nullptr, i18n("

No file name given!

")); return QString(); } QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { //KMessageBox::error( nullptr, i18n("Could not open file %1").arg( fileName ) ); return QString(); } QTextStream stream(&file); QString content = stream.readAll(); file.close(); return content; } } //namespace HTMLGenerator::Generator::Generator(const Setup &setup, QWidget *parent) : QProgressDialog(parent) , m_tempDirHandle() , m_tempDir(m_tempDirHandle.path()) , m_hasEnteredLoop(false) { setLabelText(i18n("Generating images for HTML page ")); m_setup = setup; m_eventLoop = new QEventLoop; m_avconv = QStandardPaths::findExecutable(QString::fromUtf8("avconv")); if (m_avconv.isNull()) m_avconv = QStandardPaths::findExecutable(QString::fromUtf8("ffmpeg")); m_tempDirHandle.setAutoRemove(true); } HTMLGenerator::Generator::~Generator() { delete m_eventLoop; } void HTMLGenerator::Generator::generate() { qCDebug(HTMLGeneratorLog) << "Generating gallery" << m_setup.title() << "containing" << m_setup.imageList().size() << "entries in" << m_setup.baseDir(); // Generate .kim file if (m_setup.generateKimFile()) { qCDebug(HTMLGeneratorLog) << "Generating .kim file..."; bool ok; QString destURL = m_setup.destURL(); ImportExport::Export exp(m_setup.imageList(), kimFileName(false), false, -1, ImportExport::ManualCopy, destURL + QDir::separator() + m_setup.outputDir(), true, &ok); if (!ok) { qCDebug(HTMLGeneratorLog) << ".kim file failed!"; return; } } // prepare the progress dialog m_total = m_waitCounter = calculateSteps(); setMaximum(m_total); setValue(0); connect(this, &QProgressDialog::canceled, this, &Generator::slotCancelGenerate); m_filenameMapper.reset(); qCDebug(HTMLGeneratorLog) << "Generating content pages..."; // Iterate over each of the image sizes needed. for (QList::ConstIterator sizeIt = m_setup.activeResolutions().begin(); sizeIt != m_setup.activeResolutions().end(); ++sizeIt) { bool ok = generateIndexPage((*sizeIt)->width(), (*sizeIt)->height()); if (!ok) return; const DB::FileNameList imageList = m_setup.imageList(); for (int index = 0; index < imageList.size(); ++index) { DB::FileName current = imageList.at(index); DB::FileName prev; DB::FileName next; if (index != 0) prev = imageList.at(index - 1); if (index != imageList.size() - 1) next = imageList.at(index + 1); ok = generateContentPage((*sizeIt)->width(), (*sizeIt)->height(), prev, current, next); if (!ok) return; } } // Now generate the thumbnail images qCDebug(HTMLGeneratorLog) << "Generating thumbnail images..."; for (const DB::FileName &fileName : m_setup.imageList()) { if (wasCanceled()) return; createImage(fileName, m_setup.thumbSize()); } if (wasCanceled()) return; if (m_waitCounter > 0) { m_hasEnteredLoop = true; m_eventLoop->exec(); } if (wasCanceled()) return; qCDebug(HTMLGeneratorLog) << "Linking image file..."; bool ok = linkIndexFile(); if (!ok) return; qCDebug(HTMLGeneratorLog) << "Copying theme files..."; // Copy over the mainpage.css, imagepage.css QString themeDir, themeAuthor, themeName; getThemeInfo(&themeDir, &themeName, &themeAuthor); QDir dir(themeDir); QStringList files = dir.entryList(QDir::Files); if (files.count() < 1) qCWarning(HTMLGeneratorLog) << QString::fromLatin1("theme '%1' doesn't have enough files to be a theme").arg(themeDir); for (QStringList::Iterator it = files.begin(); it != files.end(); ++it) { if (*it == QString::fromLatin1("kphotoalbum.theme") || *it == QString::fromLatin1("mainpage.html") || *it == QString::fromLatin1("imagepage.html")) continue; QString from = QString::fromLatin1("%1%2").arg(themeDir).arg(*it); QString to = m_tempDir.filePath(*it); ok = Utilities::copyOrOverwrite(from, to); if (!ok) { KMessageBox::error(this, i18n("Error copying %1 to %2", from, to)); return; } } // Copy files over to destination. QString outputDir = m_setup.baseDir() + QString::fromLatin1("/") + m_setup.outputDir(); qCDebug(HTMLGeneratorLog) << "Copying files from" << m_tempDir.path() << "to final location" << outputDir << "..."; KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(m_tempDir.path()), QUrl::fromUserInput(outputDir)); connect(job, &KIO::CopyJob::result, this, &Generator::showBrowser); m_eventLoop->exec(); return; } bool HTMLGenerator::Generator::generateIndexPage(int width, int height) { QString themeDir, themeAuthor, themeName; getThemeInfo(&themeDir, &themeName, &themeAuthor); QString content = readFile(QString::fromLatin1("%1mainpage.html").arg(themeDir)); if (content.isEmpty()) return false; // Adding the copyright comment after DOCTYPE not before (HTML standard requires the DOCTYPE to be first within the document) QRegExp rx(QString::fromLatin1("^(]*>)")); int position; rx.setCaseSensitivity(Qt::CaseInsensitive); position = rx.indexIn(content); if ((position += rx.matchedLength()) < 0) content = QString::fromLatin1("\n").arg(themeName).arg(themeAuthor) + content; else content.insert(position, QString::fromLatin1("\n\n").arg(themeName).arg(themeAuthor)); content.replace(QString::fromLatin1("**DESCRIPTION**"), m_setup.description()); content.replace(QString::fromLatin1("**TITLE**"), m_setup.title()); QString copyright; if (!m_setup.copyright().isEmpty()) copyright = QString::fromLatin1("© %1").arg(m_setup.copyright()); else copyright = QString::fromLatin1(" "); content.replace(QString::fromLatin1("**COPYRIGHT**"), copyright); QString kimLink = QString::fromLatin1("Share and Enjoy KPhotoAlbum export file").arg(kimFileName(true)); if (m_setup.generateKimFile()) content.replace(QString::fromLatin1("**KIMFILE**"), kimLink); else content.remove(QString::fromLatin1("**KIMFILE**")); QDomDocument doc; QDomElement elm; QDomElement col; // -------------------------------------------------- Thumbnails // Initially all of the HTML generation was done using QDom, but it turned out in the end // to be much less code simply concatenating strings. This part, however, is easier using QDom // so we keep it using QDom. int count = 0; int cols = m_setup.numOfCols(); int minWidth = 0; int minHeight = 0; int enableVideo = 0; QString first, last, images; images += QString::fromLatin1("var gallery=new Array()\nvar width=%1\nvar height=%2\nvar tsize=%3\nvar inlineVideo=%4\nvar generatedVideo=%5\n").arg(width).arg(height).arg(m_setup.thumbSize()).arg(m_setup.inlineMovies()).arg(m_setup.html5VideoGenerate()); minImageSize(minWidth, minHeight); if (minWidth == 0 && minHeight == 0) { // full size only images += QString::fromLatin1("var minPage=\"index-fullsize.html\"\n"); } else { images += QString::fromLatin1("var minPage=\"index-%1x%2.html\"\n").arg(minWidth).arg(minHeight); } QDomElement row; for (const DB::FileName &fileName : m_setup.imageList()) { const DB::ImageInfoPtr info = DB::ImageDB::instance()->info(fileName); if (wasCanceled()) return false; if (count % cols == 0) { row = doc.createElement(QString::fromLatin1("tr")); row.setAttribute(QString::fromLatin1("class"), QString::fromLatin1("thumbnail-row")); doc.appendChild(row); count = 0; } col = doc.createElement(QString::fromLatin1("td")); col.setAttribute(QString::fromLatin1("class"), QString::fromLatin1("thumbnail-col")); row.appendChild(col); if (first.isEmpty()) first = namePage(width, height, fileName); else last = namePage(width, height, fileName); if (!Utilities::isVideo(fileName)) { QMimeDatabase db; images += QString::fromLatin1("gallery.push([\"%1\", \"%2\", \"%3\", \"%4\", \"") .arg(nameImage(fileName, width)) .arg(nameImage(fileName, m_setup.thumbSize())) .arg(nameImage(fileName, maxImageSize())) .arg(db.mimeTypeForFile(nameImage(fileName, maxImageSize())).name()); } else { QMimeDatabase db; images += QString::fromLatin1("gallery.push([\"%1\", \"%2\", \"%3\"") .arg(nameImage(fileName, m_setup.thumbSize())) .arg(nameImage(fileName, m_setup.thumbSize())) .arg(nameImage(fileName, maxImageSize())); if (m_setup.html5VideoGenerate()) { images += QString::fromLatin1(", \"%1\", \"") .arg(QString::fromLatin1("video/ogg")); } else { images += QString::fromLatin1(", \"%1\", \"") .arg(db.mimeTypeForFile(fileName.relative(), QMimeDatabase::MatchExtension).name()); } enableVideo = 1; } // -------------------------------------------------- Description if (!info->description().isEmpty() && m_setup.includeCategory(QString::fromLatin1("**DESCRIPTION**"))) { images += QString::fromLatin1("%1\", \"") .arg(info->description() .replace(QString::fromLatin1("\n$"), QString::fromLatin1("")) .replace(QString::fromLatin1("\n"), QString::fromLatin1(" ")) .replace(QString::fromLatin1("\""), QString::fromLatin1("\\\""))); } else { images += QString::fromLatin1("\", \""); } QString description = populateDescription(DB::ImageDB::instance()->categoryCollection()->categories(), info); if (!description.isEmpty()) { description = QString::fromLatin1("
    %1
").arg(description); } else { description = QString::fromLatin1(""); } description.replace(QString::fromLatin1("\n$"), QString::fromLatin1("")); description.replace(QString::fromLatin1("\n"), QString::fromLatin1(" ")); description.replace(QString::fromLatin1("\""), QString::fromLatin1("\\\"")); images += description; images += QString::fromLatin1("\"]);\n"); QDomElement href = doc.createElement(QString::fromLatin1("a")); href.setAttribute(QString::fromLatin1("href"), namePage(width, height, fileName)); col.appendChild(href); QDomElement img = doc.createElement(QString::fromLatin1("img")); img.setAttribute(QString::fromLatin1("src"), nameImage(fileName, m_setup.thumbSize())); img.setAttribute(QString::fromLatin1("alt"), nameImage(fileName, m_setup.thumbSize())); href.appendChild(img); ++count; } // Adding TD elements to match the selected column amount for valid HTML if (count % cols != 0) { for (int i = count; i % cols != 0; ++i) { col = doc.createElement(QString::fromLatin1("td")); col.setAttribute(QString::fromLatin1("class"), QString::fromLatin1("thumbnail-col")); QDomText sp = doc.createTextNode(QString::fromLatin1(" ")); col.appendChild(sp); row.appendChild(col); } } content.replace(QString::fromLatin1("**THUMBNAIL-TABLE**"), doc.toString()); images += QString::fromLatin1("var enableVideo=%1\n").arg(enableVideo ? 1 : 0); content.replace(QString::fromLatin1("**JSIMAGES**"), images); if (!first.isEmpty()) content.replace(QString::fromLatin1("**FIRST**"), first); if (!last.isEmpty()) content.replace(QString::fromLatin1("**LAST**"), last); // -------------------------------------------------- Resolutions QString resolutions; QList actRes = m_setup.activeResolutions(); std::sort(actRes.begin(), actRes.end()); if (actRes.count() > 1) { resolutions += QString::fromLatin1("Resolutions: "); for (QList::ConstIterator sizeIt = actRes.constBegin(); sizeIt != actRes.constEnd(); ++sizeIt) { int w = (*sizeIt)->width(); int h = (*sizeIt)->height(); QString page = QString::fromLatin1("index-%1.html").arg(ImageSizeCheckBox::text(w, h, true)); QString text = (*sizeIt)->text(false); resolutions += QString::fromLatin1(" "); if (width == w && height == h) { resolutions += text; } else { resolutions += QString::fromLatin1("%2").arg(page).arg(text); } } } content.replace(QString::fromLatin1("**RESOLUTIONS**"), resolutions); if (wasCanceled()) return false; // -------------------------------------------------- write to file QString fileName = m_tempDir.filePath( QString::fromLatin1("index-%1.html") .arg(ImageSizeCheckBox::text(width, height, true))); bool ok = writeToFile(fileName, content); if (!ok) return false; return true; } bool HTMLGenerator::Generator::generateContentPage(int width, int height, const DB::FileName &prev, const DB::FileName ¤t, const DB::FileName &next) { QString themeDir, themeAuthor, themeName; getThemeInfo(&themeDir, &themeName, &themeAuthor); QString content = readFile(QString::fromLatin1("%1imagepage.html").arg(themeDir)); if (content.isEmpty()) return false; const DB::ImageInfoPtr info = DB::ImageDB::instance()->info(current); // Note(jzarl): is there any reason why currentFile could be different from current? const DB::FileName currentFile = info->fileName(); // Adding the copyright comment after DOCTYPE not before (HTML standard requires the DOCTYPE to be first within the document) QRegExp rx(QString::fromLatin1("^(]*>)")); int position; rx.setCaseSensitivity(Qt::CaseInsensitive); position = rx.indexIn(content); if ((position += rx.matchedLength()) < 0) content = QString::fromLatin1("\n").arg(themeName).arg(themeAuthor) + content; else content.insert(position, QString::fromLatin1("\n\n").arg(themeName).arg(themeAuthor)); // TODO: Hardcoded non-standard category names is not good practice QString title = QString::fromLatin1(""); QString name = QString::fromLatin1("Common Name"); const auto itemsOfCategory = info->itemsOfCategory(name); if (!itemsOfCategory.empty()) { #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) title += QStringList(itemsOfCategory.begin(), itemsOfCategory.end()).join(QLatin1String(" - ")); #else title += QStringList(itemsOfCategory.toList()).join(QString::fromLatin1(" - ")); #endif } else { name = QString::fromLatin1("Latin Name"); if (!itemsOfCategory.empty()) { #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) title += QStringList(itemsOfCategory.begin(), itemsOfCategory.end()).join(QString::fromLatin1(" - ")); #else title += QStringList(itemsOfCategory.toList()).join(QString::fromLatin1(" - ")); #endif } else { title = info->label(); } } content.replace(QString::fromLatin1("**TITLE**"), title); // Image or video content if (Utilities::isVideo(currentFile)) { QString videoFile = createVideo(currentFile); QString videoBase = videoFile.replace(QRegExp(QString::fromLatin1("\\..*")), QString::fromLatin1("")); if (m_setup.inlineMovies()) if (m_setup.html5Video()) content.replace(QString::fromLatin1("**IMAGE_OR_VIDEO**"), QString::fromLatin1("").arg(QString::fromLatin1("%1.mp4").arg(videoBase)).arg(createImage(current, 256)).arg(QString::fromLatin1("%1.mp4").arg(videoBase)).arg(QString::fromLatin1("%1.mp4").arg(videoBase)).arg(QString::fromLatin1("%1.ogg").arg(videoBase))); else content.replace(QString::fromLatin1("**IMAGE_OR_VIDEO**"), QString::fromLatin1("" "") .arg(videoFile) .arg(createImage(current, 256)) .arg(videoFile)); else content.replace(QString::fromLatin1("**IMAGE_OR_VIDEO**"), QString::fromLatin1("" "") .arg(videoFile) .arg(createImage(current, 256))); } else content.replace(QString::fromLatin1("**IMAGE_OR_VIDEO**"), QString::fromLatin1("\"%1\"/") .arg(createImage(current, width))); // -------------------------------------------------- Links QString link; // prev link if (!prev.isNull()) link = i18n("prev", namePage(width, height, prev)); else link = i18n("prev"); content.replace(QString::fromLatin1("**PREV**"), link); // PENDING(blackie) These next 5 line also exists exactly like that in HTMLGenerator::Generator::generateIndexPage. Please refactor. // prevfile if (!prev.isNull()) link = namePage(width, height, prev); else link = i18n("prev"); content.replace(QString::fromLatin1("**PREVFILE**"), link); // index link link = i18n("index", ImageSizeCheckBox::text(width, height, true)); content.replace(QString::fromLatin1("**INDEX**"), link); // indexfile link = QString::fromLatin1("index-%1.html").arg(ImageSizeCheckBox::text(width, height, true)); content.replace(QString::fromLatin1("**INDEXFILE**"), link); // Next Link if (!next.isNull()) link = i18n("next", namePage(width, height, next)); else link = i18n("next"); content.replace(QString::fromLatin1("**NEXT**"), link); // Nextfile if (!next.isNull()) link = namePage(width, height, next); else link = i18n("next"); content.replace(QString::fromLatin1("**NEXTFILE**"), link); if (!next.isNull()) link = namePage(width, height, next); else link = QString::fromLatin1("index-%1.html").arg(ImageSizeCheckBox::text(width, height, true)); content.replace(QString::fromLatin1("**NEXTPAGE**"), link); // -------------------------------------------------- Resolutions QString resolutions; const QList &actRes = m_setup.activeResolutions(); if (actRes.count() > 1) { for (QList::ConstIterator sizeIt = actRes.begin(); sizeIt != actRes.end(); ++sizeIt) { int w = (*sizeIt)->width(); int h = (*sizeIt)->height(); QString page = namePage(w, h, currentFile); QString text = (*sizeIt)->text(false); resolutions += QString::fromLatin1(" "); if (width == w && height == h) resolutions += text; else resolutions += QString::fromLatin1("%2").arg(page).arg(text); } } content.replace(QString::fromLatin1("**RESOLUTIONS**"), resolutions); // -------------------------------------------------- Copyright QString copyright; if (!m_setup.copyright().isEmpty()) copyright = QString::fromLatin1("© %1").arg(m_setup.copyright()); else copyright = QString::fromLatin1(" "); content.replace(QString::fromLatin1("**COPYRIGHT**"), QString::fromLatin1("%1").arg(copyright)); // -------------------------------------------------- Description QString description = populateDescription(DB::ImageDB::instance()->categoryCollection()->categories(), info); if (!description.isEmpty()) content.replace(QString::fromLatin1("**DESCRIPTION**"), QString::fromLatin1("
    \n%1\n
").arg(description)); else content.replace(QString::fromLatin1("**DESCRIPTION**"), QString::fromLatin1("")); // -------------------------------------------------- write to file QString fileName = m_tempDir.filePath(namePage(width, height, currentFile)); bool ok = writeToFile(fileName, content); if (!ok) return false; return true; } QString HTMLGenerator::Generator::namePage(int width, int height, const DB::FileName &fileName) { QString name = m_filenameMapper.uniqNameFor(fileName); QString base = QFileInfo(name).completeBaseName(); return QString::fromLatin1("%1-%2.html").arg(base).arg(ImageSizeCheckBox::text(width, height, true)); } QString HTMLGenerator::Generator::nameImage(const DB::FileName &fileName, int size) { QString name = m_filenameMapper.uniqNameFor(fileName); QString base = QFileInfo(name).completeBaseName(); if (size == maxImageSize() && !Utilities::isVideo(fileName)) { if (name.endsWith(QString::fromLatin1(".jpg"), Qt::CaseSensitive) || name.endsWith(QString::fromLatin1(".jpeg"), Qt::CaseSensitive)) return name; else return base + QString::fromLatin1(".jpg"); } else if (size == maxImageSize() && Utilities::isVideo(fileName)) { return name; } else return QString::fromLatin1("%1-%2.jpg").arg(base).arg(size); } QString HTMLGenerator::Generator::createImage(const DB::FileName &fileName, int size) { const DB::ImageInfoPtr info = DB::ImageDB::instance()->info(fileName); if (m_generatedFiles.contains(qMakePair(fileName, size))) { m_waitCounter--; } else { ImageManager::ImageRequest *request = new ImageManager::ImageRequest(fileName, QSize(size, size), info->angle(), this); request->setPriority(ImageManager::BatchTask); ImageManager::AsyncLoader::instance()->load(request); m_generatedFiles.insert(qMakePair(fileName, size)); } return nameImage(fileName, size); } QString HTMLGenerator::Generator::createVideo(const DB::FileName &fileName) { setValue(m_total - m_waitCounter); qApp->processEvents(); QString baseName = nameImage(fileName, maxImageSize()); QString destName = m_tempDir.filePath(baseName); if (!m_copiedVideos.contains(fileName)) { if (m_setup.html5VideoGenerate()) { // TODO: shouldn't we use avconv library directly instead of KRun // TODO: should check that the avconv (ffmpeg takes the same parameters on older systems) and ffmpeg2theora exist // TODO: Figure out avconv parameters to get rid of ffmpeg2theora KRun::runCommand(QString::fromLatin1("%1 -y -i %2 -vcodec libx264 -b 250k -bt 50k -acodec libfaac -ab 56k -ac 2 -s %3 %4") .arg(m_avconv) .arg(fileName.absolute()) .arg(QString::fromLatin1("320x240")) .arg(destName.replace(QRegExp(QString::fromLatin1("\\..*")), QString::fromLatin1(".mp4"))), MainWindow::Window::theMainWindow()); KRun::runCommand(QString::fromLatin1("ffmpeg2theora -v 7 -o %1 -x %2 %3") .arg(destName.replace(QRegExp(QString::fromLatin1("\\..*")), QString::fromLatin1(".ogg"))) .arg(QString::fromLatin1("320")) .arg(fileName.absolute()), MainWindow::Window::theMainWindow()); } else Utilities::copyOrOverwrite(fileName.absolute(), destName); m_copiedVideos.insert(fileName); } return baseName; } QString HTMLGenerator::Generator::kimFileName(bool relative) { if (relative) return QString::fromLatin1("%2.kim").arg(m_setup.outputDir()); else return m_tempDir.filePath(QString::fromLatin1("%2.kim").arg(m_setup.outputDir())); } bool HTMLGenerator::Generator::writeToFile(const QString &fileName, const QString &str) { QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { KMessageBox::error(this, i18n("Could not create file '%1'.", fileName), i18n("Could Not Create File")); return false; } QByteArray data = translateToHTML(str).toUtf8(); file.write(data); file.close(); return true; } QString HTMLGenerator::Generator::translateToHTML(const QString &str) { QString res; for (int i = 0; i < str.length(); ++i) { if (str[i].unicode() < 128) res.append(str[i]); else { res.append(QStringLiteral("&#%1;").arg((unsigned int)str[i].unicode())); } } return res; } bool HTMLGenerator::Generator::linkIndexFile() { ImageSizeCheckBox *resolution = m_setup.activeResolutions()[0]; QString fromFile = QString::fromLatin1("index-%1.html") .arg(resolution->text(true)); fromFile = m_tempDir.filePath(fromFile); QString destFile = m_tempDir.filePath(QString::fromLatin1("index.html")); bool ok = Utilities::copyOrOverwrite(fromFile, destFile); if (!ok) { KMessageBox::error(this, i18n("

Unable to copy %1 to %2

", fromFile, destFile)); return false; } return ok; } void HTMLGenerator::Generator::slotCancelGenerate() { ImageManager::AsyncLoader::instance()->stop(this); m_waitCounter = 0; if (m_hasEnteredLoop) m_eventLoop->exit(); } void HTMLGenerator::Generator::pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) { const DB::FileName fileName = request->databaseFileName(); const QSize imgSize = request->size(); const bool loadedOK = request->loadedOK(); setValue(m_total - m_waitCounter); m_waitCounter--; int size = imgSize.width(); QString file = m_tempDir.filePath(nameImage(fileName, size)); bool success = loadedOK && image.save(file, "JPEG"); if (!success) { // We better stop the imageloading. In case this is a full disk, we will just get all images loaded, while this // error box is showing, resulting in a bunch of error messages, and memory running out due to all the hanging // pixmapLoaded methods. slotCancelGenerate(); KMessageBox::error(this, i18n("Unable to write image '%1'.", file)); } if (!Utilities::isVideo(fileName)) { try { Exif::Info::instance()->writeInfoToFile(fileName, file); } catch (...) { } } if (m_waitCounter == 0 && m_hasEnteredLoop) { m_eventLoop->exit(); } } int HTMLGenerator::Generator::calculateSteps() { int count = m_setup.activeResolutions().count(); return m_setup.imageList().size() * (1 + count); // 1 thumbnail + 1 real image } void HTMLGenerator::Generator::getThemeInfo(QString *baseDir, QString *name, QString *author) { *baseDir = m_setup.themePath(); KConfig themeconfig(QString::fromLatin1("%1/kphotoalbum.theme").arg(*baseDir), KConfig::SimpleConfig); KConfigGroup config = themeconfig.group("theme"); *name = config.readEntry("Name"); *author = config.readEntry("Author"); } int HTMLGenerator::Generator::maxImageSize() { int res = 0; for (QList::ConstIterator sizeIt = m_setup.activeResolutions().begin(); sizeIt != m_setup.activeResolutions().end(); ++sizeIt) { res = qMax(res, (*sizeIt)->width()); } return res; } void HTMLGenerator::Generator::minImageSize(int &width, int &height) { width = height = 0; for (QList::ConstIterator sizeIt = m_setup.activeResolutions().begin(); sizeIt != m_setup.activeResolutions().end(); ++sizeIt) { if ((width == 0) && ((*sizeIt)->width() > 0)) { width = (*sizeIt)->width(); height = (*sizeIt)->height(); } else if ((*sizeIt)->width() > 0) { width = qMin(width, (*sizeIt)->width()); height = qMin(height, (*sizeIt)->height()); } } } void HTMLGenerator::Generator::showBrowser() { if (m_setup.generateKimFile()) ImportExport::Export::showUsageDialog(); if (!m_setup.baseURL().isEmpty()) new KRun(QUrl::fromUserInput(QString::fromLatin1("%1/%2/index.html").arg(m_setup.baseURL()).arg(m_setup.outputDir())), MainWindow::Window::theMainWindow()); m_eventLoop->exit(); } QString HTMLGenerator::Generator::populateDescription(QList categories, const DB::ImageInfoPtr info) { QString description; if (m_setup.includeCategory(QString::fromLatin1("**DATE**"))) description += QString::fromLatin1("
  • %1 %2
  • ").arg(i18n("Date")).arg(info->date().toString()); for (QList::Iterator it = categories.begin(); it != categories.end(); ++it) { - if ((*it)->isSpecialCategory()) + if ((*it)->isSpecialCategory()) { continue; + } - QString name = (*it)->name(); - if (!info->itemsOfCategory(name).empty() && m_setup.includeCategory(name)) { - QString val = QStringList(info->itemsOfCategory(name).toList()).join(QString::fromLatin1(", ")); - description += QString::fromLatin1("
  • %1: %2
  • ").arg(name).arg(val); + const auto name = (*it)->name(); + const auto itemsOfCategory = info->itemsOfCategory(name); + if (!itemsOfCategory.empty() && m_setup.includeCategory(name)) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + const QStringList itemsList(itemsOfCategory.begin(), itemsOfCategory.end()); +#else + const QStringList itemsList = itemsOfCategory.toList(); +#endif + description += QStringLiteral("
  • %1: %2
  • ").arg(name, itemsList.join(QLatin1String(", "))); } } if (!info->description().isEmpty() && m_setup.includeCategory(QString::fromLatin1("**DESCRIPTION**"))) { description += QString::fromLatin1("
  • Description: %1
  • ").arg(info->description()); } return description; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/FileWriter.cpp b/XMLDB/FileWriter.cpp index c12e79d6..6b5c54ad 100644 --- a/XMLDB/FileWriter.cpp +++ b/XMLDB/FileWriter.cpp @@ -1,501 +1,511 @@ /* Copyright (C) 2003-2020 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 // // // // +++++++++++++++++++++++++++++++ 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(m_db->uiDelegate()).makeNumberedBackup(); // prepare XML document for saving: m_db->m_categoryCollection.initIdMap(); QFile out(fileName + QString::fromLatin1(".tmp")); if (!out.open(QIODevice::WriteOnly | QIODevice::Text)) { m_db->uiDelegate().sorry( QString::fromUtf8("Error saving to file '%1': %2").arg(out.fileName()).arg(out.errorString()), 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()), i18n("Error while saving...")); return; } QElapsedTimer timer; if (TimingLog().isDebugEnabled()) timer.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" << timer.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)) { m_db->uiDelegate().sorry( QString::fromUtf8("Removal of file '%1' failed.").arg(fileName), i18n("

    Failed to remove old version of image database.

    " "

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

    ", fileName, out.fileName()), i18n("Error while saving...")); return; } // State: index.xml doesn't exist, index.xml.tmp has the current version. if (!out.rename(fileName)) { m_db->uiDelegate().sorry( QString::fromUtf8("Renaming index.xml to '%1' failed.").arg(out.fileName()), i18n("

    Failed to move temporary XML file to permanent location.

    " "

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

    ", out.fileName(), fileName), i18n("Error while saving...")); // 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)); */ const auto categoryItems = category->items(); for (const QString &tagName : categoryItems) { 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 const auto clipBoardImages = m_db->m_clipboard; for (const DB::ImageInfoPtr &infoPtr : clipBoardImages) { list.append(infoPtr); } { ElementWriter dummy(writer, QString::fromLatin1("images")); for (const DB::ImageInfoPtr &infoPtr : qAsConst(list)) { save(writer, infoPtr); } } } void XMLDB::FileWriter::saveBlockList(QXmlStreamWriter &writer) { ElementWriter dummy(writer, QString::fromLatin1("blocklist")); #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QList blockList(m_db->m_blockList.begin(), m_db->m_blockList.end()); #else QList blockList = m_db->m_blockList.toList(); #endif // sort blocklist to get diffable files std::sort(blockList.begin(), blockList.end()); for (const DB::FileName &block : qAsConst(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>::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()) { const 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; for (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))); } std::sort(idList.begin(), idList.end()); writer.writeAttribute(QString::fromLatin1("members"), idList.join(QString::fromLatin1(","))); } else { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + const auto groupMapItValue = groupMapIt.value(); + QStringList members(groupMapItValue.begin(), groupMapItValue.end()); +#else QStringList members = groupMapIt.value().toList(); +#endif std::sort(members.begin(), members.end()); for (const QString &member : qAsConst(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); const QStringList grps = info->availableCategories(); for (const QString &name : grps) { if (!shouldSaveCategory(name)) continue; ElementWriter categoryElm(writer, QString::fromLatin1("option"), false); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + const auto itemsOfCategory = info->itemsOfCategory(name); + QStringList items(itemsOfCategory.begin(), itemsOfCategory.end()); +#else QStringList items = info->itemsOfCategory(name).toList(); +#endif std::sort(items.begin(), items.end()); if (!items.isEmpty()) { topElm.writeStartElement(); categoryElm.writeStartElement(); writer.writeAttribute(QString::fromLatin1("name"), name); } for (const QString &itemValue : qAsConst(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; const QList categoryList = DB::ImageDB::instance()->categoryCollection()->categories(); for (const DB::CategoryPtr &category : categoryList) { QString categoryName = category->name(); if (!shouldSaveCategory(categoryName)) continue; const StringSet items = info->itemsOfCategory(categoryName); if (!items.empty()) { QStringList idList; for (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()) { std::sort(idList.begin(), idList.end()); 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; }); for (const auto &positionedTag : qAsConst(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 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("_")); s_cache.insert(str, tmp); return tmp; } // vi:expandtab:tabstop=4 shiftwidth=4: