shithub: opus-tools

Download patch

ref: 8422191e4a4fec7e20967e3b5d32f140c101f9c2
parent: 4a5897265a9a7d1f0aa5c2e62ab1d208dc6c1348
author: Timothy B. Terriberry <tterribe@xiph.org>
date: Sat Apr 27 10:12:19 EDT 2013

Refactor METADATA_BLOCK_PICTURE support.

This splits some common parts into a separate picture.h and
 picture.c.
It also allows opusinfo to verify the declared
 width/height/depth/colors information against the values in the
 actual image data (if found).

--- a/Makefile.am
+++ b/Makefile.am
@@ -20,6 +20,7 @@
                  src/opus_header.h \
                  src/opusinfo.h \
                  src/os_support.h \
+                 src/picture.h \
                  src/resample_sse.h \
                  src/speex_resampler.h \
                  src/stack_alloc.h \
@@ -48,7 +49,7 @@
 
 dist_man_MANS = man/opusenc.1 man/opusdec.1 man/opusinfo.1
 
-opusenc_SOURCES = src/opus_header.c src/opusenc.c src/resample.c src/audio-in.c src/diag_range.c src/flac.c src/lpc.c win32/unicode_support.c
+opusenc_SOURCES = src/opus_header.c src/opusenc.c src/picture.c src/resample.c src/audio-in.c src/diag_range.c src/flac.c src/lpc.c win32/unicode_support.c
 opusenc_LDADD = $(OGG_LIBS) $(Opus_LIBS) -lm
 opusenc_MANS = man/opusenc.1
 
@@ -56,7 +57,7 @@
 opusdec_LDADD = $(OGG_LIBS) $(Opus_LIBS) -lm
 opusdec_MANS = man/opusdec.1
 
-opusinfo_SOURCES = src/opus_header.c src/opusinfo.c src/info_opus.c win32/unicode_support.c
+opusinfo_SOURCES = src/opus_header.c src/opusinfo.c src/info_opus.c src/picture.c win32/unicode_support.c
 opusinfo_LDADD = $(OGG_LIBS)
 opusinfo_MANS = man/opusinfo.1
 
--- a/src/flac.c
+++ b/src/flac.c
@@ -31,6 +31,7 @@
 #include <locale.h>
 #include "flac.h"
 #include "opus_header.h"
+#include "picture.h"
 
 #if defined(HAVE_LIBFLAC)
 
@@ -78,31 +79,6 @@
   return feof(flac->f)?true:false;
 }
 
