1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2002-2005 Nadeem Hasan <nhasan@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "pcx_p.h"
9#include "util_p.h"
10
11#include <QColor>
12#include <QDataStream>
13#include <QDebug>
14#include <QImage>
15
16#pragma pack(push, 1)
17class RGB
18{
19public:
20 quint8 r;
21 quint8 g;
22 quint8 b;
23
24 static RGB from(const QRgb color)
25 {
26 RGB c;
27 c.r = qRed(rgb: color);
28 c.g = qGreen(rgb: color);
29 c.b = qBlue(rgb: color);
30 return c;
31 }
32};
33
34class Palette
35{
36public:
37 void setColor(int i, const QRgb color)
38 {
39 RGB &c = rgb[i];
40 c.r = qRed(rgb: color);
41 c.g = qGreen(rgb: color);
42 c.b = qBlue(rgb: color);
43 }
44
45 QRgb color(int i) const
46 {
47 return qRgb(r: rgb[i].r, g: rgb[i].g, b: rgb[i].b);
48 }
49
50 class RGB rgb[16];
51};
52
53class PCXHEADER
54{
55public:
56 PCXHEADER();
57
58 inline int width() const
59 {
60 return (XMax - XMin) + 1;
61 }
62 inline int height() const
63 {
64 return (YMax - YMin) + 1;
65 }
66 inline bool isCompressed() const
67 {
68 return (Encoding == 1);
69 }
70
71 quint8 Manufacturer; // Constant Flag, 10 = ZSoft .pcx
72 quint8 Version; // Version information·
73 // 0 = Version 2.5 of PC Paintbrush·
74 // 2 = Version 2.8 w/palette information·
75 // 3 = Version 2.8 w/o palette information·
76 // 4 = PC Paintbrush for Windows(Plus for
77 // Windows uses Ver 5)·
78 // 5 = Version 3.0 and > of PC Paintbrush
79 // and PC Paintbrush +, includes
80 // Publisher's Paintbrush . Includes
81 // 24-bit .PCX files·
82 quint8 Encoding; // 1 = .PCX run length encoding
83 quint8 Bpp; // Number of bits to represent a pixel
84 // (per Plane) - 1, 2, 4, or 8·
85 quint16 XMin;
86 quint16 YMin;
87 quint16 XMax;
88 quint16 YMax;
89 quint16 HDpi;
90 quint16 YDpi;
91 Palette ColorMap;
92 quint8 Reserved; // Should be set to 0.
93 quint8 NPlanes; // Number of color planes
94 quint16 BytesPerLine; // Number of bytes to allocate for a scanline
95 // plane. MUST be an EVEN number. Do NOT
96 // calculate from Xmax-Xmin.·
97 quint16 PaletteInfo; // How to interpret palette- 1 = Color/BW,
98 // 2 = Grayscale ( ignored in PB IV/ IV + )·
99 quint16 HScreenSize; // Horizontal screen size in pixels. New field
100 // found only in PB IV/IV Plus
101 quint16 VScreenSize; // Vertical screen size in pixels. New field
102 // found only in PB IV/IV Plus
103};
104
105#pragma pack(pop)
106
107static QDataStream &operator>>(QDataStream &s, RGB &rgb)
108{
109 quint8 r;
110 quint8 g;
111 quint8 b;
112
113 s >> r >> g >> b;
114 rgb.r = r;
115 rgb.g = g;
116 rgb.b = b;
117
118 return s;
119}
120
121static QDataStream &operator>>(QDataStream &s, Palette &pal)
122{
123 for (int i = 0; i < 16; ++i) {
124 s >> pal.rgb[i];
125 }
126
127 return s;
128}
129
130static QDataStream &operator>>(QDataStream &s, PCXHEADER &ph)
131{
132 quint8 m;
133 quint8 ver;
134 quint8 enc;
135 quint8 bpp;
136 s >> m >> ver >> enc >> bpp;
137 ph.Manufacturer = m;
138 ph.Version = ver;
139 ph.Encoding = enc;
140 ph.Bpp = bpp;
141 quint16 xmin;
142 quint16 ymin;
143 quint16 xmax;
144 quint16 ymax;
145 s >> xmin >> ymin >> xmax >> ymax;
146 ph.XMin = xmin;
147 ph.YMin = ymin;
148 ph.XMax = xmax;
149 ph.YMax = ymax;
150 quint16 hdpi;
151 quint16 ydpi;
152 s >> hdpi >> ydpi;
153 ph.HDpi = hdpi;
154 ph.YDpi = ydpi;
155 Palette colorMap;
156 quint8 res;
157 quint8 np;
158 s >> colorMap >> res >> np;
159 ph.ColorMap = colorMap;
160 ph.Reserved = res;
161 ph.NPlanes = np;
162 quint16 bytesperline;
163 s >> bytesperline;
164 ph.BytesPerLine = bytesperline;
165 quint16 paletteinfo;
166 s >> paletteinfo;
167 ph.PaletteInfo = paletteinfo;
168 quint16 hscreensize;
169 quint16 vscreensize;
170 s >> hscreensize;
171 ph.HScreenSize = hscreensize;
172 s >> vscreensize;
173 ph.VScreenSize = vscreensize;
174
175 // Skip the rest of the header
176 quint8 byte;
177 for (auto i = 0; i < 54; ++i) {
178 s >> byte;
179 }
180
181 return s;
182}
183
184static QDataStream &operator<<(QDataStream &s, const RGB rgb)
185{
186 s << rgb.r << rgb.g << rgb.b;
187
188 return s;
189}
190
191static QDataStream &operator<<(QDataStream &s, const Palette &pal)
192{
193 for (int i = 0; i < 16; ++i) {
194 s << pal.rgb[i];
195 }
196
197 return s;
198}
199
200static QDataStream &operator<<(QDataStream &s, const PCXHEADER &ph)
201{
202 s << ph.Manufacturer;
203 s << ph.Version;
204 s << ph.Encoding;
205 s << ph.Bpp;
206 s << ph.XMin << ph.YMin << ph.XMax << ph.YMax;
207 s << ph.HDpi << ph.YDpi;
208 s << ph.ColorMap;
209 s << ph.Reserved;
210 s << ph.NPlanes;
211 s << ph.BytesPerLine;
212 s << ph.PaletteInfo;
213 s << ph.HScreenSize;
214 s << ph.VScreenSize;
215
216 quint8 byte = 0;
217 for (int i = 0; i < 54; ++i) {
218 s << byte;
219 }
220
221 return s;
222}
223
224PCXHEADER::PCXHEADER()
225{
226 // Initialize all data to zero
227 QByteArray dummy(128, 0);
228 dummy.fill(c: 0);
229 QDataStream s(&dummy, QIODevice::ReadOnly);
230 s >> *this;
231}
232
233static bool readLine(QDataStream &s, QByteArray &buf, const PCXHEADER &header)
234{
235 quint32 i = 0;
236 quint32 size = buf.size();
237 quint8 byte;
238 quint8 count;
239
240 if (header.isCompressed()) {
241 // Uncompress the image data
242 while (i < size) {
243 count = 1;
244 s >> byte;
245 if (byte > 0xc0) {
246 count = byte - 0xc0;
247 s >> byte;
248 }
249 while (count-- && i < size) {
250 buf[i++] = byte;
251 }
252 }
253 } else {
254 // Image is not compressed (possible?)
255 while (i < size) {
256 s >> byte;
257 buf[i++] = byte;
258 }
259 }
260
261 return (s.status() == QDataStream::Ok);
262}
263
264static bool readImage1(QImage &img, QDataStream &s, const PCXHEADER &header)
265{
266 QByteArray buf(header.BytesPerLine, 0);
267
268 img = imageAlloc(width: header.width(), height: header.height(), format: QImage::Format_Mono);
269 img.setColorCount(2);
270
271 if (img.isNull()) {
272 qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height());
273 return false;
274 }
275
276 for (int y = 0; y < header.height(); ++y) {
277 if (s.atEnd()) {
278 return false;
279 }
280
281 if (!readLine(s, buf, header)) {
282 return false;
283 }
284
285 uchar *p = img.scanLine(y);
286 unsigned int bpl = qMin(a: (quint16)((header.width() + 7) / 8), b: header.BytesPerLine);
287 for (unsigned int x = 0; x < bpl; ++x) {
288 p[x] = buf[x];
289 }
290 }
291
292 // Set the color palette
293 img.setColor(i: 0, c: qRgb(r: 0, g: 0, b: 0));
294 img.setColor(i: 1, c: qRgb(r: 255, g: 255, b: 255));
295
296 return true;
297}
298
299static bool readImage4(QImage &img, QDataStream &s, const PCXHEADER &header)
300{
301 QByteArray buf(header.BytesPerLine * 4, 0);
302 QByteArray pixbuf(header.width(), 0);
303
304 img = imageAlloc(width: header.width(), height: header.height(), format: QImage::Format_Indexed8);
305 img.setColorCount(16);
306 if (img.isNull()) {
307 qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height());
308 return false;
309 }
310
311 for (int y = 0; y < header.height(); ++y) {
312 if (s.atEnd()) {
313 return false;
314 }
315
316 pixbuf.fill(c: 0);
317 if (!readLine(s, buf, header)) {
318 return false;
319 }
320
321 for (int i = 0; i < 4; i++) {
322 quint32 offset = i * header.BytesPerLine;
323 for (int x = 0; x < header.width(); ++x) {
324 if (buf[offset + (x / 8)] & (128 >> (x % 8))) {
325 pixbuf[x] = (int)(pixbuf[x]) + (1 << i);
326 }
327 }
328 }
329
330 uchar *p = img.scanLine(y);
331 if (!p) {
332 qWarning() << "Failed to get scanline for" << y << "might be out of bounds";
333 }
334 for (int x = 0; x < header.width(); ++x) {
335 p[x] = pixbuf[x];
336 }
337 }
338
339 // Read the palette
340 for (int i = 0; i < 16; ++i) {
341 img.setColor(i, c: header.ColorMap.color(i));
342 }
343
344 return true;
345}
346
347static bool readImage8(QImage &img, QDataStream &s, const PCXHEADER &header)
348{
349 QByteArray buf(header.BytesPerLine, 0);
350
351 img = imageAlloc(width: header.width(), height: header.height(), format: QImage::Format_Indexed8);
352 img.setColorCount(256);
353
354 if (img.isNull()) {
355 qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height());
356 return false;
357 }
358
359 for (int y = 0; y < header.height(); ++y) {
360 if (s.atEnd()) {
361 return false;
362 }
363
364 if (!readLine(s, buf, header)) {
365 return false;
366 }
367
368 uchar *p = img.scanLine(y);
369 if (!p) {
370 return false;
371 }
372
373 unsigned int bpl = qMin(a: header.BytesPerLine, b: (quint16)header.width());
374 for (unsigned int x = 0; x < bpl; ++x) {
375 p[x] = buf[x];
376 }
377 }
378
379 // by specification, the extended palette starts at file.size() - 769
380 quint8 flag = 0;
381 if (auto device = s.device()) {
382 if (device->isSequential()) {
383 while (flag != 12 && s.status() == QDataStream::Ok) {
384 s >> flag;
385 }
386 }
387 else {
388 device->seek(pos: device->size() - 769);
389 s >> flag;
390 }
391 }
392
393 // qDebug() << "Palette Flag: " << flag;
394 if (flag == 12 && (header.Version == 5 || header.Version == 2)) {
395 // Read the palette
396 quint8 r;
397 quint8 g;
398 quint8 b;
399 for (int i = 0; i < 256; ++i) {
400 s >> r >> g >> b;
401 img.setColor(i, c: qRgb(r, g, b));
402 }
403 }
404
405 return (s.status() == QDataStream::Ok);
406}
407
408static bool readImage24(QImage &img, QDataStream &s, const PCXHEADER &header)
409{
410 QByteArray r_buf(header.BytesPerLine, 0);
411 QByteArray g_buf(header.BytesPerLine, 0);
412 QByteArray b_buf(header.BytesPerLine, 0);
413
414 img = imageAlloc(width: header.width(), height: header.height(), format: QImage::Format_RGB32);
415
416 if (img.isNull()) {
417 qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height());
418 return false;
419 }
420
421 for (int y = 0; y < header.height(); ++y) {
422 if (s.atEnd()) {
423 return false;
424 }
425
426 if (!readLine(s, buf&: r_buf, header)) {
427 return false;
428 }
429 if (!readLine(s, buf&: g_buf, header)) {
430 return false;
431 }
432 if (!readLine(s, buf&: b_buf, header)) {
433 return false;
434 }
435
436 uint *p = (uint *)img.scanLine(y);
437 for (int x = 0; x < header.width(); ++x) {
438 p[x] = qRgb(r: r_buf[x], g: g_buf[x], b: b_buf[x]);
439 }
440 }
441
442 return true;
443}
444
445static bool writeLine(QDataStream &s, QByteArray &buf)
446{
447 quint32 i = 0;
448 quint32 size = buf.size();
449 quint8 count;
450 quint8 data;
451 char byte;
452
453 while (i < size) {
454 count = 1;
455 byte = buf[i++];
456
457 while ((i < size) && (byte == buf[i]) && (count < 63)) {
458 ++i;
459 ++count;
460 }
461
462 data = byte;
463
464 if (count > 1 || data >= 0xc0) {
465 count |= 0xc0;
466 s << count;
467 }
468
469 s << data;
470 }
471 return (s.status() == QDataStream::Ok);
472}
473
474static bool writeImage1(QImage &img, QDataStream &s, PCXHEADER &header)
475{
476 if (img.format() != QImage::Format_Mono) {
477 img = img.convertToFormat(f: QImage::Format_Mono);
478 }
479 if (img.isNull() || img.colorCount() < 1) {
480 return false;
481 }
482 auto rgb = img.color(i: 0);
483 auto minIsBlack = (qRed(rgb) + qGreen(rgb) + qBlue(rgb)) / 3 < 127;
484
485 header.Bpp = 1;
486 header.NPlanes = 1;
487 header.BytesPerLine = img.bytesPerLine();
488 if (header.BytesPerLine == 0) {
489 return false;
490 }
491
492 s << header;
493
494 QByteArray buf(header.BytesPerLine, 0);
495
496 for (int y = 0; y < header.height(); ++y) {
497 quint8 *p = img.scanLine(y);
498
499 // Invert as QImage uses reverse palette for monochrome images?
500 for (int i = 0; i < header.BytesPerLine; ++i) {
501 buf[i] = minIsBlack ? p[i] : ~p[i];
502 }
503
504 if (!writeLine(s, buf)) {
505 return false;
506 }
507 }
508 return true;
509}
510
511static bool writeImage4(QImage &img, QDataStream &s, PCXHEADER &header)
512{
513 header.Bpp = 1;
514 header.NPlanes = 4;
515 header.BytesPerLine = header.width() / 8;
516 if (header.BytesPerLine == 0) {
517 return false;
518 }
519
520 for (int i = 0; i < 16; ++i) {
521 header.ColorMap.setColor(i, color: img.color(i));
522 }
523
524 s << header;
525
526 QByteArray buf[4];
527
528 for (int i = 0; i < 4; ++i) {
529 buf[i].resize(size: header.BytesPerLine);
530 }
531
532 for (int y = 0; y < header.height(); ++y) {
533 quint8 *p = img.scanLine(y);
534
535 for (int i = 0; i < 4; ++i) {
536 buf[i].fill(c: 0);
537 }
538
539 for (int x = 0; x < header.width(); ++x) {
540 for (int i = 0; i < 4; ++i) {
541 if (*(p + x) & (1 << i)) {
542 buf[i][x / 8] = (int)(buf[i][x / 8]) | 1 << (7 - x % 8);
543 }
544 }
545 }
546
547 for (int i = 0; i < 4; ++i) {
548 if (!writeLine(s, buf&: buf[i])) {
549 return false;
550 }
551 }
552 }
553 return true;
554}
555
556static bool writeImage8(QImage &img, QDataStream &s, PCXHEADER &header)
557{
558 header.Bpp = 8;
559 header.NPlanes = 1;
560 header.BytesPerLine = img.bytesPerLine();
561 if (header.BytesPerLine == 0) {
562 return false;
563 }
564
565 s << header;
566
567 QByteArray buf(header.BytesPerLine, 0);
568
569 for (int y = 0; y < header.height(); ++y) {
570 quint8 *p = img.scanLine(y);
571
572 for (int i = 0; i < header.BytesPerLine; ++i) {
573 buf[i] = p[i];
574 }
575
576 if (!writeLine(s, buf)) {
577 return false;
578 }
579 }
580
581 // Write palette flag
582 quint8 byte = 12;
583 s << byte;
584
585 // Write palette
586 for (int i = 0; i < 256; ++i) {
587 s << RGB::from(color: img.color(i));
588 }
589
590 return (s.status() == QDataStream::Ok);
591}
592
593static bool writeImage24(QImage &img, QDataStream &s, PCXHEADER &header)
594{
595 header.Bpp = 8;
596 header.NPlanes = 3;
597 header.BytesPerLine = header.width();
598 if (header.BytesPerLine == 0) {
599 return false;
600 }
601
602 if (img.format() != QImage::Format_ARGB32 && img.format() != QImage::Format_RGB32) {
603 img = img.convertToFormat(f: QImage::Format_RGB32);
604 }
605 if (img.isNull()) {
606 return false;
607 }
608
609 s << header;
610
611 QByteArray r_buf(header.width(), 0);
612 QByteArray g_buf(header.width(), 0);
613 QByteArray b_buf(header.width(), 0);
614
615 for (int y = 0; y < header.height(); ++y) {
616 auto p = (QRgb*)img.scanLine(y);
617
618 for (int x = 0; x < header.width(); ++x) {
619 QRgb rgb = *p++;
620 r_buf[x] = qRed(rgb);
621 g_buf[x] = qGreen(rgb);
622 b_buf[x] = qBlue(rgb);
623 }
624
625 if (!writeLine(s, buf&: r_buf)) {
626 return false;
627 }
628 if (!writeLine(s, buf&: g_buf)) {
629 return false;
630 }
631 if (!writeLine(s, buf&: b_buf)) {
632 return false;
633 }
634 }
635
636 return true;
637}
638
639PCXHandler::PCXHandler()
640{
641}
642
643bool PCXHandler::canRead() const
644{
645 if (canRead(device: device())) {
646 setFormat("pcx");
647 return true;
648 }
649 return false;
650}
651
652bool PCXHandler::read(QImage *outImage)
653{
654 QDataStream s(device());
655 s.setByteOrder(QDataStream::LittleEndian);
656
657 if (s.device()->size() < 128) {
658 return false;
659 }
660
661 PCXHEADER header;
662
663 s >> header;
664
665 if (header.Manufacturer != 10 || header.BytesPerLine == 0 || s.atEnd()) {
666 return false;
667 }
668
669 auto ok = false;
670 QImage img;
671 if (header.Bpp == 1 && header.NPlanes == 1) {
672 ok = readImage1(img, s, header);
673 } else if (header.Bpp == 1 && header.NPlanes == 4) {
674 ok = readImage4(img, s, header);
675 } else if (header.Bpp == 8 && header.NPlanes == 1) {
676 ok = readImage8(img, s, header);
677 } else if (header.Bpp == 8 && header.NPlanes == 3) {
678 ok = readImage24(img, s, header);
679 }
680
681 if (img.isNull() || !ok) {
682 return false;
683 }
684
685 img.setDotsPerMeterX(qRound(d: header.HDpi / 25.4 * 1000));
686 img.setDotsPerMeterY(qRound(d: header.YDpi / 25.4 * 1000));
687 *outImage = img;
688 return true;
689}
690
691bool PCXHandler::write(const QImage &image)
692{
693 QDataStream s(device());
694 s.setByteOrder(QDataStream::LittleEndian);
695
696 QImage img = image;
697
698 const int w = img.width();
699 const int h = img.height();
700
701 if (w > 65536 || h > 65536) {
702 return false;
703 }
704
705 PCXHEADER header;
706
707 header.Manufacturer = 10;
708 header.Version = 5;
709 header.Encoding = 1;
710 header.XMin = 0;
711 header.YMin = 0;
712 header.XMax = w - 1;
713 header.YMax = h - 1;
714 header.HDpi = qRound(d: image.dotsPerMeterX() * 25.4 / 1000);
715 header.YDpi = qRound(d: image.dotsPerMeterY() * 25.4 / 1000);
716 header.Reserved = 0;
717 header.PaletteInfo = 1;
718
719 auto ok = false;
720 if (img.depth() == 1) {
721 ok = writeImage1(img, s, header);
722 } else if (img.depth() == 8 && img.colorCount() <= 16) {
723 ok = writeImage4(img, s, header);
724 } else if (img.depth() == 8) {
725 ok = writeImage8(img, s, header);
726 } else if (img.depth() >= 24) {
727 ok = writeImage24(img, s, header);
728 }
729
730 return ok;
731}
732
733bool PCXHandler::canRead(QIODevice *device)
734{
735 if (!device) {
736 qWarning(msg: "PCXHandler::canRead() called with no device");
737 return false;
738 }
739
740 qint64 oldPos = device->pos();
741
742 char head[1];
743 qint64 readBytes = device->read(data: head, maxlen: sizeof(head));
744 if (readBytes != sizeof(head)) {
745 if (device->isSequential()) {
746 while (readBytes > 0) {
747 device->ungetChar(c: head[readBytes-- - 1]);
748 }
749 } else {
750 device->seek(pos: oldPos);
751 }
752 return false;
753 }
754
755 if (device->isSequential()) {
756 while (readBytes > 0) {
757 device->ungetChar(c: head[readBytes-- - 1]);
758 }
759 } else {
760 device->seek(pos: oldPos);
761 }
762
763 return qstrncmp(str1: head, str2: "\012", len: 1) == 0;
764}
765
766QImageIOPlugin::Capabilities PCXPlugin::capabilities(QIODevice *device, const QByteArray &format) const
767{
768 if (format == "pcx") {
769 return Capabilities(CanRead | CanWrite);
770 }
771 if (!format.isEmpty()) {
772 return {};
773 }
774 if (!device->isOpen()) {
775 return {};
776 }
777
778 Capabilities cap;
779 if (device->isReadable() && PCXHandler::canRead(device)) {
780 cap |= CanRead;
781 }
782 if (device->isWritable()) {
783 cap |= CanWrite;
784 }
785 return cap;
786}
787
788QImageIOHandler *PCXPlugin::create(QIODevice *device, const QByteArray &format) const
789{
790 QImageIOHandler *handler = new PCXHandler;
791 handler->setDevice(device);
792 handler->setFormat(format);
793 return handler;
794}
795
796#include "moc_pcx_p.cpp"
797

source code of kimageformats/src/imageformats/pcx.cpp