The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
/*
=head1 NAME

tags.c - functions for manipulating an images tags list

=head1 SYNOPSIS

  i_img_tags tags;
  i_tags_new(&tags);
  i_tags_destroy(&tags);
  i_tags_addn(&tags, "name", code, idata);
  i_tags_add(&tags, "name", code, data, data_size, idata);
  if (i_tags_find(&tags, name, start, &entry)) { found }
  if (i_tags_findn(&tags, code, start, &entry)) { found }
  i_tags_delete(&tags, index);
  count = i_tags_delbyname(tags, name);
  count = i_tags_delbycode(tags, code);
  if (i_tags_get_float(&tags, name, code, &float_value)) { found }
  i_tags_set_float(&tags, name, code, value);
  i_tags_set_float2(&tags, name, code, value, sig_digits);
  i_tags_get_int(&tags, name, code, &int_value);

=head1 DESCRIPTION

Provides functions which give write access to the tags list of an image.

For read access directly access the fields (do not write any fields
directly).

A tag is represented by an i_img_tag structure:

  typedef enum {
    itt_double,
    iit_text
  } i_tag_type;

  typedef struct {
    char *name; // name of a given tag, might be NULL 
    int code; // number of a given tag, -1 if it has no meaning 
    char *data; // value of a given tag if it's not an int, may be NULL 
    int size; // size of the data 
    int idata; // value of a given tag if data is NULL 
  } i_img_tag;


=over

=cut
*/

#include "imager.h"
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>

/* useful for debugging */
void i_tags_print(i_img_tags *tags);

/*
=item i_tags_new(i_img_tags *tags)

=category Tags

Initialize a tags structure.  Should not be used if the tags structure
has been previously used.

This should be called tags member of an i_img object on creation (in
i_img_*_new() functions).

To destroy the contents use i_tags_destroy()

=cut
*/

void i_tags_new(i_img_tags *tags) {
  tags->count = tags->alloc = 0;
  tags->tags = NULL;
}

/*
=item i_tags_addn(i_img_tags *tags, char *name, int code, int idata)

Adds a tag that has an integer value.  A simple wrapper around i_tags_add().

Use i_tags_setn() instead, this function may be removed in the future.

Returns non-zero on success.

=cut
*/

int i_tags_addn(i_img_tags *tags, char const *name, int code, int idata) {
  return i_tags_add(tags, name, code, NULL, 0, idata);
}

/*
=item i_tags_add(i_img_tags *tags, char *name, int code, char *data, int size, i_tag_type type, int idata)

Adds a tag to the tags list.

Use i_tags_set() instead, this function may be removed in the future.

Returns non-zero on success.

=cut
*/

int i_tags_add(i_img_tags *tags, char const *name, int code, char const *data, 
               int size, int idata) {
  i_img_tag work = {0};
  /*printf("i_tags_add(tags %p [count %d], name %s, code %d, data %p, size %d, idata %d)\n",
    tags, tags->count, name, code, data, size, idata);*/
  if (tags->tags == NULL) {
    int alloc = 10;
    tags->tags = mymalloc(sizeof(i_img_tag) * alloc);
    if (!tags->tags)
      return 0;
    tags->alloc = alloc;
  }
  else if (tags->count == tags->alloc) {
    int newalloc = tags->alloc + 10;
    void *newtags = myrealloc(tags->tags, sizeof(i_img_tag) * newalloc);
    if (!newtags) {
      return 0;
    }
    tags->tags = newtags;
    tags->alloc = newalloc;
  }
  if (name) {
    work.name = mymalloc(strlen(name)+1);
    if (!work.name)
      return 0;
    strcpy(work.name, name);
  }
  if (data) {
    if (size == -1)
      size = strlen(data);
    work.data = mymalloc(size+1);
    if (!work.data) {
      if (work.name) myfree(work.name);
      return 0;
    }
    memcpy(work.data, data, size);
    work.data[size] = '\0'; /* convenience */
    work.size = size;
  }
  work.code = code;
  work.idata = idata;
  tags->tags[tags->count++] = work;

  /*i_tags_print(tags);*/

  return 1;
}