-/*A version of strncasecmp() that is guaranteed to only ignore the case of
-  ASCII characters.*/
-static int flac_strncasecmp(const char *_a,const char *_b,int _n){
-  int i;
-  for(i=0;i<_n;i++){
-    int a;
-    int b;
-    int d;
-    a=_a[i];
-    b=_b[i];
-    if(a>='a'&&a<='z')a-='a'-'A';
-    if(b>='a'&&b<='z')b-='a'-'A';
-    d=a-b;
-    if(d)return d;
-  }
-  return 0;
-}
-
-static void pack_u32be(char buf[4], FLAC__uint32 val){
-  buf[0]=(char)(val>>24);
-  buf[1]=(char)(val>>16);
-  buf[2]=(char)(val>>8);
-  buf[3]=(char)val;
-}
-
 /*Callback to process a metadata packet.*/
 static void metadata_callback(const FLAC__StreamDecoder *decoder,
    const FLAC__StreamMetadata *metadata,void *client_data){
@@ -151,7 +127,7 @@
           entry=(char *)comments[i].entry;
           /*Check for ReplayGain tags.
             Parse the ones we have R128 equivalents for, and skip the others.*/
-          if(flac_strncasecmp(entry,"REPLAYGAIN_REFERENCE_LOUDNESS=",30)==0){
+          if(oi_strncasecmp(entry,"REPLAYGAIN_REFERENCE_LOUDNESS=",30)==0){
             gain=strtod(entry+30,&end);
             if(end<=entry+30){
               fprintf(stderr,_("WARNING: Invalid ReplayGain tag: %s\n"),entry);
@@ -159,7 +135,7 @@
             else reference_loudness=gain;
             continue;
           }
-          if(flac_strncasecmp(entry,"REPLAYGAIN_ALBUM_GAIN=",22)==0){
+          if(oi_strncasecmp(entry,"REPLAYGAIN_ALBUM_GAIN=",22)==0){
             gain=strtod(entry+22,&end);
             if(end<=entry+22){
               fprintf(stderr,_("WARNING: Invalid ReplayGain tag: %s\n"),entry);
@@ -170,7 +146,7 @@
             }
             continue;
           }
-          if(flac_strncasecmp(entry,"REPLAYGAIN_TRACK_GAIN=",22)==0){
+          if(oi_strncasecmp(entry,"REPLAYGAIN_TRACK_GAIN=",22)==0){
             gain=strtod(entry+22,&end);
             if(end<entry+22){
               fprintf(stderr,_("WARNING: Invalid ReplayGain tag: %s\n"),entry);
@@ -181,8 +157,8 @@
             }
             continue;
           }
-          if(flac_strncasecmp(entry,"REPLAYGAIN_ALBUM_PEAK=",22)==0
-             ||flac_strncasecmp(entry,"REPLAYGAIN_TRACK_PEAK=",22)==0){
+          if(oi_strncasecmp(entry,"REPLAYGAIN_ALBUM_PEAK=",22)==0
+             ||oi_strncasecmp(entry,"REPLAYGAIN_TRACK_PEAK=",22)==0){
             continue;
           }
           if(!strchr(entry,'=')){
@@ -227,25 +203,25 @@
          +metadata->data.picture.data_length;
         buf=(char *)malloc(buf_sz);
         offs=0;
-        pack_u32be(buf+offs,metadata->data.picture.type);
+        WRITE_U32_BE(buf+offs,metadata->data.picture.type);
         offs+=4;
-        pack_u32be(buf+offs,(FLAC__uint32)mime_type_length);
+        WRITE_U32_BE(buf+offs,(FLAC__uint32)mime_type_length);
         offs+=4;
         memcpy(buf,metadata->data.picture.mime_type,mime_type_length);
         offs+=mime_type_length;
-        pack_u32be(buf+offs,(FLAC__uint32)description_length);
+        WRITE_U32_BE(buf+offs,(FLAC__uint32)description_length);
         offs+=4;
         memcpy(buf,metadata->data.picture.description,description_length);
         offs+=description_length;
-        pack_u32be(buf+offs,metadata->data.picture.width);
+        WRITE_U32_BE(buf+offs,metadata->data.picture.width);
         offs+=4;
-        pack_u32be(buf+offs,metadata->data.picture.height);
+        WRITE_U32_BE(buf+offs,metadata->data.picture.height);
         offs+=4;
-        pack_u32be(buf+offs,metadata->data.picture.depth);
+        WRITE_U32_BE(buf+offs,metadata->data.picture.depth);
         offs+=4;
-        pack_u32be(buf+offs,metadata->data.picture.colors);
+        WRITE_U32_BE(buf+offs,metadata->data.picture.colors);
         offs+=4;
-        pack_u32be(buf+offs,metadata->data.picture.data_length);
+        WRITE_U32_BE(buf+offs,metadata->data.picture.data_length);
         offs+=4;
         memcpy(buf+offs,metadata->data.picture.data,
            metadata->data.picture.data_length);
--- a/src/opusenc.c
+++ b/src/opusenc.c
@@ -64,6 +64,7 @@
 #include <ogg/ogg.h>
 #include "wav_io.h"
 
+#include "picture.h"
 #include "opus_header.h"
 #include "opusenc.h"
 #include "diag_range.h"
@@ -80,8 +81,6 @@
 
 static void comment_init(char **comments, int* length, const char *vendor_string);
 static void comment_pad(char **comments, int* length, int amount);
-static char *parse_picture_specification(const char *spec,
- const char **error_message, int *seen_file_icons);
 
 /*Write an Ogg page to a file pointer*/
 static inline int oe_write_page(ogg_page *page, FILE *fp)
@@ -1219,408 +1218,3 @@
 }
 #undef readint
 #undef writeint
-
-static const char BASE64_TABLE[64]={
-  'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
-  'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
-  'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
-  'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/'
-};
-
-/*Utility function for base64 encoding METADATA_BLOCK_PICTURE tags.
-  Stores BASE64_LENGTH(len)+1 bytes in dst (including a terminating NUL).*/
-void base64_encode(char *dst, const char *src, int len){
-  unsigned s0;
-  unsigned s1;
-  unsigned s2;
-  int      ngroups;
-  int      i;
-  ngroups=len/3;
-  for(i=0;i<ngroups;i++){
-    s0=(unsigned char)src[3*i+0];
-    s1=(unsigned char)src[3*i+1];
-    s2=(unsigned char)src[3*i+2];
-    dst[4*i+0]=BASE64_TABLE[s0>>2];
-    dst[4*i+1]=BASE64_TABLE[(s0&3)<<4|s1>>4];
-    dst[4*i+2]=BASE64_TABLE[(s1&15)<<2|s2>>6];
-    dst[4*i+3]=BASE64_TABLE[s2&63];
-  }
-  len-=3*i;
-  if(len==1){
-    s0=(unsigned char)src[3*i+0];
-    dst[4*i+0]=BASE64_TABLE[s0>>2];
-    dst[4*i+1]=BASE64_TABLE[(s0&3)<<4];
-    dst[4*i+2]='=';
-    dst[4*i+3]='=';
-    i++;
-  }
-  else if(len==2){
-    s0=(unsigned char)src[3*i+0];
-    s1=(unsigned char)src[3*i+1];
-    dst[4*i+0]=BASE64_TABLE[s0>>2];
-    dst[4*i+1]=BASE64_TABLE[(s0&3)<<4|s1>>4];
-    dst[4*i+2]=BASE64_TABLE[(s1&15)<<2];
-    dst[4*i+3]='=';
-    i++;
-  }
-  dst[4*i]='\0';
-}
-
-static int is_jpeg(const unsigned char *buf, size_t length){
-  return length>=11&&memcmp(buf,"\xFF\xD8\xFF\xE0",4)==0
-   &&(buf[4]<<8|buf[5])>=16&&memcmp(buf+6,"JFIF",5)==0;
-}
-
-static int is_png(const unsigned char *buf, size_t length){
-  return length>=8&&memcmp(buf,"\x89PNG\x0D\x0A\x1A\x0A",8)==0;
-}
-
-static int is_gif(const unsigned char *buf, size_t length){
-  return length>=6
-   &&(memcmp(buf,"GIF87a",6)==0||memcmp(buf,"GIF89a",6)==0);
-}
-
-static void pack_u32be(unsigned char buf[4], unsigned long val){
-  buf[0]=(unsigned char)(val>>24);
-  buf[1]=(unsigned char)(val>>16);
-  buf[2]=(unsigned char)(val>>8);
-  buf[3]=(unsigned char)val;
-}
-
-/*Parse a picture SPECIFICATION as given on the command-line.
-  spec: The specification.
-  error_message: Returns an error message on error.
-  seen_file_icons: Bit flags used to track if any pictures of type 1 or type 2
-   have already been added, to ensure only one is allowed.
-  Return: A Base64-encoded string suitable for use in a METADATA_BLOCK_PICTURE
-   tag.*/
-static char *parse_picture_specification(const char *spec,
-                                         const char **error_message,
-                                         int *seen_file_icons){
-  FILE          *picture_file;
-  unsigned long  picture_type;
-  unsigned long  width;
-  unsigned long  height;
-  unsigned long  depth;
-  unsigned long  colors;
-  const char    *mime_type;
-  const char    *mime_type_end;
-  const char    *description;
-  const char    *description_end;
-  const char    *filename;
-  unsigned char *buf;
-  char          *out;
-  size_t         cbuf;
-  size_t         nbuf;
-  size_t         data_offset;
-  size_t         data_length;
-  size_t         b64_length;
-  int            is_url;
-  /*If a filename has a '|' in it, there's no way we can distinguish it from a
-     full specification just from the spec string.
-    Instead, try to open the file.
-    If it exists, the user probably meant the file.*/
-  picture_type=3;
-  width=height=depth=colors=0;
-  mime_type=mime_type_end=description=description_end=filename=spec;
-  is_url=0;
-  picture_file=fopen(filename,"rb");
-  if(picture_file==NULL&&strchr(spec,'|')){
-    const char *p;
-    char       *q;
-    /*We don't have a plain file, and there is a pipe character: assume it's
-       the full form of the specification.*/
-    picture_type=strtoul(spec,&q,10);
-    if(*q!='|'||picture_type>20){
-      *error_message="invalid picture type";
-      return NULL;
-    }
-    if(picture_type>=1&&picture_type<=2&&(*seen_file_icons&picture_type)){
-      *error_message=picture_type==1?
-       "only one picture of type 1 (32x32 icon) allowed":
-       "only one picture of type 2 (icon) allowed";
-      return NULL;
-    }
-    /*An empty field implies a default of 'Cover (front)'.*/
-    if(spec==q)picture_type=3;
-    mime_type=q+1;
-    mime_type_end=mime_type+strcspn(mime_type,"|");
-    if(*mime_type_end!='|'){
-      *error_message="invalid picture specification: not enough fields";
-      return NULL;
-    }
-    /*The mime type must be composed of ASCII printable characters 0x20-0x7E.*/
-    for(p=mime_type;p<mime_type_end;p++)if(*p<0x20||*p>0x7E){
-      *error_message="invalid characters in mime type";
-      return NULL;
-    }
-    is_url=mime_type_end-mime_type==3
-     &&strncmp("-->",mime_type,mime_type_end-mime_type)==0;
-    description=mime_type_end+1;
-    description_end=description+strcspn(description,"|");
-    if(*description_end!='|'){
-      *error_message="invalid picture specification: not enough fields";
-      return NULL;
-    }
-    p=description_end+1;
-    if(*p!='|'){
-      width=strtoul(p,&q,10);
-      if(*q!='x'){
-        *error_message=
-         "invalid picture specification: can't parse resolution/color field";
-        return NULL;
-      }
-      p=q+1;
-      height=strtoul(p,&q,10);
-      if(*q!='x'){
-        *error_message=
-         "invalid picture specification: can't parse resolution/color field";
-        return NULL;
-      }
-      p=q+1;
-      depth=strtoul(p,&q,10);
-      if(*q=='/'){
-        p=q+1;
-        colors=strtoul(p,&q,10);
-      }
-      if(*q!='|'){
-        *error_message=
-         "invalid picture specification: can't parse resolution/color field";
-        return NULL;
-      }
-      p=q;
-    }
-    filename=p+1;
-    if(!is_url)picture_file=fopen(filename,"rb");
-  }
-  /*Buffer size: 8 static 4-byte fields plus 2 dynamic fields, plus the
-     file/URL data.
-    We reserve at least 10 bytes for the mime type, in case we still need to
-     extract it from the file.*/
-  data_offset=32+(description_end-description)+IMAX(mime_type_end-mime_type,10);
-  buf=NULL;
-  if(is_url){
-    /*Easy case: just stick the URL at the end.
-      We don't do anything to verify it's a valid URL.*/
-    data_length=strlen(filename);
-    cbuf=nbuf=data_offset+data_length;
-    buf=(unsigned char *)malloc(cbuf);
-    memcpy(buf+data_offset,filename,data_length);
-  }
-  else{
-    unsigned long file_width;
-    unsigned long file_height;
-    unsigned long file_depth;
-    unsigned long file_colors;
-    int           has_palette;
-    /*Complicated case: we have a real file.
-      Read it in, attempt to parse the mime type and image dimensions if
-       necessary, and validate what the user passed in.*/
-    if(picture_file==NULL){
-      *error_message="error opening picture file";
-      return NULL;
-    }
-    nbuf=data_offset;
-    /*Add a reasonable starting image file size.*/
-    cbuf=data_offset+65536;
-    for(;;){
-      unsigned char *new_buf;
-      size_t         nread;
-      new_buf=realloc(buf,cbuf);
-      if(new_buf==NULL){
-        fclose(picture_file);
-        free(buf);
-        *error_message="insufficient memory";
-        return NULL;
-      }
-      buf=new_buf;
-      nread=fread(buf+nbuf,1,cbuf-nbuf,picture_file);
-      nbuf+=nread;
-      if(nbuf<cbuf){
-        int error;
-        error=ferror(picture_file);
-        fclose(picture_file);
-        if(error){
-          free(buf);
-          *error_message="error reading picture file";
-          return NULL;
-        }
-        break;
-      }
-      if(cbuf==0xFFFFFFFF){
-        fclose(picture_file);
-        free(buf);
-        *error_message="file too large";
-        return NULL;
-      }
-      else if(cbuf>0x7FFFFFFFU)cbuf=0xFFFFFFFFU;
-      else cbuf=cbuf<<1|1;
-    }
-    data_length=nbuf-data_offset;
-    /*If there was no mimetype, try to extract it from the file data.*/
-    if(mime_type_end==mime_type){
-      if(is_jpeg(buf+data_offset,data_length)){
-        mime_type="image/jpeg";
-        mime_type_end=mime_type+10;
-      }
-      else if(is_png(buf+data_offset,data_length)){
-        mime_type="image/png";
-        mime_type_end=mime_type+9;
-      }
-      else if(is_gif(buf+data_offset,data_length)){
-        mime_type="image/gif";
-        mime_type_end=mime_type+9;
-      }
-      else{
-        free(buf);
-        *error_message="unable to guess MIME type from file, "
-         "must set it explicitly";
-        return NULL;
-      }
-    }
-    /*Try to extract the image dimensions/color information from the file.*/
-    file_width=file_height=file_depth=file_colors=0;
-    has_palette=-1;
-    if(strncmp("image/png",mime_type,mime_type_end-mime_type)==0){
-      if(is_png(buf+data_offset,data_length)){
-        size_t offs;
-        offs=data_offset+8;
-        while(nbuf-offs>=12){
-          opus_uint32 chunk_len;
-          chunk_len=buf[offs+0]<<24|buf[offs+1]<<16|buf[offs+2]<<8|buf[offs+3];
-          if(chunk_len>nbuf-(offs+12))break;
-          else if(chunk_len==13&&memcmp(buf+offs+4,"IHDR",4)==0){
-            int color_type;
-            file_width=
-             buf[offs+8]<<24|buf[offs+9]<<16|buf[offs+10]<<8|buf[offs+11];
-            file_height=
-             buf[offs+12]<<24|buf[offs+13]<<16|buf[offs+14]<<8|buf[offs+15];
-            color_type=buf[offs+17];
-            if(color_type==3){
-              file_depth=24;
-              has_palette=1;
-            }
-            else{
-              int sample_depth=buf[offs+16];
-              if(color_type==0)file_depth=sample_depth;
-              else if(color_type==2)file_depth=sample_depth*3;
-              else if(color_type==4)file_depth=sample_depth*2;
-              else if(color_type==6)file_depth=sample_depth*4;
-              file_colors=0;
-              has_palette=0;
-              break;
-            }
-          }
-          else if(has_palette>0&&memcmp(buf+offs+4,"PLTE",4)==0){
-            file_colors=chunk_len/3;
-            break;
-          }
-          offs+=12+chunk_len;
-        }
-      }
-    }
-    else if(strncmp("image/gif",mime_type,mime_type_end-mime_type)==0){
-      if(is_gif(buf+data_offset,data_length)&&data_length>=14){
-        file_width=buf[data_offset+6]|buf[data_offset+7]<<8;
-        file_height=buf[data_offset+8]|buf[data_offset+9]<<8;
-        /*libFLAC hard-codes the depth to 24.*/
-        file_depth=24;
-        file_colors=1<<((buf[data_offset+10]&7)+1);
-        has_palette=1;
-      }
-    }
-    else if(strncmp("image/jpeg",mime_type,mime_type_end-mime_type)==0){
-      if(is_jpeg(buf+data_offset,data_length)){
-        size_t offs;
-        offs=data_offset+2;
-        for(;;){
-          size_t segment_len;
-          int    marker;
-          while(offs<nbuf&&buf[offs]!=0xFF)offs++;
-          while(offs<nbuf&&buf[offs]==0xFF)offs++;
-          marker=buf[offs];
-          offs++;
-          /*If we hit EOI* (end of image), or another SOI* (start of image),
-             or SOS (start of scan), then stop now.*/
-          if(offs>=nbuf||(marker>=0xD8&&marker<=0xDA))break;
-          /*RST* (restart markers): skip (no segment length).*/
-          else if(marker>=0xD0&&marker<=0xD7)continue;
-          /*Read the length of the marker segment.*/
-          if(nbuf-offs<2)break;
-          segment_len=buf[offs]<<8|buf[offs+1];
-          if(segment_len<2||nbuf-offs<segment_len)break;
-          if(marker==0xC0||(marker>0xC0&&marker<0xD0&&(marker&3)!=0)){
-            /*Found a SOFn (start of frame) marker segment:*/
-            if(segment_len>=8){
-              file_height=buf[offs+3]<<8|buf[offs+4];
-              file_width=buf[offs+5]<<8|buf[offs+6];
-              file_depth=buf[offs+2]*buf[offs+7];
-              file_colors=0;
-              has_palette=0;
-            }
-            break;
-          }
-          /*Other markers: skip the whole marker segment.*/
-          offs+=segment_len;
-        }
-      }
-    }
-    if(!width)width=file_width;
-    if(!height)height=file_height;
-    if(!depth)depth=file_depth;
-    if(!colors)colors=file_colors;
-    if((file_width&&width!=file_width)
-     ||(file_height&&height!=file_height)
-     ||(file_depth&&depth!=file_depth)
-     /*We use has_palette to ensure we also reject non-0 user color counts for
-        images we've positively identified as non-paletted.*/
-     ||(has_palette>=0&&colors!=file_colors)){
-      free(buf);
-      *error_message="invalid picture specification: "
-       "resolution/color field does not match file";
-      return NULL;
-    }
-  }
-  /*These fields MUST be set correctly OR all set to zero.
-    So if any of them (except colors, for which 0 is a valid value) are still
-     zero, clear the rest to zero.*/
-  if(width==0||height==0||depth==0)width=height=depth=colors=0;
-  if(picture_type==1&&(width!=32||height!=32
-   ||strncmp("image/png",mime_type,mime_type_end-mime_type)!=0)){
-    free(buf);
-    *error_message="pictures of type 1 MUST be 32x32 PNGs";
-    return NULL;
-  }
-  /*Build the METADATA_BLOCK_PICTURE buffer.
-    We do this backwards from data_offset, because we didn't necessarily know
-     how big the mime type string was before we read the data in.*/
-  data_offset-=4;
-  pack_u32be(buf+data_offset,(unsigned long)data_length);
-  data_offset-=4;
-  pack_u32be(buf+data_offset,colors);
-  data_offset-=4;
-  pack_u32be(buf+data_offset,depth);
-  data_offset-=4;
-  pack_u32be(buf+data_offset,height);
-  data_offset-=4;
-  pack_u32be(buf+data_offset,width);
-  data_offset-=description_end-description;
-  memcpy(buf+data_offset,description,description_end-description);
-  data_offset-=4;
-  pack_u32be(buf+data_offset,(unsigned long)(description_end-description));
-  data_offset-=mime_type_end-mime_type;
-  memcpy(buf+data_offset,mime_type,mime_type_end-mime_type);
-  data_offset-=4;
-  pack_u32be(buf+data_offset,(unsigned long)(mime_type_end-mime_type));
-  data_offset-=4;
-  pack_u32be(buf+data_offset,picture_type);
-  data_length=nbuf-data_offset;
-  b64_length=BASE64_LENGTH(data_length);
-  out=(char *)malloc(b64_length+1);
-  if(out!=NULL){
-    base64_encode(out,(char *)buf+data_offset,data_length);
-    if(picture_type>=1&&picture_type<=2)*seen_file_icons|=picture_type;
-  }
-  free(buf);
-  return out;
-}
--- a/src/opusenc.h
+++ b/src/opusenc.h
@@ -104,8 +104,4 @@
 long wav_read(void *, float *buffer, int samples);
 long wav_ieee_read(void *, float *buffer, int samples);
 
-#define BASE64_LENGTH(len) (((len)+2)/3*4)
-
-void base64_encode(char *dst, const char *src, int len);
-
 #endif /* __OPUSENC_H */
--- a/src/opusinfo.c
+++ b/src/opusinfo.c
@@ -30,6 +30,7 @@
 #include "opusinfo.h"
 #include "opus_header.h"
 #include "info_opus.h"
+#include "picture.h"
 
 #if defined WIN32 || defined _WIN32 || defined WIN64 || defined _WIN64
 # include "unicode_support.h"
@@ -97,47 +98,9 @@
     va_end(ap);
 }
 
-/*A version of strncasecmp() that is guaranteed to only ignore the case of
-   ASCII characters.*/
-static int oi_strncasecmp(const char *a, const char *b, int n){
-    int i;
-    for (i = 0; i < n; i++) {
-        int aval;
-        int bval;
-        int diff;
-        aval = a[i];
-        bval = b[i];
-        if(aval >= 'a' && aval <= 'z') {
-            aval -= 'a'-'A';
-        }
-        if(bval >= 'a' && bval <= 'z') {
-            bval -= 'a'-'A';
-        }
-        diff = aval-bval;
-        if(diff) {
-            return diff;
-        }
-    }
-    return 0;
-}
-
 #define READ_U32_BE(buf) \
     (((buf)[0]<<24)|((buf)[1]<<16)|((buf)[2]<<8)|((buf)[3]&0xff))
 
-static int is_jpeg(const unsigned char *buf, size_t length){
-    return length >= 11 && memcmp(buf, "\xFF\xD8\xFF\xE0", 4) == 0
-          && (buf[4]<<8|buf[5]) >= 16 && memcmp(buf+6, "JFIF", 5) == 0;
-}
-
-static int is_png(const unsigned char *buf, size_t length){
-    return length >= 8 && memcmp(buf, "\x89PNG\x0D\x0A\x1A\x0A", 8) == 0;
-}
-
-static int is_gif(const unsigned char *buf, size_t length){
-    return length >= 6
-          && (memcmp(buf, "GIF87a", 6) == 0 || memcmp(buf, "GIF89a", 6) == 0);
-}
-
 void check_xiph_comment(stream_processor *stream, int i, const char *comment,
     int comment_length)
 {
@@ -301,10 +264,17 @@
          ogg_uint32_t   depth;
          ogg_uint32_t   colors;
          ogg_uint32_t   image_length;
+         ogg_uint32_t   file_width;
+         ogg_uint32_t   file_height;
+         ogg_uint32_t   file_depth;
+         ogg_uint32_t   file_colors;
          unsigned char *data;
          int            data_sz;
          int            len;
          int            is_url;
+         int            format;
+         int            has_palette;
+         int            colors_set;
          len=comment_length - (sep+1-comment);
          /*Decode the Base64 encoded data.*/
          if(len&3) {
@@ -361,7 +331,8 @@
                  else {
                      oi_warn(_("WARNING: Illegal Base64 character in "
                            "METADATA_BLOCK_PICTURE comment %d (stream %d): "
-                           "'%c'\n"), i, stream->num, (char)c);
+                           "'%c' (0x%02X)\n"), i, stream->num,
+                           (char)(c<0x20||c>0x7E?'?':c), c);
                      free(data);
                      return;
                  }
@@ -390,8 +361,7 @@
              oi_warn(_("WARNING: Unknown picture type in "
                    "METADATA_BLOCK_PICTURE comment %d (stream %d): "
                    "%li\n"), i, stream->num, (long)picture_type);
-             free(data);
-             return;
+             broken = 1;
          }
          if(picture_type >= 1 && picture_type <= 2) {
              if(stream->seen_file_icons & picture_type) {
@@ -400,8 +370,7 @@
                        " %s\n"), i, stream->num, picture_type == 1 ?
                        _("only one picture of type 1 (32x32 icon) allowed") :
                        _("only one picture of type 2 (icon) allowed"));
-                 free(data);
-                 return;
+                 broken = 1;
              }
              stream->seen_file_icons |= picture_type;
          }
@@ -420,8 +389,7 @@
                  oi_warn(_("WARNING: Invalid character in mime type of "
                        "METADATA_BLOCK_PICTURE comment %d (stream %d): "
                        "0x%02X\n"), i, stream->num, data[j]);
-                 free(data);
-                 return;
+                 broken = 1;
              }
          }
          description_length = READ_U32_BE(data+j);
