shithub: opusfile

Download patch

ref: 116b703442e52a5dca635b5050d3d7165d2c9662
parent: 26ff5e089106de677328d2b53d08a26d22af37e6
author: Timothy B. Terriberry <[email protected]>
date: Sat Aug 10 07:19:33 EDT 2013

Add an API to parse picture tags.

This makes it easier for applications to support album art.
This is still relatively untested.

--- a/examples/opusfile_example.c
+++ b/examples/opusfile_example.c
@@ -93,6 +93,24 @@
   else fprintf(_fp,"%li%s%c",(long)val,_spacer,SUFFIXES[shift]);
 }
 
+/*A version of strncasecmp() that is guaranteed to only ignore the case of
+   ASCII characters.*/
+static int local_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 put_le32(unsigned char *_dst,opus_uint32 _x){
   _dst[0]=(unsigned char)(_x&0xFF);
   _dst[1]=(unsigned char)(_x>>8&0xFF);
@@ -256,7 +274,28 @@
         tags=op_tags(of,li);
         fprintf(stderr,"  Encoded by: %s\n",tags->vendor);
         for(ci=0;ci<tags->comments;ci++){
-          fprintf(stderr,"  %s\n",tags->user_comments[ci]);
+          const char *comment;
+          comment=tags->user_comments[ci];
+          if(local_strncasecmp(comment,"METADATA_BLOCK_PICTURE=",23)==0){
+            OpusPictureTag pic;
+            int            err;
+            err=opus_picture_tag_parse(&pic,comment);
+            fprintf(stderr,"  %.23s",comment);
+            if(err>=0){
+              fprintf(stderr,"%u|%s|%s|%ux%ux%u",pic.type,pic.mime_type,
+               pic.description,pic.width,pic.height,pic.depth);
+              if(pic.colors!=0)fprintf(stderr,"/%u",pic.colors);
+              if(pic.format==OP_PIC_FORMAT_URL){
+                fprintf(stderr,"|%s\n",pic.data);
+              }
+              else{
+                fprintf(stderr,"|<%u bytes of image data>\n",pic.data_length);
+              }
+              opus_picture_tag_clear(&pic);
+            }
+            else fprintf(stderr,"<error parsing picture tag>\n");
+          }
+          else fprintf(stderr,"  %s\n",tags->user_comments[ci]);
         }
         fprintf(stderr,"\n");
         if(!op_seekable(of)){
--- a/include/opusfile.h
+++ b/include/opusfile.h
@@ -122,9 +122,10 @@
 #  pragma GCC visibility push(default)
 # endif
 
-typedef struct OpusHead    OpusHead;
-typedef struct OpusTags    OpusTags;
-typedef struct OggOpusFile OggOpusFile;
+typedef struct OpusHead       OpusHead;
+typedef struct OpusTags       OpusTags;
+typedef struct OpusPictureTag OpusPictureTag;
+typedef struct OggOpusFile    OggOpusFile;
 
 /*Warning attributes for libopusfile functions.*/
 # if OP_GNUC_PREREQ(3,4)
@@ -301,6 +302,87 @@
   char  *vendor;
 };
 
+/**\name Picture tag image formats*/
+/*@{*/
+
+/**The MIME type was not recognized, or the image data did not match the
+    declared MIME type.*/
+#define OP_PIC_FORMAT_UNKNOWN (-1)
+/**The MIME type indicates the image data is really a URL.*/
+#define OP_PIC_FORMAT_URL     (0)
+/**The image is a JPEG.*/
+#define OP_PIC_FORMAT_JPEG    (1)
+/**The image is a PNG.*/
+#define OP_PIC_FORMAT_PNG     (2)
+/**The image is a GIF.*/
+#define OP_PIC_FORMAT_GIF     (3)
+
+/*@}*/
+
+/**The contents of a METADATA_BLOCK_PICTURE tag.*/
+struct OpusPictureTag{
+  /**The picture type according to the ID3v2 APIC frame:
+     <ol start="0">
+     <li>Other</li>
+     <li>32x32 pixels 'file icon' (PNG only)</li>
+     <li>Other file icon</li>
+     <li>Cover (front)</li>
+     <li>Cover (back)</li>
+     <li>Leaflet page</li>
+     <li>Media (e.g. label side of CD)</li>
+     <li>Lead artist/lead performer/soloist</li>
+     <li>Artist/performer</li>
+     <li>Conductor</li>
+     <li>Band/Orchestra</li>
+     <li>Composer</li>
+     <li>Lyricist/text writer</li>
+     <li>Recording Location</li>
+     <li>During recording</li>
+     <li>During performance</li>
+     <li>Movie/video screen capture</li>
+     <li>A bright colored fish</li>
+     <li>Illustration</li>
+     <li>Band/artist logotype</li>
+     <li>Publisher/Studio logotype</li>
+     </ol>
+     Others are reserved and should not be used.
+     There may only be one each of picture type 1 and 2 in a file.*/
+  opus_int32     type;
+  /**The MIME type of the picture, in printable ASCII characters 0x20-0x7E.
+     The MIME type may also be <code>"-->"</code> to signify that the data part
+      is a URL pointing to the picture instead of the picture data itself.
+     In this case, a terminating NUL is appended to the URL string in #data,
+      but #data_length is set to the length of the string excluding that
+      terminating NUL.*/
+  char          *mime_type;
+  /**The description of the picture, in UTF-8.*/
+  char          *description;
+  /**The width of the picture in pixels.*/
+  opus_uint32    width;
+  /**The height of the picture in pixels.*/
+  opus_uint32    height;
+  /**The color depth of the picture in bits-per-pixel (<em>not</em>
+      bits-per-channel).*/
+  opus_uint32    depth;
+  /**For indexed-color pictures (e.g., GIF), the number of colors used, or 0
+      for non-indexed pictures.*/
+  opus_uint32    colors;
+  /**The length of the picture data in bytes.*/
+  opus_uint32    data_length;
+  /**The binary picture data.*/
+  unsigned char *data;
+  /**The format of the picture data, if known.
+     One of
+     <ul>
+     <li>#OP_PIC_FORMAT_UNKNOWN,</li>
+     <li>#OP_PIC_FORMAT_URL,</li>
+     <li>#OP_PIC_FORMAT_JPEG,</li>
+     <li>#OP_PIC_FORMAT_PNG,</li>
+     <li>#OP_PIC_FORMAT_GIF, or</li>
+     </ul>.*/
+  int            format;
+};
+
 /**\name Functions for manipulating header data
 
    These functions manipulate the #OpusHead and #OpusTags structures,
@@ -457,6 +539,51 @@
    It will free all memory used by the structure members.
    \param _tags The #OpusTags structure to clear.*/
 void opus_tags_clear(OpusTags *_tags) OP_ARG_NONNULL(1);
+
+/**Parse a single METADATA_BLOCK_PICTURE tag.
+   This decodes the BASE64-encoded content of the tag and returns a structure
+    with the MIME type, description, image parameters (if known), and the
+    compressed image data.
+   If the MIME type indicates the presence of an image format we recognize
+    (JPEG, PNG, or GIF) and the actual image data contains the magic signature
+    associated with that format, then the OpusPictureTag::format field will be
+    set to the corresponding format.
+   This is provided as a convenience to avoid requiring applications to parse
+    the MIME type and/or do their own format detection for the commonly used
+    formats.
+   In this case, we also attempt to extract the image parameters directly from
+    the image data (overriding any that were present in the tag, which the
+    specification says applications are not meant to rely on).
+   The application must still provide its own support for actually decoding the
+    image data and, if applicable, retrieving that data from URLs.
+   \param[out] _pic Returns the parsed picture data.
+                    No sanitation is done on the type, MIME type, or
+                     description fields, so these might return invalid values.
+                    The contents of this structure are left unmodified on
+                     failure.
+   \param      _tag The METADATA_BLOCK_PICTURE tag contents.
+                    The leading "METADATA_BLOCK_PICTURE=" portion is optional,
+                     to allow the function to be used on either directly on the
+                     values in OpusTags::user_comments or on the return value
+                     of opus_tags_query().
+   \return 0 on success or a negative value on error.
+   \retval #OP_ENOTFORMAT The METADATA_BLOCK_PICTURE contents were not valid.
+   \retval #OP_EFAULT     A memory allocation failed.*/
+int opus_picture_tag_parse(OpusPictureTag *_pic,const char *_tag)
+ OP_ARG_NONNULL(1) OP_ARG_NONNULL(2);
+
+/**Initializes an #OpusPictureTag structure.
+   This should be called on a freshly allocated #OpusPictureTag structure
+    before attempting to use it.
+   \param _pic The #OpusPictureTag structure to initialize.*/
+void opus_picture_tag_init(OpusPictureTag *_pic) OP_ARG_NONNULL(1);
+
+/**Clears the #OpusPictureTag structure.
+   This should be called on an #OpusPictureTag structure after it is no longer
+    needed.
+   It will free all memory used by the structure members.
+   \param _pic The #OpusPictureTag structure to clear.*/
+void opus_picture_tag_clear(OpusPictureTag *_pic) OP_ARG_NONNULL(1);
 
 /*@}*/
 
--- a/src/info.c
+++ b/src/info.c
@@ -31,6 +31,10 @@
   return _data[0]|_data[1]<<8|_data[2]<<16|_data[3]<<24;
 }
 