/*
=item i_tags_destroy(tags)

=category Tags

Destroys the given tags structure.  Called by i_img_destroy().

=cut
*/

void i_tags_destroy(i_img_tags *tags) {
  if (tags->tags) {
    int i;
    for (i = 0; i < tags->count; ++i) {
      if (tags->tags[i].name)
	myfree(tags->tags[i].name);
      if (tags->tags[i].data)
	myfree(tags->tags[i].data);
    }
    myfree(tags->tags);
  }
}

/*
=item i_tags_find(tags, name, start, &entry)

=category Tags

Searches for a tag of the given I<name> starting from index I<start>.

On success returns true and sets *I<entry>.

On failure returns false.

=cut
*/

int i_tags_find(i_img_tags *tags, char const *name, int start, int *entry) {
  if (tags->tags) {
    while (start < tags->count) {
      if (tags->tags[start].name && strcmp(name, tags->tags[start].name) == 0) {
	*entry = start;
	return 1;
      }
      ++start;
    }
  }
  return 0;
}

/*
=item i_tags_findn(tags, code, start, &entry)

=category Tags

Searches for a tag of the given I<code> starting from index I<start>.

On success returns true and sets *I<entry>.

On failure returns false.

=cut
*/

int i_tags_findn(i_img_tags *tags, int code, int start, int *entry) {
  if (tags->tags) {
    while (start < tags->count) {
      if (tags->tags[start].code == code) {
	*entry = start;
	return 1;
      }
      ++start;
    }
  }
  return 0;
}

/*
=item i_tags_delete(tags, index)

=category Tags

Delete a tag by index.

Returns true on success.

=cut
*/
int i_tags_delete(i_img_tags *tags, int entry) {
  /*printf("i_tags_delete(tags %p [count %d], entry %d)\n",
    tags, tags->count, entry);*/
  if (tags->tags && entry >= 0 && entry < tags->count) {
    i_img_tag old = tags->tags[entry];
    memmove(tags->tags+entry, tags->tags+entry+1,
	    (tags->count-entry-1) * sizeof(i_img_tag));
    if (old.name)
      myfree(old.name);
    if (old.data)
      myfree(old.data);
    --tags->count;

    return 1;
  }
  return 0;
}

/*
=item i_tags_delbyname(tags, name)

=category Tags

Delete any tags with the given name.

Returns the number of tags deleted.

=cut
*/

int i_tags_delbyname(i_img_tags *tags, char const *name) {
  int count = 0;
  int i;
  /*printf("i_tags_delbyname(tags %p [count %d], name %s)\n",
    tags, tags->count, name);*/
  if (tags->tags) {
    for (i = tags->count-1; i >= 0; --i) {
      if (tags->tags[i].name && strcmp(name, tags->tags[i].name) == 0) {
        ++count;
        i_tags_delete(tags, i);
      }
    }
  }
  /*i_tags_print(tags);*/

  return count;
}

/*
=item i_tags_delbycode(tags, code)

=category Tags

Delete any tags with the given code.

Returns the number of tags deleted.

=cut
*/

int i_tags_delbycode(i_img_tags *tags, int code) {
  int count = 0;
  int i;
  if (tags->tags) {
    for (i = tags->count-1; i >= 0; --i) {
      if (tags->tags[i].code == code) {
        ++count;
        i_tags_delete(tags, i);
      }
    }
  }
  return count;
}

/*
=item i_tags_get_float(tags, name, code, value)

=category Tags

Retrieves a tag as a floating point value.  

If the tag has a string value then that is parsed as a floating point
number, otherwise the integer value of the tag is used.

On success sets *I<value> and returns true.

On failure returns false.

=cut
*/

int i_tags_get_float(i_img_tags *tags, char const *name, int code, 
                     double *value) {
  int index;
  i_img_tag *entry;

  if (name) {
    if (!i_tags_find(tags, name, 0, &index))
      return 0;
  }
  else {
    if (!i_tags_findn(tags, code, 0, &index))
      return 0;
  }
  entry = tags->tags+index;
  if (entry->data)
    *value = atof(entry->data);
  else
    *value = entry->idata;

  return 1;
}