@@ -443,10 +411,12 @@
          j += 4;
          colors = READ_U32_BE(data+j);
          j += 4;
+         /*If any value is non-zero, then they all MUST be valid values, and
+           so colors should be treated as set (even if zero).*/
+         colors_set = width != 0 || height != 0 || depth != 0 || colors != 0;
          /*This isn't triggered if colors == 0, since that can be a valid
            value.*/
-         if((width == 0 || height == 0 || depth == 0)
-               && (width != 0 || height != 0 || depth != 0 || colors != 0)) {
+         if((width == 0 || height == 0 || depth == 0) && colors_set) {
              oi_warn(_("WARNING: Invalid picture parameters in "
                    "METADATA_BLOCK_PICTURE comment %d (stream %d): "
                    "width (%i), height (%i), depth (%i), and colors (%i) MUST "
@@ -453,8 +423,7 @@
                    "either be set to valid values or all set to 0\n"), i,
                    stream->num, (int)width, (int)height, (int)depth,
                    (int)colors);
-             free(data);
-             return;
+             broken = 1;
          }
          image_length = READ_U32_BE(data+j);
          j += 4;
@@ -468,6 +437,7 @@
              return;
          }
          is_url = 0;
+         format = -1;
          if(mime_type_length == 10
                && oi_strncasecmp((const char*)data+8, "image/jpeg",
                      mime_type_length) == 0) {
@@ -479,6 +449,7 @@
                  free(data);
                  return;
              }
