diff --git a/vips/bmp.h b/vips/bmp.h index c46fe58d..83e1bc61 100644 --- a/vips/bmp.h +++ b/vips/bmp.h @@ -26,6 +26,8 @@ VIPS_API int vips_bmpload_source(VipsSource *source, VipsImage **out, ...) G_GNUC_NULL_TERMINATED; int vips_bmpload_source_go(VipsImgproxySource *source, VipsImage **out); +int +vips_bmpload_buffer(void *buf, size_t len, VipsImage **out, ...); // defined in bmpsave.c int vips_bmpsave_target_go(VipsImage *in, VipsTarget *target); diff --git a/vips/bmpload.c b/vips/bmpload.c index b74ec9a4..906da842 100644 --- a/vips/bmpload.c +++ b/vips/bmpload.c @@ -795,6 +795,117 @@ vips_foreign_load_bmp_source_init(VipsForeignLoadBmpSource *source) { } +typedef struct _VipsForeignLoadBmpBuffer { + VipsForeignLoadBmp parent_object; + + /* Load from a buffer. + */ + VipsBlob *blob; + +} VipsForeignLoadBmpBuffer; + +typedef VipsForeignLoadBmpClass VipsForeignLoadBmpBufferClass; + +G_DEFINE_TYPE(VipsForeignLoadBmpBuffer, vips_foreign_load_bmp_buffer, + vips_foreign_load_bmp_get_type()); + +static int +vips_foreign_load_bmp_buffer_build(VipsObject *object) +{ + VipsForeignLoadBmp *bmp = (VipsForeignLoadBmp *) object; + VipsForeignLoadBmpBuffer *buffer = (VipsForeignLoadBmpBuffer *) object; + + if (buffer->blob && + !(bmp->source = vips_source_new_from_memory( + VIPS_AREA(buffer->blob)->data, + VIPS_AREA(buffer->blob)->length))) + return -1; + + return VIPS_OBJECT_CLASS(vips_foreign_load_bmp_buffer_parent_class) + ->build(object); +} + +/** + * Checks if the source is a BMP image + */ +static gboolean +vips_foreign_load_bmp_buffer_is_a_buffer(const void *buf, size_t len) +{ + if (len < 2) { + vips_error("vips_foreign_load_bmp_buffer_is_a_buffer", "unable to sniff source"); + return 0; + } + + return ((VipsPel *) buf)[0] == 'B' && + ((VipsPel *) buf)[1] == 'M'; +} + +static void +vips_foreign_load_bmp_buffer_class_init(VipsForeignLoadBmpBufferClass *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(class); + VipsObjectClass *object_class = (VipsObjectClass *) class; + VipsForeignLoadClass *load_class = (VipsForeignLoadClass *) class; + + gobject_class->set_property = vips_object_set_property; + gobject_class->get_property = vips_object_get_property; + + object_class->nickname = "bmpload_buffer"; + object_class->description = "load bmp from buffer"; + object_class->build = vips_foreign_load_bmp_buffer_build; + + load_class->is_a_buffer = vips_foreign_load_bmp_buffer_is_a_buffer; + + VIPS_ARG_BOXED(class, "buffer", 1, + "Buffer", + "Buffer to load from", + VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(VipsForeignLoadBmpBuffer, blob), + VIPS_TYPE_BLOB); +} + +static void +vips_foreign_load_bmp_buffer_init(VipsForeignLoadBmpBuffer *buffer) +{ +} + +/** + * vips_pngload_buffer: + * @buf: (array length=len) (element-type guint8): memory area to load + * @len: (type gsize): size of memory area + * @out: (out): image to write + * @...: `NULL`-terminated list of optional named arguments + * + * Exactly as [ctor@Image.pngload], but read from a PNG-formatted memory block. + * + * You must not free the buffer while @out is active. The + * [signal@Object::postclose] signal on @out is a good place to free. + * + * ::: seealso + * [ctor@Image.pngload]. + * + * Returns: 0 on success, -1 on error. + */ +int +vips_bmpload_buffer(void *buf, size_t len, VipsImage **out, ...) +{ + va_list ap; + VipsBlob *blob; + int result; + + /* We don't take a copy of the data or free it. + */ + blob = vips_blob_new(NULL, buf, len); + + va_start(ap, out); + result = vips_call_split("bmpload_buffer", ap, blob, out); + va_end(ap); + + vips_area_unref(VIPS_AREA(blob)); + + return result; +} + /** * vips_bmpload_source: * @source: source to load diff --git a/vips/ico.h b/vips/ico.h index 27f3ce30..bf029422 100644 --- a/vips/ico.h +++ b/vips/ico.h @@ -1,10 +1,40 @@ /* - * ICO save/load + * ICO save/load. ICO is a container for one+ PNG/BMP images. */ #ifndef __ICO_H__ #define __ICO_H__ +#include + +#define ICO_TYPE_ICO 1 +#define ICO_TYPE_CURSOR 2 + +// ICO file header +typedef struct __attribute__((packed)) _ICONDIR_IcoHeader { + uint16_t reserved; // Reserved, always 0 + uint16_t type; // 1 for ICO, 2 for CUR + uint16_t image_count; // Number of images in the file +} ICONDIR_IcoHeader; + +// ICO image header +typedef struct __attribute__((packed)) _ICONDIRENTRY_IcoHeader { + uint8_t width; // Width of the icon in pixels (0 for 256) + uint8_t height; // Height of the icon in pixels (0 for 256 + uint8_t number_of_colors; // Number of colors, not used in our case + uint8_t reserved; // Reserved, always 0 + uint16_t color_planes; // Color planes, always 1 + uint16_t bpp; // Bits per pixel + uint32_t data_size; // Image data size + uint32_t data_offset; // Image data offset, always 22 +} ICONDIRENTRY_IcoHeader; + // defined in icosave.c int vips_icosave_target_go(VipsImage *in, VipsTarget *target); +// defined in icoload.c +VIPS_API +int icoload_source(VipsSource *source, VipsImage **out, ...) + G_GNUC_NULL_TERMINATED; +int vips_icoload_source_go(VipsImgproxySource *source, VipsImage **out); + #endif diff --git a/vips/icoload.c b/vips/icoload.c new file mode 100644 index 00000000..fef648ca --- /dev/null +++ b/vips/icoload.c @@ -0,0 +1,392 @@ +// ICO loader +// +// See: https://en.wikipedia.org/wiki/ICO_(file_format) + +#include "vips.h" + +#include +#include +#include + +/** + * ICO ForeignLoad VIPS class implementation (generic) + */ +typedef struct _VipsForeignLoadIco { + VipsForeignLoad parent_object; + VipsSource *source; + + VipsImage **internal; // internal image +} VipsForeignLoadIco; + +typedef VipsForeignLoadClass VipsForeignLoadIcoClass; + +G_DEFINE_ABSTRACT_TYPE(VipsForeignLoadIco, vips_foreign_load_ico, + VIPS_TYPE_FOREIGN_LOAD); + +static void +vips_foreign_load_ico_dispose(GObject *gobject) +{ + VipsForeignLoadIco *ico = (VipsForeignLoadIco *) gobject; + + VIPS_UNREF(ico->source); + + G_OBJECT_CLASS(vips_foreign_load_ico_parent_class)->dispose(gobject); +} + +static int +vips_foreign_load_ico_build(VipsObject *object) +{ + VipsForeignLoadIco *ico = (VipsForeignLoadIco *) object; + + return VIPS_OBJECT_CLASS(vips_foreign_load_ico_parent_class) + ->build(object); +} + +static VipsForeignFlags +vips_foreign_load_ico_get_flags(VipsForeignLoad *load) +{ + return VIPS_FOREIGN_SEQUENTIAL; +} + +/** + * Checks if the source is a ICO image + */ +static gboolean +vips_foreign_load_ico_source_is_a_source(VipsSource *source) +{ + // There is no way of detecting ICO files (it has no signature). + // However, vips requires this method to be present. + unsigned char *buf = vips_source_sniff(source, 4); + if (!buf) { + vips_error("vips_foreign_load_bmp_source_is_a_source", "unable to sniff source"); + return 0; + } + + return buf[0] == 0 && buf[1] == 0 && buf[2] == 1 && buf[3] == 0; +} + +/** + * Checks if the ICO image is a PNG image. + */ +static bool +vips_foreign_load_ico_is_png(VipsForeignLoadIco *ico, VipsPel *data, uint32_t data_size) +{ + // Check if the ICO data is PNG + // ICO files can contain PNG images, so we need to check the magic bytes + if (data_size < 8) { + return false; // Not enough data to be a PNG + } + + // Check the PNG signature + return (data[0] == 137 && data[1] == 'P' && data[2] == 'N' && + data[3] == 'G' && data[4] == '\r' && data[5] == '\n' && + data[6] == 26 && data[7] == '\n'); +} + +void +vips_foreign_load_ico_free_buffer( + VipsObject *self, + gpointer user_data) +{ + VIPS_FREE(user_data); +} + +/** + * Loads the header of the ICO image from the source. + */ +static int +vips_foreign_load_ico_header(VipsForeignLoad *load) +{ + VipsForeignLoadIco *ico = (VipsForeignLoadIco *) load; + + // Rewind the source to the beginning + if (vips_source_rewind(ico->source)) + return -1; + + ICONDIR_IcoHeader file_header; + + // Read the header + if (vips_foreign_load_read_full(ico->source, &file_header, sizeof(file_header)) <= 0) { + vips_error("vips_foreign_load_ico_header", "unable to read file header from the source"); + return -1; + } + + // Get the image count from the file header + uint16_t count = GUINT16_FROM_LE(file_header.image_count); + + // Now, let's find the largest image in the set + ICONDIRENTRY_IcoHeader largest_image_header; + memset(&largest_image_header, 0, sizeof(largest_image_header)); + + for (int i = 0; i < count; i++) { + ICONDIRENTRY_IcoHeader image_header; + + // Read the next header + if (vips_foreign_load_read_full(ico->source, &image_header, sizeof(image_header)) <= 0) { + vips_error("vips_foreign_load_ico_header", "unable to read file header from the source"); + return -1; + } + + // this image width/height is greater than the largest image width/height + // or this image width/height is 0 (which means 256) + bool image_is_larger = ((image_header.width > largest_image_header.width) || (image_header.height > largest_image_header.height) || (image_header.width == 0) || (image_header.height == 0)); + + // Update the largest image header + if (image_is_larger) { + memcpy(&largest_image_header, &image_header, sizeof(largest_image_header)); + } + } + + // We failed to find any image which fits + if (largest_image_header.data_offset == 0) { + vips_error("vips_foreign_load_ico_header", "ICO file has no image which fits"); + return -1; + } + + // Let's move to the ico image data offset. + if (vips_source_seek(ico->source, GUINT32_FROM_LE(largest_image_header.data_offset), SEEK_SET) < 1) { + vips_error("vips_foreign_load_ico_header", "unable to seek to ICO image data"); + return -1; + } + + // Read the image into memory (otherwise, it would be too complex to handle). It's fine for ICO: + // ICO files are usually small, and we can read them into memory without any issues. + uint32_t data_size = GUINT32_FROM_LE(largest_image_header.data_size); + + // BMP file explicitly excludes BITMAPFILEHEADER, so we need to add it manually. We reserve + // space for it at the beginning of the data buffer. + VipsPel *data = (VipsPel *) VIPS_MALLOC(NULL, data_size + BMP_FILE_HEADER_LEN); + void *actual_data = data + BMP_FILE_HEADER_LEN; + + if (vips_foreign_load_read_full(ico->source, actual_data, data_size) <= 0) { + vips_error("vips_foreign_load_ico_header", "unable to read ICO image data from the source"); + return -1; + } + + // Now, let's load the internal image + ico->internal = (VipsImage **) vips_object_local_array(VIPS_OBJECT(load), 1); + + if (vips_foreign_load_ico_is_png(ico, actual_data, data_size)) { + if ( + vips_pngload_buffer( + actual_data, data_size, + &ico->internal[0], + "access", VIPS_ACCESS_SEQUENTIAL, + NULL) < 0) { + VIPS_FREE(data); + vips_error("vips_foreign_load_ico_header", "unable to load ICO image as PNG"); + return -1; + } + } + else { + // Otherwise, we assume it's a BMP image. + // According to ICO file format, it explicitly excludes BITMAPFILEHEADER (why???), + // hence, we need to restore it to make bmp loader work. + + // Read num_colors and bpp from the BITMAPINFOHEADER + uint32_t num_colors = GUINT32_FROM_LE(*(uint32_t *) (actual_data + 32)); + uint16_t bpp = GUINT16_FROM_LE(*(uint16_t *) (actual_data + 14)); + uint32_t pix_offset; + + if ((num_colors == 0) && (bpp <= 8)) { + // If there are no colors and bpp is <= 8, we assume it's a palette image + pix_offset = BMP_FILE_HEADER_LEN + BMP_BITMAP_INFO_HEADER_LEN + 4 * (1 << bpp); + } + else { + // Otherwise, we use the number of colors + pix_offset = BMP_FILE_HEADER_LEN + BMP_BITMAP_INFO_HEADER_LEN + 4 * num_colors; + } + + // ICO file used to store alpha mask. By historical reasons, height of the ICO bmp + // is still stored doubled to cover the alpha mask data even if they're zero + // or not present. + int32_t height = GINT32_FROM_LE(*(int32_t *) (actual_data + 8)); + height = height / 2; + + // Magic bytes + data[0] = 'B'; + data[1] = 'M'; + + // Size of the BMP file (data size + BMP file header length) + (*(uint32_t *) (data + 2)) = GUINT32_TO_LE(data_size + BMP_FILE_HEADER_LEN); + (*(uint32_t *) (data + 6)) = 0; // reserved + (*(uint32_t *) (data + 10)) = GUINT32_TO_LE(pix_offset); // offset to the pixel data + (*(int32_t *) (actual_data + 8)) = GINT32_TO_LE(height); // height + + if ( + vips_bmpload_buffer( + data, data_size + BMP_FILE_HEADER_LEN, + &ico->internal[0], + "access", VIPS_ACCESS_SEQUENTIAL, + NULL) < 0) { + VIPS_FREE(data); + vips_error("vips_foreign_load_ico_header", "unable to load ICO image as BMP"); + return -1; + } + } + + // It is recommended that we free the buffer in postclose callback + g_signal_connect( + ico->internal[0], "postclose", + G_CALLBACK(vips_foreign_load_ico_free_buffer), data); + + // Check that target dimensions match the source dimensions. + int lhw = largest_image_header.width == 0 ? 256 : largest_image_header.width; + int lhh = largest_image_header.height == 0 ? 256 : largest_image_header.height; + + if (vips_image_get_width(ico->internal[0]) != lhw || + vips_image_get_height(ico->internal[0]) != lhh) { + vips_error("vips_foreign_load_ico_header", "ICO image has unexpected dimensions"); + return -1; + } + + // Copy the image metadata parameters to the load->out image. + // This should be sufficient, as we do not care much about the rest of the + // metadata inside .ICO files. At least, at this stage. + vips_image_init_fields( + load->out, + vips_image_get_width(ico->internal[0]), + vips_image_get_height(ico->internal[0]), + vips_image_get_bands(ico->internal[0]), + vips_image_get_format(ico->internal[0]), + vips_image_get_coding(ico->internal[0]), + vips_image_get_interpretation(ico->internal[0]), + vips_image_get_xres(ico->internal[0]), + vips_image_get_yres(ico->internal[0])); + + vips_source_minimise(ico->source); + + return 0; +} + +/** + * Loads a ICO image from the source. + */ +static int +vips_foreign_load_ico_load(VipsForeignLoad *load) +{ + VipsForeignLoadIco *ico = (VipsForeignLoadIco *) load; + VipsImage *image = ico->internal[0]; + + // Just copy the internal image to the output image + if (vips_image_write(image, load->real)) { + vips_error("vips_foreign_load_ico_load", "unable to copy ICO image to output"); + return -1; + } + + return 0; +} + +static void +vips_foreign_load_ico_class_init(VipsForeignLoadIcoClass *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(class); + VipsObjectClass *object_class = (VipsObjectClass *) class; + VipsForeignClass *foreign_class = (VipsForeignClass *) class; + VipsForeignLoadClass *load_class = (VipsForeignLoadClass *) class; + + gobject_class->dispose = vips_foreign_load_ico_dispose; + gobject_class->set_property = vips_object_set_property; + gobject_class->get_property = vips_object_get_property; + + object_class->nickname = "icoload_base"; + object_class->description = "load ico image (internal format for video thumbs)"; + object_class->build = vips_foreign_load_ico_build; + + load_class->get_flags = vips_foreign_load_ico_get_flags; + load_class->header = vips_foreign_load_ico_header; + load_class->load = vips_foreign_load_ico_load; +} + +static void +vips_foreign_load_ico_init(VipsForeignLoadIco *load) +{ +} + +typedef struct _VipsForeignLoadIcoSource { + VipsForeignLoadIco parent_object; + + VipsSource *source; +} VipsForeignLoadIcoSource; + +typedef VipsForeignLoadIcoClass VipsForeignLoadIcoSourceClass; + +G_DEFINE_TYPE(VipsForeignLoadIcoSource, vips_foreign_load_ico_source, + vips_foreign_load_ico_get_type()); + +static int +vips_foreign_load_ico_source_build(VipsObject *object) +{ + VipsForeignLoadIco *ico = (VipsForeignLoadIco *) object; + VipsForeignLoadIcoSource *source = + (VipsForeignLoadIcoSource *) object; + + if (source->source) { + ico->source = source->source; + g_object_ref(ico->source); + } + + return VIPS_OBJECT_CLASS(vips_foreign_load_ico_source_parent_class) + ->build(object); +} + +static void +vips_foreign_load_ico_source_class_init( + VipsForeignLoadIcoSourceClass *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(class); + VipsObjectClass *object_class = (VipsObjectClass *) class; + VipsOperationClass *operation_class = VIPS_OPERATION_CLASS(class); + VipsForeignLoadClass *load_class = (VipsForeignLoadClass *) class; + + gobject_class->set_property = vips_object_set_property; + gobject_class->get_property = vips_object_get_property; + + object_class->nickname = "icoload_source"; + object_class->description = "load image from ico source"; + object_class->build = vips_foreign_load_ico_source_build; + + load_class->is_a_source = vips_foreign_load_ico_source_is_a_source; + + VIPS_ARG_OBJECT(class, "source", 1, + "Source", + "Source to load from", + VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET(VipsForeignLoadIcoSource, source), + VIPS_TYPE_SOURCE); +} + +static void +vips_foreign_load_ico_source_init(VipsForeignLoadIcoSource *source) +{ +} + +/** + * vips_icoload_source: + * @source: source to load + * @out: (out): image to write + * @...: `NULL`-terminated list of optional named arguments + * + * Read a RAWRGB-formatted memory block into a VIPS image. + * + * Returns: 0 on success, -1 on error. + */ +int +vips_icoload_source(VipsSource *source, VipsImage **out, ...) +{ + va_list ap; + int result; + + va_start(ap, out); + result = vips_call_split("icoload_source", ap, source, out); + va_end(ap); + + return result; +} + +// wrapper function which hiders varargs (...) from CGo +int +vips_icoload_source_go(VipsImgproxySource *source, VipsImage **out) +{ + return vips_icoload_source(VIPS_SOURCE(source), out, "access", VIPS_ACCESS_SEQUENTIAL, NULL); +} diff --git a/vips/icosave.c b/vips/icosave.c index bcfb2973..11c9fd46 100644 --- a/vips/icosave.c +++ b/vips/icosave.c @@ -121,7 +121,7 @@ vips_foreign_save_ico_build(VipsObject *object) } // Write data - if (vips_target_write(ico->target, &buffer, sizeof(header))) { + if (vips_target_write(ico->target, buffer, data_size)) { g_free(buffer); vips_error("vips_foreign_save_ico_build", "unable to write ICO header to target"); return -1; @@ -129,6 +129,9 @@ vips_foreign_save_ico_build(VipsObject *object) g_free(buffer); + if (vips_target_end(ico->target)) + return -1; + return 0; } diff --git a/vips/vips.c b/vips/vips.c index 552caa78..f43922e4 100644 --- a/vips/vips.c +++ b/vips/vips.c @@ -14,9 +14,15 @@ vips_initialize() extern GType vips_foreign_load_bmp_source_get_type(void); vips_foreign_load_bmp_source_get_type(); + extern GType vips_foreign_load_bmp_buffer_get_type(void); + vips_foreign_load_bmp_buffer_get_type(); + extern GType vips_foreign_save_bmp_target_get_type(void); vips_foreign_save_bmp_target_get_type(); + extern GType vips_foreign_load_ico_source_get_type(void); + vips_foreign_load_ico_source_get_type(); + extern GType vips_foreign_save_ico_target_get_type(void); vips_foreign_save_ico_target_get_type(); diff --git a/vips/vips.go b/vips/vips.go index caac4a59..5a725df3 100644 --- a/vips/vips.go +++ b/vips/vips.go @@ -355,10 +355,6 @@ func (img *Image) Pages() int { } func (img *Image) Load(imgdata *imagedata.ImageData, shrink int, scale float64, pages int) error { - if imgdata.Type == imagetype.ICO { - return img.loadIco(imgdata.Data, shrink, scale, pages) - } - var tmp *C.VipsImage err := C.int(0) @@ -386,6 +382,8 @@ func (img *Image) Load(imgdata *imagedata.ImageData, shrink int, scale float64, err = C.vips_tiffload_source_go(source, &tmp) case imagetype.BMP: err = C.vips_bmpload_source_go(source, &tmp) + case imagetype.ICO: + err = C.vips_icoload_source_go(source, &tmp) default: return newVipsError("Usupported image type to load") }