shithub: opus-tools

Download patch

ref: d0fa4337002a47a95efa5492ea44a2f9ac4b27f0
parent: b6e554c46ff879376dbab65bbd54680d55822b6e
author: Timothy B. Terriberry <tterribe@xiph.org>
date: Sat Apr 27 06:10:36 EDT 2013

Add support for METADATA_BLOCK_PICTURE.

The allows embedding album art in Opus files.
The format is the same as for Vorbis files, which is a base64
 encoded VorbisComment version of the format used in FLAC files.
It extends opusenc to provide a --picture option that works the
 same as FLAC's, and automatically parses mime type and image
 dimensions for JPEG, PNG, and GIF files.
It also updates the FLAC input module to convert FLAC's native
 metadata picture blocks to the base64 encoded version.

--- a/src/flac.c
+++ b/src/flac.c
@@ -96,6 +96,13 @@
   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){
@@ -205,6 +212,52 @@
         }
       }
       break;
+    case FLAC__METADATA_TYPE_PICTURE:
+      {
+        char  *buf;
+        char  *b64;
+        size_t mime_type_length;
+        size_t description_length;
+        size_t buf_sz;
+        size_t b64_sz;
+        size_t offs;
+        mime_type_length=strlen(metadata->data.picture.mime_type);
+        description_length=strlen((char *)metadata->data.picture.description);
+        buf_sz=32+mime_type_length+description_length
+         +metadata->data.picture.data_length;
+        buf=(char *)malloc(buf_sz);
+        offs=0;
+        pack_u32be(buf+offs,metadata->data.picture.type);
+        offs+=4;
+        pack_u32be(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);
+        offs+=4;
+        memcpy(buf,metadata->data.picture.description,description_length);
+        offs+=description_length;
+        pack_u32be(buf+offs,metadata->data.picture.width);
+        offs+=4;
+        pack_u32be(buf+offs,metadata->data.picture.height);
+        offs+=4;
+        pack_u32be(buf+offs,metadata->data.picture.depth);
+        offs+=4;
+        pack_u32be(buf+offs,metadata->data.picture.colors);
+        offs+=4;
+        pack_u32be(buf+offs,metadata->data.picture.data_length);
+        offs+=4;
+        memcpy(buf+offs,metadata->data.picture.data,
+           metadata->data.picture.data_length);
+        b64_sz=BASE64_LENGTH(buf_sz)+1;
+        b64=(char *)malloc(b64_sz);
+        base64_encode(b64,buf,buf_sz);
+        free(buf);
+        comment_add(&inopt->comments,&inopt->comments_length,
+           "METADATA_BLOCK_PICTURE",b64);
+        free(b64);
+      }
+      break;
     default:
       break;
   }
@@ -331,9 +384,11 @@
   flac=malloc(sizeof(*flac));
   flac->decoder=FLAC__stream_decoder_new();
   FLAC__stream_decoder_set_md5_checking(flac->decoder,false);
-  /*We get STREAMINFO packets by default, but not VORBIS_COMMENT.*/
+  /*We get STREAMINFO packets by default, but not VORBIS_COMMENT or PICTURE.*/
   FLAC__stream_decoder_set_metadata_respond(flac->decoder,
      FLAC__METADATA_TYPE_VORBIS_COMMENT);
+  FLAC__stream_decoder_set_metadata_respond(flac->decoder,
+     FLAC__METADATA_TYPE_PICTURE);
   flac->inopt=opt;
   flac->f=in;
   flac->oldbuf=malloc(buflen*sizeof(*flac->oldbuf));
--- a/src/opusenc.c
+++ b/src/opusenc.c
@@ -80,6 +80,8 @@
 
 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)
@@ -155,6 +157,70 @@
   printf(" --album            Album or collection this track belongs to\n");
   printf(" --date             Date for this track\n");
   printf(" --genre            Genre for this track\n");