+             format = PIC_FORMAT_JPEG;
          }
          else if(mime_type_length == 9
                && oi_strncasecmp((const char *)data+8, "image/png",
@@ -491,6 +462,7 @@
                  free(data);
                  return;
              }
+             format = PIC_FORMAT_PNG;
          }
          else if(mime_type_length == 9
                && oi_strncasecmp((const char *)data+8, "image/gif",
@@ -503,6 +475,7 @@
                  free(data);
                  return;
              }
+             format = PIC_FORMAT_GIF;
          }
          else if(mime_type_length == 3
                && strncmp((const char *)data+8, "-->", mime_type_length) == 0) {
@@ -509,33 +482,104 @@
              is_url = 1;
              /*TODO: validate URL.*/
          }
-         else if(mime_type_length > 0 && (mime_type_length != 6 ||
+         else if(mime_type_length == 0 || (mime_type_length == 6 &&
                oi_strncasecmp((const char *)data+8, "image/",
-                     mime_type_length) != 0)) {
+                     mime_type_length) == 0)) {
+             if(is_jpeg(data+j, image_length)) {
+                 format = PIC_FORMAT_JPEG;
+             }
+             else if(is_png(data+j, image_length)) {
+                 format = PIC_FORMAT_PNG;
+             }
+             else if(is_gif(data+j, image_length)) {
+                 format = PIC_FORMAT_GIF;
+             }
+             else {
+                 oi_warn(_("WARNING: Unknown image format in "
+                       "METADATA_BLOCK_PICTURE comment %d (stream %d): "
+                       "\"%.*s\" may not be well-supported\n"), i, stream->num,
+                       mime_type_length, data+8);
+             }
+         }
+         else {
              oi_warn(_("WARNING: Unknown mime type in "
                    "METADATA_BLOCK_PICTURE comment %d (stream %d): "
                    "\"%.*s\" may not be well-supported\n"), i, stream->num,
                    mime_type_length, data+8);
          }
