Fri, 13 Jun 2008 21:32:57 +0200 <thp@perli.net>

Automatically download channel cover file; improve channel cover handling

	* data/gpodder.glade: Simplify and clean-up the podcast editor dialog,
	especially with respect to the cover art stuff
	* src/gpodder/config.py: Add configuration option
	"podcast_list_icon_size" that determines the pixel size of the cover
	art displayed in the podcast list
	* src/gpodder/gui.py: Add cover cache, register with the cover
	downloader service in the main window, handle messages from the cover
	downloader (removed and download finished); request covers for
	channels when refreshing the channel list; make sure drag'n'drop of
	image files to the channel list works directly and sets the
	corresponding channel cover; Rework cover download handling and add an
	open dialog as suggested by the May 2008 Usability Evaluation
	* src/gpodder/libgpodder.py: Remove old, attic image downloading code
	from gPodderLib, because it now has its own service class
	* src/gpodder/libpodcasts.py: Remove unneeded get_cover_pixbuf helper
	function for podcastChannel; improve channels_to_model to take
	advantage of the new cover downloader service
	* src/gpodder/services.py: Add CoverDownloader service that acts as a
	central hub for all downloading and modifying of channel cover art,
	including notification of observers (through ObservableService)
	* src/gpodder/util.py: Add resize_pixbuf_keep_ratio helper function to
	resize a gtk pixbuf while keeping the aspect radio (with optional
	caching support through a dictionary parameter)
	(Closes: http://bugs.gpodder.org/show_bug.cgi?id=88)

Fri, 13 Jun 2008 20:10:13 +0200 <thp@perli.net>
Fix a bug in the experimental file naming support

	* src/gpodder/util.py: Fix bug that stopped the experimental file
	naming patch from working; thanks to Shane Donohoe for reporting



git-svn-id: svn://svn.berlios.de/gpodder/trunk@737 b0d088ad-0a06-0410-aad2-9ed5178a7e87
This commit is contained in:
Thomas Perl 2008-06-14 11:43:53 +00:00
parent 0aec290ec2
commit d3533acc1f
8 changed files with 294 additions and 211 deletions

View File

@ -1,3 +1,37 @@
Fri, 13 Jun 2008 21:32:57 +0200 <thp@perli.net>
Automatically download channel cover file; improve channel cover handling
* data/gpodder.glade: Simplify and clean-up the podcast editor dialog,
especially with respect to the cover art stuff
* src/gpodder/config.py: Add configuration option
"podcast_list_icon_size" that determines the pixel size of the cover
art displayed in the podcast list
* src/gpodder/gui.py: Add cover cache, register with the cover
downloader service in the main window, handle messages from the cover
downloader (removed and download finished); request covers for
channels when refreshing the channel list; make sure drag'n'drop of
image files to the channel list works directly and sets the
corresponding channel cover; Rework cover download handling and add an
open dialog as suggested by the May 2008 Usability Evaluation
* src/gpodder/libgpodder.py: Remove old, attic image downloading code
from gPodderLib, because it now has its own service class
* src/gpodder/libpodcasts.py: Remove unneeded get_cover_pixbuf helper
function for podcastChannel; improve channels_to_model to take
advantage of the new cover downloader service
* src/gpodder/services.py: Add CoverDownloader service that acts as a
central hub for all downloading and modifying of channel cover art,
including notification of observers (through ObservableService)
* src/gpodder/util.py: Add resize_pixbuf_keep_ratio helper function to
resize a gtk pixbuf while keeping the aspect radio (with optional
caching support through a dictionary parameter)
(Closes: http://bugs.gpodder.org/show_bug.cgi?id=88)
Fri, 13 Jun 2008 20:10:13 +0200 <thp@perli.net>
Fix a bug in the experimental file naming support
* src/gpodder/util.py: Fix bug that stopped the experimental file
naming patch from working; thanks to Shane Donohoe for reporting
Fri, 13 Jun 2008 16:08:16 +0200 <thp@perli.net>
Merge patch to add experimental support for "normal" file naming

View File

@ -1428,7 +1428,7 @@
<widget class="GtkWindow" id="gPodderChannel">
<property name="border_width">10</property>
<property name="visible">True</property>
<property name="visible">False</property>
<property name="title" translatable="yes">gPodder Podcast Editor</property>
<property name="type">GTK_WINDOW_TOPLEVEL</property>
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
@ -2108,31 +2108,6 @@
</packing>
</child>
<child>
<widget class="GtkLabel" id="labelCoverStatus">
<property name="visible">True</property>
<property name="label" translatable="yes"></property>
<property name="use_underline">False</property>
<property name="use_markup">False</property>
<property name="justify">GTK_JUSTIFY_LEFT</property>
<property name="wrap">False</property>
<property name="selectable">False</property>
<property name="xalign">0.5</property>
<property name="yalign">0.5</property>
<property name="xpad">0</property>
<property name="ypad">4</property>
<property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
<property name="width_chars">-1</property>
<property name="single_line_mode">False</property>
<property name="angle">0</property>
</widget>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">False</property>
</packing>
</child>
<child>
<widget class="GtkHButtonBox" id="hbuttonbox1">
<property name="visible">True</property>
@ -2142,86 +2117,23 @@
<child>
<widget class="GtkButton" id="btnDownloadCover">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="sensitive">True</property>
<property name="can_default">True</property>
<property name="can_focus">True</property>
<property name="use_stock">True</property>
<property name="label">gtk-open</property>
<property name="relief">GTK_RELIEF_NORMAL</property>
<property name="focus_on_click">True</property>
<signal name="clicked" handler="on_btnDownloadCover_clicked" last_modification_time="Wed, 19 Sep 2007 14:09:48 GMT"/>
<child>
<widget class="GtkAlignment" id="alignment17">
<property name="visible">True</property>
<property name="xalign">0.5</property>
<property name="yalign">0.5</property>
<property name="xscale">0</property>
<property name="yscale">0</property>
<property name="top_padding">0</property>
<property name="bottom_padding">0</property>
<property name="left_padding">0</property>
<property name="right_padding">0</property>
<child>
<widget class="GtkHBox" id="hbox28">
<property name="visible">True</property>
<property name="homogeneous">False</property>
<property name="spacing">2</property>
<child>
<widget class="GtkImage" id="image2631">
<property name="visible">True</property>
<property name="stock">gtk-go-down</property>
<property name="icon_size">4</property>
<property name="xalign">0.5</property>
<property name="yalign">0.5</property>
<property name="xpad">0</property>
<property name="ypad">0</property>
</widget>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">False</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label99">
<property name="visible">True</property>
<property name="label" translatable="yes">Download</property>
<property name="use_underline">True</property>
<property name="use_markup">False</property>
<property name="justify">GTK_JUSTIFY_LEFT</property>
<property name="wrap">False</property>
<property name="selectable">False</property>
<property name="xalign">0.5</property>
<property name="yalign">0.5</property>
<property name="xpad">0</property>
<property name="ypad">0</property>
<property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
<property name="width_chars">-1</property>
<property name="single_line_mode">False</property>
<property name="angle">0</property>
</widget>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">False</property>
</packing>
</child>
</widget>
</child>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkButton" id="btnClearCover">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="sensitive">True</property>
<property name="can_default">True</property>
<property name="can_focus">True</property>
<property name="label">gtk-clear</property>
<property name="label">gtk-refresh</property>
<property name="use_stock">True</property>
<property name="relief">GTK_RELIEF_NORMAL</property>
<property name="focus_on_click">True</property>

View File

@ -113,6 +113,7 @@ gPodderSettings = {
'rockbox_copy_coverart' : (bool, False),
'rockbox_coverart_size' : (int, 100),
'experimental_file_naming': (bool, False),
'podcast_list_icon_size': (int, 32),
# Hide the cover/pill from the podcast sidebar when it gets too small
'podcast_sidebar_save_space': (bool, True),

View File

@ -456,6 +456,9 @@ class gPodder(GladeWidget):
services.download_status_manager.register( 'list-changed', self.download_status_updated)
services.download_status_manager.register( 'progress-changed', self.download_progress_updated)
services.cover_downloader.register('cover-available', self.cover_download_finished)
services.cover_downloader.register('cover-removed', self.cover_file_removed)
self.cover_cache = {}
self.treeDownloads.set_model( services.download_status_manager.tree_model)
@ -680,6 +683,33 @@ class gPodder(GladeWidget):
else:
self.on_gPodder_delete_event(widget)
def cover_file_removed(self, channel_url):
"""
The Cover Downloader calls this when a previously-
available cover has been removed from the disk. We
have to update our cache to reflect this change.
"""
(COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
for row in self.treeChannels.get_model():
if row[COLUMN_URL] == channel_url:
row[COLUMN_PIXBUF] = None
del self.cover_cache[(channel_url, gl.config.podcast_list_icon_size, \
gl.config.podcast_list_icon_size)]
def cover_download_finished(self, channel_url, pixbuf):
"""
The Cover Downloader calls this when it has finished
downloading (or registering, if already downloaded)
a new channel cover, which is ready for displaying.
"""
if pixbuf is not None:
(COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
for row in self.treeChannels.get_model():
if row[COLUMN_URL] == channel_url and row[COLUMN_PIXBUF] is None:
new_pixbuf = util.resize_pixbuf_keep_ratio(pixbuf, gl.config.podcast_list_icon_size, gl.config.podcast_list_icon_size, channel_url, self.cover_cache)
row[COLUMN_PIXBUF] = new_pixbuf or pixbuf
def save_episode_as_file( self, url, *args):
episode = self.active_channel.find_episode( url)
@ -1028,8 +1058,10 @@ class gPodder(GladeWidget):
selected_url = model.get_value(iter, 0)
rect = self.treeChannels.get_visible_rect()
self.treeChannels.set_model(channels_to_model(self.channels))
self.treeChannels.set_model(channels_to_model(self.channels, self.cover_cache, gl.config.podcast_list_icon_size, gl.config.podcast_list_icon_size))
util.idle_add(self.treeChannels.scroll_to_point, rect.x, rect.y)
for channel in self.channels:
services.cover_downloader.request_cover(channel)
try:
selected_path = (0,)
@ -1060,8 +1092,24 @@ class gPodder(GladeWidget):
self.treeAvailable.get_model().clear()
def drag_data_received(self, widget, context, x, y, sel, ttype, time):
(path, column, rx, ry) = self.treeChannels.get_path_at_pos( x, y) or (None,)*4
dnd_channel = None
if path is not None:
model = self.treeChannels.get_model()
iter = model.get_iter(path)
url = model.get_value(iter, 0)
for channel in self.channels:
if channel.url == url:
dnd_channel = channel
break
result = sel.data
self.add_new_channel( result)
rl = result.strip().lower()
if (rl.endswith('.jpg') or rl.endswith('.png') or rl.endswith('.gif') or rl.endswith('.svg')) and dnd_channel is not None:
services.cover_downloader.replace_cover(dnd_channel, result)
else:
self.add_new_channel(result)
def add_new_channel(self, result=None, ask_download_new=True):
result = util.normalize_feed_url( result)
@ -2072,6 +2120,7 @@ class gPodderChannel(GladeWidget):
def new(self):
global WEB_BROWSER_ICON
self.changed = False
self.image3167.set_property('icon-name', WEB_BROWSER_ICON)
self.gPodderChannel.set_title( self.channel.title)
self.entryTitle.set_text( self.channel.title)
@ -2088,8 +2137,8 @@ class gPodderChannel(GladeWidget):
if self.channel.password:
self.FeedPassword.set_text( self.channel.password)
self.on_btnClearCover_clicked( self.btnClearCover, delete_file = False)
self.on_btnDownloadCover_clicked( self.btnDownloadCover, url = False)
services.cover_downloader.register('cover-available', self.cover_download_finished)
services.cover_downloader.request_cover(self.channel)
# Hide the website button if we don't have a valid URL
if not self.channel.link:
@ -2109,29 +2158,27 @@ class gPodderChannel(GladeWidget):
def on_btn_website_clicked(self, widget):
util.open_website(self.channel.link)
def on_btnClearCover_clicked( self, widget, delete_file = True):
self.imgCover.clear()
if delete_file:
util.delete_file( self.channel.cover_file)
self.btnClearCover.set_sensitive( os.path.exists( self.channel.cover_file))
self.btnDownloadCover.set_sensitive( not os.path.exists( self.channel.cover_file) and bool(self.channel.image))
self.labelCoverStatus.set_text( _('You can drag a cover file here.'))
self.labelCoverStatus.show()
def on_btnDownloadCover_clicked(self, widget):
if gpodder.interface == gpodder.GUI:
dlg = gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=gtk.FILE_CHOOSER_ACTION_OPEN)
dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
elif gpodder.interface == gpodder.MAEMO:
dlg = hildon.FileChooserDialog(self.gPodderChannel, gtk.FILE_CHOOSER_ACTION_OPEN)
def on_btnDownloadCover_clicked( self, widget, url = None):
if url is None:
url = self.channel.image
if dlg.run() == gtk.RESPONSE_OK:
url = dlg.get_uri()
services.cover_downloader.replace_cover(self.channel, url)
if url != False:
self.btnDownloadCover.set_sensitive( False)
dlg.destroy()
self.labelCoverStatus.show()
gl.get_image_from_url(url, self.imgCover.set_from_pixbuf, self.labelCoverStatus.set_text, self.cover_download_finished, self.channel.cover_file)
def on_btnClearCover_clicked(self, widget):
services.cover_downloader.replace_cover(self.channel)
def cover_download_finished( self):
self.labelCoverStatus.hide()
self.btnClearCover.set_sensitive( os.path.exists( self.channel.cover_file))
self.btnDownloadCover.set_sensitive( not os.path.exists( self.channel.cover_file) and bool(self.channel.image))
def cover_download_finished(self, channel_url, pixbuf):
if pixbuf is not None:
self.imgCover.set_from_pixbuf(pixbuf)
self.gPodderChannel.show()
def drag_data_received( self, widget, content, x, y, sel, ttype, time):
files = sel.data.strip().split('\n')
@ -2141,12 +2188,8 @@ class gPodderChannel(GladeWidget):
file = files[0]
if file.startswith( 'file://') or file.startswith( 'http://'):
self.on_btnClearCover_clicked( self.btnClearCover)
if file.startswith( 'file://'):
filename = file[len('file://'):]
shutil.copyfile( filename, self.channel.cover_file)
self.on_btnDownloadCover_clicked( self.btnDownloadCover, url = file)
if file.startswith('file://') or file.startswith('http://'):
services.cover_downloader.replace_cover(self.channel, file)
return
self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
@ -2171,6 +2214,7 @@ class gPodderChannel(GladeWidget):
self.channel.password = self.FeedPassword.get_text()
self.channel.save_settings()
services.cover_downloader.unregister('cover-available', self.cover_download_finished)
self.gPodderChannel.destroy()
class gPodderAddPodcastDialog(GladeWidget):

View File

@ -241,60 +241,6 @@ class gPodderLib(object):
return ( False, command_line[0] )
return ( True, command_line[0] )
def image_download_thread( self, url, callback_pixbuf = None, callback_status = None, callback_finished = None, cover_file = None):
if callback_status is not None:
util.idle_add(callback_status, _('Downloading podcast cover...'))
pixbuf = gtk.gdk.PixbufLoader()
if cover_file is None:
log( 'Downloading %s', url)
pixbuf.write( urllib.urlopen(url).read())
if cover_file is not None and not os.path.exists(cover_file):
log( 'Downloading cover to %s', cover_file)
cachefile = open( cover_file, "w")
cachefile.write( urllib.urlopen(url).read())
cachefile.close()
if cover_file is not None:
log( 'Reading cover from %s', cover_file)
try:
pixbuf.write( open( cover_file, "r").read())
except:
# Probably a data error, delete temp file
log('Data error while reading pixbuf. Deleting %s', cover_file, sender=self)
util.delete_file(cover_file)
try:
pixbuf.close()
except:
# data error, delete temp file
util.delete_file( cover_file)
MAX_SIZE = 400
if callback_pixbuf is not None:
pb = pixbuf.get_pixbuf()
if pb:
if pb.get_width() > MAX_SIZE:
factor = MAX_SIZE*1.0/pb.get_width()
pb = pb.scale_simple( int(pb.get_width()*factor), int(pb.get_height()*factor), gtk.gdk.INTERP_BILINEAR)
if pb.get_height() > MAX_SIZE:
factor = MAX_SIZE*1.0/pb.get_height()
pb = pb.scale_simple( int(pb.get_width()*factor), int(pb.get_height()*factor), gtk.gdk.INTERP_BILINEAR)
util.idle_add(callback_pixbuf, pb)
if callback_status is not None:
util.idle_add(callback_status, '')
if callback_finished is not None:
util.idle_add(callback_finished)
def get_image_from_url( self, url, callback_pixbuf = None, callback_status = None, callback_finished = None, cover_file = None):
if not url and not os.path.exists( cover_file):
return
args = ( url, callback_pixbuf, callback_status, callback_finished, cover_file )
thread = threading.Thread( target = self.image_download_thread, args = args)
thread.start()
def invoke_torrent( self, url, torrent_filename, target_filename):
self.history_mark_played( url)

View File

@ -581,16 +581,6 @@ class podcastChannel(list):
cover_file = property(fget=get_cover_file)
def get_cover_pixbuf(self, size=128):
fn = self.cover_file
if os.path.exists(fn) and os.path.getsize(fn) > 0:
try:
return gtk.gdk.pixbuf_new_from_file_at_size(fn, size, size)
except:
pass
return None
def delete_episode_by_url(self, url):
global_lock.acquire()
downloaded_episodes = self.load_downloaded_episodes()
@ -843,7 +833,7 @@ class podcastItem(object):
def channels_to_model(channels):
def channels_to_model(channels, cover_cache=None, max_width=0, max_height=0):
new_model = gtk.ListStore(str, str, str, gtk.gdk.Pixbuf, int, gtk.gdk.Pixbuf, str)
for channel in channels:
@ -872,27 +862,13 @@ def channels_to_model(channels):
else:
new_model.set( new_iter, 4, pango.WEIGHT_NORMAL)
channel_cover_found = False
if os.path.exists( channel.cover_file) and os.path.getsize(channel.cover_file) > 0:
try:
new_model.set( new_iter, 5, gtk.gdk.pixbuf_new_from_file_at_size( channel.cover_file, 32, 32))
channel_cover_found = True
except:
exctype, value = sys.exc_info()[:2]
log( 'Could not convert icon file "%s", error was "%s"', channel.cover_file, value )
util.delete_file(channel.cover_file)
if not channel_cover_found:
iconsize = gtk.icon_size_from_name('channel-icon')
if not iconsize:
iconsize = gtk.icon_size_register('channel-icon',32,32)
icon_theme = gtk.icon_theme_get_default()
globe_icon_name = 'applications-internet'
try:
new_model.set( new_iter, 5, icon_theme.load_icon(globe_icon_name, iconsize, 0))
except:
log( 'Cannot load "%s" icon (using an old or incomplete icon theme?)', globe_icon_name)
new_model.set( new_iter, 5, None)
# Load the cover if we have it, but don't download
# it if it's not available (to avoid blocking here)
pixbuf = services.cover_downloader.get_cover(channel, avoid_downloading=True)
new_pixbuf = None
if pixbuf is not None:
new_pixbuf = util.resize_pixbuf_keep_ratio(pixbuf, max_width, max_height, channel.url, cover_cache)
new_model.set(new_iter, 5, new_pixbuf or pixbuf)
return new_model

View File

@ -33,6 +33,9 @@ import gtk
import gobject
import threading
import urllib2
import os
import os.path
class ObservableService(object):
@ -67,6 +70,130 @@ class ObservableService(object):
log('Signal "%s" is not available for notification.', signal_name, sender=self)
class CoverDownloader(ObservableService):
"""
This class manages downloading cover art and notification
of other parts of the system. Downloading cover art can
happen either synchronously via get_cover() or in
asynchronous mode via request_cover(). When in async mode,
the cover downloader will send the cover via the
'cover-available' message (via the ObservableService).
"""
# Maximum width/height of the cover in pixels
MAX_SIZE = 400
def __init__(self):
signal_names = ['cover-available', 'cover-removed']
ObservableService.__init__(self, signal_names)
def request_cover(self, channel, custom_url=None):
"""
Sends an asynchronous request to download a
cover for the specific channel.
After the cover has been downloaded, the
"cover-available" signal will be sent with
the channel url and new cover as pixbuf.
If you specify a custom_url, the cover will
be downloaded from the specified URL and not
taken from the channel metadata.
"""
args = [channel, custom_url, True]
threading.Thread(target=self.__get_cover, args=args).start()
def get_cover(self, channel, custom_url=None, avoid_downloading=False):
"""
Sends a synchronous request to download a
cover for the specified channel.
The cover will be returned to the caller.
The custom_url has the same semantics as
in request_cover().
The optional parameter "avoid_downloading",
when true, will make sure we return only
already-downloaded covers and return None
when we have no cover on the local disk.
"""
(url, pixbuf) = self.__get_cover(channel, custom_url, False, avoid_downloading)
return pixbuf
def remove_cover(self, channel):
"""
Removes the current cover for the channel
so that a new one is downloaded the next
time we request the channel cover.
"""
util.delete_file(channel.cover_file)
self.notify('cover-removed', channel.url)
def replace_cover(self, channel, custom_url=None):
"""
This is a convenience function that deletes
the current cover file and requests a new
cover from the URL specified.
"""
self.remove_cover(channel)
self.request_cover(channel, custom_url)
def __get_cover(self, channel, url, async=False, avoid_downloading=False):
if not async and avoid_downloading and not os.path.exists(channel.cover_file):
return (channel.url, None)
loader = gtk.gdk.PixbufLoader()
pixbuf = None
if not os.path.exists(channel.cover_file):
if url is None:
url = channel.image
if url is not None:
image_data = None
try:
log('Trying to download: %s', url, sender=self)
image_data = urllib2.urlopen(url).read()
except:
log('Cannot get image from %s', url, sender=self)
if image_data is not None:
log('Saving image data to %s', channel.cover_file, sender=self)
fp = open(channel.cover_file, 'wb')
fp.write(image_data)
fp.close()
if os.path.exists(channel.cover_file):
loader.write(open(channel.cover_file, 'rb').read())
try:
loader.close()
pixbuf = loader.get_pixbuf()
except:
log('Data error while loading %s', channel.cover_file, sender=self)
else:
try:
loader.close()
except:
log('Cannot load channel from %s', url, sender=self)
if pixbuf is not None:
new_pixbuf = util.resize_pixbuf_keep_ratio(pixbuf, self.MAX_SIZE, self.MAX_SIZE)
if new_pixbuf is not None:
# Save the resized cover so we do not have to
# resize it next time we load it
new_pixbuf.save(channel.cover_file, 'png')
pixbuf = new_pixbuf
if async:
self.notify('cover-available', channel.url, pixbuf)
else:
return (channel.url, pixbuf)
cover_downloader = CoverDownloader()
class DownloadStatusManager(ObservableService):
COLUMN_NAMES = { 0: 'episode', 1: 'speed', 2: 'progress', 3: 'url' }
COLUMN_TYPES = ( gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_FLOAT, gobject.TYPE_STRING )

View File

@ -940,8 +940,8 @@ def sanitize_filename(filename, max_length=0):
characters and encode in the native language) and
trim filename if greater than max_length (0 = no limit).
"""
if not max_length and len(filename) > max_length:
log('Limiting file/folder name "%s" to %d characters.', filename, max_length, sender=self)
if max_length > 0 and len(filename) > max_length:
log('Limiting file/folder name "%s" to %d characters.', filename, max_length)
filename = filename[:max_length]
global encoding
@ -964,3 +964,46 @@ def find_mount_point(directory):
return '/'
def resize_pixbuf_keep_ratio(pixbuf, max_width, max_height, key=None, cache=None):
"""
Resizes a GTK Pixbuf but keeps its aspect ratio.
Returns None if the pixbuf does not need to be
resized or the newly resized pixbuf if it does.
The optional parameter "key" is used to identify
the image in the "cache", which is a dict-object
that holds already-resized pixbufs to access.
"""
changed = False
if cache is not None:
if (key, max_width, max_height) in cache:
log('Loading from cache: %s', key)
return cache[(key, max_width, max_height)]
# Resize if too wide
if pixbuf.get_width() > max_width:
f = float(max_width)/pixbuf.get_width()
(width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
changed = True
# Resize if too high
if pixbuf.get_height() > max_height:
f = float(max_height)/pixbuf.get_height()
(width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
changed = True
if changed:
result = pixbuf
if cache is not None:
log('Storing in cache: %s', key)
cache[(key, max_width, max_height)] = result
else:
result = None
return result