+static opus_uint32 op_parse_uint32be(const unsigned char *_data){
+  return _data[3]|_data[2]<<8|_data[1]<<16|_data[0]<<24;
+}
+
 int opus_head_parse(OpusHead *_head,const unsigned char *_data,size_t _len){
   OpusHead head;
   if(_len<8)return OP_ENOTFORMAT;
@@ -325,4 +329,322 @@
     }
   }
   return OP_FALSE;
+}
+
+static int op_is_jpeg(const unsigned char *_buf,size_t _buf_sz){
+  return _buf_sz>=11&&memcmp(_buf,"\xFF\xD8\xFF\xE0",4)==0
+   &&(_buf[4]<<8|_buf[5])>=16&&memcmp(_buf+6,"JFIF",5)==0;
+}
+
+/*Tries to extract the width, height, bits per pixel, and palette size of a
+   JPEG.
+  On failure, simply leaves its outputs unmodified.*/
+static void op_extract_jpeg_params(const unsigned char *_buf,size_t _buf_sz,
+ opus_uint32 *_width,opus_uint32 *_height,
+ opus_uint32 *_depth,opus_uint32 *_colors,int *_has_palette){
+  if(op_is_jpeg(_buf,_buf_sz)){
+    size_t offs;
+    offs=2;
+    for(;;){
+      size_t segment_len;
+      int    marker;
+      while(offs<_buf_sz&&_buf[offs]!=0xFF)offs++;
+      while(offs<_buf_sz&&_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>=_buf_sz||(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(_buf_sz-offs<2)break;
+      segment_len=_buf[offs]<<8|_buf[offs+1];
+      if(segment_len<2||_buf_sz-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=_buf[offs+3]<<8|_buf[offs+4];
+          *_width=_buf[offs+5]<<8|_buf[offs+6];
+          *_depth=_buf[offs+2]*_buf[offs+7];
+          *_colors=0;
+          *_has_palette=0;
+        }
+        break;
+      }
+      /*Other markers: skip the whole marker segment.*/
+      offs+=segment_len;
+    }
+  }
+}
+
+static int op_is_png(const unsigned char *_buf,size_t _buf_sz){
+  return _buf_sz>=8&&memcmp(_buf,"\x89PNG\x0D\x0A\x1A\x0A",8)==0;
+}
+
+/*Tries to extract the width, height, bits per pixel, and palette size of a
+   PNG.
+  On failure, simply leaves its outputs unmodified.*/
+static void op_extract_png_params(const unsigned char *_buf,size_t _buf_sz,
+ opus_uint32 *_width,opus_uint32 *_height,
+ opus_uint32 *_depth,opus_uint32 *_colors,int *_has_palette){
+  if(op_is_png(_buf,_buf_sz)){
+    size_t offs;
+    offs=8;
+    while(_buf_sz-offs>=12){
+      ogg_uint32_t chunk_len;
+      chunk_len=op_parse_uint32be(_buf+offs);
+      if(chunk_len>_buf_sz-(offs+12))break;
+      else if(chunk_len==13&&memcmp(_buf+offs+4,"IHDR",4)==0){
+        int color_type;
+        *_width=op_parse_uint32be(_buf+offs+8);
+        *_height=op_parse_uint32be(_buf+offs+12);
+        color_type=_buf[offs+17];
+        if(color_type==3){
+          *_depth=24;
+          *_has_palette=1;
+        }
+        else{
+          int sample_depth;
+          sample_depth=_buf[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(_buf+offs+4,"PLTE",4)==0){
+        *_colors=chunk_len/3;
+        break;
+      }
+      offs+=12+chunk_len;
+    }
+  }
+}
+
+static int op_is_gif(const unsigned char *_buf,size_t _buf_sz){
+  return _buf_sz>=6&&(memcmp(_buf,"GIF87a",6)==0||memcmp(_buf,"GIF89a",6)==0);
+}
+
+/*Tries to extract the width, height, bits per pixel, and palette size of a
+   GIF.
+  On failure, simply leaves its outputs unmodified.*/
+static void op_extract_gif_params(const unsigned char *_buf,size_t _buf_sz,
+ opus_uint32 *_width,opus_uint32 *_height,
+ opus_uint32 *_depth,opus_uint32 *_colors,int *_has_palette){
+  if(op_is_gif(_buf,_buf_sz)&&_buf_sz>=14){
+    *_width=_buf[6]|_buf[7]<<8;
+    *_height=_buf[8]|_buf[9]<<8;
+    /*libFLAC hard-codes the depth to 24.*/
+    *_depth=24;
+    *_colors=1<<((_buf[10]&7)+1);
+    *_has_palette=1;
+  }
+}
+
+/*The actual implementation of opus_picture_tag_parse().
+  Unlike the public API, this function requires _pic to already be
+   initialized, modifies its contents before success is guaranteed, and assumes
+   the caller will clear it on error.*/
+static int opus_picture_tag_parse_impl(OpusPictureTag *_pic,const char *_tag,
+ unsigned char *_buf,size_t _buf_sz,size_t _base64_sz){
+  opus_int32   picture_type;
+  opus_uint32  mime_type_length;
+  char        *mime_type;
+  opus_uint32  description_length;
+  char        *description;
+  opus_uint32  width;
+  opus_uint32  height;
+  opus_uint32  depth;
+  opus_uint32  colors;
+  opus_uint32  data_length;
+  opus_uint32  file_width;
+  opus_uint32  file_height;
+  opus_uint32  file_depth;
+  opus_uint32  file_colors;
+  int          format;
+  int          has_palette;
+  int          colors_set;
+  size_t       i;
+  /*Decode the BASE64 data.*/
+  for(i=0;i<_base64_sz;i++){
+    opus_uint32 value;
+    int         j;
+    for(j=0;j<4;j++){
+      unsigned c;
+      unsigned d;
+      c=(unsigned char)_tag[4*i+j];
+      if(c=='+')d=62;
+      else if(c=='/')d=63;
+      else if(c>='0'&&c<='9')d=52+c-'0';
+      else if(c>='a'&&c<='z')d=26+c-'a';
+      else if(c>='A'&&c<='Z')d=c-'A';
+      else if(c=='='&&3*i+j>_buf_sz)d=0;
+      else return OP_ENOTFORMAT;
+      value=value<<6|d;
+    }
+    _buf[3*i]=(unsigned char)(value>>16);
+    if(3*i+1<_buf_sz){
+      _buf[3*i+1]=(unsigned char)(value>>8);
+      if(3*i+2<_buf_sz)_buf[3*i+2]=(unsigned char)value;
+    }
+  }
+  i=0;
+  picture_type=op_parse_uint32be(_buf+i);
+  i+=4;
+  /*Extract the MIME type.*/
+  mime_type_length=op_parse_uint32be(_buf+i);
+  i+=4;
+  if(mime_type_length>_buf_sz-32)return OP_ENOTFORMAT;
+  mime_type=(char *)_ogg_malloc(sizeof(*_pic->mime_type)*(mime_type_length+1));
+  if(mime_type==NULL)return OP_EFAULT;
+  memcpy(mime_type,_buf+i,sizeof(*mime_type)*mime_type_length);
+  mime_type[mime_type_length]='\0';
+  _pic->mime_type=mime_type;
+  i+=mime_type_length;
+  /*Extract the description string.*/
+  description_length=op_parse_uint32be(_buf+i);
+  i+=4;
+  if(description_length>_buf_sz-mime_type_length-32)return OP_ENOTFORMAT;
+  description=
+   (char *)_ogg_malloc(sizeof(*_pic->mime_type)*(description_length+1));
+  if(description==NULL)return OP_EFAULT;
+  memcpy(description,_buf+i,sizeof(*description)*description_length);
+  description[description_length]='\0';
+  _pic->description=description;
+  i+=description_length;
+  /*Extract the remaining fields.*/
+  width=op_parse_uint32be(_buf+i);
+  i+=4;
+  height=op_parse_uint32be(_buf+i);
+  i+=4;
+  depth=op_parse_uint32be(_buf+i);
+  i+=4;
+  colors=op_parse_uint32be(_buf+i);
+  i+=4;
+  /*If one of these is set, they all must be, but colors==0 is a valid value.*/
+  colors_set=width!=0||height!=0||depth!=0||colors!=0;
+  if(width==0||height==0||depth==0&&colors_set)return OP_ENOTFORMAT;
+  data_length=op_parse_uint32be(_buf+i);
+  i+=4;
+  if(data_length>_buf_sz-i)return OP_ENOTFORMAT;
+  /*Trim extraneous data so we don't copy it below.*/
+  _buf_sz=i+data_length;
+  /*Attempt to determine the image format.*/
+  format=OP_PIC_FORMAT_UNKNOWN;
+  if(mime_type_length==3&&strcmp(mime_type,"-->")==0){
+    format=OP_PIC_FORMAT_URL;
+    /*Picture type 1 must be a 32x32 PNG.*/
+    if(picture_type==1&&(width!=0||height!=0)&&(width!=32||height!=32)){
+      return OP_ENOTFORMAT;
+    }
+    /*Append a terminating NUL for the convenience of our callers.*/
+    _buf[_buf_sz++]='\0';
+  }
+  else{
+    if(mime_type_length==10
+     &&op_strncasecmp(mime_type,"image/jpeg",mime_type_length)==0){
+      if(op_is_jpeg(_buf+i,data_length))format=OP_PIC_FORMAT_JPEG;
+    }
+    else if(mime_type_length==9
+     &&op_strncasecmp(mime_type,"image/png",mime_type_length)==0){
+      if(op_is_png(_buf+i,data_length))format=OP_PIC_FORMAT_PNG;
+    }
+    else if(mime_type_length==9
+     &&op_strncasecmp(mime_type,"image/gif",mime_type_length)==0){
+      if(op_is_gif(_buf+i,data_length))format=OP_PIC_FORMAT_GIF;
+    }
+    else if(mime_type_length==0||(mime_type_length==6
+     &&op_strncasecmp(mime_type,"image/",mime_type_length)==0)){
+      if(op_is_jpeg(_buf+i,data_length))format=OP_PIC_FORMAT_JPEG;
+      else if(op_is_png(_buf+i,data_length))format=OP_PIC_FORMAT_PNG;
+      else if(op_is_gif(_buf+i,data_length))format=OP_PIC_FORMAT_GIF;
+    }
+    file_width=file_height=file_depth=file_colors=0;
+    has_palette=-1;
+    switch(format){
+      case OP_PIC_FORMAT_JPEG:{
+        op_extract_jpeg_params(_buf+i,data_length,
+         &file_width,&file_height,&file_depth,&file_colors,&has_palette);
+      }break;
+      case OP_PIC_FORMAT_PNG:{
+        op_extract_png_params(_buf+i,data_length,
+         &file_width,&file_height,&file_depth,&file_colors,&has_palette);
+      }break;
+      case OP_PIC_FORMAT_GIF:{
+        op_extract_gif_params(_buf+i,data_length,
+         &file_width,&file_height,&file_depth,&file_colors,&has_palette);
+      }break;
+    }
+    if(has_palette>=0){
+      /*If we successfully extracted these parameters from the image, override
+         any declared values.*/
+      width=file_width;
+      height=file_height;
+      depth=file_depth;
+      colors=file_colors;
+    }
+    /*Picture type 1 must be a 32x32 PNG.*/
+    if(picture_type==1&&(format!=OP_PIC_FORMAT_PNG||width!=32||height!=32)){
+      return OP_ENOTFORMAT;
+    }
+  }
+  /*Adjust _buf_sz instead of using data_length to capture the terminating NUL
+     for URLs.*/
+  _buf_sz-=i;
+  memmove(_buf,_buf+i,sizeof(*_buf)*_buf_sz);
+  _buf=(unsigned char *)_ogg_realloc(_buf,_buf_sz);
+  if(_buf_sz>0&&_buf==NULL)return OP_EFAULT;
+  _pic->type=picture_type;
+  _pic->width=width;
+  _pic->height=height;
+  _pic->depth=depth;
+  _pic->colors=colors;
+  _pic->data_length=data_length;
+  _pic->data=_buf;
+  _pic->format=format;
+  return 0;
+}
+
+int opus_picture_tag_parse(OpusPictureTag *_pic,const char *_tag){
+  OpusPictureTag  pic;
+  unsigned char  *buf;
+  size_t          base64_sz;
+  size_t          buf_sz;
+  size_t          tag_length;
+  int             ret;
+  if(op_strncasecmp(_tag,"METADATA_BLOCK_PICTURE=",23)==0)_tag+=23;
+  /*Figure out how much BASE64-encoded data we have.*/
+  tag_length=strlen(_tag);
+  if(tag_length&3)return OP_ENOTFORMAT;
+  base64_sz=tag_length>>2;
+  buf_sz=3*base64_sz;
+  if(buf_sz<32)return OP_ENOTFORMAT;
+  if(_tag[tag_length-1]=='=')buf_sz--;
+  if(_tag[tag_length-2]=='=')buf_sz--;
+  if(buf_sz<32)return OP_ENOTFORMAT;
+  /*Allocate an extra byte to allow appending a terminating NUL to URL data.*/
+  buf=(unsigned char *)_ogg_malloc(sizeof(*buf)*(buf_sz+1));
+  if(buf==NULL)return OP_EFAULT;
+  opus_picture_tag_init(&pic);
+  ret=opus_picture_tag_parse_impl(&pic,_tag,buf,buf_sz,base64_sz);
+  if(ret<0){
+    opus_picture_tag_clear(&pic);
+    _ogg_free(buf);
+  }
+  else *_pic=*&pic;
+  return ret;
+}
+
+void opus_picture_tag_init(OpusPictureTag *_pic){
+  memset(_pic,0,sizeof(*_pic));
+}
+
+void opus_picture_tag_clear(OpusPictureTag *_pic){
+  _ogg_free(_pic->description);
+  _ogg_free(_pic->mime_type);
+  _ogg_free(_pic->data);
 }