-         /*TODO: Extract width/height/depth/colors from image and compare to
-           encoded values. See code in opusenc.c*/
+         file_width = file_height = file_depth = file_colors = 0;
+         has_palette = -1;
+         switch(format) {
+             case PIC_FORMAT_JPEG:
+                extract_jpeg_params(data+j, image_length,
+                      &file_width, &file_height, &file_depth, &file_colors,
+                      &has_palette);
+                break;
+             case PIC_FORMAT_PNG:
+                extract_png_params(data+j, image_length,
+                      &file_width, &file_height, &file_depth, &file_colors,
+                      &has_palette);
+                break;
+             case PIC_FORMAT_GIF:
+                extract_gif_params(data+j, image_length,
+                      &file_width, &file_height, &file_depth, &file_colors,
+                      &has_palette);
+                break;
+         }
+         if(format >= 0 && has_palette < 0) {
+             /*We should have been able to affirmatively determine whether or
+               not there was a palette if we parsed the image successfully.*/
+             oi_warn(_("WARNING: Could not parse image parameters in"
+                   "METADATA_BLOCK_PICTURE comment %d (stream %d): "
+                   "possibly corrupt image?\n"), i, stream->num);
+             broken = 1;
+         }
+         if(width && width != file_width) {
+             oi_warn(_("WARNING: Mismatched picture parameters in "
+                   "METADATA_BLOCK_PICTURE comment %d (stream %d): "
+                   "width declared as %u but appears to be %u\n"), i,
+                   stream->num, (unsigned)width, (unsigned)file_width);
+             broken = 1;
+         }
+         if(height && height != file_height) {
+             oi_warn(_("WARNING: Mismatched picture parameters in "
+                   "METADATA_BLOCK_PICTURE comment %d (stream %d): "
+                   "height declared as %u but appears to be %u\n"), i,
+                   stream->num, (unsigned)height, (unsigned)file_height);
+             broken = 1;
+         }
+         if(depth && depth != file_depth) {
+             oi_warn(_("WARNING: Mismatched picture parameters in "
+                   "METADATA_BLOCK_PICTURE comment %d (stream %d): "
+                   "depth declared as %u but appears to be %u\n"), i,
+                   stream->num, (unsigned)depth, (unsigned)file_depth);
+             broken = 1;
+         }
+         if(has_palette >= 0 && colors_set && colors != file_colors) {
+             oi_warn(_("WARNING: Mismatched picture parameters in "
+                   "METADATA_BLOCK_PICTURE comment %d (stream %d): "
+                   "palette size declared as %u but appears to be %u\n"), i,
+                   stream->num, (unsigned)colors, (unsigned)file_colors);
+             broken = 1;
+         }
          if(picture_type == 1
-               && (width != 0 && (width != 32 || height != 32))) {
+               && ((is_url && (width != 0 || height != 0)
+                           && (width != 32 || height != 32))
+                     || (!is_url && (file_width != 32 || file_height != 32)))) {
              oi_warn(_("WARNING: Invalid picture in "
                    "METADATA_BLOCK_PICTURE comment %d (stream %d): "
                    "picture of type 1 (32x32 icon) MUST be a 32x32 PNG, but "
                    "the image has dimensions %ux%u\n"), i, stream->num,
-                   (unsigned)width, (unsigned)height);
-             free(data);
-             return;
+                   (unsigned)is_url?width:file_width,
+                   (unsigned)is_url?height:file_height);
+             broken = 1;
          }
