SCT: added read only support

I keep adding old formats to ensure interoperability with as many programs as possible...

This plugin adds read-only support for the Scitex SCT format only. This format is also supported by Photoshop in read and write (SCT test cases were created by Photoshop).

[Scitex HandShake Formats Specifications](/uploads/a3e213e48349d898b260375d6c052521/Scitex_HandShake_Formats.pdf)
This commit is contained in:
Mirco Miranda 2024-11-04 13:05:59 +00:00
parent d233e80dbb
commit 4dedd88c08
13 changed files with 524 additions and 1 deletions

View File

@ -22,6 +22,7 @@ The following image formats have read-only support:
- Portable FloatMap (pfm)
- Photoshop documents (psd, psb, pdd, psdt)
- Radiance HDR (hdr)
- Scitex CT (sct)
- Sun Raster (im1, im8, im24, im32, ras, sun)
The following image formats have read and write support:
@ -127,6 +128,7 @@ plugin ('n/a' means no limit, i.e. the limit depends on the format encoding).
- RAS: n/a (large image)
- RAW: n/a (depends on the RAW format loaded)
- RGB: 65,535 x 65,535 pixels
- SCT: 300,000 x 300,000 pixels
- TGA: 65,535 x 65,535 pixels
- XCF: 300,000 x 300,000 pixels
@ -157,7 +159,7 @@ been used or the maximum size of the image that can be saved has been limited.
PSD plugin loads CMYK, Lab and Multichannel images and converts them to RGB
without using the ICC profile.
JXR and PSD plugins natively support 4-channel CMYK images when compiled
JXR, PSD and SCT plugins natively support 4-channel CMYK images when compiled
with Qt 6.8+.
### The HEIF plugin

View File

@ -72,6 +72,7 @@ kimageformats_read_tests(
qoi
ras
rgb
sct
tga
)

BIN
autotests/read/sct/cmyk.sct Normal file

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"minQtVersion" : "6.8.0",
"fileName" : "cmyk.tif"
},
{
"maxQtVersion" : "6.7.99",
"unsupportedFormat" : true,
"comment" : "Qt versions lower than 6.8 do not support CMYK format so this test should be skipped."
}
]

BIN
autotests/read/sct/cmyk.tif Normal file

Binary file not shown.

BIN
autotests/read/sct/gray.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
autotests/read/sct/gray.sct Normal file

Binary file not shown.

BIN
autotests/read/sct/rgb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
autotests/read/sct/rgb.sct Normal file

Binary file not shown.

View File

@ -107,6 +107,10 @@ kimageformats_add_plugin(kimg_rgb SOURCES rgb.cpp)
##################################
kimageformats_add_plugin(kimg_sct SOURCES scitex.cpp)
##################################
kimageformats_add_plugin(kimg_tga SOURCES tga.cpp)
##################################

459
src/imageformats/scitex.cpp Normal file
View File

