瀏覽代碼

IMG-25: load ICO files (#1458)

* disk_sink + fixes

* Always 4 bands for v3+ bmp on save

* bmpsave fixed (w/o orientation)

* fixed ico for target save

* icosave.c

* Removed leftover comments

* icoload.c

* Row buffer (skripatch) is not needed anymore

* Minor fixes for icoload/save

* Use wips_image_write instead of vips_copy

* Forgotten &

* Check resulting dimensions

* -debug printf
Victor Sokolov 2 月之前
父節點
當前提交
b18b678df2
共有 7 個文件被更改,包括 548 次插入6 次删除
  1. 2 0
      vips/bmp.h
  2. 111 0
      vips/bmpload.c
  3. 31 1
      vips/ico.h
  4. 392 0
      vips/icoload.c
  5. 4 1
      vips/icosave.c
  6. 6 0
      vips/vips.c
  7. 2 4
      vips/vips.go

+ 2 - 0
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);

+ 111 - 0
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

+ 31 - 1
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 <stdint.h>
+
+#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

+ 392 - 0
vips/icoload.c

@@ -0,0 +1,392 @@
+// ICO loader
+//
+// See: https://en.wikipedia.org/wiki/ICO_(file_format)
+
+#include "vips.h"
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+/**
+ * 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);
+}

+ 4 - 1
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;
 }
 

+ 6 - 0
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();
 

+ 2 - 4
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")
 	}