1# Copyright 2013 Christoph Reiter 2# 3# This program is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; either version 2 of the License, or 6# (at your option) any later version. 7 8from ._misc import AudioFileError 9 10 11class ImageContainer(object): 12 """Mixin/Interface for AudioFile to support basic embedded image editing""" 13 14 def get_primary_image(self): 15 """Returns the primary embedded image or None. 16 17 In case of an error returns None. 18 """ 19 20 return 21 22 def get_images(self): 23 """Returns a list of embedded images, primary first. 24 25 In case of an error returns an empty list. 26 """ 27 28 # fall back to the single implementation 29 image = self.get_primary_image() 30 if image: 31 return [image] 32 return [] 33 34 @property 35 def has_images(self): 36 """Fast way to check for images, might be False if the file 37 was modified externally. 38 """ 39 40 return "~picture" in self 41 42 @has_images.setter 43 def has_images(self, value): 44 if value: 45 self["~picture"] = "y" 46 else: 47 self.pop("~picture", None) 48 49 @property 50 def can_change_images(self): 51 """Return True IFF `clear_images()` and `set_images()` are 52 implemented""" 53 54 return False 55 56 def clear_images(self): 57 """Delete all embedded images. 58 59 Raises: 60 AudioFileError 61 """ 62 63 raise AudioFileError("Not supported for this format") 64 65 def set_image(self, image): 66 """Replaces all embedded images by the passed image. 67 68 The image type recorded in the file will be APICType.COVER_FRONT, 69 disregarding image.type. 70 71 Raises: 72 AudioFileError 73 """ 74 75 raise AudioFileError("Not supported for this format") 76 77 78class APICType(object): 79 """Enumeration of image types defined by the ID3 standard but also reused 80 in WMA/FLAC/VorbisComment 81 """ 82 83 # Other 84 OTHER = 0 85 # 32x32 pixels 'file icon' (PNG only) 86 FILE_ICON = 1 87 # Other file icon 88 OTHER_FILE_ICON = 2 89 # Cover (front) 90 COVER_FRONT = 3 91 # Cover (back) 92 COVER_BACK = 4 93 # Leaflet page 94 LEAFLET_PAGE = 5 95 # Media (e.g. label side of CD) 96 MEDIA = 6 97 # Lead artist/lead performer/soloist 98 LEAD_ARTIST = 7 99 # Artist/performer 100 ARTIST = 8 101 # Conductor 102 CONDUCTOR = 9 103 # Band/Orchestra 104 BAND = 10 105 # Composer 106 COMPOSER = 11 107 # Lyricist/text writer 108 LYRISCIST = 12 109 # Recording Location 110 RECORDING_LOCATION = 13 111 # During recording 112 DURING_RECORDING = 14 113 # During performance 114 DURING_PERFORMANCE = 15 115 # Movie/video screen capture 116 SCREEN_CAPTURE = 16 117 # A bright coloured fish 118 FISH = 17 119 # Illustration 120 ILLUSTRATION = 18 121 # Band/artist logotype 122 BAND_LOGOTYPE = 19 123 # Publisher/Studio logotype 124 PUBLISHER_LOGOTYPE = 20 125 126 @classmethod 127 def to_string(cls, value): 128 for k, v in cls.__dict__.items(): 129 if v == value: 130 return k 131 return "" 132 133 @classmethod 134 def is_valid(cls, value): 135 return cls.OTHER <= value <= cls.PUBLISHER_LOGOTYPE 136 137 @classmethod 138 def sort_key(cls, value): 139 """Sorts picture types, most important picture is the lowest. 140 Important is defined as most representative of an album release, ymmv. 141 """ 142 143 # index value -> important 144 important = [ 145 cls.LEAFLET_PAGE, cls.MEDIA, 146 cls.COVER_BACK, cls.COVER_FRONT 147 ] 148 149 try: 150 return -important.index(value) 151 except ValueError: 152 if value < cls.COVER_FRONT: 153 return 100 - value 154 else: 155 return value 156 157 158class EmbeddedImage(object): 159 """Embedded image, contains most of the properties needed 160 for FLAC and ID3 images. 161 """ 162 163 def __init__(self, fileobj, mime_type, width=-1, height=-1, color_depth=-1, 164 type_=APICType.OTHER): 165 self.mime_type = mime_type 166 self.width = width 167 self.height = height 168 self.color_depth = color_depth 169 self.file = fileobj 170 self.type = type_ 171 172 def __repr__(self): 173 return "<%s mime_type=%r width=%d height=%d type=%s file=%r>" % ( 174 type(self).__name__, self.mime_type, self.width, self.height, 175 APICType.to_string(self.type), self.file) 176 177 def read(self): 178 """Read the raw image data 179 180 Returns: 181 bytes 182 Raises: 183 IOError 184 """ 185 186 self.file.seek(0) 187 data = self.file.read() 188 self.file.seek(0) 189 return data 190 191 @property 192 def sort_key(self): 193 return APICType.sort_key(self.type) 194 195 @property 196 def extensions(self): 197 """A possibly empty list of extensions e.g. ["jpeg", jpg"]""" 198 199 from gi.repository import GdkPixbuf 200 201 for format_ in GdkPixbuf.Pixbuf.get_formats(): 202 if self.mime_type in format_.get_mime_types(): 203 return format_.get_extensions() 204 return [] 205 206 @classmethod 207 def from_path(cls, path): 208 """Reads the header of `path` and creates a new image instance 209 or None. 210 """ 211 212 from gi.repository import GdkPixbuf, GLib 213 214 pb = [] 215 216 # Feed data to PixbufLoader until it emits area-prepared, 217 # get the partially filled pixbuf and extract the needed 218 # information. 219 220 def area_prepared(loader): 221 pb.append(loader.get_pixbuf()) 222 223 loader = GdkPixbuf.PixbufLoader() 224 loader.connect("area-prepared", area_prepared) 225 226 try: 227 with open(path, "rb") as h: 228 while not pb: 229 data = h.read(1024) 230 if data: 231 loader.write(data) 232 else: 233 break 234 except (EnvironmentError, GLib.GError): 235 return 236 finally: 237 try: 238 loader.close() 239 except GLib.GError: 240 pass 241 242 if not pb: 243 return 244 245 pb = pb[0] 246 247 width = pb.get_width() 248 height = pb.get_height() 249 color_depth = pb.get_bits_per_sample() 250 251 format_ = loader.get_format() 252 mime_types = format_.get_mime_types() 253 mime_type = mime_types and mime_types[0] or "" 254 255 try: 256 return cls(open(path, "rb"), mime_type, width, height, color_depth) 257 except EnvironmentError: 258 return 259