+  printf(" --picture          Album art for this track\n");
+  printf("                      More than one --picture option can be specified.\n");
+  printf("                      Either a FILENAME for the picture file or a more\n");
+  printf("                      complete SPECIFICATION form can be used. The\n");
+  printf("                      SPECIFICATION is a string whose parts are\n");
+  printf("                      separated by | (pipe) characters. Some parts may\n");
+  printf("                      be left empty to invoke default values. A\n");
+  printf("                      FILENAME is just shorthand for \"||||FILENAME\".\n");
+  printf("                      The format of SPECIFICATION is\n");
+  printf("\n");
+  printf("                      [TYPE]|[MIME-TYPE]|[DESCRIPTION]|[WIDTHxHEIGHT\n");
+  printf("                      xDEPTH[/COLORS]]|FILENAME\n");
+  printf("\n");
+  printf("                      TYPE is an optional number from one of:\n");
+  printf("                      0: Other\n");
+  printf("                      1: 32x32 pixel 'file icon' (PNG only)\n");
+  printf("                      2: Other file icon\n");
+  printf("                      3: Cover (front)\n");
+  printf("                      4: Cover (back)\n");
+  printf("                      5: Leaflet page\n");
+  printf("                      6: Media (e.g., label side of a CD)\n");
+  printf("                      7: Lead artist/lead performer/soloist\n");
+  printf("                      8: Artist/performer\n");
+  printf("                      9: Conductor\n");
+  printf("                      10: Band/Orchestra\n");
+  printf("                      11: Composer\n");
+  printf("                      12: Lyricist/text writer\n");
+  printf("                      13: Recording location\n");
+  printf("                      14: During recording\n");
+  printf("                      15: During performance\n");
+  printf("                      16: Movie/video screen capture\n");
+  printf("                      17: A bright colored fish\n");
+  printf("                      18: Illustration\n");
+  printf("                      19: Band/artist logotype\n");
+  printf("                      20: Publisher/studio logotype\n");
+  printf("\n");
+  printf("                      The default is 3 (front cover). There may only be\n");
+  printf("                      one picture each of type 1 and 2 in a file.\n");
+  printf("\n");
+  printf("                      MIME-TYPE is optional. If left blank, it will be\n");
+  printf("                      detected from the file. For best compatibility\n");
+  printf("                      with players, use pictures with a MIME-TYPE of\n");
+  printf("                      image/jpeg or image/png. The MIME-TYPE can also\n");
+  printf("                      be --> to mean that FILENAME is actually a URL to\n");
+  printf("                      an image, though this use is discouraged. The\n");
+  printf("                      file at the URL will not be fetched. The URL\n");
+  printf("                      itself is stored in the metadata.\n");
+  printf("\n");
+  printf("                      DESCRIPTION is optional. The default is an empty\n");
+  printf("                      string.\n");
+  printf("\n");
+  printf("                      The next part specifies the resolution and color\n");
+  printf("                      information. If the MIME-TYPE is image/jpeg,\n");
+  printf("                      image/png, or image/gif, you can usually leave\n");
+  printf("                      this empty and they can be detected from the\n");
+  printf("                      file. Otherwise, you must specify the width in\n");
+  printf("                      pixels, height in pixels, and color depth in\n");
+  printf("                      bits-per-pixel. If the image has indexed colors\n");
+  printf("                      you should also specify the number of colors\n");
+  printf("                      used. If possible, these are checked against the\n");
+  printf("                      file for accuracy.\n");
+  printf("\n");
+  printf("                      FILENAME is the path to the picture file to be\n");
+  printf("                      imported, or the URL if the MIME-TYPE is -->.\n");
   printf(" --padding n        Extra bytes to reserve for metadata (default: 512)\n");
   printf(" --discard-comments Don't keep metadata when transcoding\n");
   printf("\nInput options:\n");
@@ -219,6 +285,7 @@
     {"album", required_argument, NULL, 0},
     {"date", required_argument, NULL, 0},
     {"genre", required_argument, NULL, 0},
+    {"picture", required_argument, NULL, 0},
     {"padding", required_argument, NULL, 0},
     {"discard-comments", no_argument, NULL, 0},
     {0, 0, 0, 0}
@@ -280,6 +347,7 @@
   int                *opt_ctls_ctlval;
   int                opt_ctls=0;
   int                max_ogg_delay=48000; /*48kHz samples*/
+  int                seen_file_icons=0;
   int                comment_padding=512;
   int                serialno;
   opus_int32         lookahead=0;
@@ -492,6 +560,19 @@
         } else if(strcmp(long_options[option_index].name,"genre")==0){
           save_cmd=0;
           comment_add(&inopt.comments, &inopt.comments_length, "genre", optarg);
+        } else if(strcmp(long_options[option_index].name,"picture")==0){
+          const char *error_message;
+          char       *picture_data;
+          save_cmd=0;
+          picture_data=parse_picture_specification(optarg,&error_message,
+                                                   &seen_file_icons);
+          if(picture_data==NULL){
+            fprintf(stderr,"Error parsing picture option: %s\n",error_message);
+            exit(1);
+          }
+          comment_add(&inopt.comments,&inopt.comments_length,
+                      "METADATA_BLOCK_PICTURE",picture_data);
+          free(picture_data);
         } else if(strcmp(long_options[option_index].name,"padding")==0){
           comment_padding=atoi(optarg);
         } else if(strcmp(long_options[option_index].name,"discard-comments")==0){
@@ -1138,3 +1219,408 @@
 }
 #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,4 +104,8 @@
 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 */