2022-10-12 19:49:57 +03:00
|
|
|
/* This file is part of the KDE libraries
|
|
|
|
Copyright (C) 2022 Ivailo Monev <xakepa10@gmail.com>
|
|
|
|
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
|
|
modify it under the terms of the GNU Library General Public
|
|
|
|
License version 2, as published by the Free Software Foundation.
|
|
|
|
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
Library General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Library General Public License
|
|
|
|
along with this library; see the file COPYING.LIB. If not, write to
|
|
|
|
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
|
|
Boston, MA 02110-1301, USA.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "ico.h"
|
|
|
|
|
|
|
|
#include <QImage>
|
|
|
|
#include <kdebug.h>
|
|
|
|
|
2022-10-13 01:43:48 +03:00
|
|
|
#include <limits.h>
|
|
|
|
|
2022-10-12 19:49:57 +03:00
|
|
|
static const char* const s_icopluginformat = "ico";
|
|
|
|
|
|
|
|
static const ushort s_peekbuffsize = 32;
|
|
|
|
static const uchar s_pngheader[] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };
|
2022-10-13 01:21:54 +03:00
|
|
|
static const ushort s_pngheadersize = 8;
|
2022-10-12 19:49:57 +03:00
|
|
|
// for reference:
|
|
|
|
// https://en.wikipedia.org/wiki/List_of_file_signatures
|
|
|
|
static const uchar s_icoheader[] = { 0x0, 0x0, 0x1, 0x0, 0x0 };
|
2022-10-13 01:29:13 +03:00
|
|
|
static const uchar s_icoheader1[] = { 0x0, 0x0, 0x1, 0x0, 0x01 };
|
|
|
|
static const uchar s_icoheader2[] = { 0x0, 0x0, 0x1, 0x0, 0x02 };
|
|
|
|
static const uchar s_icoheader3[] = { 0x0, 0x0, 0x1, 0x0, 0x03 };
|
|
|
|
static const uchar s_icoheader4[] = { 0x0, 0x0, 0x1, 0x0, 0x04 };
|
|
|
|
static const uchar s_icoheader5[] = { 0x0, 0x0, 0x1, 0x0, 0x05 };
|
|
|
|
static const uchar s_icoheader6[] = { 0x0, 0x0, 0x1, 0x0, 0x06 };
|
|
|
|
static const uchar s_icoheader7[] = { 0x0, 0x0, 0x1, 0x0, 0x07 };
|
|
|
|
static const uchar s_icoheader8[] = { 0x0, 0x0, 0x1, 0x0, 0x08 };
|
|
|
|
static const uchar s_icoheader9[] = { 0x0, 0x0, 0x1, 0x0, 0x09 };
|
2022-10-12 19:49:57 +03:00
|
|
|
|
|
|
|
static const struct HeadersTblData {
|
|
|
|
const uchar *header;
|
|
|
|
const int headersize;
|
|
|
|
} HeadersTbl[] = {
|
2022-10-12 21:24:03 +03:00
|
|
|
{ s_icoheader, 5 },
|
2022-10-13 01:29:13 +03:00
|
|
|
{ s_icoheader1, 5 },
|
|
|
|
{ s_icoheader2, 5 },
|
|
|
|
{ s_icoheader3, 5 },
|
|
|
|
{ s_icoheader4, 5 },
|
|
|
|
{ s_icoheader5, 5 },
|
|
|
|
{ s_icoheader6, 5 },
|
|
|
|
{ s_icoheader7, 5 },
|
|
|
|
{ s_icoheader8, 5 },
|
|
|
|
{ s_icoheader9, 5 }
|
2022-10-12 19:49:57 +03:00
|
|
|
};
|
|
|
|
static const qint16 HeadersTblSize = sizeof(HeadersTbl) / sizeof(HeadersTblData);
|
|
|
|
|
|
|
|
enum ICOType {
|
|
|
|
IconType = 1,
|
|
|
|
CursorType = 2
|
|
|
|
};
|
|
|
|
|
2022-10-13 01:21:54 +03:00
|
|
|
enum BMPCompression {
|
|
|
|
CompressionRGB = 0,
|
|
|
|
CompressionRLE8 = 1,
|
|
|
|
CompressionRLE4 = 2,
|
|
|
|
CompressionBitFields = 3,
|
|
|
|
CompressionJPEG = 4,
|
|
|
|
CompressionPNG = 5,
|
|
|
|
CompressionAlphaBitFields = 6,
|
|
|
|
CompressionCMYK = 7,
|
|
|
|
CompressionCMYKRLE8 = 8,
|
|
|
|
CompressionCMYKRLE4 = 9,
|
|
|
|
};
|
|
|
|
|
2022-10-12 19:49:57 +03:00
|
|
|
ICOHandler::ICOHandler()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
ICOHandler::~ICOHandler()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ICOHandler::canRead() const
|
|
|
|
{
|
|
|
|
if (ICOHandler::canRead(device())) {
|
|
|
|
setFormat(s_icopluginformat);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// for reference:
|
|
|
|
// https://en.wikipedia.org/wiki/ICO_(file_format)
|
|
|
|
// https://en.wikipedia.org/wiki/BMP_file_format
|
|
|
|
bool ICOHandler::read(QImage *image)
|
|
|
|
{
|
|
|
|
Q_ASSERT(sizeof(uchar) == 1);
|
|
|
|
Q_ASSERT(sizeof(ushort) == 2);
|
|
|
|
Q_ASSERT(sizeof(uint) == 4);
|
|
|
|
|
|
|
|
ushort icoreserved = 0;
|
|
|
|
ushort icotype = 0;
|
|
|
|
ushort iconimages = 0;
|
|
|
|
QDataStream datastream(device());
|
|
|
|
datastream.setByteOrder(QDataStream::LittleEndian);
|
|
|
|
|
|
|
|
datastream >> icoreserved;
|
|
|
|
datastream >> icotype;
|
|
|
|
datastream >> iconimages;
|
|
|
|
if (datastream.atEnd()) {
|
|
|
|
kWarning() << "Reached end of data before valid ICO";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-10-13 01:21:54 +03:00
|
|
|
if (icotype == ICOType::CursorType) {
|
2022-10-12 19:49:57 +03:00
|
|
|
kWarning() << "Cursor icons are not supported";
|
|
|
|
return false;
|
2022-10-13 01:21:54 +03:00
|
|
|
} else if (icotype != ICOType::IconType) {
|
2022-10-12 19:49:57 +03:00
|
|
|
kWarning() << "Invalid icon type" << icotype;
|
|
|
|
return false;
|
|
|
|
} else if (iconimages < 1) {
|
|
|
|
kDebug() << "No images" << iconimages;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (ushort ii = 0; ii < iconimages; ii++) {
|
|
|
|
uchar icowidth = 0;
|
|
|
|
uchar icoheight = 0;
|
|
|
|
uchar icocolors = 0;
|
|
|
|
uchar icoreserved2 = 0;
|
|
|
|
ushort icoplaneorhhs = 0;
|
|
|
|
ushort icobpporvhs = 0;
|
|
|
|
uint icoimagesize = 0;
|
|
|
|
uint icoimageoffset = 0;
|
|
|
|
datastream >> icowidth;
|
|
|
|
datastream >> icoheight;
|
|
|
|
datastream >> icocolors;
|
|
|
|
datastream >> icoreserved2;
|
|
|
|
datastream >> icoplaneorhhs;
|
|
|
|
datastream >> icobpporvhs;
|
|
|
|
datastream >> icoimagesize;
|
|
|
|
datastream >> icoimageoffset;
|
|
|
|
|
|
|
|
if (icoimageoffset > datastream.device()->size()) {
|
|
|
|
kWarning() << "Invalid image offset" << icoimageoffset;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-10-13 01:43:48 +03:00
|
|
|
if (icoimagesize >= INT_MAX) {
|
|
|
|
kWarning() << "ICO image size is too big" << icoimagesize;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-10-12 19:49:57 +03:00
|
|
|
datastream.device()->seek(icoimageoffset);
|
|
|
|
QByteArray imagebytes(icoimagesize, char(0));
|
|
|
|
if (datastream.readRawData(imagebytes.data(), icoimagesize) != icoimagesize) {
|
|
|
|
kWarning() << "Could not read image data";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-10-13 01:21:54 +03:00
|
|
|
if (imagebytes.size() > s_pngheadersize &&
|
|
|
|
::memcmp(imagebytes.constData(), s_pngheader, s_pngheadersize) != 0) {
|
2022-10-13 02:06:16 +03:00
|
|
|
datastream.device()->seek(icoimageoffset);
|
2022-10-12 19:49:57 +03:00
|
|
|
uint bmpheadersize = 0;
|
|
|
|
uint bmpwidth = 0;
|
|
|
|
uint bmpheight = 0;
|
|
|
|
ushort bmpplanes = 0;
|
|
|
|
ushort bmpbpp = 0;
|
|
|
|
uint bmpcompression = 0;
|
|
|
|
uint bmpimagesize = 0;
|
|
|
|
uint bmphppm = 0;
|
|
|
|
uint bmpvppm = 0;
|
|
|
|
uint bmpncolors = 0;
|
|
|
|
uint bmpnimportantcolors = 0;
|
|
|
|
datastream >> bmpheadersize;
|
|
|
|
datastream >> bmpwidth;
|
|
|
|
datastream >> bmpheight;
|
|
|
|
datastream >> bmpplanes;
|
|
|
|
datastream >> bmpbpp;
|
|
|
|
datastream >> bmpcompression;
|
|
|
|
datastream >> bmpimagesize;
|
|
|
|
datastream >> bmphppm;
|
|
|
|
datastream >> bmpvppm;
|
|
|
|
datastream >> bmpncolors;
|
|
|
|
datastream >> bmpnimportantcolors;
|
|
|
|
|
|
|
|
if (bmpheadersize != 40) {
|
|
|
|
kWarning() << "Invalid BMP info header size" << bmpheadersize;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-10-13 01:21:54 +03:00
|
|
|
if (bmpcompression != BMPCompression::CompressionRGB) {
|
|
|
|
kWarning() << "Unsupported BMP compression" << bmpcompression;
|
|
|
|
continue;
|
|
|
|
}
|
2022-10-12 19:49:57 +03:00
|
|
|
|
|
|
|
QImage::Format imageformat = QImage::Format_ARGB32;
|
|
|
|
switch (bmpbpp) {
|
|
|
|
case 32: {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default: {
|
|
|
|
// TODO:
|
|
|
|
kWarning() << "Unsupported BMP bytes per-pixel" << bmpbpp;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-13 01:43:48 +03:00
|
|
|
if (bmpimagesize >= INT_MAX) {
|
|
|
|
kWarning() << "BMP image size is too big" << bmpimagesize;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-10-12 19:49:57 +03:00
|
|
|
imagebytes.resize(bmpimagesize);
|
|
|
|
if (datastream.readRawData(imagebytes.data(), bmpimagesize) != bmpimagesize) {
|
|
|
|
kWarning() << "Could not read BMP image data";
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-10-13 01:21:54 +03:00
|
|
|
// fallbacks
|
|
|
|
const int imagewidth = (icowidth ? icowidth : bmpwidth);
|
|
|
|
const int imageheight = (icoheight ? icoheight : bmpheight);
|
|
|
|
|
2022-10-13 01:43:48 +03:00
|
|
|
if (imagewidth > USHRT_MAX || imageheight > USHRT_MAX) {
|
|
|
|
kWarning() << "Image width or height is too big" << imagewidth << imageheight;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-10-12 19:49:57 +03:00
|
|
|
QImage bmpimage(imagewidth, imageheight, imageformat);
|
|
|
|
if (bmpimage.isNull()) {
|
|
|
|
kWarning() << "Could not create BMP image" << imagewidth << imageheight << imageformat;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
QRgb* bmpimagebits = reinterpret_cast<QRgb*>(bmpimage.bits());
|
|
|
|
for (uint bi = 0; bi < bmpimagesize; bi += 4) {
|
|
|
|
*bmpimagebits = qRgba(imagebytes[bi + 2], imagebytes[bi + 1], imagebytes[bi], imagebytes[bi + 3]);
|
|
|
|
bmpimagebits++;
|
|
|
|
}
|
|
|
|
// pixel data is backwards so flip the image vertically
|
|
|
|
*image = bmpimage.mirrored(false, true);
|
|
|
|
kDebug() << "Valid BMP image" << ii;
|
2022-10-13 01:43:48 +03:00
|
|
|
return true;
|
2022-10-12 19:49:57 +03:00
|
|
|
}
|
|
|
|
|
2022-10-13 01:21:54 +03:00
|
|
|
const QImage pngimage = QImage::fromData(imagebytes.constData(), imagebytes.size(), "PNG");
|
|
|
|
if (!pngimage.isNull()) {
|
2022-10-12 19:49:57 +03:00
|
|
|
kDebug() << "Valid PNG image" << ii;
|
2022-10-13 01:21:54 +03:00
|
|
|
*image = pngimage;
|
2022-10-12 19:49:57 +03:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
kWarning() << "No images could be loaded";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ICOHandler::write(const QImage &image)
|
|
|
|
{
|
|
|
|
// this plugin is a read-only kind of plugin
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
QByteArray ICOHandler::name() const
|
|
|
|
{
|
|
|
|
return s_icopluginformat;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ICOHandler::canRead(QIODevice *device)
|
|
|
|
{
|
|
|
|
if (Q_UNLIKELY(!device)) {
|
|
|
|
kWarning() << "Called with no device";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const QByteArray data = device->peek(s_peekbuffsize);
|
|
|
|
|
|
|
|
// ICONDIR, ICONDIRENTRY and one bit of data
|
|
|
|
if (data.size() < 23) {
|
|
|
|
kDebug() << "Not enough data for ICO";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int i = 0; i < HeadersTblSize; i++) {
|
|
|
|
if (data.size() >= HeadersTbl[i].headersize &&
|
2022-10-12 21:24:03 +03:00
|
|
|
::memcmp(data.constData(), HeadersTbl[i].header, HeadersTbl[i].headersize) == 0) {
|
2022-10-12 19:49:57 +03:00
|
|
|
kDebug() << "Header detected";
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
QStringList ICOPlugin::keys() const
|
|
|
|
{
|
|
|
|
return QStringList() << s_icopluginformat;
|
|
|
|
}
|
|
|
|
|
|
|
|
QList<QByteArray> ICOPlugin::mimeTypes() const
|
|
|
|
{
|
|
|
|
static const QList<QByteArray> list = QList<QByteArray>()
|
|
|
|
<< "image/vnd.microsoft.icon";
|
|
|
|
return list;
|
|
|
|
}
|
|
|
|
|
|
|
|
QImageIOPlugin::Capabilities ICOPlugin::capabilities(QIODevice *device, const QByteArray &format) const
|
|
|
|
{
|
|
|
|
if (format == s_icopluginformat)
|
|
|
|
return QImageIOPlugin::Capabilities(QImageIOPlugin::CanRead);
|
|
|
|
if (!format.isEmpty())
|
|
|
|
return 0;
|
|
|
|
if (!device->isOpen())
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
QImageIOPlugin::Capabilities cap;
|
|
|
|
if (device->isReadable() && ICOHandler::canRead(device))
|
|
|
|
cap |= QImageIOPlugin::CanRead;
|
|
|
|
return cap;
|
|
|
|
}
|
|
|
|
|
|
|
|
QImageIOHandler *ICOPlugin::create(QIODevice *device, const QByteArray &format) const
|
|
|
|
{
|
|
|
|
QImageIOHandler *handler = new ICOHandler();
|
|
|
|
handler->setDevice(device);
|
|
|
|
handler->setFormat(format);
|
|
|
|
return handler;
|
|
|
|
}
|
|
|
|
|
|
|
|
Q_EXPORT_PLUGIN2(ico, ICOPlugin)
|