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