-         if(picture_type == 1 && !is_url && !is_png(data+j, image_length)) {
+         if(picture_type == 1 && !is_url && format != PIC_FORMAT_PNG) {
              oi_warn(_("WARNING: Invalid picture in "
                    "METADATA_BLOCK_PICTURE comment %d (stream %d): "
                    "picture of type 1 (32x32 icon) MUST be a 32x32 PNG, but "
                    "the image does not appear to be a PNG\n"), i, stream->num);
-             free(data);
-             return;
+             broken = 1;
          }
          /*Print the contents of the block using the same format as the
            SPECIFICATION argument to opusenc/flac/etc. (except without an image
--- /dev/null
+++ b/src/picture.c
@@ -1,0 +1,499 @@
+/* Copyright (C)2007-2013 Xiph.Org Foundation
+   File: picture.c
+
+   Redistribution and use in source and binary forms, with or without
+   modification, are permitted provided that the following conditions
+   are met:
+
+   - Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+   - Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+   ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+   A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR
+   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "picture.h"
+
+static const char BASE64_TABLE[64]={
+  'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
+  'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
+  'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
+  'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/'
+};
+
+/*Utility function for base64 encoding METADATA_BLOCK_PICTURE tags.
+  Stores BASE64_LENGTH(len)+1 bytes in dst (including a terminating NUL).*/
+void base64_encode(char *dst, const char *src, int len){
+  unsigned s0;
+  unsigned s1;
+  unsigned s2;
+  int      ngroups;
+  int      i;
+  ngroups=len/3;
+  for(i=0;i<ngroups;i++){
+    s0=(unsigned char)src[3*i+0];
+    s1=(unsigned char)src[3*i+1];
+    s2=(unsigned char)src[3*i+2];
+    dst[4*i+0]=BASE64_TABLE[s0>>2];
+    dst[4*i+1]=BASE64_TABLE[(s0&3)<<4|s1>>4];
+    dst[4*i+2]=BASE64_TABLE[(s1&15)<<2|s2>>6];
+    dst[4*i+3]=BASE64_TABLE[s2&63];
+  }
+  len-=3*i;
+  if(len==1){
+    s0=(unsigned char)src[3*i+0];
+    dst[4*i+0]=BASE64_TABLE[s0>>2];
+    dst[4*i+1]=BASE64_TABLE[(s0&3)<<4];
+    dst[4*i+2]='=';
+    dst[4*i+3]='=';
+    i++;
+  }
+  else if(len==2){
+    s0=(unsigned char)src[3*i+0];
+    s1=(unsigned char)src[3*i+1];
+    dst[4*i+0]=BASE64_TABLE[s0>>2];
+    dst[4*i+1]=BASE64_TABLE[(s0&3)<<4|s1>>4];
+    dst[4*i+2]=BASE64_TABLE[(s1&15)<<2];
+    dst[4*i+3]='=';
+    i++;
+  }
+  dst[4*i]='\0';
+}
+
+/*A version of strncasecmp() that is guaranteed to only ignore the case of
+   ASCII characters.*/
+int oi_strncasecmp(const char *a, const char *b, int n){
+  int i;
+  for(i=0;i<n;i++){
+    int aval;
+    int bval;
+    int diff;
+    aval=a[i];
+    bval=b[i];
+    if(aval>='a'&&aval<='z') {
+      aval-='a'-'A';
+    }
+    if(bval>='a'&&bval<='z'){
+      bval-='a'-'A';
+    }
+    diff=aval-bval;
+    if(diff){
+      return diff;
+    }
+  }
+  return 0;
+}
+
+int is_jpeg(const unsigned char *buf, size_t length){
+  return length>=11&&memcmp(buf,"\xFF\xD8\xFF\xE0",4)==0
+   &&(buf[4]<<8|buf[5])>=16&&memcmp(buf+6,"JFIF",5)==0;
+}
+
+int is_png(const unsigned char *buf, size_t length){
+  return length>=8&&memcmp(buf,"\x89PNG\x0D\x0A\x1A\x0A",8)==0;
+}
+
+int is_gif(const unsigned char *buf, size_t length){
+  return length>=6
+   &&(memcmp(buf,"GIF87a",6)==0||memcmp(buf,"GIF89a",6)==0);
+}
+
+#define READ_U32_BE(buf) \
+    (((buf)[0]<<24)|((buf)[1]<<16)|((buf)[2]<<8)|((buf)[3]&0xff))
+
+/*Tries to extract the width, height, bits per pixel, and palette size of a
+   PNG.
+  On failure, simply leaves its outputs unmodified.*/
+void extract_png_params(const unsigned char *data, size_t data_length,
+                        ogg_uint32_t *width, ogg_uint32_t *height,
+                        ogg_uint32_t *depth, ogg_uint32_t *colors,
+                        int *has_palette){
+  if(is_png(data,data_length)){
+    size_t offs;
+    offs=8;
+    while(data_length-offs>=12){
+      ogg_uint32_t chunk_len;
+      chunk_len=READ_U32_BE(data+offs);
+      if(chunk_len>data_length-(offs+12))break;
+      else if(chunk_len==13&&memcmp(data+offs+4,"IHDR",4)==0){
+        int color_type;
+        *width=READ_U32_BE(data+offs+8);
+        *height=READ_U32_BE(data+offs+12);
+        color_type=data[offs+17];
+        if(color_type==3){
+          *depth=24;
+          *has_palette=1;
+        }
+        else{
+          int sample_depth;
+          sample_depth=data[offs+16];
+          if(color_type==0)*depth=sample_depth;
+          else if(color_type==2)*depth=sample_depth*3;
+          else if(color_type==4)*depth=sample_depth*2;
+          else if(color_type==6)*depth=sample_depth*4;
+          *colors=0;
+          *has_palette=0;
+          break;
+        }
+      }
+      else if(*has_palette>0&&memcmp(data+offs+4,"PLTE",4)==0){
+        *colors=chunk_len/3;
+        break;
+      }
+      offs+=12+chunk_len;
+    }
+  }
+}
+
+/*Tries to extract the width, height, bits per pixel, and palette size of a
+   GIF.
+  On failure, simply leaves its outputs unmodified.*/
+void extract_gif_params(const unsigned char *data, size_t data_length,
+                        ogg_uint32_t *width, ogg_uint32_t *height,
+                        ogg_uint32_t *depth, ogg_uint32_t *colors,
+                        int *has_palette){
+  if(is_gif(data,data_length)&&data_length>=14){
+    *width=data[6]|data[7]<<8;
+    *height=data[8]|data[9]<<8;
+    /*libFLAC hard-codes the depth to 24.*/
+    *depth=24;
+    *colors=1<<((data[10]&7)+1);
+    *has_palette=1;
+  }
+}
+
+
+/*Tries to extract the width, height, bits per pixel, and palette size of a
+   JPEG.
+  On failure, simply leaves its outputs unmodified.*/
+void extract_jpeg_params(const unsigned char *data, size_t data_length,
+                         ogg_uint32_t *width, ogg_uint32_t *height,
+                         ogg_uint32_t *depth, ogg_uint32_t *colors,
+                         int *has_palette){
+  if(is_jpeg(data,data_length)){
+    size_t offs;
+    offs=2;
+    for(;;){
+      size_t segment_len;
+      int    marker;
+      while(offs<data_length&&data[offs]!=0xFF)offs++;
+      while(offs<data_length&&data[offs]==0xFF)offs++;
+      marker=data[offs];
+      offs++;
+      /*If we hit EOI* (end of image), or another SOI* (start of image),
+         or SOS (start of scan), then stop now.*/
+      if(offs>=data_length||(marker>=0xD8&&marker<=0xDA))break;
+      /*RST* (restart markers): skip (no segment length).*/
+      else if(marker>=0xD0&&marker<=0xD7)continue;
+      /*Read the length of the marker segment.*/
+      if(data_length-offs<2)break;
+      segment_len=data[offs]<<8|data[offs+1];
+      if(segment_len<2||data_length-offs<segment_len)break;
+      if(marker==0xC0||(marker>0xC0&&marker<0xD0&&(marker&3)!=0)){
+        /*Found a SOFn (start of frame) marker segment:*/
+        if(segment_len>=8){
+          *height=data[offs+3]<<8|data[offs+4];
+          *width=data[offs+5]<<8|data[offs+6];
+          *depth=data[offs+2]*data[offs+7];
+          *colors=0;
+          *has_palette=0;
+        }
+        break;
+      }
+      /*Other markers: skip the whole marker segment.*/
+      offs+=segment_len;
+    }
+  }
+}
+
+#define IMAX(a,b) ((a) > (b) ? (a) : (b))
+
+/*Parse a picture SPECIFICATION as given on the command-line.
+  spec: The specification.
+  error_message: Returns an error message on error.
+  seen_file_icons: Bit flags used to track if any pictures of type 1 or type 2
+   have already been added, to ensure only one is allowed.
+  Return: A Base64-encoded string suitable for use in a METADATA_BLOCK_PICTURE
+   tag.*/
+char *parse_picture_specification(const char *spec,
+                                  const char **error_message,
+                                  int *seen_file_icons){
+  FILE          *picture_file;
+  unsigned long  picture_type;
+  unsigned long  width;
+  unsigned long  height;
+  unsigned long  depth;
+  unsigned long  colors;
+  const char    *mime_type;
+  const char    *mime_type_end;
+  const char    *description;
+  const char    *description_end;
+  const char    *filename;
+  unsigned char *buf;
+  char          *out;
+  size_t         cbuf;
+  size_t         nbuf;
+  size_t         data_offset;
+  size_t         data_length;
+  size_t         b64_length;
+  int            is_url;
+  /*If a filename has a '|' in it, there's no way we can distinguish it from a
+     full specification just from the spec string.
+    Instead, try to open the file.
+    If it exists, the user probably meant the file.*/
+  picture_type=3;
+  width=height=depth=colors=0;
+  mime_type=mime_type_end=description=description_end=filename=spec;
+  is_url=0;
+  picture_file=fopen(filename,"rb");
+  if(picture_file==NULL&&strchr(spec,'|')){
+    const char *p;
+    char       *q;
+    /*We don't have a plain file, and there is a pipe character: assume it's
+       the full form of the specification.*/
+    picture_type=strtoul(spec,&q,10);
+    if(*q!='|'||picture_type>20){
+      *error_message="invalid picture type";
+      return NULL;
+    }
+    if(picture_type>=1&&picture_type<=2&&(*seen_file_icons&picture_type)){
+      *error_message=picture_type==1?
+       "only one picture of type 1 (32x32 icon) allowed":
+       "only one picture of type 2 (icon) allowed";
+      return NULL;
+    }
+    /*An empty field implies a default of 'Cover (front)'.*/
+    if(spec==q)picture_type=3;
+    mime_type=q+1;
+    mime_type_end=mime_type+strcspn(mime_type,"|");
+    if(*mime_type_end!='|'){
+      *error_message="invalid picture specification: not enough fields";
+      return NULL;
+    }
+    /*The mime type must be composed of ASCII printable characters 0x20-0x7E.*/
+    for(p=mime_type;p<mime_type_end;p++)if(*p<0x20||*p>0x7E){
+      *error_message="invalid characters in mime type";
+      return NULL;
+    }
+    is_url=mime_type_end-mime_type==3
+     &&strncmp("-->",mime_type,mime_type_end-mime_type)==0;
+    description=mime_type_end+1;
+    description_end=description+strcspn(description,"|");
+    if(*description_end!='|'){
+      *error_message="invalid picture specification: not enough fields";
+      return NULL;
+    }
+    p=description_end+1;
+    if(*p!='|'){
+      width=strtoul(p,&q,10);
+      if(*q!='x'){
+        *error_message=
+         "invalid picture specification: can't parse resolution/color field";
+        return NULL;
+      }
+      p=q+1;
+      height=strtoul(p,&q,10);
+      if(*q!='x'){
+        *error_message=
+         "invalid picture specification: can't parse resolution/color field";
+        return NULL;
+      }
+      p=q+1;
+      depth=strtoul(p,&q,10);
+      if(*q=='/'){
+        p=q+1;
+        colors=strtoul(p,&q,10);
+      }
+      if(*q!='|'){
+        *error_message=
+         "invalid picture specification: can't parse resolution/color field";
+        return NULL;
+      }
+      p=q;
+    }
+    filename=p+1;
+    if(!is_url)picture_file=fopen(filename,"rb");
+  }
+  /*Buffer size: 8 static 4-byte fields plus 2 dynamic fields, plus the
+     file/URL data.
+    We reserve at least 10 bytes for the mime type, in case we still need to
+     extract it from the file.*/
+  data_offset=32+(description_end-description)+IMAX(mime_type_end-mime_type,10);
+  buf=NULL;
+  if(is_url){
+    /*Easy case: just stick the URL at the end.
+      We don't do anything to verify it's a valid URL.*/
+    data_length=strlen(filename);
+    cbuf=nbuf=data_offset+data_length;
+    buf=(unsigned char *)malloc(cbuf);
+    memcpy(buf+data_offset,filename,data_length);
+  }
+  else{
+    ogg_uint32_t file_width;
+    ogg_uint32_t file_height;
+    ogg_uint32_t file_depth;
+    ogg_uint32_t file_colors;
+    int          has_palette;
+    /*Complicated case: we have a real file.
+      Read it in, attempt to parse the mime type and image dimensions if
+       necessary, and validate what the user passed in.*/
+    if(picture_file==NULL){
+      *error_message="error opening picture file";
+      return NULL;
+    }
+    nbuf=data_offset;
+    /*Add a reasonable starting image file size.*/
+    cbuf=data_offset+65536;
+    for(;;){
+      unsigned char *new_buf;
+      size_t         nread;
+      new_buf=realloc(buf,cbuf);
+      if(new_buf==NULL){
+        fclose(picture_file);
+        free(buf);
+        *error_message="insufficient memory";
+        return NULL;
+      }
+      buf=new_buf;
+      nread=fread(buf+nbuf,1,cbuf-nbuf,picture_file);
+      nbuf+=nread;
+      if(nbuf<cbuf){
+        int error;
+        error=ferror(picture_file);
+        fclose(picture_file);
+        if(error){
+          free(buf);
+          *error_message="error reading picture file";
+          return NULL;
+        }
+        break;
+      }
+      if(cbuf==0xFFFFFFFF){
+        fclose(picture_file);
+        free(buf);
+        *error_message="file too large";
+        return NULL;
+      }
+      else if(cbuf>0x7FFFFFFFU)cbuf=0xFFFFFFFFU;
+      else cbuf=cbuf<<1|1;
+    }
+    data_length=nbuf-data_offset;
+    /*If there was no mimetype, try to extract it from the file data.*/
+    if(mime_type_end==mime_type){
+      if(is_jpeg(buf+data_offset,data_length)){
+        mime_type="image/jpeg";
+        mime_type_end=mime_type+10;
+      }
+      else if(is_png(buf+data_offset,data_length)){
+        mime_type="image/png";
+        mime_type_end=mime_type+9;
+      }
+      else if(is_gif(buf+data_offset,data_length)){
+        mime_type="image/gif";
+        mime_type_end=mime_type+9;
+      }
+      else{
+        free(buf);
+        *error_message="unable to guess MIME type from file, "
+         "must set it explicitly";
+        return NULL;
+      }
+    }
+    /*Try to extract the image dimensions/color information from the file.*/
+    file_width=file_height=file_depth=file_colors=0;
+    has_palette=-1;
+    if(mime_type_end-mime_type==9
+     &&oi_strncasecmp("image/png",mime_type,mime_type_end-mime_type)==0){
+      extract_png_params(buf+data_offset,data_length,
+       &file_width,&file_height,&file_depth,&file_colors,&has_palette);
+    }
+    else if(mime_type_end-mime_type==9
+     &&oi_strncasecmp("image/gif",mime_type,mime_type_end-mime_type)==0){
+      extract_gif_params(buf+data_offset,data_length,
+       &file_width,&file_height,&file_depth,&file_colors,&has_palette);
+    }
+    else if(mime_type_end-mime_type==10
+     &&oi_strncasecmp("image/jpeg",mime_type,mime_type_end-mime_type)==0){
+      extract_jpeg_params(buf+data_offset,data_length,
+       &file_width,&file_height,&file_depth,&file_colors,&has_palette);
+    }
+    if(!width)width=file_width;
+    if(!height)height=file_height;
+    if(!depth)depth=file_depth;
+    if(!colors)colors=file_colors;
+    if((file_width&&width!=file_width)
+     ||(file_height&&height!=file_height)
+     ||(file_depth&&depth!=file_depth)
+     /*We use has_palette to ensure we also reject non-0 user color counts for
+        images we've positively identified as non-paletted.*/
+     ||(has_palette>=0&&colors!=file_colors)){
+      free(buf);
+      *error_message="invalid picture specification: "
+       "resolution/color field does not match file";
+      return NULL;
+    }
+  }
+  /*These fields MUST be set correctly OR all set to zero.
+    So if any of them (except colors, for which 0 is a valid value) are still
+     zero, clear the rest to zero.*/
+  if(width==0||height==0||depth==0)width=height=depth=colors=0;
+  if(picture_type==1&&(width!=32||height!=32
+   ||mime_type_end-mime_type!=9
+   ||oi_strncasecmp("image/png",mime_type,mime_type_end-mime_type)!=0)){
+    free(buf);
+    *error_message="pictures of type 1 MUST be 32x32 PNGs";
+    return NULL;
+  }
+  /*Build the METADATA_BLOCK_PICTURE buffer.
+    We do this backwards from data_offset, because we didn't necessarily know
+     how big the mime type string was before we read the data in.*/
+  data_offset-=4;
+  WRITE_U32_BE(buf+data_offset,(unsigned long)data_length);
+  data_offset-=4;
+  WRITE_U32_BE(buf+data_offset,colors);
+  data_offset-=4;
+  WRITE_U32_BE(buf+data_offset,depth);
+  data_offset-=4;
+  WRITE_U32_BE(buf+data_offset,height);
+  data_offset-=4;
+  WRITE_U32_BE(buf+data_offset,width);
+  data_offset-=description_end-description;
+  memcpy(buf+data_offset,description,description_end-description);
+  data_offset-=4;
+  WRITE_U32_BE(buf+data_offset,(unsigned long)(description_end-description));
+  data_offset-=mime_type_end-mime_type;
+  memcpy(buf+data_offset,mime_type,mime_type_end-mime_type);
+  data_offset-=4;
+  WRITE_U32_BE(buf+data_offset,(unsigned long)(mime_type_end-mime_type));
+  data_offset-=4;
+  WRITE_U32_BE(buf+data_offset,picture_type);
+  data_length=nbuf-data_offset;
+  b64_length=BASE64_LENGTH(data_length);
+  out=(char *)malloc(b64_length+1);
+  if(out!=NULL){
+    base64_encode(out,(char *)buf+data_offset,data_length);
+    if(picture_type>=1&&picture_type<=2)*seen_file_icons|=picture_type;
+  }
+  free(buf);
+  return out;
+}
--- /dev/null
+++ b/src/picture.h
@@ -1,0 +1,50 @@
+#ifndef __PICTURE_H
+#define __PICTURE_H
+
+#include <ogg/ogg.h>
+
+typedef enum{
+  PIC_FORMAT_JPEG,
+  PIC_FORMAT_PNG,
+  PIC_FORMAT_GIF
+}picture_format;
+
+#define BASE64_LENGTH(len) (((len)+2)/3*4)
+
+/*Utility function for base64 encoding METADATA_BLOCK_PICTURE tags.
+  Stores BASE64_LENGTH(len)+1 bytes in dst (including a terminating NUL).*/
+void base64_encode(char *dst, const char *src, int len);
+
+int oi_strncasecmp(const char *a, const char *b, int n);
+
+int is_jpeg(const unsigned char *buf, size_t length);
+int is_png(const unsigned char *buf, size_t length);
+int is_gif(const unsigned char *buf, size_t length);
+
+void extract_png_params(const unsigned char *data, size_t data_length,
+                        ogg_uint32_t *width, ogg_uint32_t *height,
+                        ogg_uint32_t *depth, ogg_uint32_t *colors,
+                        int *has_palette);
+void extract_gif_params(const unsigned char *data, size_t data_length,
+                        ogg_uint32_t *width, ogg_uint32_t *height,
+                        ogg_uint32_t *depth, ogg_uint32_t *colors,
+                        int *has_palette);
+void extract_jpeg_params(const unsigned char *data, size_t data_length,
+                         ogg_uint32_t *width, ogg_uint32_t *height,
+                         ogg_uint32_t *depth, ogg_uint32_t *colors,
+                         int *has_palette);
+
+char *parse_picture_specification(const char *spec,
+                                  const char **error_message,
+                                  int *seen_file_icons);
+
+#define WRITE_U32_BE(buf, val) \
+  do{ \
+    (buf)[0]=(unsigned char)((val)>>24); \
+    (buf)[1]=(unsigned char)((val)>>16); \
+    (buf)[2]=(unsigned char)((val)>>8); \
+    (buf)[3]=(unsigned char)(val); \
+  } \
+  while(0);
+
+#endif /* __PICTURE_H */