/*
=item i_tags_set_float(tags, name, code, value)

=category Tags

Equivalent to i_tags_set_float2(tags, name, code, value, 30).

=cut
*/

int i_tags_set_float(i_img_tags *tags, char const *name, int code, 
                     double value) {
  return i_tags_set_float2(tags, name, code, value, 30);
}

/*
=item i_tags_set_float2(tags, name, code, value, places)

=category Tags

Sets the tag with the given name and code to the given floating point
value.

Since tags are strings or ints, we convert the value to a string before
storage at the precision specified by C<places>.

=cut
*/

int i_tags_set_float2(i_img_tags *tags, char const *name, int code, 
                      double value, int places) {
  char temp[40];

  if (places < 0) 
    places = 30;
  else if (places > 30) 
    places = 30;

  sprintf(temp, "%.*g", places, value);
  if (name)
    i_tags_delbyname(tags, name);
  else
    i_tags_delbycode(tags, code);

  return i_tags_add(tags, name, code, temp, strlen(temp), 0);
}

/*
=item i_tags_get_int(tags, name, code, &value)

=category Tags

Retrieve a tag specified by name or code as an integer.

On success sets the int *I<value> to the integer and returns true.

On failure returns false.

=cut
*/

int i_tags_get_int(i_img_tags *tags, char const *name, int code, int *value) {
  int index;
  i_img_tag *entry;

  if (name) {
    if (!i_tags_find(tags, name, 0, &index))
      return 0;
  }
  else {
    if (!i_tags_findn(tags, code, 0, &index))
      return 0;
  }
  entry = tags->tags+index;
  if (entry->data)
    *value = atoi(entry->data);
  else
    *value = entry->idata;

  return 1;
}

static int parse_long(char *data, char **end, long *out) {
  long result;
  int savederr = errno;
  char *myend;

  errno = 0;
  result = strtol(data, &myend, 10);
  if (((result == LONG_MIN || result == LONG_MAX) && errno == ERANGE)
      || myend == data) {
    errno = savederr;
    return 0;
  }

  errno = savederr;
  *out = result;
  *end = myend;

  return 1;
}

/* parse a comma-separated list of integers
   returns when it has maxcount numbers, finds a non-comma after a number
   or can't parse a number
   if it can't parse a number after a comma, that's considered an error
*/
static int parse_long_list(char *data, char **end, int maxcount, long *out) {
  int i;

  i = 0;
  while (i < maxcount-1) {
    if (!parse_long(data, &data, out))
      return 0;
    out++;
    i++;
    if (*data != ',')
      return i;
    ++data;
  }
  if (!parse_long(data, &data, out))
    return 0;
  ++i;
  *end = data;
  return i;
}

/* parse "color(red,green,blue,alpha)" */
static int parse_color(char *data, char **end, i_color *value) {
  long n[4];
  int count, i;
  
  if (memcmp(data, "color(", 6))
    return 0; /* not a color */
  data += 6;
  count = parse_long_list(data, &data, 4, n);
  if (count < 3)
    return 0;
  for (i = 0; i < count; ++i)
    value->channel[i] = n[i];
  if (count < 4)
    value->channel[3] = 255;

  return 1;
}

/*
=item i_tags_get_color(tags, name, code, &value)

=category Tags

Retrieve a tag specified by name or code as color.

On success sets the i_color *I<value> to the color and returns true.

On failure returns false.

=cut
*/

int i_tags_get_color(i_img_tags *tags, char const *name, int code, 
                     i_color *value) {
  int index;
  i_img_tag *entry;
  char *end;

  if (name) {
    if (!i_tags_find(tags, name, 0, &index))
      return 0;
  }
  else {
    if (!i_tags_findn(tags, code, 0, &index))
      return 0;
  }
  entry = tags->tags+index;
  if (!entry->data) 
    return 0;

  if (!parse_color(entry->data, &end, value))
    return 0;
  
  /* for now we're sloppy about the end */

  return 1;
}

