1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2024 Mirco Miranda <mircomir@outlook.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "sct_p.h"
9#include "scanlineconverter_p.h"
10#include "util_p.h"
11
12#include <array>
13
14#include <QtGlobal>
15#include <QIODevice>
16#include <QBuffer>
17#include <QImage>
18#include <QLoggingCategory>
19
20Q_LOGGING_CATEGORY(LOG_SCTPLUGIN, "kf.imageformats.plugins.scitex", QtWarningMsg)
21
22/* *** SCT_MAX_IMAGE_WIDTH and SCT_MAX_IMAGE_HEIGHT ***
23 * The maximum size in pixel allowed by the plugin.
24 */
25#ifndef SCT_MAX_IMAGE_WIDTH
26#define SCT_MAX_IMAGE_WIDTH KIF_LARGE_IMAGE_PIXEL_LIMIT
27#endif
28#ifndef SCT_MAX_IMAGE_HEIGHT
29#define SCT_MAX_IMAGE_HEIGHT SCT_MAX_IMAGE_WIDTH
30#endif
31
32#define CTRLBLOCK_SIZE 256
33#define PRMSBLOCK_SIZE_CT 256
34
35// For file stored on disk, each block is followed by 768 pads
36#define HEADER_SIZE_CT (CTRLBLOCK_SIZE + PRMSBLOCK_SIZE_CT + 768 + 768)
37
38#define FILETYPE_CT "CT"
39#define FILETYPE_LW "LW"
40#define FILETYPE_BM "BM"
41#define FILETYPE_PG "PG"
42#define FILETYPE_TX "TX"
43
44class ScitexCtrlBlock
45{
46 using pchar_t = char *;
47public:
48 ScitexCtrlBlock() {}
49 ScitexCtrlBlock(const ScitexCtrlBlock& other) = default;
50 ScitexCtrlBlock& operator =(const ScitexCtrlBlock& other) = default;
51
52 bool load(QIODevice *device)
53 {
54 auto ok = (device && device->isOpen());
55 ok = ok && device->read(data: pchar_t(_name.data()), maxlen: _name.size()) == qint64(_name.size());
56 ok = ok && device->read(data: pchar_t(_fileType.data()), maxlen: _fileType.size()) == qint64(_fileType.size());
57 ok = ok && device->read(data: pchar_t(_blockSize.data()), maxlen: _blockSize.size()) == qint64(_blockSize.size());
58 ok = ok && device->read(data: pchar_t(_reserved.data()), maxlen: _reserved.size()) == qint64(_reserved.size());
59 ok = ok && device->read(data: pchar_t(&_count), maxlen: sizeof(_count)) == qint64(sizeof(_count));
60 ok = ok && device->read(data: pchar_t(_padding.data()), maxlen: _padding.size()) == qint64(_padding.size());
61 return ok;
62 }
63
64 QString name() const
65 {
66 return QString::fromLatin1(str: pchar_t(_name.data()), size: _name.size());
67 }
68
69 QString fileType() const
70 {
71 return QString::fromLatin1(str: pchar_t(_fileType.data()), size: _fileType.size());
72 }
73
74 quint8 sequenceCount() const
75 {
76 return _count;
77 }
78
79 std::array<quint8, 80> _name = {};
80 std::array<quint8, 2> _fileType = {};
81 std::array<quint8, 12> _blockSize = {};
82 std::array<quint8, 12> _reserved = {};
83 quint8 _count = 0;
84 std::array<quint8, 149> _padding = {};
85};
86
87class ScitexParamsBlock
88{
89 using pchar_t = char *;
90public:
91 ScitexParamsBlock() {}
92 ScitexParamsBlock(const ScitexParamsBlock& other) = default;
93 ScitexParamsBlock& operator =(const ScitexParamsBlock& other) = default;
94
95 bool load(QIODevice *device)
96 {
97 auto ok = (device && device->isOpen());
98 ok = ok && device->read(data: pchar_t(&_unitsOfMeasurement), maxlen: sizeof(_unitsOfMeasurement)) == qint64(sizeof(_unitsOfMeasurement));
99 ok = ok && device->read(data: pchar_t(&_numOfColorSeparations), maxlen: sizeof(_numOfColorSeparations)) == qint64(sizeof(_numOfColorSeparations));
100 ok = ok && device->read(data: pchar_t(_separationBitMask.data()), maxlen: _separationBitMask.size()) == qint64(_separationBitMask.size());
101 ok = ok && device->read(data: pchar_t(_heightInUnits.data()), maxlen: _heightInUnits.size()) == qint64(_heightInUnits.size());
102 ok = ok && device->read(data: pchar_t(_widthInUnits.data()), maxlen: _widthInUnits.size()) == qint64(_widthInUnits.size());
103 ok = ok && device->read(data: pchar_t(_heightInPixels.data()), maxlen: _heightInPixels.size()) == qint64(_heightInPixels.size());
104 ok = ok && device->read(data: pchar_t(_widthInPixels.data()), maxlen: _widthInPixels.size()) == qint64(_widthInPixels.size());
105 ok = ok && device->read(data: pchar_t(&_scanDirection), maxlen: sizeof(_scanDirection)) == qint64(sizeof(_scanDirection));
106 ok = ok && device->read(data: pchar_t(_reserved.data()), maxlen: _reserved.size()) == qint64(_reserved.size());
107 return ok;
108 }
109
110 quint8 colorCount() const
111 {
112 return _numOfColorSeparations;
113 }
114
115 quint16 bitMask() const
116 {
117 return ((_separationBitMask.at(n: 0) << 8) | _separationBitMask.at(n: 1));
118 }
119
120 quint8 _unitsOfMeasurement = 0;
121 quint8 _numOfColorSeparations = 0;
122 std::array<quint8, 2> _separationBitMask = {};
123 std::array<quint8, 14> _heightInUnits = {};
124 std::array<quint8, 14> _widthInUnits = {};
125 std::array<quint8, 12> _heightInPixels = {};
126 std::array<quint8, 12> _widthInPixels = {};
127 quint8 _scanDirection = 0;
128 std::array<quint8, 199> _reserved = {};
129};
130
131class ScitexHandlerPrivate
132{
133 using pchar_t = char *;
134public:
135 ScitexHandlerPrivate()
136 {
137 }
138 ~ScitexHandlerPrivate()
139 {
140 }
141
142 /*!
143 * \brief isSupported
144 * \return If the plugin can load it.
145 */
146 bool isSupported() const
147 {
148 if (!isValid()) {
149 return false;
150 }
151 // Set a reasonable upper limit
152 if (width() > SCT_MAX_IMAGE_WIDTH || height() > SCT_MAX_IMAGE_HEIGHT) {
153 return false;
154 }
155 return m_cb.fileType() == QStringLiteral(FILETYPE_CT) && format() != QImage::Format_Invalid;
156 }
157
158 /*!
159 * \brief isValid
160 * \return True if is a valid Scitex image file.
161 */
162 bool isValid() const
163 {
164 if (width() == 0 || height() == 0) {
165 return false;
166 }
167 QStringList ft = {
168 QStringLiteral(FILETYPE_CT),
169 QStringLiteral(FILETYPE_LW),
170 QStringLiteral(FILETYPE_BM),
171 QStringLiteral(FILETYPE_PG),
172 QStringLiteral(FILETYPE_TX)
173 };
174 return ft.contains(str: m_cb.fileType());
175 }
176
177 QImage::Format format() const
178 {
179 auto format = QImage::Format_Invalid;
180 if (m_pb.colorCount() == 4) {
181 if (m_pb.bitMask() == 15)
182 format = QImage::Format_CMYK8888;
183 }
184 if (m_pb.colorCount() == 3) {
185 if (m_pb.bitMask() == 7)
186 format = QImage::Format_RGB888;
187 }
188 if (m_pb.colorCount() == 1) {
189 if (m_pb.bitMask() == 8)
190 format = QImage::Format_Grayscale8;
191 }
192 return format;
193 }
194
195 quint32 width() const
196 {
197 auto ok = false;
198 auto&& px = m_pb._widthInPixels;
199 auto v = QString::fromLatin1(str: pchar_t(px.data()), size: px.size()).toUInt(ok: &ok);
200 return ok ? v : 0;
201 }
202
203 quint32 height() const
204 {
205 auto ok = false;
206 auto&& px = m_pb._heightInPixels;
207 auto v = QString::fromLatin1(str: pchar_t(px.data()), size: px.size()).toUInt(ok: &ok);
208 return ok ? v : 0;
209 }
210
211 qint32 dotsPerMeterX() const {
212 auto ok = false;
213 auto&& res = m_pb._widthInUnits;
214 auto v = QString::fromLatin1(str: pchar_t(res.data()), size: res.size()).toDouble(ok: &ok);
215 if (ok && v > 0) {
216 if (m_pb._unitsOfMeasurement) { // Inches
217 return dpi2ppm(dpi: width() / v);
218 }
219 // Millimeters
220 return qRoundOrZero(d: width() / v * 1000);
221 }
222 return 0;
223 }
224
225 qint32 dotsPerMeterY() const {
226 auto ok = false;
227 auto&& res = m_pb._heightInUnits;
228 auto v = QString::fromLatin1(str: pchar_t(res.data()), size: res.size()).toDouble(ok: &ok);
229 if (ok && v > 0) {
230 if (m_pb._unitsOfMeasurement) { // Inches
231 return dpi2ppm(dpi: height() / v);
232 }
233 // Millimeters
234 return qRoundOrZero(d: height() / v * 1000);
235 }
236 return 0;
237 }
238
239 QImageIOHandler::Transformation transformation() const
240 {
241 auto t = QImageIOHandler::TransformationNone;
242 switch (m_pb._scanDirection) {
243 case 1:
244 t = QImageIOHandler::TransformationFlip;
245 break;
246 case 2:
247 t = QImageIOHandler::TransformationMirror;
248 break;
249 case 3:
250 t = QImageIOHandler::TransformationRotate180;
251 break;
252 case 4:
253 t = QImageIOHandler::TransformationFlipAndRotate90;
254 break;
255 case 5:
256 t = QImageIOHandler::TransformationRotate270;
257 break;
258 case 6:
259 t = QImageIOHandler::TransformationRotate90;
260 break;
261 case 7:
262 t = QImageIOHandler::TransformationMirrorAndRotate90;
263 break;
264 default:
265 t = QImageIOHandler::TransformationNone;
266 break;
267 }
268 return t;
269 }
270
271 bool peekHeader(QIODevice *device)
272 {
273 if (device == nullptr) {
274 return false;
275 }
276 auto ba = device->peek(HEADER_SIZE_CT);
277 if (ba.size() != HEADER_SIZE_CT) {
278 return false;
279 }
280 QBuffer b;
281 b.setData(ba);
282 if (!b.open(openMode: QIODevice::ReadOnly)) {
283 return false;
284 }
285 return loadHeader(device: &b);
286 }
287
288 bool loadHeader(QIODevice *device) {
289 if (device == nullptr) {
290 return false;
291 }
292 if (!m_cb.load(device)) {
293 return false;
294 }
295 auto pad1 = device->read(maxlen: 768);
296 if (pad1.size() != 768) {
297 return false;
298 }
299 if (!m_pb.load(device)) {
300 return false;
301 }
302 auto pad2 = device->read(maxlen: 768);
303 if (pad2.size() != 768) {
304 return false;
305 }
306 return true;
307 }
308
309 ScitexCtrlBlock m_cb;
310 ScitexParamsBlock m_pb;
311};
312
313ScitexHandler::ScitexHandler()
314 : QImageIOHandler()
315 , d(new ScitexHandlerPrivate)
316{
317}
318
319bool ScitexHandler::canRead() const
320{
321 if (canRead(device: device())) {
322 setFormat("sct");
323 return true;
324 }
325 return false;
326}
327
328bool ScitexHandler::canRead(QIODevice *device)
329{
330 if (!device) {
331 qCWarning(LOG_SCTPLUGIN) << "ScitexHandler::canRead() called with no device";
332 return false;
333 }
334 ScitexHandlerPrivate hp;
335 if (hp.peekHeader(device)) {
336 return hp.isSupported();
337 }
338 return false;
339}
340
341bool ScitexHandler::read(QImage *image)
342{
343 auto dev = device();
344 if (dev == nullptr) {
345 qCWarning(LOG_SCTPLUGIN) << "ScitexHandler::read() called with no device";
346 return false;
347 }
348 if (!d->loadHeader(device: dev)) {
349 return false;
350 }
351 if (!d->isSupported()) {
352 return false;
353 }
354
355 auto img = imageAlloc(width: d->width(), height: d->height(), format: d->format());
356 if (img.isNull()) {
357 return false;
358 }
359
360 auto hres = d->dotsPerMeterX();
361 if (hres > 0) {
362 img.setDotsPerMeterX(hres);
363 }
364 auto vres = d->dotsPerMeterY();
365 if (vres > 0) {
366 img.setDotsPerMeterY(vres);
367 }
368
369 QByteArray line(img.width() * d->m_pb.colorCount(), char());
370 if (img.bytesPerLine() < line.size()) {
371 return false;
372 }
373 for (qint32 y = 0, h = img.height(); y < h; ++y) {
374 if (dev->read(data: line.data(), maxlen: line.size()) != line.size()) {
375 return false;
376 }
377 auto scanLine = img.scanLine(y);
378 for (qint32 c = 0, cc = d->m_pb.colorCount(); c < cc; ++c) {
379 for (qint32 x = 0, w = img.width(); x < w; ++x) {
380 scanLine[x * cc + c] = cc == 4 ? uchar(255) - uchar(line.at(i: c * w + x)) : uchar(line.at(i: c * w + x));
381 }
382 }
383 }
384
385 *image = img;
386 return true;
387}
388
389bool ScitexHandler::supportsOption(ImageOption option) const
390{
391 if (option == QImageIOHandler::Size) {
392 return true;
393 }
394
395 if (option == QImageIOHandler::ImageFormat) {
396 return true;
397 }
398
399 if (option == QImageIOHandler::ImageTransformation) {
400 return true;
401 }
402
403 return false;
404}
405
406QVariant ScitexHandler::option(ImageOption option) const
407{
408 QVariant v;
409
410 if (option == QImageIOHandler::Size) {
411 if (!d->isValid()) {
412 d->peekHeader(device: device());
413 }
414 if (d->isSupported()) {
415 v = QSize(d->width(), d->height());
416 }
417 }
418
419 if (option == QImageIOHandler::ImageFormat) {
420 if (!d->isValid()) {
421 d->peekHeader(device: device());
422 }
423 if (d->isSupported()) {
424 v = d->format();
425 }
426 }
427
428 if (option == QImageIOHandler::ImageTransformation) {
429 if (!d->isValid()) {
430 d->peekHeader(device: device());
431 }
432 if (d->isSupported()) {
433 v = int(d->transformation());
434 }
435 }
436
437 return v;
438}
439
440QImageIOPlugin::Capabilities ScitexPlugin::capabilities(QIODevice *device, const QByteArray &format) const
441{
442 if (format == "sct") {
443 return Capabilities(CanRead);
444 }
445 if (!format.isEmpty()) {
446 return {};
447 }
448 if (!device->isOpen()) {
449 return {};
450 }
451
452 Capabilities cap;
453 if (device->isReadable() && ScitexHandler::canRead(device)) {
454 cap |= CanRead;
455 }
456 return cap;
457}
458
459QImageIOHandler *ScitexPlugin::create(QIODevice *device, const QByteArray &format) const
460{
461 QImageIOHandler *handler = new ScitexHandler;
462 handler->setDevice(device);
463 handler->setFormat(format);
464 return handler;
465}
466
467#include "moc_sct_p.cpp"
468

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