1 /*  This file is part of the KDE project
2    Copyright (c) 2005 Boudewijn Rempt <boud@valdyas.org>
3    Copyright (c) 2016 L. E. Segovia <amy@amyspark.me>
4 
5 
6     This library is free software; you can redistribute it and/or
7     modify it under the terms of the GNU Lesser General Public
8     License as published by the Free Software Foundation; either
9     version 2.1 of the License, or (at your option) any later version.
10 
11     This library is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14     Lesser General Public License for more details.
15 
16     You should have received a copy of the GNU Lesser General Public
17     License along with this library; if not, write to the Free Software
18     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
19  */
20 #include <resources/KoColorSet.h>
21 
22 #include <sys/types.h>
23 
24 #include <QFile>
25 #include <QFileInfo>
26 #include <QBuffer>
27 #include <QVector>
28 #include <QTextStream>
29 #include <QTextCodec>
30 #include <QHash>
31 #include <QList>
32 #include <QByteArray>
33 #include <QDomDocument>
34 #include <QDomElement>
35 #include <QDomNodeList>
36 #include <QString>
37 #include <QStringList>
38 #include <QImage>
39 #include <QPainter>
40 #include <QXmlStreamReader>
41 #include <QXmlStreamAttributes>
42 #include <QtEndian> // qFromLittleEndian
43 
44 #include <DebugPigment.h>
45 #include <klocalizedstring.h>
46 
47 #include <KoStore.h>
48 #include <KoColor.h>
49 #include <KoColorSpace.h>
50 #include <KoColorSpaceRegistry.h>
51 #include <KoColorProfile.h>
52 #include <KoColorModelStandardIds.h>
53 #include "KisSwatch.h"
54 
55 #include "KoColorSet.h"
56 #include "KoColorSet_p.h"
57 
58 namespace {
59 
60 /**
61  * readAllLinesSafe() reads all the lines in the byte array
62  * using the automated UTF8 and CR/LF transformations. That
63  * might be necessary for opening GPL palettes created on Linux
64  * in Windows environment.
65  */
readAllLinesSafe(QByteArray * data)66 QStringList readAllLinesSafe(QByteArray *data)
67 {
68     QStringList lines;
69 
70     QBuffer buffer(data);
71     buffer.open(QBuffer::ReadOnly);
72     QTextStream stream(&buffer);
73 
74     QString line;
75     while (stream.readLineInto(&line)) {
76         lines << line;
77     }
78 
79     return lines;
80 }
81 }
82 
83 
84 const QString KoColorSet::GLOBAL_GROUP_NAME = QString();
85 const QString KoColorSet::KPL_VERSION_ATTR = "version";
86 const QString KoColorSet::KPL_GROUP_ROW_COUNT_ATTR = "rows";
87 const QString KoColorSet::KPL_PALETTE_COLUMN_COUNT_ATTR = "columns";
88 const QString KoColorSet::KPL_PALETTE_NAME_ATTR = "name";
89 const QString KoColorSet::KPL_PALETTE_COMMENT_ATTR = "comment";
90 const QString KoColorSet::KPL_PALETTE_FILENAME_ATTR = "filename";
91 const QString KoColorSet::KPL_PALETTE_READONLY_ATTR = "readonly";
92 const QString KoColorSet::KPL_COLOR_MODEL_ID_ATTR = "colorModelId";
93 const QString KoColorSet::KPL_COLOR_DEPTH_ID_ATTR = "colorDepthId";
94 const QString KoColorSet::KPL_GROUP_NAME_ATTR = "name";
95 const QString KoColorSet::KPL_SWATCH_ROW_ATTR = "row";
96 const QString KoColorSet::KPL_SWATCH_COL_ATTR = "column";
97 const QString KoColorSet::KPL_SWATCH_NAME_ATTR = "name";
98 const QString KoColorSet::KPL_SWATCH_ID_ATTR = "id";
99 const QString KoColorSet::KPL_SWATCH_SPOT_ATTR = "spot";
100 const QString KoColorSet::KPL_SWATCH_BITDEPTH_ATTR = "bitdepth";
101 const QString KoColorSet::KPL_PALETTE_PROFILE_TAG = "Profile";
102 const QString KoColorSet::KPL_SWATCH_POS_TAG = "Position";
103 const QString KoColorSet::KPL_SWATCH_TAG = "ColorSetEntry";
104 const QString KoColorSet::KPL_GROUP_TAG = "Group";
105 const QString KoColorSet::KPL_PALETTE_TAG = "ColorSet";
106 
107 const int MAXIMUM_ALLOWED_COLUMNS = 4096;
108 
KoColorSet(const QString & filename)109 KoColorSet::KoColorSet(const QString& filename)
110     : KoResource(filename)
111     , d(new Private(this))
112 {
113     if (!filename.isEmpty()) {
114         QFileInfo f(filename);
115         setIsEditable(f.isWritable());
116     }
117 }
118 
119 /// Create an copied palette
KoColorSet(const KoColorSet & rhs)120 KoColorSet::KoColorSet(const KoColorSet& rhs)
121     : QObject(0)
122     , KoResource(rhs)
123     , d(new Private(this))
124 {
125     d->paletteType = rhs.d->paletteType;
126     d->data = rhs.d->data;
127     d->comment = rhs.d->comment;
128     d->groupNames = rhs.d->groupNames;
129     d->groups = rhs.d->groups;
130     d->isGlobal = rhs.d->isGlobal;
131     d->isEditable = rhs.d->isEditable;
132 }
133 
~KoColorSet()134 KoColorSet::~KoColorSet()
135 { }
136 
load()137 bool KoColorSet::load()
138 {
139     QFile file(filename());
140     if (file.size() == 0) return false;
141     if (!file.open(QIODevice::ReadOnly)) {
142         warnPigment << "Can't open file " << filename();
143         return false;
144     }
145     bool res = loadFromDevice(&file);
146     file.close();
147     if (!QFileInfo(filename()).isWritable()) {
148         setIsEditable(false);
149     }
150     return res;
151 }
152 
loadFromDevice(QIODevice * dev)153 bool KoColorSet::loadFromDevice(QIODevice *dev)
154 {
155     if (!dev->isOpen()) dev->open(QIODevice::ReadOnly);
156 
157     d->data = dev->readAll();
158 
159     Q_ASSERT(d->data.size() != 0);
160 
161     return d->init();
162 }
163 
164 
save()165 bool KoColorSet::save()
166 {
167     if (d->isGlobal) {
168         // save to resource dir
169         QFile file(filename());
170         if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
171             return false;
172         }
173         saveToDevice(&file);
174         file.close();
175         return true;
176     } else {
177         return true; // palette is not global, but still indicate that it's saved
178     }
179 }
180 
saveToDevice(QIODevice * dev) const181 bool KoColorSet::saveToDevice(QIODevice *dev) const
182 {
183     bool res;
184     switch(d->paletteType) {
185     case GPL:
186         res = d->saveGpl(dev);
187         break;
188     default:
189         res = d->saveKpl(dev);
190     }
191     if (res) {
192         KoResource::saveToDevice(dev);
193     }
194     return res;
195 }
196 
toByteArray() const197 QByteArray KoColorSet::toByteArray() const
198 {
199     QBuffer s;
200     s.open(QIODevice::WriteOnly);
201     if (!saveToDevice(&s)) {
202         warnPigment << "saving palette failed:" << name();
203         return QByteArray();
204     }
205     s.close();
206     s.open(QIODevice::ReadOnly);
207     QByteArray res = s.readAll();
208     s.close();
209     return res;
210 }
211 
fromByteArray(QByteArray & data)212 bool KoColorSet::fromByteArray(QByteArray &data)
213 {
214     QBuffer buf(&data);
215     buf.open(QIODevice::ReadOnly);
216     return loadFromDevice(&buf);
217 }
218 
paletteType() const219 KoColorSet::PaletteType KoColorSet::paletteType() const
220 {
221     return d->paletteType;
222 }
223 
setPaletteType(PaletteType paletteType)224 void KoColorSet::setPaletteType(PaletteType paletteType)
225 {
226     d->paletteType = paletteType;
227     QString suffix;
228     switch(d->paletteType) {
229     case GPL:
230         suffix = ".gpl";
231         break;
232     case ACT:
233         suffix = ".act";
234         break;
235     case RIFF_PAL:
236     case PSP_PAL:
237         suffix = ".pal";
238         break;
239     case ACO:
240         suffix = ".aco";
241         break;
242     case XML:
243         suffix = ".xml";
244         break;
245     case KPL:
246         suffix = ".kpl";
247         break;
248     case SBZ:
249         suffix = ".sbz";
250         break;
251     default:
252         suffix = defaultFileExtension();
253     }
254     QStringList fileName = filename().split(".");
255     fileName.last() = suffix.replace(".", "");
256     setFilename(fileName.join("."));
257 }
258 
259 
colorCount() const260 quint32 KoColorSet::colorCount() const
261 {
262     int colorCount = 0;
263     for (KisSwatchGroup &g : d->groups.values()) {
264         colorCount += g.colorCount();
265     }
266     return colorCount;
267 }
268 
add(const KisSwatch & c,const QString & groupName)269 void KoColorSet::add(const KisSwatch &c, const QString &groupName)
270 {
271     KisSwatchGroup &modifiedGroup = d->groups.contains(groupName)
272             ? d->groups[groupName] : d->global();
273     modifiedGroup.addEntry(c);
274 }
275 
setEntry(const KisSwatch & e,int x,int y,const QString & groupName)276 void KoColorSet::setEntry(const KisSwatch &e, int x, int y, const QString &groupName)
277 {
278     KisSwatchGroup &modifiedGroup = d->groups.contains(groupName)
279             ? d->groups[groupName] : d->global();
280     modifiedGroup.setEntry(e, x, y);
281 }
282 
clear()283 void KoColorSet::clear()
284 {
285     d->groups.clear();
286     d->groupNames.clear();
287     d->groups[GLOBAL_GROUP_NAME] = KisSwatchGroup();
288     d->groupNames.append(GLOBAL_GROUP_NAME);
289 }
290 
getColorGlobal(quint32 x,quint32 y) const291 KisSwatch KoColorSet::getColorGlobal(quint32 x, quint32 y) const
292 {
293     for (const QString &groupName : getGroupNames()) {
294         if (d->groups.contains(groupName)) {
295             if ((int)y < d->groups[groupName].rowCount()) {
296                 return d->groups[groupName].getEntry(x, y);
297             } else {
298                 y -= d->groups[groupName].rowCount();
299             }
300         }
301     }
302     return KisSwatch();
303 }
304 
getColorGroup(quint32 x,quint32 y,QString groupName)305 KisSwatch KoColorSet::getColorGroup(quint32 x, quint32 y, QString groupName)
306 {
307     KisSwatch e;
308     const KisSwatchGroup &sourceGroup = groupName == QString()
309             ? d->global() : d->groups[groupName];
310     if (sourceGroup.checkEntry(x, y)) {
311         e = sourceGroup.getEntry(x, y);
312     }
313     return e;
314 }
315 
getGroupNames() const316 QStringList KoColorSet::getGroupNames() const
317 {
318     if (d->groupNames.size() != d->groups.size()) {
319         warnPigment << "mismatch between groups and the groupnames list.";
320         return QStringList(d->groups.keys());
321     }
322     return d->groupNames;
323 }
324 
changeGroupName(const QString & oldGroupName,const QString & newGroupName)325 bool KoColorSet::changeGroupName(const QString &oldGroupName, const QString &newGroupName)
326 {
327     if (!d->groups.contains(oldGroupName)) {
328         return false;
329     }
330     if (oldGroupName == newGroupName) {
331         return true;
332     }
333     d->groups[newGroupName] = d->groups[oldGroupName];
334     d->groups.remove(oldGroupName);
335     d->groups[newGroupName].setName(newGroupName);
336     //rename the string in the stringlist;
337     int index = d->groupNames.indexOf(oldGroupName);
338     d->groupNames.replace(index, newGroupName);
339     return true;
340 }
341 
setColumnCount(int columns)342 void KoColorSet::setColumnCount(int columns)
343 {
344     for (auto it = d->groups.begin(); it != d->groups.end(); ++it) {
345         KisSwatchGroup &g = *it;
346         g.setColumnCount(columns);
347     }
348 }
349 
columnCount() const350 int KoColorSet::columnCount() const
351 {
352     return d->groups[GLOBAL_GROUP_NAME].columnCount();
353 }
354 
comment()355 QString KoColorSet::comment()
356 {
357     return d->comment;
358 }
359 
setComment(QString comment)360 void KoColorSet::setComment(QString comment)
361 {
362     d->comment = comment;
363 }
364 
addGroup(const QString & groupName)365 bool KoColorSet::addGroup(const QString &groupName)
366 {
367     if (d->groups.contains(groupName) || getGroupNames().contains(groupName)) {
368         return false;
369     }
370     d->groupNames.append(groupName);
371     d->groups[groupName] = KisSwatchGroup();
372     d->groups[groupName].setName(groupName);
373     return true;
374 }
375 
moveGroup(const QString & groupName,const QString & groupNameInsertBefore)376 bool KoColorSet::moveGroup(const QString &groupName, const QString &groupNameInsertBefore)
377 {
378     if (!d->groupNames.contains(groupName) || d->groupNames.contains(groupNameInsertBefore)==false) {
379         return false;
380     }
381     if (groupNameInsertBefore != GLOBAL_GROUP_NAME && groupName != GLOBAL_GROUP_NAME) {
382         d->groupNames.removeAt(d->groupNames.indexOf(groupName));
383         int index = d->groupNames.indexOf(groupNameInsertBefore);
384         d->groupNames.insert(index, groupName);
385     }
386     return true;
387 }
388 
removeGroup(const QString & groupName,bool keepColors)389 bool KoColorSet::removeGroup(const QString &groupName, bool keepColors)
390 {
391     if (!d->groups.contains(groupName)) {
392         return false;
393     }
394 
395     if (groupName == GLOBAL_GROUP_NAME) {
396         return false;
397     }
398 
399     if (keepColors) {
400         // put all colors directly below global
401         int startingRow = d->groups[GLOBAL_GROUP_NAME].rowCount();
402         for (const KisSwatchGroup::SwatchInfo &info : d->groups[groupName].infoList()) {
403             d->groups[GLOBAL_GROUP_NAME].setEntry(info.swatch,
404                                                   info.column,
405                                                   info.row + startingRow);
406         }
407     }
408 
409     d->groupNames.removeAt(d->groupNames.indexOf(groupName));
410     d->groups.remove(groupName);
411     return true;
412 }
413 
defaultFileExtension() const414 QString KoColorSet::defaultFileExtension() const
415 {
416     return QString(".kpl");
417 }
418 
419 
rowCount() const420 int KoColorSet::rowCount() const
421 {
422     int res = 0;
423     for (const QString &name : getGroupNames()) {
424         res += d->groups[name].rowCount();
425     }
426     return res;
427 }
428 
getGroup(const QString & name)429 KisSwatchGroup *KoColorSet::getGroup(const QString &name)
430 {
431     if (!d->groups.contains(name)) {
432         return 0;
433     }
434     return &(d->groups[name]);
435 }
436 
getGlobalGroup()437 KisSwatchGroup *KoColorSet::getGlobalGroup()
438 {
439     return getGroup(GLOBAL_GROUP_NAME);
440 }
441 
isGlobal() const442 bool KoColorSet::isGlobal() const
443 {
444     return d->isGlobal;
445 }
446 
setIsGlobal(bool isGlobal)447 void KoColorSet::setIsGlobal(bool isGlobal)
448 {
449     d->isGlobal = isGlobal;
450 }
451 
isEditable() const452 bool KoColorSet::isEditable() const
453 {
454     return d->isEditable;
455 }
456 
setIsEditable(bool isEditable)457 void KoColorSet::setIsEditable(bool isEditable)
458 {
459     d->isEditable = isEditable;
460 }
461 
getClosestColorInfo(KoColor compare,bool useGivenColorSpace)462 KisSwatchGroup::SwatchInfo KoColorSet::getClosestColorInfo(KoColor compare, bool useGivenColorSpace)
463 {
464     KisSwatchGroup::SwatchInfo res;
465 
466     quint8 highestPercentage = 0;
467     quint8 testPercentage = 0;
468 
469     for (const QString &groupName : getGroupNames()) {
470         KisSwatchGroup *group = getGroup(groupName);
471         for (const KisSwatchGroup::SwatchInfo &currInfo : group->infoList()) {
472             KoColor color = currInfo.swatch.color();
473             if (useGivenColorSpace == true && compare.colorSpace() != color.colorSpace()) {
474                 color.convertTo(compare.colorSpace());
475 
476             } else if (compare.colorSpace() != color.colorSpace()) {
477                 compare.convertTo(color.colorSpace());
478             }
479             testPercentage = (255 - compare.colorSpace()->difference(compare.data(), color.data()));
480             if (testPercentage > highestPercentage)
481             {
482                 highestPercentage = testPercentage;
483                 res = currInfo;
484             }
485         }
486     }
487     return res;
488 }
489 
490 /********************************KoColorSet::Private**************************/
491 
Private(KoColorSet * a_colorSet)492 KoColorSet::Private::Private(KoColorSet *a_colorSet)
493     : colorSet(a_colorSet)
494 {
495     groups[KoColorSet::GLOBAL_GROUP_NAME] = KisSwatchGroup();
496     groupNames.append(KoColorSet::GLOBAL_GROUP_NAME);
497 }
498 
detectFormat(const QString & fileName,const QByteArray & ba)499 KoColorSet::PaletteType KoColorSet::Private::detectFormat(const QString &fileName, const QByteArray &ba)
500 {
501     QFileInfo fi(fileName);
502 
503     // .pal
504     if (ba.startsWith("RIFF") && ba.indexOf("PAL data", 8)) {
505         return KoColorSet::RIFF_PAL;
506     }
507     // .gpl
508     else if (ba.startsWith("GIMP Palette")) {
509         return KoColorSet::GPL;
510     }
511     // .pal
512     else if (ba.startsWith("JASC-PAL")) {
513         return KoColorSet::PSP_PAL;
514     }
515     else if (fi.suffix().toLower() == "aco") {
516         return KoColorSet::ACO;
517     }
518     else if (fi.suffix().toLower() == "act") {
519         return KoColorSet::ACT;
520     }
521     else if (fi.suffix().toLower() == "xml") {
522         return KoColorSet::XML;
523     }
524     else if (fi.suffix().toLower() == "kpl") {
525         return KoColorSet::KPL;
526     }
527     else if (fi.suffix().toLower() == "sbz") {
528         return KoColorSet::SBZ;
529     }
530     return KoColorSet::UNKNOWN;
531 }
532 
scribusParseColor(KoColorSet * set,QXmlStreamReader * xml)533 void KoColorSet::Private::scribusParseColor(KoColorSet *set, QXmlStreamReader *xml)
534 {
535     KisSwatch colorEntry;
536     // It's a color, retrieve it
537     QXmlStreamAttributes colorProperties = xml->attributes();
538 
539     QStringRef colorName = colorProperties.value("NAME");
540     colorEntry.setName(colorName.isEmpty() || colorName.isNull() ? i18n("Untitled") : colorName.toString());
541 
542     // RGB or CMYK?
543     if (colorProperties.hasAttribute("RGB")) {
544         dbgPigment << "Color " << colorProperties.value("NAME") << ", RGB " << colorProperties.value("RGB");
545 
546         KoColor currentColor(KoColorSpaceRegistry::instance()->rgb8());
547         QStringRef colorValue = colorProperties.value("RGB");
548 
549         if (colorValue.length() != 7 && colorValue.at(0) != '#') { // Color is a hexadecimal number
550             xml->raiseError("Invalid rgb8 color (malformed): " + colorValue);
551             return;
552         } else {
553             bool rgbOk;
554             quint32 rgb = colorValue.mid(1).toUInt(&rgbOk, 16);
555             if  (!rgbOk) {
556                 xml->raiseError("Invalid rgb8 color (unable to convert): " + colorValue);
557                 return;
558             }
559 
560             quint8 r = rgb >> 16 & 0xff;
561             quint8 g = rgb >> 8 & 0xff;
562             quint8 b = rgb & 0xff;
563 
564             dbgPigment << "Color parsed: "<< r << g << b;
565 
566             currentColor.data()[0] = r;
567             currentColor.data()[1] = g;
568             currentColor.data()[2] = b;
569             currentColor.setOpacity(OPACITY_OPAQUE_U8);
570             colorEntry.setColor(currentColor);
571 
572             set->add(colorEntry);
573 
574             while(xml->readNextStartElement()) {
575                 //ignore - these are all unknown or the /> element tag
576                 xml->skipCurrentElement();
577             }
578             return;
579         }
580     }
581     else if (colorProperties.hasAttribute("CMYK")) {
582         dbgPigment << "Color " << colorProperties.value("NAME") << ", CMYK " << colorProperties.value("CMYK");
583 
584         KoColor currentColor(KoColorSpaceRegistry::instance()->colorSpace(CMYKAColorModelID.id(), Integer8BitsColorDepthID.id(), QString()));
585 
586         QStringRef colorValue = colorProperties.value("CMYK");
587         if (colorValue.length() != 9 && colorValue.at(0) != '#') { // Color is a hexadecimal number
588             xml->raiseError("Invalid cmyk color (malformed): " % colorValue);
589             return;
590         }
591         else {
592             bool cmykOk;
593             quint32 cmyk = colorValue.mid(1).toUInt(&cmykOk, 16); // cmyk uses the full 32 bits
594             if  (!cmykOk) {
595                 xml->raiseError("Invalid cmyk color (unable to convert): " % colorValue);
596                 return;
597             }
598 
599             quint8 c = cmyk >> 24 & 0xff;
600             quint8 m = cmyk >> 16 & 0xff;
601             quint8 y = cmyk >> 8 & 0xff;
602             quint8 k = cmyk & 0xff;
603 
604             dbgPigment << "Color parsed: "<< c << m << y << k;
605 
606             currentColor.data()[0] = c;
607             currentColor.data()[1] = m;
608             currentColor.data()[2] = y;
609             currentColor.data()[3] = k;
610             currentColor.setOpacity(OPACITY_OPAQUE_U8);
611             colorEntry.setColor(currentColor);
612 
613             set->add(colorEntry);
614 
615             while(xml->readNextStartElement()) {
616                 //ignore - these are all unknown or the /> element tag
617                 xml->skipCurrentElement();
618             }
619             return;
620         }
621     }
622     else {
623         xml->raiseError("Unknown color space for color " + colorEntry.name());
624     }
625 }
626 
loadScribusXmlPalette(KoColorSet * set,QXmlStreamReader * xml)627 bool KoColorSet::Private::loadScribusXmlPalette(KoColorSet *set, QXmlStreamReader *xml)
628 {
629 
630     //1. Get name
631     QXmlStreamAttributes paletteProperties = xml->attributes();
632     QStringRef paletteName = paletteProperties.value("Name");
633     dbgPigment << "Processed name of palette:" << paletteName;
634     set->setName(paletteName.toString());
635 
636     //2. Inside the SCRIBUSCOLORS, there are lots of colors. Retrieve them
637 
638     while(xml->readNextStartElement()) {
639         QStringRef currentElement = xml->name();
640         if(QStringRef::compare(currentElement, "COLOR", Qt::CaseInsensitive) == 0) {
641             scribusParseColor(set, xml);
642         }
643         else {
644             xml->skipCurrentElement();
645         }
646     }
647 
648     if(xml->hasError()) {
649         return false;
650     }
651 
652     return true;
653 }
654 
readShort(QIODevice * io)655 quint16 KoColorSet::Private::readShort(QIODevice *io) {
656     quint16 val;
657     quint64 read = io->read((char*)&val, 2);
658     if (read != 2) return false;
659     return qFromBigEndian(val);
660 }
661 
init()662 bool KoColorSet::Private::init()
663 {
664     // just in case this is a reload (eg by KoEditColorSetDialog),
665     groupNames.clear();
666     groups.clear();
667     groupNames.append(KoColorSet::GLOBAL_GROUP_NAME);
668     groups[KoColorSet::GLOBAL_GROUP_NAME] = KisSwatchGroup();
669 
670     if (colorSet->filename().isNull()) {
671         warnPigment << "Cannot load palette" << colorSet->name() << "there is no filename set";
672         return false;
673     }
674     if (data.isNull()) {
675         QFile file(colorSet->filename());
676         if (file.size() == 0) {
677             warnPigment << "Cannot load palette" << colorSet->name() << "there is no data available";
678             return false;
679         }
680         file.open(QIODevice::ReadOnly);
681         data = file.readAll();
682         file.close();
683     }
684 
685     bool res = false;
686     paletteType = detectFormat(colorSet->filename(), data);
687     switch(paletteType) {
688     case GPL:
689         res = loadGpl();
690         break;
691     case ACT:
692         res = loadAct();
693         break;
694     case RIFF_PAL:
695         res = loadRiff();
696         break;
697     case PSP_PAL:
698         res = loadPsp();
699         break;
700     case ACO:
701         res = loadAco();
702         break;
703     case XML:
704         res = loadXml();
705         break;
706     case KPL:
707         res = loadKpl();
708         break;
709     case SBZ:
710         res = loadSbz();
711         break;
712     default:
713         res = false;
714     }
715     colorSet->setValid(res);
716 
717     QImage img(global().columnCount() * 4, global().rowCount() * 4, QImage::Format_ARGB32);
718     QPainter gc(&img);
719     gc.fillRect(img.rect(), Qt::darkGray);
720     for (const KisSwatchGroup::SwatchInfo &info : global().infoList()) {
721         QColor c = info.swatch.color().toQColor();
722         gc.fillRect(info.column * 4, info.row * 4, 4, 4, c);
723     }
724     colorSet->setImage(img);
725     colorSet->setValid(res);
726 
727     data.clear();
728     return res;
729 }
730 
saveGpl(QIODevice * dev) const731 bool KoColorSet::Private::saveGpl(QIODevice *dev) const
732 {
733     Q_ASSERT(dev->isOpen());
734     Q_ASSERT(dev->isWritable());
735 
736     QTextStream stream(dev);
737     stream << "GIMP Palette\nName: " << colorSet->name() << "\nColumns: " << colorSet->columnCount() << "\n#\n";
738 
739     /*
740      * Qt doesn't provide an interface to get a const reference to a QHash, that is
741      * the underlying data structure of groups. Therefore, directly use
742      * groups[KoColorSet::GLOBAL_GROUP_NAME] so that saveGpl can stay const
743      */
744 
745     for (int y = 0; y < groups[KoColorSet::GLOBAL_GROUP_NAME].rowCount(); y++) {
746         for (int x = 0; x < colorSet->columnCount(); x++) {
747             if (!groups[KoColorSet::GLOBAL_GROUP_NAME].checkEntry(x, y)) {
748                 continue;
749             }
750             const KisSwatch& entry = groups[KoColorSet::GLOBAL_GROUP_NAME].getEntry(x, y);
751             QColor c = entry.color().toQColor();
752             stream << c.red() << " " << c.green() << " " << c.blue() << "\t";
753             if (entry.name().isEmpty())
754                 stream << "Untitled\n";
755             else
756                 stream << entry.name() << "\n";
757         }
758     }
759 
760     return true;
761 }
762 
loadGpl()763 bool KoColorSet::Private::loadGpl()
764 {
765     if (data.isEmpty() || data.isNull() || data.length() < 50) {
766         warnPigment << "Illegal Gimp palette file: " << colorSet->filename();
767         return false;
768     }
769 
770     quint32 index = 0;
771 
772     QStringList lines = readAllLinesSafe(&data);
773 
774     if (lines.size() < 3) {
775         warnPigment << "Not enough lines in palette file: " << colorSet->filename();
776         return false;
777     }
778 
779     QString columnsText;
780     qint32 r, g, b;
781     KisSwatch e;
782 
783     // Read name
784     if (!lines[0].startsWith("GIMP") || !lines[1].toLower().contains("name")) {
785         warnPigment << "Illegal Gimp palette file: " << colorSet->filename();
786         return false;
787     }
788 
789     colorSet->setName(i18n(lines[1].split(":")[1].trimmed().toLatin1()));
790 
791     index = 2;
792 
793     // Read columns
794     int columns = 0;
795     if (lines[index].toLower().contains("columns")) {
796         columnsText = lines[index].split(":")[1].trimmed();
797         columns = columnsText.toInt();
798         if (columns > MAXIMUM_ALLOWED_COLUMNS) {
799             warnPigment << "Refusing to set unreasonable number of columns (" << columns << ") in GIMP Palette file " << colorSet->filename() << " - using maximum number of allowed columns instead";
800             global().setColumnCount(MAXIMUM_ALLOWED_COLUMNS);
801         }
802         else {
803             global().setColumnCount(columns);
804         }
805         index = 3;
806     }
807 
808 
809     for (qint32 i = index; i < lines.size(); i++) {
810         if (lines[i].startsWith('#')) {
811             comment += lines[i].mid(1).trimmed() + ' ';
812         } else if (!lines[i].isEmpty()) {
813             QStringList a = lines[i].replace('\t', ' ').split(' ', QString::SkipEmptyParts);
814 
815             if (a.count() < 3) {
816                 continue;
817             }
818 
819             r = qBound(0, a[0].toInt(), 255);
820             g = qBound(0, a[1].toInt(), 255);
821             b = qBound(0, a[2].toInt(), 255);
822 
823             e.setColor(KoColor(QColor(r, g, b), KoColorSpaceRegistry::instance()->rgb8()));
824 
825             for (int i = 0; i != 3; i++) {
826                 a.pop_front();
827             }
828             QString name = a.join(" ");
829             e.setName(name.isEmpty() || name == "Untitled" ? i18n("Untitled") : name);
830 
831             global().addEntry(e);
832         }
833     }
834     int rowCount = global().colorCount()/ global().columnCount();
835     if (global().colorCount() % global().columnCount()>0) {
836         rowCount ++;
837     }
838     global().setRowCount(rowCount);
839     return true;
840 }
841 
loadAct()842 bool KoColorSet::Private::loadAct()
843 {
844     QFileInfo info(colorSet->filename());
845     colorSet->setName(info.completeBaseName());
846     KisSwatch e;
847     for (int i = 0; i < data.size(); i += 3) {
848         quint8 r = data[i];
849         quint8 g = data[i+1];
850         quint8 b = data[i+2];
851         e.setColor(KoColor(QColor(r, g, b), KoColorSpaceRegistry::instance()->rgb8()));
852         global().addEntry(e);
853     }
854     return true;
855 }
856 
loadRiff()857 bool KoColorSet::Private::loadRiff()
858 {
859     // https://worms2d.info/Palette_file
860     QFileInfo info(colorSet->filename());
861     colorSet->setName(info.completeBaseName());
862     KisSwatch e;
863 
864     RiffHeader header;
865     memcpy(&header, data.constData(), sizeof(RiffHeader));
866     header.colorcount = qFromBigEndian(header.colorcount);
867 
868     for (int i = sizeof(RiffHeader);
869          (i < (int)(sizeof(RiffHeader) + header.colorcount) && i < data.size());
870          i += 4) {
871         quint8 r = data[i];
872         quint8 g = data[i+1];
873         quint8 b = data[i+2];
874         e.setColor(KoColor(QColor(r, g, b), KoColorSpaceRegistry::instance()->rgb8()));
875         groups[KoColorSet::GLOBAL_GROUP_NAME].addEntry(e);
876     }
877     return true;
878 }
879 
880 
loadPsp()881 bool KoColorSet::Private::loadPsp()
882 {
883     QFileInfo info(colorSet->filename());
884     colorSet->setName(info.completeBaseName());
885     KisSwatch e;
886     qint32 r, g, b;
887 
888     QStringList l = readAllLinesSafe(&data);
889     if (l.size() < 4) return false;
890     if (l[0] != "JASC-PAL") return false;
891     if (l[1] != "0100") return false;
892 
893     int entries = l[2].toInt();
894 
895     for (int i = 0; i < entries; ++i)  {
896 
897         QStringList a = l[i + 3].replace('\t', ' ').split(' ', QString::SkipEmptyParts);
898 
899         if (a.count() != 3) {
900             continue;
901         }
902 
903         r = qBound(0, a[0].toInt(), 255);
904         g = qBound(0, a[1].toInt(), 255);
905         b = qBound(0, a[2].toInt(), 255);
906 
907         e.setColor(KoColor(QColor(r, g, b),
908                            KoColorSpaceRegistry::instance()->rgb8()));
909 
910         QString name = a.join(" ");
911         e.setName(name.isEmpty() ? i18n("Untitled") : name);
912 
913         groups[KoColorSet::GLOBAL_GROUP_NAME].addEntry(e);
914     }
915     return true;
916 }
917 
loadKpl()918 bool KoColorSet::Private::loadKpl()
919 {
920     QBuffer buf(&data);
921     buf.open(QBuffer::ReadOnly);
922 
923     QScopedPointer<KoStore> store(KoStore::createStore(&buf, KoStore::Read, "krita/x-colorset", KoStore::Zip));
924     if (!store || store->bad()) { return false; }
925 
926     if (store->hasFile("profiles.xml")) {
927         if (!store->open("profiles.xml")) { return false; }
928         QByteArray data;
929         data.resize(store->size());
930         QByteArray ba = store->read(store->size());
931         store->close();
932 
933         QDomDocument doc;
934         doc.setContent(ba);
935         QDomElement e = doc.documentElement();
936         QDomElement c = e.firstChildElement(KPL_PALETTE_PROFILE_TAG);
937         while (!c.isNull()) {
938             QString name = c.attribute(KPL_PALETTE_NAME_ATTR);
939             QString filename = c.attribute(KPL_PALETTE_FILENAME_ATTR);
940             QString colorModelId = c.attribute(KPL_COLOR_MODEL_ID_ATTR);
941             QString colorDepthId = c.attribute(KPL_COLOR_DEPTH_ID_ATTR);
942             if (!KoColorSpaceRegistry::instance()->profileByName(name)) {
943                 store->open(filename);
944                 QByteArray data;
945                 data.resize(store->size());
946                 data = store->read(store->size());
947                 store->close();
948 
949                 const KoColorProfile *profile = KoColorSpaceRegistry::instance()->createColorProfile(colorModelId, colorDepthId, data);
950                 if (profile && profile->valid()) {
951                     KoColorSpaceRegistry::instance()->addProfile(profile);
952                 }
953             }
954 
955             c = c.nextSiblingElement();
956         }
957     }
958 
959     {
960         if (!store->open("colorset.xml")) { return false; }
961         QByteArray data;
962         data.resize(store->size());
963         QByteArray ba = store->read(store->size());
964         store->close();
965 
966         int desiredColumnCount;
967 
968         QDomDocument doc;
969         doc.setContent(ba);
970         QDomElement e = doc.documentElement();
971         colorSet->setName(e.attribute(KPL_PALETTE_NAME_ATTR));
972         colorSet->setIsEditable(e.attribute(KPL_PALETTE_READONLY_ATTR) != "true");
973         comment = e.attribute(KPL_PALETTE_COMMENT_ATTR);
974 
975         desiredColumnCount = e.attribute(KPL_PALETTE_COLUMN_COUNT_ATTR).toInt();
976         if (desiredColumnCount > MAXIMUM_ALLOWED_COLUMNS) {
977             warnPigment << "Refusing to set unreasonable number of columns (" << desiredColumnCount << ") in KPL palette file " << colorSet->filename() << " - setting maximum allowed column count instead.";
978             colorSet->setColumnCount(MAXIMUM_ALLOWED_COLUMNS);
979         }
980         else {
981             colorSet->setColumnCount(desiredColumnCount);
982         }
983 
984         loadKplGroup(doc, e, colorSet->getGlobalGroup());
985 
986         QDomElement g = e.firstChildElement(KPL_GROUP_TAG);
987         while (!g.isNull()) {
988             QString groupName = g.attribute(KPL_GROUP_NAME_ATTR);
989             colorSet->addGroup(groupName);
990             loadKplGroup(doc, g, colorSet->getGroup(groupName));
991             g = g.nextSiblingElement(KPL_GROUP_TAG);
992         }
993     }
994 
995     buf.close();
996     return true;
997 }
998 
loadAco()999 bool KoColorSet::Private::loadAco()
1000 {
1001     QFileInfo info(colorSet->filename());
1002     colorSet->setName(info.completeBaseName());
1003 
1004     QBuffer buf(&data);
1005     buf.open(QBuffer::ReadOnly);
1006 
1007     quint16 version = readShort(&buf);
1008     quint16 numColors = readShort(&buf);
1009     KisSwatch e;
1010 
1011     if (version == 1 && buf.size() > 4+numColors*10) {
1012         buf.seek(4+numColors*10);
1013         version = readShort(&buf);
1014         numColors = readShort(&buf);
1015     }
1016 
1017     const quint16 quint16_MAX = 65535;
1018 
1019     for (int i = 0; i < numColors && !buf.atEnd(); ++i) {
1020 
1021         quint16 colorSpace = readShort(&buf);
1022         quint16 ch1 = readShort(&buf);
1023         quint16 ch2 = readShort(&buf);
1024         quint16 ch3 = readShort(&buf);
1025         quint16 ch4 = readShort(&buf);
1026 
1027         bool skip = false;
1028         if (colorSpace == 0) { // RGB
1029             const KoColorProfile *srgb = KoColorSpaceRegistry::instance()->rgb8()->profile();
1030             KoColor c(KoColorSpaceRegistry::instance()->rgb16(srgb));
1031             reinterpret_cast<quint16*>(c.data())[0] = ch3;
1032             reinterpret_cast<quint16*>(c.data())[1] = ch2;
1033             reinterpret_cast<quint16*>(c.data())[2] = ch1;
1034             c.setOpacity(OPACITY_OPAQUE_U8);
1035             e.setColor(c);
1036         }
1037         else if (colorSpace == 1) { // HSB
1038             QColor qc;
1039             qc.setHsvF(ch1 / 65536.0, ch2 / 65536.0, ch3 / 65536.0);
1040             KoColor c(qc, KoColorSpaceRegistry::instance()->rgb16());
1041             c.setOpacity(OPACITY_OPAQUE_U8);
1042             e.setColor(c);
1043         }
1044         else if (colorSpace == 2) { // CMYK
1045             KoColor c(KoColorSpaceRegistry::instance()->colorSpace(CMYKAColorModelID.id(), Integer16BitsColorDepthID.id(), QString()));
1046             reinterpret_cast<quint16*>(c.data())[0] = quint16_MAX - ch1;
1047             reinterpret_cast<quint16*>(c.data())[1] = quint16_MAX - ch2;
1048             reinterpret_cast<quint16*>(c.data())[2] = quint16_MAX - ch3;
1049             reinterpret_cast<quint16*>(c.data())[3] = quint16_MAX - ch4;
1050             c.setOpacity(OPACITY_OPAQUE_U8);
1051             e.setColor(c);
1052         }
1053         else if (colorSpace == 7) { // LAB
1054             KoColor c = KoColor(KoColorSpaceRegistry::instance()->lab16());
1055             reinterpret_cast<quint16*>(c.data())[0] = ch3;
1056             reinterpret_cast<quint16*>(c.data())[1] = ch2;
1057             reinterpret_cast<quint16*>(c.data())[2] = ch1;
1058             c.setOpacity(OPACITY_OPAQUE_U8);
1059             e.setColor(c);
1060         }
1061         else if (colorSpace == 8) { // GRAY
1062             KoColor c(KoColorSpaceRegistry::instance()->colorSpace(GrayAColorModelID.id(), Integer16BitsColorDepthID.id(), QString()));
1063             reinterpret_cast<quint16*>(c.data())[0] = ch1 * (quint16_MAX / 10000);
1064             c.setOpacity(OPACITY_OPAQUE_U8);
1065             e.setColor(c);
1066         }
1067         else {
1068             warnPigment << "Unsupported colorspace in palette" << colorSet->filename() << "(" << colorSpace << ")";
1069             skip = true;
1070         }
1071         if (version == 2) {
1072             quint16 v2 = readShort(&buf); //this isn't a version, it's a marker and needs to be skipped.
1073             Q_UNUSED(v2);
1074             quint16 size = readShort(&buf) -1; //then comes the length
1075             if (size>0) {
1076                 QByteArray ba = buf.read(size*2);
1077                 if (ba.size() == size*2) {
1078                     QTextCodec *Utf16Codec = QTextCodec::codecForName("UTF-16BE");
1079                     e.setName(Utf16Codec->toUnicode(ba));
1080                 } else {
1081                     warnPigment << "Version 2 name block is the wrong size" << colorSet->filename();
1082                 }
1083             }
1084             v2 = readShort(&buf); //end marker also needs to be skipped.
1085             Q_UNUSED(v2);
1086         }
1087         if (!skip) {
1088             groups[KoColorSet::GLOBAL_GROUP_NAME].addEntry(e);
1089         }
1090     }
1091     return true;
1092 }
1093 
loadSbz()1094 bool KoColorSet::Private::loadSbz() {
1095     QBuffer buf(&data);
1096     buf.open(QBuffer::ReadOnly);
1097 
1098     // &buf is a subclass of QIODevice
1099     QScopedPointer<KoStore> store(KoStore::createStore(&buf, KoStore::Read, "application/x-swatchbook", KoStore::Zip));
1100     if (!store || store->bad()) return false;
1101 
1102     if (store->hasFile("swatchbook.xml")) { // Try opening...
1103 
1104         if (!store->open("swatchbook.xml")) { return false; }
1105         QByteArray data;
1106         data.resize(store->size());
1107         QByteArray ba = store->read(store->size());
1108         store->close();
1109 
1110         dbgPigment << "XML palette: " << colorSet->filename() << ", SwatchBooker format";
1111 
1112         QDomDocument doc;
1113         int errorLine, errorColumn;
1114         QString errorMessage;
1115         bool status = doc.setContent(ba, &errorMessage, &errorLine, &errorColumn);
1116         if (!status) {
1117             warnPigment << "Illegal XML palette:" << colorSet->filename();
1118             warnPigment << "Error (line" << errorLine << ", column" << errorColumn << "):" << errorMessage;
1119             return false;
1120         }
1121 
1122         QDomElement e = doc.documentElement(); // SwatchBook
1123 
1124         // Start reading properties...
1125         QDomElement metadata = e.firstChildElement("metadata");
1126 
1127         if (e.isNull()) {
1128             warnPigment << "Palette metadata not found";
1129             return false;
1130         }
1131 
1132         QDomElement title = metadata.firstChildElement("dc:title");
1133         QString colorName = title.text();
1134         colorName = colorName.isEmpty() ? i18n("Untitled") : colorName;
1135         colorSet->setName(colorName);
1136         dbgPigment << "Processed name of palette:" << colorSet->name();
1137         // End reading properties
1138 
1139         // Now read colors...
1140         QDomElement materials = e.firstChildElement("materials");
1141         if (materials.isNull()) {
1142             warnPigment << "Materials (color definitions) not found";
1143             return false;
1144         }
1145         // This one has lots of "color" elements
1146         QDomElement colorElement = materials.firstChildElement("color");
1147         if (colorElement.isNull()) {
1148             warnPigment << "Color definitions not found (line" << materials.lineNumber() << ", column" << materials.columnNumber() << ")";
1149             return false;
1150         }
1151 
1152         // Also read the swatch book...
1153         QDomElement book = e.firstChildElement("book");
1154         if (book.isNull()) {
1155             warnPigment << "Palette book (swatch composition) not found (line" << e.lineNumber() << ", column" << e.columnNumber() << ")";
1156             return false;
1157         }
1158         // Which has lots of "swatch"es (todo: support groups)
1159         QDomElement swatch = book.firstChildElement();
1160         if (swatch.isNull()) {
1161             warnPigment << "Swatches/groups definition not found (line" << book.lineNumber() << ", column" << book.columnNumber() << ")";
1162             return false;
1163         }
1164 
1165         // We'll store colors here, and as we process swatches
1166         // we'll add them to the palette
1167         QHash<QString, KisSwatch> materialsBook;
1168         QHash<QString, const KoColorSpace*> fileColorSpaces;
1169 
1170         // Color processing
1171         for(; !colorElement.isNull(); colorElement = colorElement.nextSiblingElement("color"))
1172         {
1173             KisSwatch currentEntry;
1174             // Set if color is spot
1175             currentEntry.setSpotColor(colorElement.attribute("usage") == "spot");
1176 
1177             // <metadata> inside contains id and name
1178             // one or more <values> define the color
1179             QDomElement currentColorMetadata = colorElement.firstChildElement("metadata");
1180             QDomNodeList currentColorValues = colorElement.elementsByTagName("values");
1181             // Get color name
1182             QDomElement colorTitle = currentColorMetadata.firstChildElement("dc:title");
1183             QDomElement colorId = currentColorMetadata.firstChildElement("dc:identifier");
1184             // Is there an id? (we need that at the very least for identifying a color)
1185             if (colorId.text().isEmpty()) {
1186                 warnPigment << "Unidentified color (line" << colorId.lineNumber()<< ", column" << colorId.columnNumber() << ")";
1187                 return false;
1188             }
1189             if (materialsBook.contains(colorId.text())) {
1190                 warnPigment << "Duplicated color definition (line" << colorId.lineNumber()<< ", column" << colorId.columnNumber() << ")";
1191                 return false;
1192             }
1193 
1194             // Get a valid color name
1195             currentEntry.setId(colorId.text());
1196             currentEntry.setName(colorTitle.text().isEmpty() ? colorId.text() : colorTitle.text());
1197 
1198             // Get a valid color definition
1199             if (currentColorValues.isEmpty()) {
1200                 warnPigment << "Color definitions not found (line" << colorElement.lineNumber() << ", column" << colorElement.columnNumber() << ")";
1201                 return false;
1202             }
1203 
1204             bool firstDefinition = false;
1205             const KoColorProfile *srgb = KoColorSpaceRegistry::instance()->rgb8()->profile();
1206             // Priority: Lab, otherwise the first definition found
1207             for(int j = 0; j < currentColorValues.size(); j++) {
1208                 QDomNode colorValue = currentColorValues.at(j);
1209                 QDomElement colorValueE = colorValue.toElement();
1210                 QString model = colorValueE.attribute("model", QString());
1211 
1212                 // sRGB,RGB,HSV,HSL,CMY,CMYK,nCLR: 0 -> 1
1213                 // YIQ: Y 0 -> 1 : IQ -0.5 -> 0.5
1214                 // Lab: L 0 -> 100 : ab -128 -> 127
1215                 // XYZ: 0 -> ~100
1216                 if (model == "Lab") {
1217                     QStringList lab = colorValueE.text().split(" ");
1218                     if (lab.length() != 3) {
1219                         warnPigment << "Invalid Lab color definition (line" << colorValueE.lineNumber() << ", column" << colorValueE.columnNumber() << ")";
1220                     }
1221                     float l = lab.at(0).toFloat(&status);
1222                     float a = lab.at(1).toFloat(&status);
1223                     float b = lab.at(2).toFloat(&status);
1224                     if (!status) {
1225                         warnPigment << "Invalid float definition (line" << colorValueE.lineNumber() << ", column" << colorValueE.columnNumber() << ")";
1226                     }
1227 
1228                     KoColor c(KoColorSpaceRegistry::instance()->colorSpace(LABAColorModelID.id(), Float32BitsColorDepthID.id(), QString()));
1229                     reinterpret_cast<float*>(c.data())[0] = l;
1230                     reinterpret_cast<float*>(c.data())[1] = a;
1231                     reinterpret_cast<float*>(c.data())[2] = b;
1232                     c.setOpacity(OPACITY_OPAQUE_F);
1233                     firstDefinition = true;
1234                     currentEntry.setColor(c);
1235                     break; // Immediately add this one
1236                 }
1237                 else if (model == "sRGB" && !firstDefinition) {
1238                     QStringList rgb = colorValueE.text().split(" ");
1239                     if (rgb.length() != 3) {
1240                         warnPigment << "Invalid sRGB color definition (line" << colorValueE.lineNumber() << ", column" << colorValueE.columnNumber() << ")";
1241                     }
1242                     float r = rgb.at(0).toFloat(&status);
1243                     float g = rgb.at(1).toFloat(&status);
1244                     float b = rgb.at(2).toFloat(&status);
1245                     if (!status) {
1246                         warnPigment << "Invalid float definition (line" << colorValueE.lineNumber() << ", column" << colorValueE.columnNumber() << ")";
1247                     }
1248 
1249                     KoColor c(KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), Float32BitsColorDepthID.id(), srgb));
1250                     reinterpret_cast<float*>(c.data())[0] = r;
1251                     reinterpret_cast<float*>(c.data())[1] = g;
1252                     reinterpret_cast<float*>(c.data())[2] = b;
1253                     c.setOpacity(OPACITY_OPAQUE_F);
1254                     currentEntry.setColor(c);
1255                     firstDefinition = true;
1256                 }
1257                 else if (model == "XYZ" && !firstDefinition) {
1258                     QStringList xyz = colorValueE.text().split(" ");
1259                     if (xyz.length() != 3) {
1260                         warnPigment << "Invalid XYZ color definition (line" << colorValueE.lineNumber() << ", column" << colorValueE.columnNumber() << ")";
1261                     }
1262                     float x = xyz.at(0).toFloat(&status);
1263                     float y = xyz.at(1).toFloat(&status);
1264                     float z = xyz.at(2).toFloat(&status);
1265                     if (!status) {
1266                         warnPigment << "Invalid float definition (line" << colorValueE.lineNumber() << ", column" << colorValueE.columnNumber() << ")";
1267                     }
1268 
1269                     KoColor c(KoColorSpaceRegistry::instance()->colorSpace(XYZAColorModelID.id(), Float32BitsColorDepthID.id(), QString()));
1270                     reinterpret_cast<float*>(c.data())[0] = x;
1271                     reinterpret_cast<float*>(c.data())[1] = y;
1272                     reinterpret_cast<float*>(c.data())[2] = z;
1273                     c.setOpacity(OPACITY_OPAQUE_F);
1274                     currentEntry.setColor(c);
1275                     firstDefinition = true;
1276                 }
1277                 // The following color spaces admit an ICC profile (in SwatchBooker)
1278                 else if (model == "CMYK" && !firstDefinition) {
1279                     QStringList cmyk = colorValueE.text().split(" ");
1280                     if (cmyk.length() != 4) {
1281                         warnPigment << "Invalid CMYK color definition (line" << colorValueE.lineNumber() << ", column" << colorValueE.columnNumber() << ")";
1282                     }
1283                     float c = cmyk.at(0).toFloat(&status);
1284                     float m = cmyk.at(1).toFloat(&status);
1285                     float y = cmyk.at(2).toFloat(&status);
1286                     float k = cmyk.at(3).toFloat(&status);
1287                     if (!status) {
1288                         warnPigment << "Invalid float definition (line" << colorValueE.lineNumber() << ", column" << colorValueE.columnNumber() << ")";
1289                     }
1290 
1291                     QString space = colorValueE.attribute("space");
1292                     const KoColorSpace *colorSpace = KoColorSpaceRegistry::instance()->colorSpace(CMYKAColorModelID.id(), Float32BitsColorDepthID.id(), QString());
1293 
1294                     if (!space.isEmpty()) {
1295                         // Try loading the profile and add it to the registry
1296                         if (!fileColorSpaces.contains(space)) {
1297                             store->enterDirectory("profiles");
1298                             store->open(space);
1299                             QByteArray data;
1300                             data.resize(store->size());
1301                             data = store->read(store->size());
1302                             store->close();
1303 
1304                             const KoColorProfile *profile = KoColorSpaceRegistry::instance()->createColorProfile(CMYKAColorModelID.id(), Float32BitsColorDepthID.id(), data);
1305                             if (profile && profile->valid()) {
1306                                 KoColorSpaceRegistry::instance()->addProfile(profile);
1307                                 colorSpace = KoColorSpaceRegistry::instance()->colorSpace(CMYKAColorModelID.id(), Float32BitsColorDepthID.id(), profile);
1308                                 fileColorSpaces.insert(space, colorSpace);
1309                             }
1310                         }
1311                         else {
1312                             colorSpace = fileColorSpaces.value(space);
1313                         }
1314                     }
1315 
1316                     KoColor color(colorSpace);
1317                     reinterpret_cast<float*>(color.data())[0] = c;
1318                     reinterpret_cast<float*>(color.data())[1] = m;
1319                     reinterpret_cast<float*>(color.data())[2] = y;
1320                     reinterpret_cast<float*>(color.data())[3] = k;
1321                     color.setOpacity(OPACITY_OPAQUE_F);
1322                     currentEntry.setColor(color);
1323                     firstDefinition = true;
1324                 }
1325                 else if (model == "GRAY" && !firstDefinition) {
1326                     QString gray = colorValueE.text();
1327 
1328                     float g = gray.toFloat(&status);
1329                     if (!status) {
1330                         warnPigment << "Invalid float definition (line" << colorValueE.lineNumber() << ", column" << colorValueE.columnNumber() << ")";
1331                     }
1332 
1333                     QString space = colorValueE.attribute("space");
1334                     const KoColorSpace *colorSpace = KoColorSpaceRegistry::instance()->colorSpace(GrayAColorModelID.id(), Float32BitsColorDepthID.id(), QString());
1335 
1336                     if (!space.isEmpty()) {
1337                         // Try loading the profile and add it to the registry
1338                         if (!fileColorSpaces.contains(space)) {
1339                             store->enterDirectory("profiles");
1340                             store->open(space);
1341                             QByteArray data;
1342                             data.resize(store->size());
1343                             data = store->read(store->size());
1344                             store->close();
1345 
1346                             const KoColorProfile *profile = KoColorSpaceRegistry::instance()->createColorProfile(CMYKAColorModelID.id(), Float32BitsColorDepthID.id(), data);
1347                             if (profile && profile->valid()) {
1348                                 KoColorSpaceRegistry::instance()->addProfile(profile);
1349                                 colorSpace = KoColorSpaceRegistry::instance()->colorSpace(CMYKAColorModelID.id(), Float32BitsColorDepthID.id(), profile);
1350                                 fileColorSpaces.insert(space, colorSpace);
1351                             }
1352                         }
1353                         else {
1354                             colorSpace = fileColorSpaces.value(space);
1355                         }
1356                     }
1357 
1358                     KoColor c(colorSpace);
1359                     reinterpret_cast<float*>(c.data())[0] = g;
1360                     c.setOpacity(OPACITY_OPAQUE_F);
1361                     currentEntry.setColor(c);
1362                     firstDefinition = true;
1363                 }
1364                 else if (model == "RGB" && !firstDefinition) {
1365                     QStringList rgb = colorValueE.text().split(" ");
1366                     if (rgb.length() != 3) {
1367                         warnPigment << "Invalid RGB color definition (line" << colorValueE.lineNumber() << ", column" << colorValueE.columnNumber() << ")";
1368                     }
1369                     float r = rgb.at(0).toFloat(&status);
1370                     float g = rgb.at(1).toFloat(&status);
1371                     float b = rgb.at(2).toFloat(&status);
1372                     if (!status) {
1373                         warnPigment << "Invalid float definition (line" << colorValueE.lineNumber() << ", column" << colorValueE.columnNumber() << ")";
1374                     }
1375 
1376                     QString space = colorValueE.attribute("space");
1377                     const KoColorSpace *colorSpace = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), Float32BitsColorDepthID.id(), srgb);
1378 
1379                     if (!space.isEmpty()) {
1380                         // Try loading the profile and add it to the registry
1381                         if (!fileColorSpaces.contains(space)) {
1382                             store->enterDirectory("profiles");
1383                             store->open(space);
1384                             QByteArray data;
1385                             data.resize(store->size());
1386                             data = store->read(store->size());
1387                             store->close();
1388 
1389                             const KoColorProfile *profile = KoColorSpaceRegistry::instance()->createColorProfile(RGBAColorModelID.id(), Float32BitsColorDepthID.id(), data);
1390                             if (profile && profile->valid()) {
1391                                 KoColorSpaceRegistry::instance()->addProfile(profile);
1392                                 colorSpace = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), Float32BitsColorDepthID.id(), profile);
1393                                 fileColorSpaces.insert(space, colorSpace);
1394                             }
1395                         }
1396                         else {
1397                             colorSpace = fileColorSpaces.value(space);
1398                         }
1399                     }
1400 
1401                     KoColor c(colorSpace);
1402                     reinterpret_cast<float*>(c.data())[0] = r;
1403                     reinterpret_cast<float*>(c.data())[1] = g;
1404                     reinterpret_cast<float*>(c.data())[2] = b;
1405                     c.setOpacity(OPACITY_OPAQUE_F);
1406                     currentEntry.setColor(c);
1407                     firstDefinition = true;
1408                 }
1409                 else {
1410                     warnPigment << "Color space not implemented:" << model << "(line" << colorValueE.lineNumber() << ", column "<< colorValueE.columnNumber() << ")";
1411                 }
1412             }
1413             if (firstDefinition) {
1414                 materialsBook.insert(currentEntry.id(), currentEntry);
1415             }
1416             else {
1417                 warnPigment << "No supported color  spaces for the current color (line" << colorElement.lineNumber() << ", column "<< colorElement.columnNumber() << ")";
1418                 return false;
1419             }
1420         }
1421         // End colors
1422         // Now decide which ones will go into the palette
1423 
1424         for(;!swatch.isNull(); swatch = swatch.nextSiblingElement()) {
1425             QString type = swatch.tagName();
1426             if (type.isEmpty() || type.isNull()) {
1427                 warnPigment << "Invalid swatch/group definition (no id) (line" << swatch.lineNumber() << ", column" << swatch.columnNumber() << ")";
1428                 return false;
1429             }
1430             else if (type == "swatch") {
1431                 QString id = swatch.attribute("material");
1432                 if (id.isEmpty() || id.isNull()) {
1433                     warnPigment << "Invalid swatch definition (no material id) (line" << swatch.lineNumber() << ", column" << swatch.columnNumber() << ")";
1434                     return false;
1435                 }
1436                 if (materialsBook.contains(id)) {
1437                     groups[KoColorSet::GLOBAL_GROUP_NAME].addEntry(materialsBook.value(id));
1438                 }
1439                 else {
1440                     warnPigment << "Invalid swatch definition (material not found) (line" << swatch.lineNumber() << ", column" << swatch.columnNumber() << ")";
1441                     return false;
1442                 }
1443             }
1444             else if (type == "group") {
1445                 QDomElement groupMetadata = swatch.firstChildElement("metadata");
1446                 if (groupMetadata.isNull()) {
1447                     warnPigment << "Invalid group definition (missing metadata) (line" << groupMetadata.lineNumber() << ", column" << groupMetadata.columnNumber() << ")";
1448                     return false;
1449                 }
1450                 QDomElement groupTitle = metadata.firstChildElement("dc:title");
1451                 if (groupTitle.isNull()) {
1452                     warnPigment << "Invalid group definition (missing title) (line" << groupTitle.lineNumber() << ", column" << groupTitle.columnNumber() << ")";
1453                     return false;
1454                 }
1455                 QString currentGroupName = groupTitle.text();
1456 
1457                 QDomElement groupSwatch = swatch.firstChildElement("swatch");
1458 
1459                 while(!groupSwatch.isNull()) {
1460                     QString id = groupSwatch.attribute("material");
1461                     if (id.isEmpty() || id.isNull()) {
1462                         warnPigment << "Invalid swatch definition (no material id) (line" << groupSwatch.lineNumber() << ", column" << groupSwatch.columnNumber() << ")";
1463                         return false;
1464                     }
1465                     if (materialsBook.contains(id)) {
1466                         groups[currentGroupName].addEntry(materialsBook.value(id));
1467                     }
1468                     else {
1469                         warnPigment << "Invalid swatch definition (material not found) (line" << groupSwatch.lineNumber() << ", column" << groupSwatch.columnNumber() << ")";
1470                         return false;
1471                     }
1472                     groupSwatch = groupSwatch.nextSiblingElement("swatch");
1473                 }
1474             }
1475         }
1476         // End palette
1477     }
1478 
1479     buf.close();
1480     return true;
1481 }
1482 
loadXml()1483 bool KoColorSet::Private::loadXml() {
1484     bool res = false;
1485 
1486     QXmlStreamReader *xml = new QXmlStreamReader(data);
1487 
1488     if (xml->readNextStartElement()) {
1489         QStringRef paletteId = xml->name();
1490         if (QStringRef::compare(paletteId, "SCRIBUSCOLORS", Qt::CaseInsensitive) == 0) { // Scribus
1491             dbgPigment << "XML palette: " << colorSet->filename() << ", Scribus format";
1492             res = loadScribusXmlPalette(colorSet, xml);
1493         }
1494         else {
1495             // Unknown XML format
1496             xml->raiseError("Unknown XML palette format. Expected SCRIBUSCOLORS, found " + paletteId);
1497         }
1498     }
1499 
1500     // If there is any error (it should be returned through the stream)
1501     if (xml->hasError() || !res) {
1502         warnPigment << "Illegal XML palette:" << colorSet->filename();
1503         warnPigment << "Error (line"<< xml->lineNumber() << ", column" << xml->columnNumber() << "):" << xml->errorString();
1504         return false;
1505     }
1506     else {
1507         dbgPigment << "XML palette parsed successfully:" << colorSet->filename();
1508         return true;
1509     }
1510 }
1511 
saveKpl(QIODevice * dev) const1512 bool KoColorSet::Private::saveKpl(QIODevice *dev) const
1513 {
1514     QScopedPointer<KoStore> store(KoStore::createStore(dev, KoStore::Write, "krita/x-colorset", KoStore::Zip));
1515     if (!store || store->bad()) return false;
1516 
1517     QSet<const KoColorSpace *> colorSpaces;
1518 
1519     {
1520         QDomDocument doc;
1521         QDomElement root = doc.createElement(KPL_PALETTE_TAG);
1522         root.setAttribute(KPL_VERSION_ATTR, "1.0");
1523         root.setAttribute(KPL_PALETTE_NAME_ATTR, colorSet->name());
1524         root.setAttribute(KPL_PALETTE_COMMENT_ATTR, comment);
1525         root.setAttribute(KPL_PALETTE_READONLY_ATTR,
1526                           (colorSet->isEditable() || !colorSet->isGlobal()) ? "false" : "true");
1527         root.setAttribute(KPL_PALETTE_COLUMN_COUNT_ATTR, colorSet->columnCount());
1528         root.setAttribute(KPL_GROUP_ROW_COUNT_ATTR, groups[KoColorSet::GLOBAL_GROUP_NAME].rowCount());
1529 
1530         saveKplGroup(doc, root, colorSet->getGroup(KoColorSet::GLOBAL_GROUP_NAME), colorSpaces);
1531 
1532         for (const QString &groupName : groupNames) {
1533             if (groupName == KoColorSet::GLOBAL_GROUP_NAME) { continue; }
1534             QDomElement gl = doc.createElement(KPL_GROUP_TAG);
1535             gl.setAttribute(KPL_GROUP_NAME_ATTR, groupName);
1536             root.appendChild(gl);
1537             saveKplGroup(doc, gl, colorSet->getGroup(groupName), colorSpaces);
1538         }
1539 
1540         doc.appendChild(root);
1541         if (!store->open("colorset.xml")) { return false; }
1542         QByteArray ba = doc.toByteArray();
1543         if (store->write(ba) != ba.size()) { return false; }
1544         if (!store->close()) { return false; }
1545     }
1546 
1547     QDomDocument doc;
1548     QDomElement profileElement = doc.createElement("Profiles");
1549 
1550     for (const KoColorSpace *colorSpace : colorSpaces) {
1551         QString fn = QFileInfo(colorSpace->profile()->fileName()).fileName();
1552         if (!store->open(fn)) { return false; }
1553         QByteArray profileRawData = colorSpace->profile()->rawData();
1554         if (!store->write(profileRawData)) { return false; }
1555         if (!store->close()) { return false; }
1556         QDomElement el = doc.createElement(KPL_PALETTE_PROFILE_TAG);
1557         el.setAttribute(KPL_PALETTE_FILENAME_ATTR, fn);
1558         el.setAttribute(KPL_PALETTE_NAME_ATTR, colorSpace->profile()->name());
1559         el.setAttribute(KPL_COLOR_MODEL_ID_ATTR, colorSpace->colorModelId().id());
1560         el.setAttribute(KPL_COLOR_DEPTH_ID_ATTR, colorSpace->colorDepthId().id());
1561         profileElement.appendChild(el);
1562 
1563     }
1564     doc.appendChild(profileElement);
1565     if (!store->open("profiles.xml")) { return false; }
1566     QByteArray ba = doc.toByteArray();
1567     if (store->write(ba) != ba.size()) { return false; }
1568     if (!store->close()) { return false; }
1569 
1570     return store->finalize();
1571 }
1572 
saveKplGroup(QDomDocument & doc,QDomElement & groupEle,const KisSwatchGroup * group,QSet<const KoColorSpace * > & colorSetSet) const1573 void KoColorSet::Private::saveKplGroup(QDomDocument &doc,
1574                                        QDomElement &groupEle,
1575                                        const KisSwatchGroup *group,
1576                                        QSet<const KoColorSpace *> &colorSetSet) const
1577 {
1578     groupEle.setAttribute(KPL_GROUP_ROW_COUNT_ATTR, QString::number(group->rowCount()));
1579 
1580     for (const SwatchInfoType &info : group->infoList()) {
1581         const KoColorProfile *profile = info.swatch.color().colorSpace()->profile();
1582         // Only save non-builtin profiles.=
1583         if (!profile->fileName().isEmpty()) {
1584             colorSetSet.insert(info.swatch.color().colorSpace());
1585         }
1586         QDomElement swatchEle = doc.createElement(KPL_SWATCH_TAG);
1587         swatchEle.setAttribute(KPL_SWATCH_NAME_ATTR, info.swatch.name());
1588         swatchEle.setAttribute(KPL_SWATCH_ID_ATTR, info.swatch.id());
1589         swatchEle.setAttribute(KPL_SWATCH_SPOT_ATTR, info.swatch.spotColor() ? "true" : "false");
1590         swatchEle.setAttribute(KPL_SWATCH_BITDEPTH_ATTR, info.swatch.color().colorSpace()->colorDepthId().id());
1591         info.swatch.color().toXML(doc, swatchEle);
1592 
1593         QDomElement positionEle = doc.createElement(KPL_SWATCH_POS_TAG);
1594         positionEle.setAttribute(KPL_SWATCH_ROW_ATTR, info.row);
1595         positionEle.setAttribute(KPL_SWATCH_COL_ATTR, info.column);
1596         swatchEle.appendChild(positionEle);
1597 
1598         groupEle.appendChild(swatchEle);
1599     }
1600 }
1601 
loadKplGroup(const QDomDocument & doc,const QDomElement & parentEle,KisSwatchGroup * group)1602 void KoColorSet::Private::loadKplGroup(const QDomDocument &doc, const QDomElement &parentEle, KisSwatchGroup *group)
1603 {
1604     Q_UNUSED(doc);
1605     if (!parentEle.attribute(KPL_GROUP_ROW_COUNT_ATTR).isNull()) {
1606         group->setRowCount(parentEle.attribute(KPL_GROUP_ROW_COUNT_ATTR).toInt());
1607     }
1608     group->setColumnCount(colorSet->columnCount());
1609 
1610     for (QDomElement swatchEle = parentEle.firstChildElement(KPL_SWATCH_TAG);
1611          !swatchEle.isNull();
1612          swatchEle = swatchEle.nextSiblingElement(KPL_SWATCH_TAG)) {
1613         QString colorDepthId = swatchEle.attribute(KPL_SWATCH_BITDEPTH_ATTR, Integer8BitsColorDepthID.id());
1614         KisSwatch entry;
1615 
1616         entry.setColor(KoColor::fromXML(swatchEle.firstChildElement(), colorDepthId));
1617         entry.setName(swatchEle.attribute(KPL_SWATCH_NAME_ATTR));
1618         entry.setId(swatchEle.attribute(KPL_SWATCH_ID_ATTR));
1619         entry.setSpotColor(swatchEle.attribute(KPL_SWATCH_SPOT_ATTR, "false") == "true" ? true : false);
1620         QDomElement positionEle = swatchEle.firstChildElement(KPL_SWATCH_POS_TAG);
1621         if (!positionEle.isNull()) {
1622             int rowNumber = positionEle.attribute(KPL_SWATCH_ROW_ATTR).toInt();
1623             int columnNumber = positionEle.attribute(KPL_SWATCH_COL_ATTR).toInt();
1624             if (columnNumber < 0 ||
1625                     columnNumber >= colorSet->columnCount() ||
1626                     rowNumber < 0
1627                     ) {
1628                 warnPigment << "Swatch" << entry.name()
1629                             << "of palette" << colorSet->name()
1630                             << "has invalid position.";
1631                 continue;
1632             }
1633             group->setEntry(entry, columnNumber, rowNumber);
1634         } else {
1635             group->addEntry(entry);
1636         }
1637     }
1638 
1639     if (parentEle.attribute(KPL_GROUP_ROW_COUNT_ATTR).isNull()
1640             && group->colorCount() > 0
1641             && group->columnCount() > 0
1642             && (group->colorCount() / (group->columnCount()) + 1) < 20) {
1643         group->setRowCount((group->colorCount() / group->columnCount()) + 1);
1644     }
1645 
1646 }
1647