// license:BSD-3-Clause
// copyright-holders:Maurizio Petrarota, Victor Laskin
/***************************************************************************
ui/icorender.h
Windows icon file parser.
Previously based on code by Victor Laskin (victor.laskin@gmail.com)
http://vitiy.info/Code/ico.cpp
TODO:
* Add variant that loads all images from the file
* Allow size hint for choosing best candidate
* Allow selecting amongst candidates based on colour depth
***************************************************************************/
#include "emu.h"
#include "icorender.h"
#include "util/ioprocs.h"
#include "util/msdib.h"
#include "util/png.h"
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstring>
#include <tuple>
// need to set LOG_OUTPUT_FUNC or LOG_OUTPUT_STREAM because there's no logerror outside devices
#define LOG_OUTPUT_FUNC osd_printf_verbose
//#define VERBOSE (LOG_GENERAL)
#include "logmacro.h"
namespace ui {
namespace {
// ICO file header
struct icon_dir_t
{
uint16_t reserved; // must be 0
uint16_t type; // 1 for icon or 2 for cursor
uint16_t count; // number of images in the file
};
// ICO file directory entry
struct icon_dir_entry_t
{
constexpr unsigned get_width() const { return width ? width : 256U; }
constexpr unsigned get_height() const { return height ? height : 256U; }
void byteswap()
{
planes = little_endianize_int16(planes);
bpp = little_endianize_int16(bpp);
size = little_endianize_int32(size);
offset = little_endianize_int32(offset);
}
uint8_t width; // 0 means 256
uint8_t height; // 0 means 256
uint8_t colors; // for indexed colour, or 0 for direct colour
uint8_t reserved; // documentation says this should be 0 but .NET writes 255
uint16_t planes; // or hotspot X for cursor
uint16_t bpp; // 0 to infer from image data, or hotspot Y for cursor
uint32_t size; // image data size in bytes
uint32_t offset; // offset to image data from start of file
};
bool load_ico_png(util::random_read &fp, icon_dir_entry_t const &dir, bitmap_argb32 &bitmap)
{
// skip out if the data isn't a reasonable size - PNG magic alone is eight bytes
if (9U >= dir.size)
return false;
fp.seek(dir.offset, SEEK_SET);
std::error_condition const err(util::png_read_bitmap(fp, bitmap));
if (!err)
{
// found valid PNG image
assert(bitmap.valid());
if ((dir.get_width() == bitmap.width()) && ((dir.get_height() == bitmap.height())))
{
LOG("Loaded %d*%d pixel PNG image from ICO file\n", bitmap.width(), bitmap.height());
}
else
{
LOG(
"Loaded %d*%d pixel PNG image from ICO file (directory indicated %u*%u)\n",
bitmap.width(),
bitmap.height(),
dir.get_width(),
dir.get_height());
}
return true;
}
else if (util::png_error::BAD_SIGNATURE == err)
{
// doesn't look like PNG data - just fall back to DIB without the file header
return false;
}
else
{
// invalid PNG data or I/O error
LOG(
"Error %s:%d %s reading PNG image data from ICO file at offset %u (directory size %u)\n",
err.category().name(),
err.value(),
err.message(),
dir.offset,
dir.size);
return false;
}
}
bool load_ico_dib(util::random_read &fp, icon_dir_entry_t const &dir, bitmap_argb32 &bitmap)
{
fp.seek(dir.offset, SEEK_SET);
util::msdib_error const err(util::msdib_read_bitmap_data(fp, bitmap, dir.size, dir.get_height()));
switch (err)
{
case util::msdib_error::NONE:
// found valid DIB image
assert(bitmap.valid());
if ((dir.get_width() == bitmap.width()) && ((dir.get_height() == bitmap.height())))
{
LOG("Loaded %d*%d pixel DIB image from ICO file\n", bitmap.width(), bitmap.height());
}
else
{
LOG(
"Loaded %d*%d pixel DIB image from ICO file (directory indicated %u*%u)\n",
bitmap.width(),
bitmap.height(),
dir.get_width(),
dir.get_height());
}
return true;
default:
// invalid DIB data or I/O error
LOG(
"Error %u reading DIB image data from ICO file at offset %u (directory size %u)\n",
unsigned(err),
dir.offset,
dir.size);
return false;
}
}
bool load_ico_image(util::random_read &fp, unsigned index, icon_dir_entry_t const &dir, bitmap_argb32 &bitmap)
{
// try loading PNG image data (contains PNG file magic if used), and then fall back
if (load_ico_png(fp, dir, bitmap))
{
LOG("Successfully loaded PNG image from ICO file entry %u\n", index);
return true;
}
else if (load_ico_dib(fp, dir, bitmap))
{
LOG("Successfully loaded DIB image from ICO file entry %u\n", index);
return true;
}
// no luck
return false;
}
bool load_ico_image(util::random_read &fp, unsigned count, unsigned index, bitmap_argb32 &bitmap)
{
// read the directory entry
std::error_condition err;
size_t actual;
icon_dir_entry_t dir;
err = fp.seek(sizeof(icon_dir_t) + (sizeof(icon_dir_entry_t) * index), SEEK_SET);
if (!err)
std::tie(err, actual) = read(fp, &dir, sizeof(dir));
if (err || (sizeof(dir) != actual))
{
LOG("Failed to read ICO file directory entry %u\n", index);
return false;
}
dir.byteswap();
if ((sizeof(icon_dir_t) + (sizeof(icon_dir_entry_t) * count)) > dir.offset)
{
LOG(
"ICO file image %u data starting at %u overlaps %u bytes of file header and directory\n",
index,
dir.offset,
sizeof(icon_dir_t) + (sizeof(icon_dir_entry_t) * count));
return false;
}
else
{
return load_ico_image(fp, index, dir, bitmap);
}
}
} // anonymous namespace
int images_in_ico(util::random_read &fp)
{
// read and check the icon file header
std::error_condition err;
size_t actual;
icon_dir_t header;
err = fp.seek(0, SEEK_SET);
if (!err)
std::tie(err, actual) = read(fp, &header, sizeof(header));
if (err || (sizeof(header) != actual))
{
LOG("Failed to read ICO file header\n");
return -1;
}
header.reserved = little_endianize_int16(header.reserved);
header.type = little_endianize_int16(header.type);
header.count = little_endianize_int16(header.count);
if (0U != header.reserved)
{
LOG("Invalid ICO file header reserved field %u (expected 0)\n", header.reserved);
return -1;
}
if ((1U != header.type) && (2U != header.type))
{
LOG("Invalid ICO file header type field %u (expected 1 or 2)\n", header.type);
return -1;
}
return int(unsigned(little_endianize_int16(header.count)));
}
void render_load_ico(util::random_read &fp, unsigned index, bitmap_argb32 &bitmap)
{
// check that these things haven't been padded somehow
static_assert(sizeof(icon_dir_t) == 6U, "compiler has applied padding to icon_dir_t");
static_assert(sizeof(icon_dir_entry_t) == 16U, "compiler has applied padding to icon_dir_entry_t");
// read and check the icon file header, then try to load the specified image
int const count(images_in_ico(fp));
if (0 > count)
{
// images_in_ico already logged an error
}
else if (index >= count)
{
osd_printf_verbose("Requested image %u from ICO file containing %d images\n", index, count);
}
else if (load_ico_image(fp, count, index, bitmap))
{
return;
}
bitmap.reset();
}
void render_load_ico_first(util::random_read &fp, bitmap_argb32 &bitmap)
{
int const count(images_in_ico(fp));
for (int i = 0; count > i; ++i)
{
if (load_ico_image(fp, count, i, bitmap))
return;
}
bitmap.reset();
}
void render_load_ico_highest_detail(util::random_read &fp, bitmap_argb32 &bitmap)
{
// read and check the icon file header - logs a message on error
int const count(images_in_ico(fp));
if (0 > count)
{
bitmap.reset();
return;
}
// now load all the directory entries
size_t const dir_bytes(sizeof(icon_dir_entry_t) * count);
std::unique_ptr<icon_dir_entry_t []> dir(new (std::nothrow) icon_dir_entry_t [count]);
std::unique_ptr<unsigned []> index(new (std::nothrow) unsigned [count]);
if (count)
{
if (!dir || !index)
{
LOG("Failed to allocate memory for ICO file directory entries\n");
bitmap.reset();
return;
}
auto const [err, actual] = read(fp, dir.get(), dir_bytes);
if (err || (dir_bytes != actual))
{
LOG("Failed to read ICO file directory entries\n");
bitmap.reset();
return;
}
}
// byteswap and sort by (pixels, depth)
for (int i = 0; count > i; ++i)
{
dir[i].byteswap();
index[i] = i;
}
std::stable_sort(
index.get(),
index.get() + count,
[&dir] (unsigned x, unsigned y)
{
unsigned const x_pixels(dir[x].get_width() * dir[x].get_height());
unsigned const y_pixels(dir[y].get_width() * dir[y].get_height());
if (x_pixels > y_pixels)
return true;
else if (x_pixels < y_pixels)
return false;
else
return dir[x].bpp > dir[y].bpp;
});
// walk down until something works
for (int i = 0; count > i; ++i)
{
LOG(
"Try loading ICO file entry %u: %u*%u, %u bits per pixel\n",
index[i],
dir[index[i]].get_width(),
dir[index[i]].get_height(),
dir[index[i]].bpp);
if (load_ico_image(fp, index[i], dir[index[i]], bitmap))
return;
}
bitmap.reset();
}
} // namespace ui