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