/*
=item i_tags_set_color(tags, name, code, &value)

=category Tags

Stores the given color as a tag with the given name and code.

=cut
*/

int i_tags_set_color(i_img_tags *tags, char const *name, int code, 
                     i_color const *value) {
  char temp[80];

  sprintf(temp, "color(%d,%d,%d,%d)", value->channel[0], value->channel[1],
          value->channel[2], value->channel[3]);
  if (name)
    i_tags_delbyname(tags, name);
  else
    i_tags_delbycode(tags, code);

  return i_tags_add(tags, name, code, temp, strlen(temp), 0);
}

/*
=item i_tags_get_string(tags, name, code, value, value_size)

=category Tags

Retrieves a tag by name or code as a string.

On success copies the string to value for a max of value_size and
returns true.

On failure returns false.

value_size must be at least large enough for a string representation
of an integer.

The copied value is always C<NUL> terminated.

=cut
*/

int i_tags_get_string(i_img_tags *tags, char const *name, int code, 
                      char *value, size_t value_size) {
  int index;
  i_img_tag *entry;

  if (name) {
    if (!i_tags_find(tags, name, 0, &index))
      return 0;
  }
  else {
    if (!i_tags_findn(tags, code, 0, &index))
      return 0;
  }
  entry = tags->tags+index;
  if (entry->data) {
    size_t cpsize = value_size < entry->size ? value_size : entry->size;
    memcpy(value, entry->data, cpsize);
    if (cpsize == value_size)
      --cpsize;
    value[cpsize] = '\0';
  }
  else {
    sprintf(value, "%d", entry->idata);
  }

  return 1;
}

/*
=item i_tags_set(tags, name, data, size)
=synopsis i_tags_set(&img->tags, "i_comment", -1);
=category Tags

Sets the given tag to the string I<data>

If size is -1 then the strlen(I<data>) bytes are stored.

Even on failure, if an existing tag I<name> exists, it will be
removed.

=cut
*/

int
i_tags_set(i_img_tags *tags, char const *name, char const *data, int size) {
  i_tags_delbyname(tags, name);

  return i_tags_add(tags, name, 0, data, size, 0);
}

/*
=item i_tags_setn(C<tags>, C<name>, C<idata>)
=synopsis i_tags_setn(&img->tags, "i_xres", 204);
=synopsis i_tags_setn(&img->tags, "i_yres", 196);
=category Tags

Sets the given tag to the integer C<idata>

Even on failure, if an existing tag C<name> exists, it will be
removed.

=cut
*/

int
i_tags_setn(i_img_tags *tags, char const *name, int idata) {
  i_tags_delbyname(tags, name);

  return i_tags_addn(tags, name, 0, idata);
}

void i_tags_print(i_img_tags *tags) {
  int i;
  printf("Alloc %d\n", tags->alloc);
  printf("Count %d\n", tags->count);
  for (i = 0; i < tags->count; ++i) {
    i_img_tag *tag = tags->tags + i;
    printf("Tag %d\n", i);
    if (tag->name)
      printf(" Name : %s (%p)\n", tag->name, tag->name);
    printf(" Code : %d\n", tag->code);
    if (tag->data) {
      int pos;
      printf(" Data : %d (%p) => '", tag->size, tag->data);
      for (pos = 0; pos < tag->size; ++pos) {
	if (tag->data[pos] == '\\' || tag->data[pos] == '\'') {
	  putchar('\\');
	  putchar(tag->data[pos]);
	}
	else if (tag->data[pos] < ' ' || tag->data[pos] >= '\x7E')
	  printf("\\x%02X", tag->data[pos]);
	else
	  putchar(tag->data[pos]);
      }
      printf("'\n");
      printf(" Idata: %d\n", tag->idata);
    }
  }
}

/*
=back

=head1 AUTHOR

Tony Cook <tony@develop-help.com>

=head1 SEE ALSO

Imager(3)

=cut
*/