1 /* This file is part of the KDE project
2 
3     SPDX-FileCopyrightText: 2001, 2003 Lukas Tinkl <lukas@kde.org>
4     SPDX-FileCopyrightText: Andreas Schlapbach <schlpbch@iam.unibe.ch>
5 
6     SPDX-License-Identifier: LGPL-2.0-only
7 */
8 
9 #include "imgalleryplugin.h"
10 
11 #include <QTextStream>
12 #include <QFile>
13 #include <QDateTime>
14 #include <QPixmap>
15 #include <QImage>
16 #include <QTextCodec>
17 #include <QApplication>
18 #include <QDesktopServices>
19 #include <QImageReader>
20 #include <QMimeDatabase>
21 #include <QMimeType>
22 #include <QPushButton>
23 #include <QProgressDialog>
24 
25 #include <klocalizedstring.h>
26 #include <kmessagebox.h>
27 #include <kpluginfactory.h>
28 #include <kactioncollection.h>
29 
30 #include <kparts/part.h>
31 
32 #include "imgallerydialog.h"
33 #include "imgallery_debug.h"
34 
K_PLUGIN_FACTORY(KImGalleryPluginFactory,registerPlugin<KImGalleryPlugin> ();)35 K_PLUGIN_FACTORY(KImGalleryPluginFactory, registerPlugin<KImGalleryPlugin>();)
36 
37 // Eliminate lots of deprecation warnings with Qt 5.15.
38 // Using a macro to redefine well known symbols is not good practice, but
39 // the alternative is lots of QT_VERSION_CHECK conditionals everywhere.
40 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
41 #define endl Qt::endl
42 #endif
43 
44 static QString directory(const QUrl &url) {
45     return url.adjusted(QUrl::StripTrailingSlash).adjusted(QUrl::RemoveFilename).toLocalFile();
46 }
47 
KImGalleryPlugin(QObject * parent,const QVariantList &)48 KImGalleryPlugin::KImGalleryPlugin(QObject *parent, const QVariantList &)
49     : KParts::Plugin(parent), m_commentMap(nullptr)
50 {
51     QAction *a = actionCollection()->addAction(QStringLiteral("create_img_gallery"));
52     a->setText(i18n("&Create Image Gallery..."));
53     a->setIcon(QIcon::fromTheme(QStringLiteral("imagegallery")));
54     actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL | Qt::Key_I));
55     connect(a, &QAction::triggered, this, &KImGalleryPlugin::slotExecute);
56 }
57 
slotExecute()58 void KImGalleryPlugin::slotExecute()
59 {
60     m_progressDlg = nullptr;
61     if (!parent()) {
62         KMessageBox::sorry(nullptr, i18n("Could not create the plugin, please report a bug."));
63         return;
64     }
65     m_part = qobject_cast<KParts::ReadOnlyPart *>(parent());
66 
67     if (!m_part || !m_part->url().isLocalFile()) {  //TODO support remote URLs too?
68         KMessageBox::sorry(m_part->widget(), i18n("Creating an image gallery works only on local folders."));
69         return;
70     }
71 
72     QString path = m_part->url().adjusted(QUrl::StripTrailingSlash).toLocalFile() + '/';
73     m_configDlg = new KIGPDialog(m_part->widget(), path);
74 
75     if (m_configDlg->exec() == QDialog::Accepted) {
76         qCDebug(IMAGEGALLERY_LOG) << "dialog is ok";
77         m_configDlg->writeConfig();
78         m_copyFiles = m_configDlg->copyOriginalFiles();
79         m_recurseSubDirectories = m_configDlg->recurseSubDirectories();
80         m_useCommentFile = m_configDlg->useCommentFile();
81         m_imagesPerRow = m_configDlg->getImagesPerRow();
82 
83         QUrl url(m_configDlg->getImageUrl());
84         if (!url.isEmpty() && url.isValid()) {
85             m_progressDlg = new QProgressDialog(m_part->widget());
86             connect(m_progressDlg, &QProgressDialog::canceled, this, &KImGalleryPlugin::slotCancelled);
87 
88             m_progressDlg->setLabelText(i18n("Creating thumbnails"));
89             QPushButton *button = new QPushButton(m_progressDlg);
90             KGuiItem::assign(button, KStandardGuiItem::cancel());
91             m_progressDlg->setCancelButton(button);
92             m_cancelled = false;
93             m_progressDlg->show();
94             if (createHtml(url, m_part->url().path(), m_configDlg->recursionLevel() > 0 ? m_configDlg->recursionLevel() + 1 : 0, m_configDlg->getImageFormat())) {
95                 QDesktopServices::openUrl(url);		// Open a browser to show the result
96             } else {
97                 deleteCancelledGallery(url, m_part->url().path(), m_configDlg->recursionLevel() > 0 ? m_configDlg->recursionLevel() + 1 : 0, m_configDlg->getImageFormat());
98             }
99         }
100     }
101     delete m_progressDlg;
102 }
103 
createDirectory(const QDir & dir,const QString & imgGalleryDir,const QString & dirName)104 bool KImGalleryPlugin::createDirectory(const QDir &dir, const QString &imgGalleryDir, const QString &dirName)
105 {
106     QDir thumb_dir(dir);
107 
108     if (!thumb_dir.exists()) {
109         thumb_dir.setPath(imgGalleryDir);
110         if (!(thumb_dir.mkdir(dirName/*, false*/))) {
111             KMessageBox::sorry(m_part->widget(), i18n("Could not create folder: %1", thumb_dir.path()));
112             return false;
113         } else {
114             thumb_dir.setPath(imgGalleryDir + '/' + dirName + '/');
115             return true;
116         }
117     } else {
118         return true;
119     }
120 }
121 
createHead(QTextStream & stream)122 void KImGalleryPlugin::createHead(QTextStream &stream)
123 {
124     const QString chsetName = QTextCodec::codecForLocale()->name();
125 
126     stream << "<?xml version=\"1.0\" encoding=\"" +  chsetName + "\" ?>" << endl;
127     stream << "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">" << endl;
128     stream << "<html xmlns=\"http://www.w3.org/1999/xhtml\">" << endl;
129     stream << "<head>" << endl;
130     stream << "<title>" << m_configDlg->getTitle().toHtmlEscaped() << "</title>" << endl;
131     stream << "<meta http-equiv=\"content-type\" content=\"text/html; charset=" << chsetName << "\"/>" << endl;
132     stream << "<meta name=\"GENERATOR\" content=\"KDE Konqueror KImgallery plugin version " KDE_VERSION_STRING "\"/>" << endl;
133     createCSSSection(stream);
134     stream << "</head>" << endl;
135 }
136 
createCSSSection(QTextStream & stream)137 void KImGalleryPlugin::createCSSSection(QTextStream &stream)
138 {
139     const QString backgroundColor = m_configDlg->getBackgroundColor().name();
140     const QString foregroundColor = m_configDlg->getForegroundColor().name();
141     //adding a touch of style
142     stream << "<style type='text/css'>\n";
143     stream << "BODY {color: " << foregroundColor << "; background: " << backgroundColor << ";" << endl;
144     stream << "          font-family: " << m_configDlg->getFontName() << ", sans-serif;" << endl;
145     stream << "          font-size: " << m_configDlg->getFontSize() << "pt; margin: 8%; }" << endl;
146     stream << "H1       {color: " << foregroundColor << ";}" << endl;
147     stream << "TABLE    {text-align: center; margin-left: auto; margin-right: auto;}" << endl;
148     stream << "TD       { color: " << foregroundColor << "; padding: 1em}" << endl;
149     stream << "IMG      { border: 1px solid " << foregroundColor << "; }" << endl;
150     stream << "</style>" << endl;
151 }
152 
extension(const QString & imageFormat)153 QString KImGalleryPlugin::extension(const QString &imageFormat)
154 {
155     if (imageFormat == QLatin1String("PNG")) {
156         return QStringLiteral(".png");
157     }
158     if (imageFormat == QLatin1String("JPEG")) {
159         return QStringLiteral(".jpg");
160     }
161     Q_ASSERT(false);
162     return QString();
163 }
164 
createBody(QTextStream & stream,const QString & sourceDirName,const QStringList & subDirList,const QDir & imageDir,const QUrl & url,const QString & imageFormat)165 void KImGalleryPlugin::createBody(QTextStream &stream, const QString &sourceDirName, const QStringList &subDirList,
166                                   const QDir &imageDir, const QUrl &url, const QString &imageFormat)
167 {
168     int numOfImages = imageDir.count();
169     const QString imgGalleryDir = directory(url);
170     const QString today(QLocale().toString(QDate::currentDate()));
171 
172     stream << "<body>\n<h1>" << m_configDlg->getTitle().toHtmlEscaped() << "</h1><p>" << endl;
173     stream << i18n("<i>Number of images</i>: %1", numOfImages) << "<br/>" << endl;
174     stream << i18n("<i>Created on</i>: %1", today) << "</p>" << endl;
175 
176     stream << "<hr/>" << endl;
177 
178     if (m_recurseSubDirectories && subDirList.count() > 2) { //subDirList.count() is always >= 2 because of the "." and ".." directories
179         stream << i18n("<i>Subfolders</i>:") << "<br>" << endl;
180         for (QStringList::ConstIterator it = subDirList.constBegin(); it != subDirList.constEnd(); it++) {
181             if (*it == QLatin1String(".") || *it == QLatin1String("..")) {
182                 continue;    //disregard the "." and ".." directories
183             }
184             stream << "<a href=\"" << *it << "/" << url.fileName()
185                    << "\">" << *it << "</a><br>" << endl;
186         }
187         stream << "<hr/>" << endl;
188     }
189 
190     stream << "<table>" << endl;
191 
192     //table with images
193     int imgIndex;
194     QFileInfo imginfo;
195     QPixmap  imgProp;
196     for (imgIndex = 0; !m_cancelled && (imgIndex < numOfImages);) {
197         stream << "<tr>" << endl;
198 
199         for (int col = 0; !m_cancelled && (col < m_imagesPerRow) && (imgIndex < numOfImages); col++) {
200             const QString imgName = imageDir[imgIndex];
201 
202             if (m_copyFiles) {
203                 stream << "<td align='center'>\n<a href=\"images/" << imgName << "\">";
204             } else {
205                 stream << "<td align='center'>\n<a href=\"" << imgName << "\">";
206             }
207 
208             if (createThumb(imgName, sourceDirName, imgGalleryDir, imageFormat)) {
209                 const QString imgPath("thumbs/" + imgName + extension(imageFormat));
210                 stream << "<img src=\"" << imgPath << "\" width=\"" << m_imgWidth << "\" ";
211                 stream << "height=\"" << m_imgHeight << "\" alt=\"" << imgPath << "\"/>";
212                 m_progressDlg->setLabelText(i18n("Created thumbnail for: \n%1", imgName));
213             } else {
214                 qCDebug(IMAGEGALLERY_LOG) << "Creating thumbnail for " << imgName << " failed";
215                 m_progressDlg->setLabelText(i18n("Creating thumbnail for: \n%1\n failed", imgName));
216             }
217             stream << "</a>" << endl;
218 
219             if (m_configDlg->printImageName()) {
220                 stream << "<div>" << imgName << "</div>" << endl;
221             }
222 
223             if (m_configDlg->printImageProperty()) {
224                 imgProp.load(imageDir.absoluteFilePath(imgName));
225                 stream << "<div>" << imgProp.width() << " x " << imgProp.height() << "</div>" << endl;
226             }
227 
228             if (m_configDlg->printImageSize()) {
229                 imginfo.setFile(imageDir, imgName);
230                 stream << "<div>(" << (imginfo.size() / 1024) << " " <<  i18n("KiB") << ")" << "</div>" << endl;
231             }
232 
233             if (m_useCommentFile) {
234                 QString imgComment = (*m_commentMap)[imgName];
235                 stream << "<div>" << imgComment.toHtmlEscaped() << "</div>" << endl;
236             }
237             stream << "</td>" << endl;
238 
239             m_progressDlg->setMaximum(numOfImages);
240             m_progressDlg->setValue(imgIndex);
241             qApp->processEvents();
242             imgIndex++;
243         }
244         stream << "</tr>" << endl;
245     }
246     //close the HTML
247     stream << "</table>\n</body>\n</html>" << endl;
248 }
249 
createHtml(const QUrl & url,const QString & sourceDirName,int recursionLevel,const QString & imageFormat)250 bool KImGalleryPlugin::createHtml(const QUrl &url, const QString &sourceDirName, int recursionLevel, const QString &imageFormat)
251 {
252     if (m_cancelled) {
253         return false;
254     }
255 
256     if (!parent() || !parent()->inherits("DolphinPart")) {
257         return false;
258     }
259 
260     QStringList subDirList;
261     if (m_recurseSubDirectories && (recursionLevel >= 0)) { //recursionLevel == 0 means endless
262         QDir toplevel_dir = QDir(sourceDirName);
263         toplevel_dir.setFilter(QDir::Dirs | QDir::Readable | QDir::Writable);
264         subDirList = toplevel_dir.entryList();
265 
266         for (QStringList::ConstIterator it = subDirList.constBegin(); it != subDirList.constEnd() && !m_cancelled; it++) {
267             const QString currentDir = *it;
268             if (currentDir == QLatin1String(".") || currentDir == QLatin1String("..")) {
269                 continue;   //disregard the "." and ".." directories
270             }
271             QDir subDir = QDir(directory(url) + '/' + currentDir);
272             if (!subDir.exists()) {
273                 subDir.setPath(directory(url));
274                 if (!(subDir.mkdir(currentDir/*, false*/))) {
275                     KMessageBox::sorry(m_part->widget(), i18n("Could not create folder: %1", subDir.path()));
276                     continue;
277                 } else {
278                     subDir.setPath(directory(url) + '/' + currentDir);
279                 }
280             }
281             if (!createHtml(QUrl::fromLocalFile(subDir.path() + '/' + url.fileName()), sourceDirName + '/' + currentDir,
282                             recursionLevel > 1 ? recursionLevel - 1 : 0, imageFormat)) {
283                 return false;
284             }
285         }
286     }
287 
288     if (m_useCommentFile) {
289         loadCommentFile();
290     }
291 
292     qCDebug(IMAGEGALLERY_LOG) << "sourceDirName: " << sourceDirName;
293 
294     QMimeDatabase db;
295     QStringList imageNameFilters;
296     const QList<QByteArray> &mimeNames = QImageReader::supportedMimeTypes();
297     for (const QByteArray &mimeName : mimeNames)
298     {
299         const QMimeType mimeType = db.mimeTypeForName(mimeName);
300         imageNameFilters.append(mimeType.globPatterns());
301     }
302 
303     QDir imageDir(sourceDirName, QString(),
304                   QDir::Name | QDir::IgnoreCase, QDir::Files | QDir::Readable);
305     imageDir.setNameFilters(imageNameFilters);
306 
307     const QString imgGalleryDir = directory(url);
308     qCDebug(IMAGEGALLERY_LOG) << "imgGalleryDir: " << imgGalleryDir;
309 
310     // Create the "thumbs" subdirectory if necessary
311     QDir thumb_dir(imgGalleryDir + QLatin1String("/thumbs/"));
312     if (createDirectory(thumb_dir, imgGalleryDir, QStringLiteral("thumbs")) == false) {
313         return false;
314     }
315 
316     // Create the "images" subdirectory if necessary
317     QDir images_dir(imgGalleryDir + QLatin1String("/images/"));
318     if (m_copyFiles) {
319         if (createDirectory(images_dir, imgGalleryDir, QStringLiteral("images")) == false) {
320             return false;
321         }
322     }
323 
324     QFile file(url.path());
325     qCDebug(IMAGEGALLERY_LOG) << "url.path(): " << url.path() << ", thumb_dir: " << thumb_dir.path()
326                   << ", imageDir: " << imageDir.path();
327 
328     if (imageDir.exists() && file.open(QIODevice::WriteOnly)) {
329         QTextStream stream(&file);
330         stream.setCodec(QTextCodec::codecForLocale());
331 
332         createHead(stream);
333         createBody(stream, sourceDirName, subDirList, imageDir, url, imageFormat); //ugly
334 
335         file.close();
336 
337         return !m_cancelled;
338 
339     } else {
340         QString path = url.toLocalFile();
341         if (!path.endsWith("/")) {
342             path += '/';
343         }
344         KMessageBox::sorry(m_part->widget(), i18n("Could not open file: %1", path));
345         return false;
346     }
347 }
348 
deleteCancelledGallery(const QUrl & url,const QString & sourceDirName,int recursionLevel,const QString & imageFormat)349 void KImGalleryPlugin::deleteCancelledGallery(const QUrl &url, const QString &sourceDirName, int recursionLevel, const QString &imageFormat)
350 {
351     if (m_recurseSubDirectories && (recursionLevel >= 0)) {
352         QStringList subDirList;
353         QDir toplevel_dir = QDir(sourceDirName);
354         toplevel_dir.setFilter(QDir::Dirs);
355         subDirList = toplevel_dir.entryList();
356 
357         for (QStringList::ConstIterator it = subDirList.constBegin(); it != subDirList.constEnd(); it++) {
358             if (*it == QLatin1String(".") || *it == QLatin1String("..") || *it == QLatin1String("thumbs") || (m_copyFiles && *it == QLatin1String("images"))) {
359                 continue; //disregard the "." and ".." directories
360             }
361             deleteCancelledGallery(QUrl::fromLocalFile(directory(url) + '/' + *it + '/' + url.fileName()),
362                                    sourceDirName + '/' + *it,
363                                    recursionLevel > 1 ? recursionLevel - 1 : 0, imageFormat);
364         }
365     }
366 
367     const QString imgGalleryDir = directory(url);
368     QDir thumb_dir(imgGalleryDir + QLatin1String("/thumbs/"));
369     QDir images_dir(imgGalleryDir + QLatin1String("/images/"));
370     QDir imageDir(sourceDirName, QStringLiteral("*.png *.PNG *.gif *.GIF *.jpg *.JPG *.jpeg *.JPEG *.bmp *.BMP"),
371                   QDir::Name | QDir::IgnoreCase, QDir::Files | QDir::Readable);
372     QFile file(url.path());
373 
374     // Remove the image file ..
375     file.remove();
376     // ..all the thumbnails ..
377     for (uint i = 0; i < imageDir.count(); i++) {
378         const QString imgName = imageDir[i];
379         const QString imgNameFormat = imgName + extension(imageFormat);
380         bool isRemoved = thumb_dir.remove(imgNameFormat);
381         qCDebug(IMAGEGALLERY_LOG) << "removing: " << thumb_dir.path() << "/" << imgNameFormat << "; " << isRemoved;
382     }
383     // ..and the thumb directory
384     thumb_dir.rmdir(thumb_dir.path());
385 
386     // ..and the images directory if images were to be copied
387     if (m_copyFiles) {
388         for (uint i = 0; i < imageDir.count(); i++) {
389             const QString imgName = imageDir[i];
390             bool isRemoved = images_dir.remove(imgName);
391             qCDebug(IMAGEGALLERY_LOG) << "removing: " << images_dir.path() << "/" << imgName << "; " << isRemoved;
392         }
393         images_dir.rmdir(images_dir.path());
394     }
395 }
396 
loadCommentFile()397 void KImGalleryPlugin::loadCommentFile()
398 {
399     QFile file(m_configDlg->getCommentFile());
400     if (file.open(QIODevice::ReadOnly)) {
401         qCDebug(IMAGEGALLERY_LOG) << "File opened.";
402 
403         QTextStream *m_textStream = new QTextStream(&file);
404         m_textStream->setCodec(QTextCodec::codecForLocale());
405 
406         delete m_commentMap;
407         m_commentMap = new CommentMap;
408 
409         QString picName, picComment, curLine, curLineStripped;
410         while (!m_textStream->atEnd()) {
411             curLine = m_textStream->readLine();
412             curLineStripped = curLine.trimmed();
413             // Lines starting with '#' are comment
414             if (!(curLineStripped.isEmpty()) && !curLineStripped.startsWith(QLatin1String("#"))) {
415                 if (curLineStripped.endsWith(QLatin1String(":"))) {
416                     picComment.clear();
417                     picName = curLineStripped.left(curLineStripped.length() - 1);
418                     qCDebug(IMAGEGALLERY_LOG) << "picName: " << picName;
419                 } else {
420                     do {
421                         //qCDebug(IMAGEGALLERY_LOG) << "picComment";
422                         picComment += curLine + '\n';
423                         curLine = m_textStream->readLine();
424                     } while (!m_textStream->atEnd() && !(curLine.trimmed().isEmpty()) &&
425                              !curLine.trimmed().startsWith(QLatin1String("#")));
426                     //qCDebug(IMAGEGALLERY_LOG) << "Pic comment: " << picComment;
427                     m_commentMap->insert(picName, picComment);
428                 }
429             }
430         }
431         CommentMap::ConstIterator it;
432         for (it = m_commentMap->constBegin(); it != m_commentMap->constEnd(); ++it) {
433             qCDebug(IMAGEGALLERY_LOG) << "picName: " << it.key() << ", picComment: " << it.value();
434         }
435         file.close();
436         qCDebug(IMAGEGALLERY_LOG) << "File closed.";
437         delete m_textStream;
438     } else {
439         KMessageBox::sorry(m_part->widget(), i18n("Could not open file: %1", m_configDlg->getCommentFile()));
440         m_useCommentFile = false;
441     }
442 }
443 
createThumb(const QString & imgName,const QString & sourceDirName,const QString & imgGalleryDir,const QString & imageFormat)444 bool KImGalleryPlugin::createThumb(const QString &imgName, const QString &sourceDirName,
445                                    const QString &imgGalleryDir, const QString &imageFormat)
446 {
447     QImage img;
448     const QString pixPath = sourceDirName + QLatin1String("/") + imgName;
449 
450     if (m_copyFiles) {
451         QFile::copy(pixPath, imgGalleryDir + QLatin1String("/images/") + imgName);
452     }
453 
454     const QString imgNameFormat = imgName + extension(imageFormat);
455     const QString thumbDir = imgGalleryDir + QLatin1String("/thumbs/");
456     int extent = m_configDlg->getThumbnailSize();
457 
458     // this code is stolen from kdebase/kioslave/thumbnail/imagecreator.cpp
459     // (c) 2000 gis and malte
460 
461     m_imgWidth = 120; // Setting the size of the images is
462     m_imgHeight = 90; // required to generate faster 'loading' pages
463     if (img.load(pixPath)) {
464         int w = img.width(), h = img.height();
465         // scale to pixie size
466         // qCDebug(IMAGEGALLERY_LOG) << "w: " << w << " h: " << h;
467         // Resizing if to big
468         if (w > extent || h > extent) {
469             if (w > h) {
470                 h = (int)((double)(h * extent) / w);
471                 if (h == 0) {
472                     h = 1;
473                 }
474                 w = extent;
475                 Q_ASSERT(h <= extent);
476             } else {
477                 w = (int)((double)(w * extent) / h);
478                 if (w == 0) {
479                     w = 1;
480                 }
481                 h = extent;
482                 Q_ASSERT(w <= extent);
483             }
484             const QImage scaleImg(img.scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
485             if (scaleImg.width() != w || scaleImg.height() != h) {
486                 qCDebug(IMAGEGALLERY_LOG) << "Resizing failed. Aborting.";
487                 return false;
488             }
489             img = scaleImg;
490             if (m_configDlg->colorDepthSet() == true) {
491                 QImage::Format format;
492                 switch (m_configDlg->getColorDepth()) {
493                 case 1:
494                     format = QImage::Format_Mono;
495                     break;
496                 case 8:
497                     format = QImage::Format_Indexed8;
498                     break;
499                 case 16:
500                     format = QImage::Format_RGB16;
501                     break;
502                 case 32:
503                 default:
504                     format = QImage::Format_RGB32;
505                     break;
506                 }
507 
508                 const QImage depthImg(img.convertToFormat(format));
509                 img = depthImg;
510             }
511         }
512         qCDebug(IMAGEGALLERY_LOG) << "Saving thumbnail to: " << thumbDir + imgNameFormat;
513         if (!img.save(thumbDir + imgNameFormat, imageFormat.toLatin1())) {
514             qCDebug(IMAGEGALLERY_LOG) << "Saving failed. Aborting.";
515             return false;
516         }
517         m_imgWidth = w;
518         m_imgHeight = h;
519         return true;
520     }
521     return false;
522 }
523 
slotCancelled()524 void KImGalleryPlugin::slotCancelled()
525 {
526     m_cancelled = true;
527 }
528 
529 #include "imgalleryplugin.moc"
530