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 <QColorSpace> |
13 | #include <QDataStream> |
14 | #include <QDebug> |
15 | #include <QImage> |
16 | |
17 | #pragma pack(push, 1) |
18 | class RGB |
19 | { |
20 | public: |
21 | quint8 r; |
22 | quint8 g; |
23 | quint8 b; |
24 | |
25 | static RGB from(const QRgb color) |
26 | { |
27 | RGB c; |
28 | c.r = qRed(rgb: color); |
29 | c.g = qGreen(rgb: color); |
30 | c.b = qBlue(rgb: color); |
31 | return c; |
32 | } |
33 | }; |
34 | |
35 | class Palette |
36 | { |
37 | public: |
38 | void setColor(int i, const QRgb color) |
39 | { |
40 | RGB &c = rgb[i]; |
41 | c.r = qRed(rgb: color); |
42 | c.g = qGreen(rgb: color); |
43 | c.b = qBlue(rgb: color); |
44 | } |
45 | |
46 | QRgb color(int i) const |
47 | { |
48 | return qRgb(r: rgb[i].r, g: rgb[i].g, b: rgb[i].b); |
49 | } |
50 | |
51 | class RGB rgb[16]; |
52 | }; |
53 | |
54 | class |
55 | { |
56 | public: |
57 | PCXHEADER(); |
58 | |
59 | inline int () const |
60 | { |
61 | return (XMax - XMin) + 1; |
62 | } |
63 | inline int () const |
64 | { |
65 | return (YMax - YMin) + 1; |
66 | } |
67 | inline bool () const |
68 | { |
69 | return (Encoding == 1); |
70 | } |
71 | /*! |
72 | * \brief isValid |
73 | * Checks if the header data are valid for the PCX. |
74 | * \note Put here the header sanity checks. |
75 | * \return True if the header is a valid PCX header, otherwise false. |
76 | */ |
77 | inline bool () const |
78 | { |
79 | return Manufacturer == 10 && BytesPerLine != 0; |
80 | } |
81 | /*! |
82 | * \brief isSupported |
83 | * \return True if the header is valid and the PCX format is supported by the plugin. Otherwise false. |
84 | */ |
85 | inline bool () const |
86 | { |
87 | return isValid() && format() != QImage::Format_Invalid; |
88 | } |
89 | inline QImage::Format () const |
90 | { |
91 | auto fmt = QImage::Format_Invalid; |
92 | if (Bpp == 1 && NPlanes == 1) { |
93 | fmt = QImage::Format_Mono; |
94 | } else if (Bpp == 1 && NPlanes == 4) { |
95 | fmt = QImage::Format_Indexed8; |
96 | } else if (Bpp == 1 && NPlanes == 3) { |
97 | fmt = QImage::Format_Indexed8; |
98 | } else if (Bpp == 4 && NPlanes == 1) { |
99 | fmt = QImage::Format_Indexed8; |
100 | } else if (Bpp == 2 && NPlanes == 1) { |
101 | fmt = QImage::Format_Indexed8; |
102 | } else if (Bpp == 8 && NPlanes == 1) { |
103 | fmt = QImage::Format_Indexed8; |
104 | } else if (Bpp == 8 && NPlanes == 3) { |
105 | fmt = QImage::Format_RGB32; |
106 | } else if (Bpp == 8 && NPlanes == 4) { |
107 | fmt = QImage::Format_ARGB32; |
108 | } |
109 | return fmt; |
110 | } |
111 | |
112 | quint8 ; // Constant Flag, 10 = ZSoft .pcx |
113 | quint8 ; // Version information· |
114 | // 0 = Version 2.5 of PC Paintbrush· |
115 | // 2 = Version 2.8 w/palette information· |
116 | // 3 = Version 2.8 w/o palette information· |
117 | // 4 = PC Paintbrush for Windows(Plus for |
118 | // Windows uses Ver 5)· |
119 | // 5 = Version 3.0 and > of PC Paintbrush |
120 | // and PC Paintbrush +, includes |
121 | // Publisher's Paintbrush . Includes |
122 | // 24-bit .PCX files· |
123 | quint8 ; // 1 = .PCX run length encoding |
124 | quint8 ; // Number of bits to represent a pixel |
125 | // (per Plane) - 1, 2, 4, or 8· |
126 | quint16 ; |
127 | quint16 ; |
128 | quint16 ; |
129 | quint16 ; |
130 | quint16 ; |
131 | quint16 ; |
132 | Palette ; |
133 | quint8 ; // Should be set to 0. |
134 | quint8 ; // Number of color planes |
135 | quint16 ; // Number of bytes to allocate for a scanline |
136 | // plane. MUST be an EVEN number. Do NOT |
137 | // calculate from Xmax-Xmin.· |
138 | quint16 ; // How to interpret palette- 1 = Color/BW, |
139 | // 2 = Grayscale ( ignored in PB IV/ IV + )· |
140 | quint16 ; // Horizontal screen size in pixels. New field |
141 | // found only in PB IV/IV Plus |
142 | quint16 ; // Vertical screen size in pixels. New field |
143 | // found only in PB IV/IV Plus |
144 | |
145 | quint8 [54]; |
146 | }; |
147 | |
148 | #pragma pack(pop) |
149 | |
150 | static QDataStream &operator>>(QDataStream &s, RGB &rgb) |
151 | { |
152 | quint8 r; |
153 | quint8 g; |
154 | quint8 b; |
155 | |
156 | s >> r >> g >> b; |
157 | rgb.r = r; |
158 | rgb.g = g; |
159 | rgb.b = b; |
160 | |
161 | return s; |
162 | } |
163 | |
164 | static QDataStream &operator>>(QDataStream &s, Palette &pal) |
165 | { |
166 | for (int i = 0; i < 16; ++i) { |
167 | s >> pal.rgb[i]; |
168 | } |
169 | |
170 | return s; |
171 | } |
172 | |
173 | static QDataStream &(QDataStream &s, PCXHEADER &ph) |
174 | { |
175 | quint8 m; |
176 | quint8 ver; |
177 | quint8 enc; |
178 | quint8 bpp; |
179 | s >> m >> ver >> enc >> bpp; |
180 | ph.Manufacturer = m; |
181 | ph.Version = ver; |
182 | ph.Encoding = enc; |
183 | ph.Bpp = bpp; |
184 | quint16 xmin; |
185 | quint16 ymin; |
186 | quint16 xmax; |
187 | quint16 ymax; |
188 | s >> xmin >> ymin >> xmax >> ymax; |
189 | ph.XMin = xmin; |
190 | ph.YMin = ymin; |
191 | ph.XMax = xmax; |
192 | ph.YMax = ymax; |
193 | quint16 hdpi; |
194 | quint16 ydpi; |
195 | s >> hdpi >> ydpi; |
196 | ph.HDpi = hdpi; |
197 | ph.YDpi = ydpi; |
198 | Palette colorMap; |
199 | quint8 res; |
200 | quint8 np; |
201 | s >> colorMap >> res >> np; |
202 | ph.ColorMap = colorMap; |
203 | ph.Reserved = res; |
204 | ph.NPlanes = np; |
205 | quint16 bytesperline; |
206 | s >> bytesperline; |
207 | ph.BytesPerLine = bytesperline; |
208 | quint16 paletteinfo; |
209 | s >> paletteinfo; |
210 | ph.PaletteInfo = paletteinfo; |
211 | quint16 hscreensize; |
212 | quint16 vscreensize; |
213 | s >> hscreensize; |
214 | ph.HScreenSize = hscreensize; |
215 | s >> vscreensize; |
216 | ph.VScreenSize = vscreensize; |
217 | |
218 | // Skip the rest of the header |
219 | for (size_t i = 0, n = sizeof(ph.unused); i < n; ++i) { |
220 | s >> ph.unused[i]; |
221 | } |
222 | |
223 | return s; |
224 | } |
225 | |
226 | static QDataStream &operator<<(QDataStream &s, const RGB rgb) |
227 | { |
228 | s << rgb.r << rgb.g << rgb.b; |
229 | |
230 | return s; |
231 | } |
232 | |
233 | static QDataStream &operator<<(QDataStream &s, const Palette &pal) |
234 | { |
235 | for (int i = 0; i < 16; ++i) { |
236 | s << pal.rgb[i]; |
237 | } |
238 | |
239 | return s; |
240 | } |
241 | |
242 | static QDataStream &(QDataStream &s, const PCXHEADER &ph) |
243 | { |
244 | s << ph.Manufacturer; |
245 | s << ph.Version; |
246 | s << ph.Encoding; |
247 | s << ph.Bpp; |
248 | s << ph.XMin << ph.YMin << ph.XMax << ph.YMax; |
249 | s << ph.HDpi << ph.YDpi; |
250 | s << ph.ColorMap; |
251 | s << ph.Reserved; |
252 | s << ph.NPlanes; |
253 | s << ph.BytesPerLine; |
254 | s << ph.PaletteInfo; |
255 | s << ph.HScreenSize; |
256 | s << ph.VScreenSize; |
257 | |
258 | for (size_t i = 0, n = sizeof(ph.unused); i < n; ++i) { |
259 | s << ph.unused[i]; |
260 | } |
261 | |
262 | return s; |
263 | } |
264 | |
265 | PCXHEADER::() |
266 | { |
267 | // Initialize all data to zero |
268 | QByteArray dummy(128, 0); |
269 | dummy.fill(c: 0); |
270 | QDataStream s(&dummy, QIODevice::ReadOnly); |
271 | s >> *this; |
272 | } |
273 | |
274 | bool (QIODevice *d, PCXHEADER& h) |
275 | { |
276 | auto head = d->peek(maxlen: sizeof(PCXHEADER)); |
277 | if (size_t(head.size()) < sizeof(PCXHEADER)) { |
278 | return false; |
279 | } |
280 | |
281 | QDataStream ds(head); |
282 | ds.setByteOrder(QDataStream::LittleEndian); |
283 | ds >> h; |
284 | |
285 | return ds.status() == QDataStream::Ok && h.isValid(); |
286 | } |
287 | |
288 | static bool (QDataStream &s, QByteArray &buf, const PCXHEADER &) |
289 | { |
290 | quint32 i = 0; |
291 | quint32 size = buf.size(); |
292 | quint8 byte; |
293 | quint8 count; |
294 | |
295 | if (header.isCompressed()) { |
296 | // Uncompress the image data |
297 | while (i < size) { |
298 | count = 1; |
299 | s >> byte; |
300 | if (byte > 0xc0) { |
301 | count = byte - 0xc0; |
302 | s >> byte; |
303 | } |
304 | while (count-- && i < size) { |
305 | buf[i++] = byte; |
306 | } |
307 | } |
308 | } else { |
309 | // Image is not compressed (possible?) |
310 | while (i < size) { |
311 | s >> byte; |
312 | buf[i++] = byte; |
313 | } |
314 | } |
315 | |
316 | return (s.status() == QDataStream::Ok); |
317 | } |
318 | |
319 | static bool (QImage &img, QDataStream &s, const PCXHEADER &) |
320 | { |
321 | QByteArray buf(header.BytesPerLine, 0); |
322 | |
323 | img = imageAlloc(width: header.width(), height: header.height(), format: header.format()); |
324 | img.setColorCount(2); |
325 | |
326 | if (img.isNull()) { |
327 | qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height()); |
328 | return false; |
329 | } |
330 | |
331 | for (int y = 0; y < header.height(); ++y) { |
332 | if (s.atEnd()) { |
333 | return false; |
334 | } |
335 | |
336 | if (!readLine(s, buf, header)) { |
337 | return false; |
338 | } |
339 | |
340 | uchar *p = img.scanLine(y); |
341 | unsigned int bpl = qMin(a: (quint16)((header.width() + 7) / 8), b: header.BytesPerLine); |
342 | for (unsigned int x = 0; x < bpl; ++x) { |
343 | p[x] = buf[x]; |
344 | } |
345 | } |
346 | |
347 | // Set the color palette |
348 | img.setColor(i: 0, c: qRgb(r: 0, g: 0, b: 0)); |
349 | img.setColor(i: 1, c: qRgb(r: 255, g: 255, b: 255)); |
350 | |
351 | return true; |
352 | } |
353 | |
354 | static bool (QImage &img, QDataStream &s, const PCXHEADER &) |
355 | { |
356 | QByteArray buf(header.BytesPerLine * header.NPlanes, 0); |
357 | QByteArray pixbuf(header.width(), 0); |
358 | |
359 | img = imageAlloc(width: header.width(), height: header.height(), format: header.format()); |
360 | img.setColorCount(16); |
361 | if (img.isNull()) { |
362 | qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height()); |
363 | return false; |
364 | } |
365 | |
366 | if (header.BytesPerLine < (header.width() + 7) / 8) { |
367 | qWarning() << "PCX image has invalid BytesPerLine value" ; |
368 | return false; |
369 | } |
370 | |
371 | for (int y = 0; y < header.height(); ++y) { |
372 | if (s.atEnd()) { |
373 | return false; |
374 | } |
375 | |
376 | pixbuf.fill(c: 0); |
377 | if (!readLine(s, buf, header)) { |
378 | return false; |
379 | } |
380 | |
381 | for (int i = 0; i < header.NPlanes; i++) { |
382 | quint32 offset = i * header.BytesPerLine; |
383 | for (int x = 0; x < header.width(); ++x) { |
384 | if (buf[offset + (x / 8)] & (128 >> (x % 8))) { |
385 | pixbuf[x] = (int)(pixbuf[x]) + (1 << i); |
386 | } |
387 | } |
388 | } |
389 | |
390 | uchar *p = img.scanLine(y); |
391 | if (!p) { |
392 | qWarning() << "Failed to get scanline for" << y << "might be out of bounds" ; |
393 | } |
394 | for (int x = 0; x < header.width(); ++x) { |
395 | p[x] = pixbuf[x]; |
396 | } |
397 | } |
398 | |
399 | // Read the palette |
400 | for (int i = 0; i < 16; ++i) { |
401 | img.setColor(i, c: header.ColorMap.color(i)); |
402 | } |
403 | |
404 | return true; |
405 | } |
406 | |
407 | static bool (QImage &img, QDataStream &s, const PCXHEADER &) |
408 | { |
409 | QByteArray buf(header.BytesPerLine, 0); |
410 | |
411 | img = imageAlloc(width: header.width(), height: header.height(), format: header.format()); |
412 | img.setColorCount(4); |
413 | |
414 | if (img.isNull()) { |
415 | qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height()); |
416 | return false; |
417 | } |
418 | |
419 | for (int y = 0; y < header.height(); ++y) { |
420 | if (s.atEnd()) { |
421 | return false; |
422 | } |
423 | |
424 | if (!readLine(s, buf, header)) { |
425 | return false; |
426 | } |
427 | |
428 | uchar *p = img.scanLine(y); |
429 | if (!p) { |
430 | return false; |
431 | } |
432 | |
433 | const unsigned int bpl = std::min(a: header.BytesPerLine, b: static_cast<quint16>(header.width() / 4)); |
434 | for (unsigned int x = 0; x < bpl; ++x) { |
435 | p[x * 4] = (buf[x] >> 6) & 3; |
436 | p[x * 4 + 1] = (buf[x] >> 4) & 3; |
437 | p[x * 4 + 2] = (buf[x] >> 2) & 3; |
438 | p[x * 4 + 3] = buf[x] & 3; |
439 | } |
440 | } |
441 | |
442 | // Read the palette |
443 | for (int i = 0; i < 4; ++i) { |
444 | img.setColor(i, c: header.ColorMap.color(i)); |
445 | } |
446 | |
447 | return (s.status() == QDataStream::Ok); |
448 | } |
449 | |
450 | static bool (QImage &img, QDataStream &s, const PCXHEADER &) |
451 | { |
452 | QByteArray buf(header.BytesPerLine, 0); |
453 | |
454 | img = imageAlloc(width: header.width(), height: header.height(), format: header.format()); |
455 | img.setColorCount(16); |
456 | |
457 | if (img.isNull()) { |
458 | qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height()); |
459 | return false; |
460 | } |
461 | |
462 | for (int y = 0; y < header.height(); ++y) { |
463 | if (s.atEnd()) { |
464 | return false; |
465 | } |
466 | |
467 | if (!readLine(s, buf, header)) { |
468 | return false; |
469 | } |
470 | |
471 | uchar *p = img.scanLine(y); |
472 | if (!p) { |
473 | return false; |
474 | } |
475 | |
476 | const unsigned int bpl = std::min(a: header.BytesPerLine, b: static_cast<quint16>(header.width() / 2)); |
477 | for (unsigned int x = 0; x < bpl; ++x) { |
478 | p[x * 2] = (buf[x] & 240) >> 4; |
479 | p[x * 2 + 1] = buf[x] & 15; |
480 | } |
481 | } |
482 | |
483 | // Read the palette |
484 | for (int i = 0; i < 16; ++i) { |
485 | img.setColor(i, c: header.ColorMap.color(i)); |
486 | } |
487 | |
488 | return (s.status() == QDataStream::Ok); |
489 | } |
490 | |
491 | static bool (QImage &img, QDataStream &s, const PCXHEADER &) |
492 | { |
493 | QByteArray buf(header.BytesPerLine, 0); |
494 | |
495 | img = imageAlloc(width: header.width(), height: header.height(), format: header.format()); |
496 | img.setColorCount(256); |
497 | |
498 | if (img.isNull()) { |
499 | qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height()); |
500 | return false; |
501 | } |
502 | |
503 | for (int y = 0; y < header.height(); ++y) { |
504 | if (s.atEnd()) { |
505 | return false; |
506 | } |
507 | |
508 | if (!readLine(s, buf, header)) { |
509 | return false; |
510 | } |
511 | |
512 | uchar *p = img.scanLine(y); |
513 | if (!p) { |
514 | return false; |
515 | } |
516 | |
517 | unsigned int bpl = qMin(a: header.BytesPerLine, b: (quint16)header.width()); |
518 | for (unsigned int x = 0; x < bpl; ++x) { |
519 | p[x] = buf[x]; |
520 | } |
521 | } |
522 | |
523 | // by specification, the extended palette starts at file.size() - 769 |
524 | quint8 flag = 0; |
525 | if (auto device = s.device()) { |
526 | if (device->isSequential()) { |
527 | while (flag != 12 && s.status() == QDataStream::Ok) { |
528 | s >> flag; |
529 | } |
530 | } |
531 | else { |
532 | device->seek(pos: device->size() - 769); |
533 | s >> flag; |
534 | } |
535 | } |
536 | |
537 | // qDebug() << "Palette Flag: " << flag; |
538 | if (flag == 12 && (header.Version == 5 || header.Version == 2)) { |
539 | // Read the palette |
540 | quint8 r; |
541 | quint8 g; |
542 | quint8 b; |
543 | for (int i = 0; i < 256; ++i) { |
544 | s >> r >> g >> b; |
545 | img.setColor(i, c: qRgb(r, g, b)); |
546 | } |
547 | } |
548 | |
549 | return (s.status() == QDataStream::Ok); |
550 | } |
551 | |
552 | static bool (QImage &img, QDataStream &s, const PCXHEADER &) |
553 | { |
554 | QByteArray r_buf(header.BytesPerLine, 0); |
555 | QByteArray g_buf(header.BytesPerLine, 0); |
556 | QByteArray b_buf(header.BytesPerLine, 0); |
557 | QByteArray a_buf(header.BytesPerLine, char(0xFF)); |
558 | |
559 | img = imageAlloc(width: header.width(), height: header.height(), format: header.format()); |
560 | |
561 | if (img.isNull()) { |
562 | qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width(), header.height()); |
563 | return false; |
564 | } |
565 | |
566 | const unsigned int bpl = std::min(a: header.BytesPerLine, b: static_cast<quint16>(header.width())); |
567 | |
568 | for (int y = 0; y < header.height(); ++y) { |
569 | if (s.atEnd()) { |
570 | return false; |
571 | } |
572 | |
573 | if (!readLine(s, buf&: r_buf, header)) { |
574 | return false; |
575 | } |
576 | if (!readLine(s, buf&: g_buf, header)) { |
577 | return false; |
578 | } |
579 | if (!readLine(s, buf&: b_buf, header)) { |
580 | return false; |
581 | } |
582 | if (header.NPlanes == 4 && !readLine(s, buf&: a_buf, header)) { |
583 | return false; |
584 | } |
585 | |
586 | auto p = reinterpret_cast<QRgb *>(img.scanLine(y)); |
587 | for (unsigned int x = 0; x < bpl; ++x) { |
588 | p[x] = qRgba(r: r_buf[x], g: g_buf[x], b: b_buf[x], a: a_buf[x]); |
589 | } |
590 | } |
591 | |
592 | return true; |
593 | } |
594 | |
595 | static bool writeLine(QDataStream &s, QByteArray &buf) |
596 | { |
597 | quint32 i = 0; |
598 | quint32 size = buf.size(); |
599 | quint8 count; |
600 | quint8 data; |
601 | char byte; |
602 | |
603 | while (i < size) { |
604 | count = 1; |
605 | byte = buf[i++]; |
606 | |
607 | while ((i < size) && (byte == buf[i]) && (count < 63)) { |
608 | ++i; |
609 | ++count; |
610 | } |
611 | |
612 | data = byte; |
613 | |
614 | if (count > 1 || data >= 0xc0) { |
615 | count |= 0xc0; |
616 | s << count; |
617 | } |
618 | |
619 | s << data; |
620 | } |
621 | return (s.status() == QDataStream::Ok); |
622 | } |
623 | |
624 | static bool (QImage &img, QDataStream &s, PCXHEADER &) |
625 | { |
626 | if (img.format() != QImage::Format_Mono) { |
627 | img.convertTo(f: QImage::Format_Mono); |
628 | } |
629 | if (img.isNull() || img.colorCount() < 1) { |
630 | return false; |
631 | } |
632 | auto rgb = img.color(i: 0); |
633 | auto minIsBlack = (qRed(rgb) + qGreen(rgb) + qBlue(rgb)) / 3 < 127; |
634 | |
635 | header.Bpp = 1; |
636 | header.NPlanes = 1; |
637 | header.BytesPerLine = img.bytesPerLine(); |
638 | if (header.BytesPerLine == 0) { |
639 | return false; |
640 | } |
641 | |
642 | s << header; |
643 | |
644 | QByteArray buf(header.BytesPerLine, 0); |
645 | |
646 | for (int y = 0; y < header.height(); ++y) { |
647 | auto p = img.constScanLine(y); |
648 | |
649 | // Invert as QImage uses reverse palette for monochrome images? |
650 | for (int i = 0; i < header.BytesPerLine; ++i) { |
651 | buf[i] = minIsBlack ? p[i] : ~p[i]; |
652 | } |
653 | |
654 | if (!writeLine(s, buf)) { |
655 | return false; |
656 | } |
657 | } |
658 | return true; |
659 | } |
660 | |
661 | static bool (QImage &img, QDataStream &s, PCXHEADER &) |
662 | { |
663 | header.Bpp = 1; |
664 | header.NPlanes = 4; |
665 | header.BytesPerLine = header.width() / 8; |
666 | if (header.BytesPerLine == 0) { |
667 | return false; |
668 | } |
669 | |
670 | for (int i = 0; i < 16; ++i) { |
671 | header.ColorMap.setColor(i, color: img.color(i)); |
672 | } |
673 | |
674 | s << header; |
675 | |
676 | QByteArray buf[4]; |
677 | |
678 | for (int i = 0; i < 4; ++i) { |
679 | buf[i].resize(size: header.BytesPerLine); |
680 | } |
681 | |
682 | for (int y = 0; y < header.height(); ++y) { |
683 | auto p = img.constScanLine(y); |
684 | |
685 | for (int i = 0; i < 4; ++i) { |
686 | buf[i].fill(c: 0); |
687 | } |
688 | |
689 | for (int x = 0; x < header.width(); ++x) { |
690 | for (int i = 0; i < 4; ++i) { |
691 | if (*(p + x) & (1 << i)) { |
692 | buf[i][x / 8] = (int)(buf[i][x / 8]) | 1 << (7 - x % 8); |
693 | } |
694 | } |
695 | } |
696 | |
697 | for (int i = 0; i < 4; ++i) { |
698 | if (!writeLine(s, buf&: buf[i])) { |
699 | return false; |
700 | } |
701 | } |
702 | } |
703 | return true; |
704 | } |
705 | |
706 | static bool (QImage &img, QDataStream &s, PCXHEADER &) |
707 | { |
708 | if (img.format() == QImage::Format_Grayscale16) { |
709 | img.convertTo(f: QImage::Format_Grayscale8); |
710 | } |
711 | if (img.isNull()) { |
712 | return false; |
713 | } |
714 | |
715 | header.Bpp = 8; |
716 | header.NPlanes = 1; |
717 | header.BytesPerLine = img.bytesPerLine(); |
718 | if (header.BytesPerLine == 0) { |
719 | return false; |
720 | } |
721 | |
722 | s << header; |
723 | |
724 | QByteArray buf(header.BytesPerLine, 0); |
725 | |
726 | for (int y = 0; y < header.height(); ++y) { |
727 | auto p = img.constScanLine(y); |
728 | |
729 | for (int i = 0; i < header.BytesPerLine; ++i) { |
730 | buf[i] = p[i]; |
731 | } |
732 | |
733 | if (!writeLine(s, buf)) { |
734 | return false; |
735 | } |
736 | } |
737 | |
738 | // Write palette flag |
739 | quint8 byte = 12; |
740 | s << byte; |
741 | |
742 | // Write palette |
743 | for (int i = 0; i < 256; ++i) { |
744 | if (img.format() != QImage::Format_Indexed8) |
745 | s << RGB::from(color: qRgb(r: i, g: i, b: i)); |
746 | else |
747 | s << RGB::from(color: img.color(i)); |
748 | } |
749 | |
750 | return (s.status() == QDataStream::Ok); |
751 | } |
752 | |
753 | static bool (QImage &img, QDataStream &s, PCXHEADER &) |
754 | { |
755 | auto hasAlpha = img.hasAlphaChannel(); |
756 | header.Bpp = 8; |
757 | header.NPlanes = hasAlpha ? 4 : 3; |
758 | header.BytesPerLine = header.width(); |
759 | if (header.BytesPerLine == 0) { |
760 | return false; |
761 | } |
762 | |
763 | if (img.format() != QImage::Format_ARGB32 && img.format() != QImage::Format_RGB32) { |
764 | img.convertTo(f: hasAlpha ? QImage::Format_ARGB32 : QImage::Format_RGB32); |
765 | } |
766 | if (img.isNull()) { |
767 | return false; |
768 | } |
769 | |
770 | s << header; |
771 | |
772 | QByteArray r_buf(header.width(), 0); |
773 | QByteArray g_buf(header.width(), 0); |
774 | QByteArray b_buf(header.width(), 0); |
775 | QByteArray a_buf(header.width(), char(0xFF)); |
776 | |
777 | for (int y = 0; y < header.height(); ++y) { |
778 | auto p = reinterpret_cast<const QRgb *>(img.constScanLine(y)); |
779 | |
780 | for (int x = 0; x < header.width(); ++x) { |
781 | auto &&rgb = p[x]; |
782 | r_buf[x] = qRed(rgb); |
783 | g_buf[x] = qGreen(rgb); |
784 | b_buf[x] = qBlue(rgb); |
785 | a_buf[x] = qAlpha(rgb); |
786 | } |
787 | |
788 | if (!writeLine(s, buf&: r_buf)) { |
789 | return false; |
790 | } |
791 | if (!writeLine(s, buf&: g_buf)) { |
792 | return false; |
793 | } |
794 | if (!writeLine(s, buf&: b_buf)) { |
795 | return false; |
796 | } |
797 | if (hasAlpha && !writeLine(s, buf&: a_buf)) { |
798 | return false; |
799 | } |
800 | } |
801 | |
802 | return true; |
803 | } |
804 | |
805 | class PCXHandlerPrivate |
806 | { |
807 | public: |
808 | PCXHandlerPrivate() {} |
809 | ~PCXHandlerPrivate() {} |
810 | |
811 | PCXHEADER m_header; |
812 | }; |
813 | |
814 | PCXHandler::PCXHandler() |
815 | : QImageIOHandler() |
816 | , d(new PCXHandlerPrivate) |
817 | { |
818 | } |
819 | |
820 | bool PCXHandler::canRead() const |
821 | { |
822 | if (canRead(device: device())) { |
823 | setFormat("pcx" ); |
824 | return true; |
825 | } |
826 | return false; |
827 | } |
828 | |
829 | bool PCXHandler::read(QImage *outImage) |
830 | { |
831 | QDataStream s(device()); |
832 | s.setByteOrder(QDataStream::LittleEndian); |
833 | |
834 | if (s.device()->size() < 128) { |
835 | return false; |
836 | } |
837 | |
838 | auto&& = d->m_header; |
839 | s >> header; |
840 | |
841 | if (s.status() != QDataStream::Ok || s.atEnd()) { |
842 | return false; |
843 | } |
844 | |
845 | if (!header.isSupported()) { |
846 | return false; |
847 | } |
848 | |
849 | auto ok = false; |
850 | QImage img; |
851 | if (header.Bpp == 1 && header.NPlanes == 1) { |
852 | ok = readImage1(img, s, header); |
853 | } else if (header.Bpp == 1 && (header.NPlanes == 4 || header.NPlanes == 3)) { |
854 | ok = readImage4(img, s, header); |
855 | } else if (header.Bpp == 2 && header.NPlanes == 1) { |
856 | ok = readImage2(img, s, header); |
857 | } else if (header.Bpp == 4 && header.NPlanes == 1) { |
858 | ok = readImage4v2(img, s, header); |
859 | } else if (header.Bpp == 8 && header.NPlanes == 1) { |
860 | ok = readImage8(img, s, header); |
861 | } else if (header.Bpp == 8 && (header.NPlanes == 3 || header.NPlanes == 4)) { |
862 | ok = readImage24(img, s, header); |
863 | } |
864 | |
865 | if (img.isNull() || !ok) { |
866 | return false; |
867 | } |
868 | |
869 | img.setDotsPerMeterX(qRound(d: header.HDpi / 25.4 * 1000)); |
870 | img.setDotsPerMeterY(qRound(d: header.YDpi / 25.4 * 1000)); |
871 | *outImage = img; |
872 | return true; |
873 | } |
874 | |
875 | bool PCXHandler::write(const QImage &image) |
876 | { |
877 | QDataStream s(device()); |
878 | s.setByteOrder(QDataStream::LittleEndian); |
879 | |
880 | QImage img = image; |
881 | #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) |
882 | auto cs = image.colorSpace(); |
883 | if (cs.isValid() && cs.colorModel() == QColorSpace::ColorModel::Cmyk && image.format() == QImage::Format_CMYK8888) { |
884 | img = image.convertedToColorSpace(colorSpace: QColorSpace(QColorSpace::SRgb)); |
885 | } |
886 | #endif |
887 | |
888 | const int w = img.width(); |
889 | const int h = img.height(); |
890 | |
891 | if (w > 65536 || h > 65536) { |
892 | return false; |
893 | } |
894 | |
895 | PCXHEADER ; |
896 | |
897 | header.Manufacturer = 10; |
898 | header.Version = 5; |
899 | header.Encoding = 1; |
900 | header.XMin = 0; |
901 | header.YMin = 0; |
902 | header.XMax = w - 1; |
903 | header.YMax = h - 1; |
904 | header.HDpi = qRound(d: image.dotsPerMeterX() * 25.4 / 1000); |
905 | header.YDpi = qRound(d: image.dotsPerMeterY() * 25.4 / 1000); |
906 | header.Reserved = 0; |
907 | header.PaletteInfo = 1; |
908 | |
909 | auto ok = false; |
910 | if (img.depth() == 1) { |
911 | ok = writeImage1(img, s, header); |
912 | } else if (img.format() == QImage::Format_Indexed8 && img.colorCount() <= 16) { |
913 | ok = writeImage4(img, s, header); |
914 | } else if (img.depth() == 8 || img.format() == QImage::Format_Grayscale16) { |
915 | ok = writeImage8(img, s, header); |
916 | } else if (img.depth() >= 16) { |
917 | ok = writeImage24(img, s, header); |
918 | } |
919 | |
920 | return ok; |
921 | } |
922 | |
923 | bool PCXHandler::supportsOption(ImageOption option) const |
924 | { |
925 | if (option == QImageIOHandler::Size) { |
926 | return true; |
927 | } |
928 | if (option == QImageIOHandler::ImageFormat) { |
929 | return true; |
930 | } |
931 | return false; |
932 | } |
933 | |
934 | QVariant PCXHandler::option(ImageOption option) const |
935 | { |
936 | QVariant v; |
937 | |
938 | if (option == QImageIOHandler::Size) { |
939 | auto&& = d->m_header; |
940 | if (header.isSupported()) { |
941 | v = QVariant::fromValue(value: QSize(header.width(), header.height())); |
942 | } else if (auto dev = device()) { |
943 | if (peekHeader(d: dev, h&: header) && header.isSupported()) { |
944 | v = QVariant::fromValue(value: QSize(header.width(), header.height())); |
945 | } |
946 | } |
947 | } |
948 | |
949 | if (option == QImageIOHandler::ImageFormat) { |
950 | auto&& = d->m_header; |
951 | if (header.isSupported()) { |
952 | v = QVariant::fromValue(value: header.format()); |
953 | } else if (auto dev = device()) { |
954 | if (peekHeader(d: dev, h&: header) && header.isSupported()) { |
955 | v = QVariant::fromValue(value: header.format()); |
956 | } |
957 | } |
958 | } |
959 | |
960 | return v; |
961 | } |
962 | |
963 | bool PCXHandler::canRead(QIODevice *device) |
964 | { |
965 | if (!device) { |
966 | qWarning(msg: "PCXHandler::canRead() called with no device" ); |
967 | return false; |
968 | } |
969 | |
970 | PCXHEADER ; |
971 | if (!peekHeader(d: device, h&: header)) { |
972 | return false; |
973 | } |
974 | return header.isSupported(); |
975 | } |
976 | |
977 | QImageIOPlugin::Capabilities PCXPlugin::capabilities(QIODevice *device, const QByteArray &format) const |
978 | { |
979 | if (format == "pcx" ) { |
980 | return Capabilities(CanRead | CanWrite); |
981 | } |
982 | if (!format.isEmpty()) { |
983 | return {}; |
984 | } |
985 | if (!device->isOpen()) { |
986 | return {}; |
987 | } |
988 | |
989 | Capabilities cap; |
990 | if (device->isReadable() && PCXHandler::canRead(device)) { |
991 | cap |= CanRead; |
992 | } |
993 | if (device->isWritable()) { |
994 | cap |= CanWrite; |
995 | } |
996 | return cap; |
997 | } |
998 | |
999 | QImageIOHandler *PCXPlugin::create(QIODevice *device, const QByteArray &format) const |
1000 | { |
1001 | QImageIOHandler *handler = new PCXHandler; |
1002 | handler->setDevice(device); |
1003 | handler->setFormat(format); |
1004 | return handler; |
1005 | } |
1006 | |
1007 | #include "moc_pcx_p.cpp" |
1008 | |