blob: 9733ed7b6bda70ea97a4d24300e6dd6ea3bbfe1b [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Copyright (C) 2016 Alex Char.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qicnshandler_p.h"
#include <QtCore/qmath.h>
#include <QtCore/qendian.h>
#include <QtCore/qregularexpression.h>
#include <QtCore/qbuffer.h>
#include <QtGui/qimage.h>
#ifndef QT_NO_DATASTREAM
QT_BEGIN_NAMESPACE
static const quint8 ICNSBlockHeaderSize = 8;
static const QRgb ICNSColorTableMono[] = {
qRgb(0xFF, 0xFF, 0xFF),
qRgb(0x00, 0x00, 0x00)
};
Q_STATIC_ASSERT(sizeof(ICNSColorTableMono) / sizeof(ICNSColorTableMono[0]) == (1 << ICNSEntry::DepthMono));
static const QRgb ICNSColorTable4bit[] = {
qRgb(0xFF, 0xFF, 0xFF),
qRgb(0xFC, 0xF3, 0x05),
qRgb(0xFF, 0x64, 0x02),
qRgb(0xDD, 0x08, 0x06),
qRgb(0xF2, 0x08, 0x84),
qRgb(0x46, 0x00, 0xA5),
qRgb(0x00, 0x00, 0xD4),
qRgb(0x02, 0xAB, 0xEA),
qRgb(0x1F, 0xB7, 0x14),
qRgb(0x00, 0x64, 0x11),
qRgb(0x56, 0x2C, 0x05),
qRgb(0x90, 0x71, 0x3A),
qRgb(0xC0, 0xC0, 0xC0),
qRgb(0x80, 0x80, 0x80),
qRgb(0x40, 0x40, 0x40),
qRgb(0x00, 0x00, 0x00)
};
Q_STATIC_ASSERT(sizeof(ICNSColorTable4bit) / sizeof(ICNSColorTable4bit[0]) == (1 << ICNSEntry::Depth4bit));
static const QRgb ICNSColorTable8bit[] = {
qRgb(0xFF, 0xFF, 0xFF),
qRgb(0xFF, 0xFF, 0xCC),
qRgb(0xFF, 0xFF, 0x99),
qRgb(0xFF, 0xFF, 0x66),
qRgb(0xFF, 0xFF, 0x33),
qRgb(0xFF, 0xFF, 0x00),
qRgb(0xFF, 0xCC, 0xFF),
qRgb(0xFF, 0xCC, 0xCC),
qRgb(0xFF, 0xCC, 0x99),
qRgb(0xFF, 0xCC, 0x66),
qRgb(0xFF, 0xCC, 0x33),
qRgb(0xFF, 0xCC, 0x00),
qRgb(0xFF, 0x99, 0xFF),
qRgb(0xFF, 0x99, 0xCC),
qRgb(0xFF, 0x99, 0x99),
qRgb(0xFF, 0x99, 0x66),
qRgb(0xFF, 0x99, 0x33),
qRgb(0xFF, 0x99, 0x00),
qRgb(0xFF, 0x66, 0xFF),
qRgb(0xFF, 0x66, 0xCC),
qRgb(0xFF, 0x66, 0x99),
qRgb(0xFF, 0x66, 0x66),
qRgb(0xFF, 0x66, 0x33),
qRgb(0xFF, 0x66, 0x00),
qRgb(0xFF, 0x33, 0xFF),
qRgb(0xFF, 0x33, 0xCC),
qRgb(0xFF, 0x33, 0x99),
qRgb(0xFF, 0x33, 0x66),
qRgb(0xFF, 0x33, 0x33),
qRgb(0xFF, 0x33, 0x00),
qRgb(0xFF, 0x00, 0xFF),
qRgb(0xFF, 0x00, 0xCC),
qRgb(0xFF, 0x00, 0x99),
qRgb(0xFF, 0x00, 0x66),
qRgb(0xFF, 0x00, 0x33),
qRgb(0xFF, 0x00, 0x00),
qRgb(0xCC, 0xFF, 0xFF),
qRgb(0xCC, 0xFF, 0xCC),
qRgb(0xCC, 0xFF, 0x99),
qRgb(0xCC, 0xFF, 0x66),
qRgb(0xCC, 0xFF, 0x33),
qRgb(0xCC, 0xFF, 0x00),
qRgb(0xCC, 0xCC, 0xFF),
qRgb(0xCC, 0xCC, 0xCC),
qRgb(0xCC, 0xCC, 0x99),
qRgb(0xCC, 0xCC, 0x66),
qRgb(0xCC, 0xCC, 0x33),
qRgb(0xCC, 0xCC, 0x00),
qRgb(0xCC, 0x99, 0xFF),
qRgb(0xCC, 0x99, 0xCC),
qRgb(0xCC, 0x99, 0x99),
qRgb(0xCC, 0x99, 0x66),
qRgb(0xCC, 0x99, 0x33),
qRgb(0xCC, 0x99, 0x00),
qRgb(0xCC, 0x66, 0xFF),
qRgb(0xCC, 0x66, 0xCC),
qRgb(0xCC, 0x66, 0x99),
qRgb(0xCC, 0x66, 0x66),
qRgb(0xCC, 0x66, 0x33),
qRgb(0xCC, 0x66, 0x00),
qRgb(0xCC, 0x33, 0xFF),
qRgb(0xCC, 0x33, 0xCC),
qRgb(0xCC, 0x33, 0x99),
qRgb(0xCC, 0x33, 0x66),
qRgb(0xCC, 0x33, 0x33),
qRgb(0xCC, 0x33, 0x00),
qRgb(0xCC, 0x00, 0xFF),
qRgb(0xCC, 0x00, 0xCC),
qRgb(0xCC, 0x00, 0x99),
qRgb(0xCC, 0x00, 0x66),
qRgb(0xCC, 0x00, 0x33),
qRgb(0xCC, 0x00, 0x00),
qRgb(0x99, 0xFF, 0xFF),
qRgb(0x99, 0xFF, 0xCC),
qRgb(0x99, 0xFF, 0x99),
qRgb(0x99, 0xFF, 0x66),
qRgb(0x99, 0xFF, 0x33),
qRgb(0x99, 0xFF, 0x00),
qRgb(0x99, 0xCC, 0xFF),
qRgb(0x99, 0xCC, 0xCC),
qRgb(0x99, 0xCC, 0x99),
qRgb(0x99, 0xCC, 0x66),
qRgb(0x99, 0xCC, 0x33),
qRgb(0x99, 0xCC, 0x00),
qRgb(0x99, 0x99, 0xFF),
qRgb(0x99, 0x99, 0xCC),
qRgb(0x99, 0x99, 0x99),
qRgb(0x99, 0x99, 0x66),
qRgb(0x99, 0x99, 0x33),
qRgb(0x99, 0x99, 0x00),
qRgb(0x99, 0x66, 0xFF),
qRgb(0x99, 0x66, 0xCC),
qRgb(0x99, 0x66, 0x99),
qRgb(0x99, 0x66, 0x66),
qRgb(0x99, 0x66, 0x33),
qRgb(0x99, 0x66, 0x00),
qRgb(0x99, 0x33, 0xFF),
qRgb(0x99, 0x33, 0xCC),
qRgb(0x99, 0x33, 0x99),
qRgb(0x99, 0x33, 0x66),
qRgb(0x99, 0x33, 0x33),
qRgb(0x99, 0x33, 0x00),
qRgb(0x99, 0x00, 0xFF),
qRgb(0x99, 0x00, 0xCC),
qRgb(0x99, 0x00, 0x99),
qRgb(0x99, 0x00, 0x66),
qRgb(0x99, 0x00, 0x33),
qRgb(0x99, 0x00, 0x00),
qRgb(0x66, 0xFF, 0xFF),
qRgb(0x66, 0xFF, 0xCC),
qRgb(0x66, 0xFF, 0x99),
qRgb(0x66, 0xFF, 0x66),
qRgb(0x66, 0xFF, 0x33),
qRgb(0x66, 0xFF, 0x00),
qRgb(0x66, 0xCC, 0xFF),
qRgb(0x66, 0xCC, 0xCC),
qRgb(0x66, 0xCC, 0x99),
qRgb(0x66, 0xCC, 0x66),
qRgb(0x66, 0xCC, 0x33),
qRgb(0x66, 0xCC, 0x00),
qRgb(0x66, 0x99, 0xFF),
qRgb(0x66, 0x99, 0xCC),
qRgb(0x66, 0x99, 0x99),
qRgb(0x66, 0x99, 0x66),
qRgb(0x66, 0x99, 0x33),
qRgb(0x66, 0x99, 0x00),
qRgb(0x66, 0x66, 0xFF),
qRgb(0x66, 0x66, 0xCC),
qRgb(0x66, 0x66, 0x99),
qRgb(0x66, 0x66, 0x66),
qRgb(0x66, 0x66, 0x33),
qRgb(0x66, 0x66, 0x00),
qRgb(0x66, 0x33, 0xFF),
qRgb(0x66, 0x33, 0xCC),
qRgb(0x66, 0x33, 0x99),
qRgb(0x66, 0x33, 0x66),
qRgb(0x66, 0x33, 0x33),
qRgb(0x66, 0x33, 0x00),
qRgb(0x66, 0x00, 0xFF),
qRgb(0x66, 0x00, 0xCC),
qRgb(0x66, 0x00, 0x99),
qRgb(0x66, 0x00, 0x66),
qRgb(0x66, 0x00, 0x33),
qRgb(0x66, 0x00, 0x00),
qRgb(0x33, 0xFF, 0xFF),
qRgb(0x33, 0xFF, 0xCC),
qRgb(0x33, 0xFF, 0x99),
qRgb(0x33, 0xFF, 0x66),
qRgb(0x33, 0xFF, 0x33),
qRgb(0x33, 0xFF, 0x00),
qRgb(0x33, 0xCC, 0xFF),
qRgb(0x33, 0xCC, 0xCC),
qRgb(0x33, 0xCC, 0x99),
qRgb(0x33, 0xCC, 0x66),
qRgb(0x33, 0xCC, 0x33),
qRgb(0x33, 0xCC, 0x00),
qRgb(0x33, 0x99, 0xFF),
qRgb(0x33, 0x99, 0xCC),
qRgb(0x33, 0x99, 0x99),
qRgb(0x33, 0x99, 0x66),
qRgb(0x33, 0x99, 0x33),
qRgb(0x33, 0x99, 0x00),
qRgb(0x33, 0x66, 0xFF),
qRgb(0x33, 0x66, 0xCC),
qRgb(0x33, 0x66, 0x99),
qRgb(0x33, 0x66, 0x66),
qRgb(0x33, 0x66, 0x33),
qRgb(0x33, 0x66, 0x00),
qRgb(0x33, 0x33, 0xFF),
qRgb(0x33, 0x33, 0xCC),
qRgb(0x33, 0x33, 0x99),
qRgb(0x33, 0x33, 0x66),
qRgb(0x33, 0x33, 0x33),
qRgb(0x33, 0x33, 0x00),
qRgb(0x33, 0x00, 0xFF),
qRgb(0x33, 0x00, 0xCC),
qRgb(0x33, 0x00, 0x99),
qRgb(0x33, 0x00, 0x66),
qRgb(0x33, 0x00, 0x33),
qRgb(0x33, 0x00, 0x00),
qRgb(0x00, 0xFF, 0xFF),
qRgb(0x00, 0xFF, 0xCC),
qRgb(0x00, 0xFF, 0x99),
qRgb(0x00, 0xFF, 0x66),
qRgb(0x00, 0xFF, 0x33),
qRgb(0x00, 0xFF, 0x00),
qRgb(0x00, 0xCC, 0xFF),
qRgb(0x00, 0xCC, 0xCC),
qRgb(0x00, 0xCC, 0x99),
qRgb(0x00, 0xCC, 0x66),
qRgb(0x00, 0xCC, 0x33),
qRgb(0x00, 0xCC, 0x00),
qRgb(0x00, 0x99, 0xFF),
qRgb(0x00, 0x99, 0xCC),
qRgb(0x00, 0x99, 0x99),
qRgb(0x00, 0x99, 0x66),
qRgb(0x00, 0x99, 0x33),
qRgb(0x00, 0x99, 0x00),
qRgb(0x00, 0x66, 0xFF),
qRgb(0x00, 0x66, 0xCC),
qRgb(0x00, 0x66, 0x99),
qRgb(0x00, 0x66, 0x66),
qRgb(0x00, 0x66, 0x33),
qRgb(0x00, 0x66, 0x00),
qRgb(0x00, 0x33, 0xFF),
qRgb(0x00, 0x33, 0xCC),
qRgb(0x00, 0x33, 0x99),
qRgb(0x00, 0x33, 0x66),
qRgb(0x00, 0x33, 0x33),
qRgb(0x00, 0x33, 0x00),
qRgb(0x00, 0x00, 0xFF),
qRgb(0x00, 0x00, 0xCC),
qRgb(0x00, 0x00, 0x99),
qRgb(0x00, 0x00, 0x66),
qRgb(0x00, 0x00, 0x33),
qRgb(0xEE, 0x00, 0x00),
qRgb(0xDD, 0x00, 0x00),
qRgb(0xBB, 0x00, 0x00),
qRgb(0xAA, 0x00, 0x00),
qRgb(0x88, 0x00, 0x00),
qRgb(0x77, 0x00, 0x00),
qRgb(0x55, 0x00, 0x00),
qRgb(0x44, 0x00, 0x00),
qRgb(0x22, 0x00, 0x00),
qRgb(0x11, 0x00, 0x00),
qRgb(0x00, 0xEE, 0x00),
qRgb(0x00, 0xDD, 0x00),
qRgb(0x00, 0xBB, 0x00),
qRgb(0x00, 0xAA, 0x00),
qRgb(0x00, 0x88, 0x00),
qRgb(0x00, 0x77, 0x00),
qRgb(0x00, 0x55, 0x00),
qRgb(0x00, 0x44, 0x00),
qRgb(0x00, 0x22, 0x00),
qRgb(0x00, 0x11, 0x00),
qRgb(0x00, 0x00, 0xEE),
qRgb(0x00, 0x00, 0xDD),
qRgb(0x00, 0x00, 0xBB),
qRgb(0x00, 0x00, 0xAA),
qRgb(0x00, 0x00, 0x88),
qRgb(0x00, 0x00, 0x77),
qRgb(0x00, 0x00, 0x55),
qRgb(0x00, 0x00, 0x44),
qRgb(0x00, 0x00, 0x22),
qRgb(0x00, 0x00, 0x11),
qRgb(0xEE, 0xEE, 0xEE),
qRgb(0xDD, 0xDD, 0xDD),
qRgb(0xBB, 0xBB, 0xBB),
qRgb(0xAA, 0xAA, 0xAA),
qRgb(0x88, 0x88, 0x88),
qRgb(0x77, 0x77, 0x77),
qRgb(0x55, 0x55, 0x55),
qRgb(0x44, 0x44, 0x44),
qRgb(0x22, 0x22, 0x22),
qRgb(0x11, 0x11, 0x11),
qRgb(0x00, 0x00, 0x00)
};
Q_STATIC_ASSERT(sizeof(ICNSColorTable8bit) / sizeof(ICNSColorTable8bit[0]) == (1 << ICNSEntry::Depth8bit));
static inline QDataStream &operator>>(QDataStream &in, ICNSBlockHeader &p)
{
in >> p.ostype;
in >> p.length;
return in;
}
static inline QDataStream &operator<<(QDataStream &out, const ICNSBlockHeader &p)
{
out << p.ostype;
out << p.length;
return out;
}
static inline bool isPowOf2OrDividesBy16(quint32 u, qreal r)
{
return u == r && ((u % 16 == 0) || (r >= 16 && (u & (u - 1)) == 0));
}
static inline bool isBlockHeaderValid(const ICNSBlockHeader &header, quint64 bound = 0)
{
return header.ostype != 0 && (bound == 0
|| qBound(quint64(ICNSBlockHeaderSize), quint64(header.length), bound) == header.length);
}
static inline bool isIconCompressed(const ICNSEntry &icon)
{
return icon.dataFormat == ICNSEntry::PNG || icon.dataFormat == ICNSEntry::JP2;
}
static inline bool isMaskSuitable(const ICNSEntry &mask, const ICNSEntry &icon, ICNSEntry::Depth target)
{
return mask.variant == icon.variant && mask.depth == target
&& mask.height == icon.height && mask.width == icon.width;
}
static inline QByteArray nameFromOSType(quint32 ostype)
{
const quint32 bytes = qToBigEndian(ostype);
return QByteArray((const char*)&bytes, 4);
}
static inline quint32 nameToOSType(const QByteArray &ostype)
{
if (ostype.size() != 4)
return 0;
return qFromBigEndian(*reinterpret_cast<const quint32*>(ostype.constData()));
}
static inline QByteArray nameForCompressedIcon(quint8 iconNumber)
{
const bool portable = iconNumber < 7;
const QByteArray base = portable ? QByteArrayLiteral("icp") : QByteArrayLiteral("ic");
if (!portable && iconNumber < 10)
return base + "0" + QByteArray::number(iconNumber);
return base + QByteArray::number(iconNumber);
}
static inline QVector<QRgb> getColorTable(ICNSEntry::Depth depth)
{
QVector<QRgb> table;
uint n = 1 << depth;
const QRgb *data;
switch (depth) {
case ICNSEntry::DepthMono:
data = ICNSColorTableMono;
break;
case ICNSEntry::Depth4bit:
data = ICNSColorTable4bit;
break;
case ICNSEntry::Depth8bit:
data = ICNSColorTable8bit;
break;
default:
Q_UNREACHABLE();
break;
}
table.resize(n);
memcpy(table.data(), data, sizeof(QRgb) * n);
return table;
}
static bool parseIconEntryData(ICNSEntry &icon, QIODevice *device)
{
const qint64 oldPos = device->pos();
if (oldPos != icon.dataOffset && !device->seek(icon.dataOffset))
return false;
const QByteArray magic = device->peek(12);
const bool isPNG = magic.startsWith(QByteArrayLiteral("\211PNG\r\n\032\n\000\000\000\r"));
const bool isJP2 = !isPNG && magic == QByteArrayLiteral("\000\000\000\014jP \r\n\207\n");
if (isPNG || isJP2) {
// TODO: Add parsing of png/jp2 headers to enable feature reporting by plugin?
icon.flags = ICNSEntry::IsIcon;
icon.dataFormat = isPNG? ICNSEntry::PNG : ICNSEntry::JP2;
}
if (oldPos != icon.dataOffset && !device->seek(oldPos))
return false;
return true;
}
static bool parseIconEntryInfo(ICNSEntry &icon)
{
const QString ostype = QString::fromLatin1(nameFromOSType(icon.ostype));
// Typical OSType naming: <junk><group><depth><mask>;
// For icons OSType should be strictly alphanumeric + '#' character for masks/mono.
const QString ptrn = QStringLiteral("^(?<junk>[a-z|A-Z]{0,4})(?<group>[a-z|A-Z]{1})(?<depth>[\\d]{0,2})(?<mask>[#mk]{0,2})$");
QRegularExpression regexp(ptrn);
QRegularExpressionMatch match = regexp.match(ostype);
if (!match.hasMatch()) {
qWarning("parseIconEntryInfo(): Failed, OSType doesn't match: \"%s\"", qPrintable(ostype));
return false;
}
const QString group = match.captured(QStringLiteral("group"));
const QString depth = match.captured(QStringLiteral("depth"));
const QString mask = match.captured(QStringLiteral("mask"));
// Icon group:
if (!group.isEmpty())
icon.group = ICNSEntry::Group(group.at(0).toLatin1());
// That's enough for compressed ones
if (isIconCompressed(icon))
return true;
// Icon depth:
if (!depth.isEmpty())
icon.depth = ICNSEntry::Depth(depth.toUInt());
// Try mono if depth not found
if (icon.depth == ICNSEntry::DepthUnknown)
icon.depth = ICNSEntry::DepthMono;
// Detect size:
const qreal bytespp = (qreal)icon.depth / 8;
const qreal r1 = qSqrt(icon.dataLength / bytespp);
const qreal r2 = qSqrt((icon.dataLength / bytespp) / 2);
const quint32 r1u = qRound(r1);
const quint32 r2u = qRound(r2);
const bool singleEntry = isPowOf2OrDividesBy16(r1u, r1);
const bool doubleSize = isPowOf2OrDividesBy16(r2u, r2);
if (singleEntry) {
icon.flags = mask.isEmpty() ? ICNSEntry::IsIcon : ICNSEntry::IsMask;
icon.dataFormat = ICNSEntry::RawIcon;
icon.width = r1u;
icon.height = r1u;
} else if (doubleSize) {
icon.flags = ICNSEntry::IconPlusMask;
icon.dataFormat = ICNSEntry::RawIcon;
icon.width = r2u;
icon.height = r2u;
} else if (icon.group == ICNSEntry::GroupMini) {
// Legacy 16x12 icons are an exception from the generic square formula
const bool doubleSize = icon.dataLength == 192 * bytespp * 2;
icon.flags = doubleSize ? ICNSEntry::IconPlusMask : ICNSEntry::IsIcon;
icon.dataFormat = ICNSEntry::RawIcon;
icon.width = 16;
icon.height = 12;
} else if (icon.depth == ICNSEntry::Depth32bit) {
// We have a formula mismatch in a 32bit icon there, probably RLE24
icon.dataFormat = ICNSEntry::RLE24;
icon.flags = mask.isEmpty() ? ICNSEntry::IsIcon : ICNSEntry::IsMask;
switch (icon.group) {
case ICNSEntry::GroupSmall:
icon.width = 16;
break;
case ICNSEntry::GroupLarge:
icon.width = 32;
break;
case ICNSEntry::GroupHuge:
icon.width = 48;
break;
case ICNSEntry::GroupThumbnail:
icon.width = 128;
break;
default:
qWarning("parseIconEntryInfo(): Failed, 32bit icon from an unknown group. OSType: \"%s\"",
qPrintable(ostype));
}
icon.height = icon.width;
}
return true;
}
static QImage readMask(const ICNSEntry &mask, QDataStream &stream)
{
if ((mask.flags & ICNSEntry::IsMask) == 0)
return QImage();
if (mask.depth != ICNSEntry::DepthMono && mask.depth != ICNSEntry::Depth8bit) {
qWarning("readMask(): Failed, unusual bit depth: %u OSType: \"%s\"",
mask.depth, nameFromOSType(mask.ostype).constData());
return QImage();
}
const bool isMono = mask.depth == ICNSEntry::DepthMono;
const bool doubleSize = mask.flags == ICNSEntry::IconPlusMask;
const quint32 imageDataSize = (mask.width * mask.height * mask.depth) / 8;
const qint64 pos = doubleSize ? (mask.dataOffset + imageDataSize) : mask.dataOffset;
const qint64 oldPos = stream.device()->pos();
if (!stream.device()->seek(pos))
return QImage();
QImage img(mask.width, mask.height, QImage::Format_RGB32);
quint8 byte = 0;
quint32 pixel = 0;
for (quint32 y = 0; y < mask.height; y++) {
QRgb *line = reinterpret_cast<QRgb *>(img.scanLine(y));
for (quint32 x = 0; x < mask.width; x++) {
if (pixel % (8 / mask.depth) == 0)
stream >> byte;
else if (isMono)
byte <<= 1;
const quint8 alpha = isMono ? (((byte >> 7) & 0x01) * 255) : byte;
line[x] = qRgb(alpha, alpha, alpha);
pixel++;
}
}
stream.device()->seek(oldPos);
return img;
}
template <ICNSEntry::Depth depth>
static QImage readLowDepthIcon(const ICNSEntry &icon, QDataStream &stream)
{
Q_STATIC_ASSERT(depth == ICNSEntry::DepthMono || depth == ICNSEntry::Depth4bit
|| depth == ICNSEntry::Depth8bit);
const bool isMono = depth == ICNSEntry::DepthMono;
const QImage::Format format = isMono ? QImage::Format_Mono : QImage::Format_Indexed8;
const QVector<QRgb> colortable = getColorTable(depth);
if (colortable.isEmpty())
return QImage();
QImage img(icon.width, icon.height, format);
img.setColorTable(colortable);
quint32 pixel = 0;
quint8 byte = 0;
for (quint32 y = 0; y < icon.height; y++) {
for (quint32 x = 0; x < icon.width; x++) {
if (pixel % (8 / depth) == 0)
stream >> byte;
quint8 cindex;
switch (depth) {
case ICNSEntry::DepthMono:
cindex = (byte >> 7) & 0x01; // left 1 bit
byte <<= 1;
break;
case ICNSEntry::Depth4bit:
cindex = (byte >> 4) & 0x0F; // left 4 bits
byte <<= 4;
break;
default:
cindex = byte; // 8 bits
break;
}
img.setPixel(x, y, cindex);
pixel++;
}
}
return img;
}
static QImage read32bitIcon(const ICNSEntry &icon, QDataStream &stream)
{
QImage img = QImage(icon.width, icon.height, QImage::Format_RGB32);
if (icon.dataFormat != ICNSEntry::RLE24) {
for (quint32 y = 0; y < icon.height; y++) {
QRgb *line = reinterpret_cast<QRgb *>(img.scanLine(y));
for (quint32 x = 0; x < icon.width; x++) {
quint8 r, g, b, a;
stream >> r >> g >> b >> a;
line[x] = qRgb(r, g, b);
}
}
} else {
const quint32 estPxsNum = icon.width * icon.height;
const QByteArray &bytes = stream.device()->peek(4);
if (bytes.isEmpty())
return QImage();
// Zero-padding may be present:
if (qFromBigEndian<quint32>(*bytes.constData()) == 0)
stream.skipRawData(4);
for (quint8 colorNRun = 0; colorNRun < 3; colorNRun++) {
quint32 pixel = 0;
QRgb *line = 0;
while (pixel < estPxsNum && !stream.atEnd()) {
quint8 byte, value;
stream >> byte;
const bool bitIsClear = (byte & 0x80) == 0;
// If high bit is clear: run of different values; else: same value
quint8 runLength = bitIsClear ? ((0xFF & byte) + 1) : ((0xFF & byte) - 125);
// Length of the run for for different values: 1 <= len <= 128
// Length of the run for same values: 3 <= len <= 130
if (!bitIsClear)
stream >> value;
for (quint8 i = 0; i < runLength && pixel < estPxsNum; i++) {
if (bitIsClear)
stream >> value;
const quint32 y = pixel / icon.height;
const quint32 x = pixel - (icon.width * y);
if (pixel % icon.height == 0)
line = reinterpret_cast<QRgb *>(img.scanLine(y));
QRgb rgb = line[x];
const int r = (colorNRun == 0) ? value : qRed(rgb);
const int g = (colorNRun == 1) ? value : qGreen(rgb);
const int b = (colorNRun == 2) ? value : qBlue(rgb);
line[x] = qRgb(r, g, b);
pixel++;
}
}
}
}
return img;
}
QICNSHandler::QICNSHandler() :
m_currentIconIndex(0), m_state(ScanNotScanned)
{
}
#if QT_DEPRECATED_SINCE(5, 13)
QByteArray QICNSHandler::name() const
{
return QByteArrayLiteral("icns");
}
#endif
bool QICNSHandler::canRead(QIODevice *device)
{
if (!device || !device->isReadable()) {
qWarning("QICNSHandler::canRead() called without a readable device");
return false;
}
if (device->peek(4) == QByteArrayLiteral("icns")) {
if (device->isSequential()) {
qWarning("QICNSHandler::canRead() called on a sequential device");
return false;
}
return true;
}
return false;
}
bool QICNSHandler::canRead() const
{
if (m_state == ScanNotScanned && !canRead(device()))
return false;
if (m_state != ScanError) {
setFormat(QByteArrayLiteral("icns"));
return true;
}
return false;
}
bool QICNSHandler::read(QImage *outImage)
{
QImage img;
if (!ensureScanned()) {
qWarning("QICNSHandler::read(): The device wasn't parsed properly!");
return false;
}
const ICNSEntry &icon = m_icons.at(m_currentIconIndex);
QDataStream stream(device());
stream.setByteOrder(QDataStream::BigEndian);
if (!device()->seek(icon.dataOffset))
return false;
switch (icon.dataFormat) {
case ICNSEntry::RawIcon:
case ICNSEntry::RLE24:
if (qMin(icon.width, icon.height) == 0)
break;
switch (icon.depth) {
case ICNSEntry::DepthMono:
img = readLowDepthIcon<ICNSEntry::DepthMono>(icon, stream);
break;
case ICNSEntry::Depth4bit:
img = readLowDepthIcon<ICNSEntry::Depth4bit>(icon, stream);
break;
case ICNSEntry::Depth8bit:
img = readLowDepthIcon<ICNSEntry::Depth8bit>(icon, stream);
break;
case ICNSEntry::Depth32bit:
img = read32bitIcon(icon, stream);
break;
default:
qWarning("QICNSHandler::read(): Failed, unsupported icon bit depth: %u, OSType: \"%s\"",
icon.depth, nameFromOSType(icon.ostype).constData());
}
if (!img.isNull()) {
QImage alpha = readMask(getIconMask(icon), stream);
if (!alpha.isNull())
img.setAlphaChannel(alpha);
}
break;
default:
const char *format = 0;
if (icon.dataFormat == ICNSEntry::PNG)
format = "png";
else if (icon.dataFormat == ICNSEntry::JP2)
format = "jp2";
// Even if JP2 or PNG magic is not detected, try anyway for unknown formats
img = QImage::fromData(device()->read(icon.dataLength), format);
if (img.isNull()) {
if (format == 0)
format = "unknown";
qWarning("QICNSHandler::read(): Failed, compressed format \"%s\" is not supported " \
"by your Qt library or this file is corrupt. OSType: \"%s\"",
format, nameFromOSType(icon.ostype).constData());
}
}
*outImage = img;
return !img.isNull();
}
bool QICNSHandler::write(const QImage &image)
{
/*
Notes:
* Experimental implementation. Just for simple converting tasks / testing purposes.
* Min. size is 16x16, Max. size is 1024x1024.
* Performs downscale to a square image if width != height.
* Performs upscale to 16x16, if the image is smaller.
* Performs downscale to a nearest power of two if size is not a power of two.
* Currently uses non-hardcoded OSTypes.
*/
QIODevice *device = this->device();
if (!device->isWritable() || image.isNull() || qMin(image.width(), image.height()) == 0)
return false;
const int minSize = qMin(image.width(), image.height());
const int oldSize = (minSize < 16) ? 16 : minSize;
// Calc power of two:
int size = oldSize;
uint pow = 0;
// Note: Values over 10 are reserved for retina icons.
while (pow < 10 && (size >>= 1))
pow++;
const int newSize = 1 << pow;
QImage img = image;
// Let's enforce resizing if size differs:
if (newSize != oldSize || qMax(image.width(), image.height()) != minSize)
img = img.scaled(newSize, newSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
// Construct OSType and headers:
const quint32 ostype = nameToOSType(nameForCompressedIcon(pow));
ICNSBlockHeader fileHeader;
fileHeader.ostype = ICNSBlockHeader::TypeIcns;
ICNSBlockHeader tocHeader;
tocHeader.ostype = ICNSBlockHeader::TypeToc;
ICNSBlockHeader iconEntry;
iconEntry.ostype = ostype;
QByteArray imageData;
QBuffer buffer(&imageData);
if (!buffer.open(QIODevice::WriteOnly) || !img.save(&buffer, "png"))
return false;
buffer.close();
iconEntry.length = ICNSBlockHeaderSize + imageData.size();
tocHeader.length = ICNSBlockHeaderSize * 2;
fileHeader.length = ICNSBlockHeaderSize + tocHeader.length + iconEntry.length;
if (!isBlockHeaderValid(iconEntry))
return false;
QDataStream stream(device);
// iconEntry is also a TOC entry
stream << fileHeader << tocHeader << iconEntry << iconEntry;
stream.writeRawData(imageData.constData(), imageData.size());
return stream.status() == QDataStream::Ok;
}
bool QICNSHandler::supportsOption(ImageOption option) const
{
return option == SubType;
}
QVariant QICNSHandler::option(ImageOption option) const
{
if (!supportsOption(option) || !ensureScanned())
return QVariant();
if (option == SubType) {
if (imageCount() > 0 && m_currentIconIndex <= imageCount()) {
const ICNSEntry &icon = m_icons.at(m_currentIconIndex);
if (icon.variant != 0)
return QByteArray(nameFromOSType(icon.variant) + '-' + nameFromOSType(icon.ostype));
return nameFromOSType(icon.ostype);
}
}
return QVariant();
}
int QICNSHandler::imageCount() const
{
if (!ensureScanned())
return 0;
return m_icons.size();
}
bool QICNSHandler::jumpToImage(int imageNumber)
{
if (imageNumber >= imageCount())
return false;
m_currentIconIndex = imageNumber;
return true;
}
bool QICNSHandler::jumpToNextImage()
{
return jumpToImage(m_currentIconIndex + 1);
}
bool QICNSHandler::ensureScanned() const
{
if (m_state == ScanNotScanned) {
QICNSHandler *that = const_cast<QICNSHandler *>(this);
that->m_state = that->scanDevice() ? ScanSuccess : ScanError;
}
return m_state == ScanSuccess;
}
bool QICNSHandler::addEntry(const ICNSBlockHeader &header, qint64 imgDataOffset, quint32 variant)
{
// Note: This function returns false only when a device positioning error occurred
ICNSEntry entry;
entry.ostype = header.ostype;
entry.variant = variant;
entry.dataOffset = imgDataOffset;
entry.dataLength = header.length - ICNSBlockHeaderSize;
// Check for known magic numbers:
if (!parseIconEntryData(entry, device()))
return false;
// Parse everything else and index this entry:
if (parseIconEntryInfo(entry)) {
if ((entry.flags & ICNSEntry::IsMask) != 0)
m_masks << entry;
if ((entry.flags & ICNSEntry::IsIcon) != 0)
m_icons << entry;
}
return true;
}
bool QICNSHandler::scanDevice()
{
if (m_state == ScanSuccess)
return true;
if (!device()->seek(0))
return false;
QDataStream stream(device());
stream.setByteOrder(QDataStream::BigEndian);
bool scanIsIncomplete = false;
qint64 filelength = device()->size();
ICNSBlockHeader blockHeader;
while (!stream.atEnd() || device()->pos() < filelength) {
stream >> blockHeader;
if (stream.status() != QDataStream::Ok)
return false;
const qint64 blockDataOffset = device()->pos();
if (!isBlockHeaderValid(blockHeader)) {
qWarning("QICNSHandler::scanDevice(): Failed, bad header at pos %s. OSType \"%s\", length %u",
QByteArray::number(blockDataOffset).constData(),
nameFromOSType(blockHeader.ostype).constData(), blockHeader.length);
return false;
}
const quint64 blockDataLength = blockHeader.length - ICNSBlockHeaderSize;
const qint64 nextBlockOffset = blockDataOffset + blockDataLength;
switch (blockHeader.ostype) {
case ICNSBlockHeader::TypeIcns:
if (blockDataOffset != ICNSBlockHeaderSize) {
// Icns container definition should be in the beginning of the device.
// If we meet this block somewhere else, then just ignore it.
stream.skipRawData(blockDataLength);
break;
}
filelength = blockHeader.length;
if (device()->size() < blockHeader.length) {
qWarning("QICNSHandler::scanDevice(): Failed, file is incomplete.");
return false;
}
break;
case ICNSBlockHeader::TypeIcnv:
case ICNSBlockHeader::TypeClut:
// We don't have a good use for these blocks... yet.
stream.skipRawData(blockDataLength);
break;
case ICNSBlockHeader::TypeTile:
case ICNSBlockHeader::TypeOver:
case ICNSBlockHeader::TypeOpen:
case ICNSBlockHeader::TypeDrop:
case ICNSBlockHeader::TypeOdrp:
// Icns container seems to have an embedded icon variant container
// Let's start a scan for entries
while (device()->pos() < nextBlockOffset) {
ICNSBlockHeader icon;
stream >> icon;
// Check for incorrect variant entry header and stop scan
if (!isBlockHeaderValid(icon, blockDataLength))
break;
if (!addEntry(icon, device()->pos(), blockHeader.ostype))
return false;
if (stream.skipRawData(icon.length - ICNSBlockHeaderSize) < 0)
return false;
}
if (device()->pos() != nextBlockOffset) {
// Scan of this container didn't end where we expected.
// Let's generate some output about this incident:
qWarning("Scan of the icon variant container (\"%s\") failed at pos %s.\n" \
"Reason: Scan didn't reach the end of this container's block, " \
"delta: %s bytes. This file may be corrupted.",
nameFromOSType(blockHeader.ostype).constData(),
QByteArray::number(device()->pos()).constData(),
QByteArray::number(nextBlockOffset - device()->pos()).constData());
if (!device()->seek(nextBlockOffset))
return false;
}
break;
case ICNSBlockHeader::TypeToc: {
// Quick scan, table of contents
if (blockDataOffset != ICNSBlockHeaderSize * 2) {
// TOC should be the first block in the file after container definition.
// Ignore and go on with a deep scan.
stream.skipRawData(blockDataLength);
break;
}
// First image data offset:
qint64 imgDataOffset = blockDataOffset + blockHeader.length;
for (uint i = 0, count = blockDataLength / ICNSBlockHeaderSize; i < count; i++) {
ICNSBlockHeader tocEntry;
stream >> tocEntry;
if (!isBlockHeaderValid(tocEntry)) {
// TOC contains incorrect header, we should skip TOC since we can't trust it
qWarning("QICNSHandler::scanDevice(): Warning! Table of contents contains a bad " \
"entry! Stop at device pos: %s bytes. This file may be corrupted.",
QByteArray::number(device()->pos()).constData());
if (!device()->seek(nextBlockOffset))
return false;
break;
}
if (!addEntry(tocEntry, imgDataOffset))
return false;
imgDataOffset += tocEntry.length;
// If TOC covers all the blocks in the file, then quick scan is complete
if (imgDataOffset == filelength)
return true;
}
// Else just start a deep scan to salvage anything left after TOC's end
scanIsIncomplete = true;
break;
}
default:
// Deep scan, block by block
if (scanIsIncomplete) {
// Check if entry with this offset is added somewhere
// But only if we have incomplete TOC, otherwise just try to add
bool exists = false;
for (int i = 0; i < m_icons.size() && !exists; i++)
exists = m_icons.at(i).dataOffset == blockDataOffset;
for (int i = 0; i < m_masks.size() && !exists; i++)
exists = m_masks.at(i).dataOffset == blockDataOffset;
if (!exists && !addEntry(blockHeader, blockDataOffset))
return false;
} else if (!addEntry(blockHeader, blockDataOffset)) {
return false;
}
stream.skipRawData(blockDataLength);
break;
}
}
return true;
}
const ICNSEntry &QICNSHandler::getIconMask(const ICNSEntry &icon) const
{
const bool is32bit = icon.depth == ICNSEntry::Depth32bit;
ICNSEntry::Depth targetDepth = is32bit ? ICNSEntry::Depth8bit : ICNSEntry::DepthMono;
for (int i = 0; i < m_masks.size(); i++) {
const ICNSEntry &mask = m_masks.at(i);
if (isMaskSuitable(mask, icon, targetDepth))
return mask;
}
return icon;
}
QT_END_NAMESPACE
#endif // QT_NO_DATASTREAM