@ -0,0 +1,459 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2024 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "scitex_p.h"
#include "scanlineconverter_p.h"
#include "util_p.h"
#include <array>
#include <QtGlobal>
#include <QIODevice>
#include <QBuffer>
#include <QImage>
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN)
Q_LOGGING_CATEGORY(LOG_IFFPLUGIN, "kf.imageformats.plugins.scitex", QtWarningMsg)
#define CTRLBLOCK_SIZE 256
#define PRMSBLOCK_SIZE_CT 256
// For file stored on disk, each block is followed by 768 pads
#define HEADER_SIZE_CT (CTRLBLOCK_SIZE + PRMSBLOCK_SIZE_CT + 768 + 768)
#define FILETYPE_CT "CT"
#define FILETYPE_LW "LW"
#define FILETYPE_BM "BM"
#define FILETYPE_PG "PG"
#define FILETYPE_TX "TX"
class ScitexCtrlBlock
{
using pchar_t = char *;
public:
ScitexCtrlBlock() {}
ScitexCtrlBlock(const ScitexCtrlBlock& other) = default;
ScitexCtrlBlock& operator =(const ScitexCtrlBlock& other) = default;
bool load(QIODevice *device)
{
auto ok = (device && device->isOpen());
ok = ok && device->read(pchar_t(_name.data()), _name.size()) == qint64(_name.size());
ok = ok && device->read(pchar_t(_fileType.data()), _fileType.size()) == qint64(_fileType.size());
ok = ok && device->read(pchar_t(_blockSize.data()), _blockSize.size()) == qint64(_blockSize.size());
ok = ok && device->read(pchar_t(_reserved.data()), _reserved.size()) == qint64(_reserved.size());
ok = ok && device->read(pchar_t(&_count), sizeof(_count)) == qint64(sizeof(_count));
ok = ok && device->read(pchar_t(_padding.data()), _padding.size()) == qint64(_padding.size());
return ok;
}
QString name() const
{
return QString::fromLatin1(pchar_t(_name.data()), _name.size());
}
QString fileType() const
{
return QString::fromLatin1(pchar_t(_fileType.data()), _fileType.size());
}
quint8 sequenceCount() const
{
return _count;
}
std::array<quint8, 80> _name = {};
std::array<quint8, 2> _fileType = {};
std::array<quint8, 12> _blockSize = {};
std::array<quint8, 12> _reserved = {};
quint8 _count = 0;
std::array<quint8, 149> _padding = {};
};
class ScitexParamsBlock
{
using pchar_t = char *;
public:
ScitexParamsBlock() {}
ScitexParamsBlock(const ScitexParamsBlock& other) = default;
ScitexParamsBlock& operator =(const ScitexParamsBlock& other) = default;
bool load(QIODevice *device)
{
auto ok = (device && device->isOpen());
ok = ok && device->read(pchar_t(&_unitsOfMeasurement), sizeof(_unitsOfMeasurement)) == qint64(sizeof(_unitsOfMeasurement));
ok = ok && device->read(pchar_t(&_numOfColorSeparations), sizeof(_numOfColorSeparations)) == qint64(sizeof(_numOfColorSeparations));
ok = ok && device->read(pchar_t(_separationBitMask.data()), _separationBitMask.size()) == qint64(_separationBitMask.size());
ok = ok && device->read(pchar_t(_heightInUnits.data()), _heightInUnits.size()) == qint64(_heightInUnits.size());
ok = ok && device->read(pchar_t(_widthInUnits.data()), _widthInUnits.size()) == qint64(_widthInUnits.size());
ok = ok && device->read(pchar_t(_heightInPixels.data()), _heightInPixels.size()) == qint64(_heightInPixels.size());
ok = ok && device->read(pchar_t(_widthInPixels.data()), _widthInPixels.size()) == qint64(_widthInPixels.size());
ok = ok && device->read(pchar_t(&_scanDirection), sizeof(_scanDirection)) == qint64(sizeof(_scanDirection));
ok = ok && device->read(pchar_t(_reserved.data()), _reserved.size()) == qint64(_reserved.size());
return ok;
}
quint8 colorCount() const
{
return _numOfColorSeparations;
}
quint16 bitMask() const
{
return ((_separationBitMask.at(0) << 8) | _separationBitMask.at(1));
}
quint8 _unitsOfMeasurement = 0;
quint8 _numOfColorSeparations = 0;
std::array<quint8, 2> _separationBitMask = {};
std::array<quint8, 14> _heightInUnits = {};
std::array<quint8, 14> _widthInUnits = {};
std::array<quint8, 12> _heightInPixels = {};
std::array<quint8, 12> _widthInPixels = {};
quint8 _scanDirection = 0;
std::array<quint8, 199> _reserved = {};
};
class ScitexHandlerPrivate
{
using pchar_t = char *;
public:
ScitexHandlerPrivate()
{
}
~ScitexHandlerPrivate()
{
}
/*!
* \brief isSupported
* \return If the plugin can load it.
*/
bool isSupported() const
{
if (!isValid()) {
return false;
}
// Set a reasonable upper limit
if (width() > 300000 || height() > 300000) {
return false;
}
return m_cb.fileType() == QStringLiteral(FILETYPE_CT) && format() != QImage::Format_Invalid;
}
/*!
* \brief isValid
* \return True if is a valid Scitex image file.
*/
bool isValid() const
{
if (width() == 0 || height() == 0) {
return false;
}
QStringList ft = {
QStringLiteral(FILETYPE_CT),
QStringLiteral(FILETYPE_LW),
QStringLiteral(FILETYPE_BM),
QStringLiteral(FILETYPE_PG),
QStringLiteral(FILETYPE_TX)
};
return ft.contains(m_cb.fileType());
}
QImage::Format format() const
{
auto format = QImage::Format_Invalid;
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
if (m_pb.colorCount() == 4) {
if (m_pb.bitMask() == 15)
format = QImage::Format_CMYK8888;
}
#endif
if (m_pb.colorCount() == 3) {
if (m_pb.bitMask() == 7)
format = QImage::Format_RGB888;
}
if (m_pb.colorCount() == 1) {
if (m_pb.bitMask() == 8)
format = QImage::Format_Grayscale8;
}
return format;
}
quint32 width() const
{
auto ok = false;
auto&& px = m_pb._widthInPixels;
auto v = QString::fromLatin1(pchar_t(px.data()), px.size()).toUInt(&ok);
return ok ? v : 0;
}
quint32 height() const
{
auto ok = false;
auto&& px = m_pb._heightInPixels;
auto v = QString::fromLatin1(pchar_t(px.data()), px.size()).toUInt(&ok);
return ok ? v : 0;
}
qint32 dotsPerMeterX() const {
auto ok = false;
auto&& res = m_pb._widthInUnits;
auto v = QString::fromLatin1(pchar_t(res.data()), res.size()).toDouble(&ok);
if (ok && v > 0) {
if (m_pb._unitsOfMeasurement) { // Inches
return qRound(width() / v / 25.4 * 1000);
}
// Millimeters
return qRound(width() / v * 1000);
}
return 0;
}
qint32 dotsPerMeterY() const {
auto ok = false;
auto&& res = m_pb._heightInUnits;
auto v = QString::fromLatin1(pchar_t(res.data()), res.size()).toDouble(&ok);
if (ok && v > 0) {
if (m_pb._unitsOfMeasurement) { // Inches
return qRound(width() / v / 25.4 * 1000);
}
// Millimeters
return qRound(width() / v * 1000);
}
return 0;
}
QImageIOHandler::Transformation transformation() const
{
auto t = QImageIOHandler::TransformationNone;
switch (m_pb._scanDirection) {
case 1:
t = QImageIOHandler::TransformationFlip;
break;
case 2:
t = QImageIOHandler::TransformationMirror;
break;
case 3:
t = QImageIOHandler::TransformationRotate180;
break;
case 4:
t = QImageIOHandler::TransformationFlipAndRotate90;
break;
case 5:
t = QImageIOHandler::TransformationRotate270;
break;
case 6:
t = QImageIOHandler::TransformationRotate90;
break;
case 7:
t = QImageIOHandler::TransformationMirrorAndRotate90;
break;
default:
t = QImageIOHandler::TransformationNone;
break;
}
return t;
}
bool peekHeader(QIODevice *device)
{
if (device == nullptr) {
return false;
}
auto ba = device->peek(HEADER_SIZE_CT);
if (ba.size() != HEADER_SIZE_CT) {
return false;
}
QBuffer b;
b.setData(ba);
if (!b.open(QIODevice::ReadOnly)) {
return false;
}
return loadHeader(&b);
}
bool loadHeader(QIODevice *device) {
if (device == nullptr) {
return false;
}
if (!m_cb.load(device)) {
return false;
}
auto pad1 = device->read(768);
if (pad1.size() != 768) {
return false;
}
if (!m_pb.load(device)) {
return false;
}
auto pad2 = device->read(768);
if (pad2.size() != 768) {
return false;
}
return true;
}
ScitexCtrlBlock m_cb;
ScitexParamsBlock m_pb;
};
ScitexHandler::ScitexHandler()
: QImageIOHandler()
, d(new ScitexHandlerPrivate)
{
}
bool ScitexHandler::canRead() const
{
if (canRead(device())) {
setFormat("sct");
return true;
}
return false;
}
bool ScitexHandler::canRead(QIODevice *device)
{
if (!device) {
qWarning("ScitexHandler::canRead() called with no device");
return false;
}
ScitexHandlerPrivate hp;
if (hp.peekHeader(device)) {
return hp.isSupported();
}
return false;
}
bool ScitexHandler::read(QImage *image)
{
auto dev = device();
if (dev == nullptr) {
qWarning("ScitexHandler::read() called with no device");
return false;
}
if (!d->loadHeader(dev)) {
return false;
}
if (!d->isSupported()) {
return false;
}
auto img = imageAlloc(d->width(), d->height(), d->format());
if (img.isNull()) {
return false;
}
auto hres = d->dotsPerMeterX();
if (hres > 0) {
img.setDotsPerMeterX(hres);
}
auto vres = d->dotsPerMeterY();
if (vres > 0) {
img.setDotsPerMeterY(vres);
}
QByteArray line(img.width() * d->m_pb.colorCount(), char());
if (img.bytesPerLine() < line.size()) {
return false;
}
for (qint32 y = 0, h = img.height(); y < h; ++y) {
if (dev->read(line.data(), line.size()) != line.size()) {
return false;
}
auto scanLine = img.scanLine(y);
for (qint32 c = 0, cc = d->m_pb.colorCount(); c < cc; ++c) {
for (qint32 x = 0, w = img.width(); x < w; ++x) {
scanLine[x * cc + c] = cc == 4 ? uchar(255) - uchar(line.at(c * w + x)) : uchar(line.at(c * w + x));
}
}
}
*image = img;
return true;
}
bool ScitexHandler::supportsOption(ImageOption option) const
{
if (option == QImageIOHandler::Size) {
return true;
}
if (option == QImageIOHandler::ImageFormat) {
return true;
}
if (option == QImageIOHandler::ImageTransformation) {
return true;
}
return false;
}
QVariant ScitexHandler::option(ImageOption option) const
{
QVariant v;
if (option == QImageIOHandler::Size) {
if (!d->isValid()) {
d->peekHeader(device());
}
if (d->isSupported()) {
v = QSize(d->width(), d->height());
}
}
if (option == QImageIOHandler::ImageFormat) {
if (!d->isValid()) {
d->peekHeader(device());
}
if (d->isSupported()) {
v = d->format();
}
}
if (option == QImageIOHandler::ImageTransformation) {
if (!d->isValid()) {
d->peekHeader(device());
}
if (d->isSupported()) {
v = int(d->transformation());
}
}
return v;
}
QImageIOPlugin::Capabilities ScitexPlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
if (format == "sct") {
return Capabilities(CanRead);
}
if (!format.isEmpty()) {
return {};
}
if (!device->isOpen()) {
return {};
}
Capabilities cap;
if (device->isReadable() && ScitexHandler::canRead(device)) {
cap |= CanRead;
}
return cap;
}
QImageIOHandler *ScitexPlugin::create(QIODevice *device, const QByteArray &format) const
{
QImageIOHandler *handler = new ScitexHandler;
handler->setDevice(device);
handler->setFormat(format);
return handler;
}

View File

@ -0,0 +1,4 @@
{
"Keys": [ "sct" ],
"MimeTypes": [ "image/x-sct" ]
}

View File

@ -0,0 +1,42 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2024 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef SIMG_SCITEX_P_H
#define SIMG_SCITEX_P_H
#include <QImageIOPlugin>
#include <QScopedPointer>
class ScitexHandlerPrivate;
class ScitexHandler : public QImageIOHandler
{
public:
ScitexHandler();
bool canRead() const override;
bool read(QImage *image) override;
bool supportsOption(QImageIOHandler::ImageOption option) const override;
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device);
private:
const QScopedPointer<ScitexHandlerPrivate> d;
};
class ScitexPlugin : public QImageIOPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "scitex.json")
public:
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
};
#endif // SIMG_SCITEX_P_H