Skip to content

Instantly share code, notes, and snippets.

@allan-simon
Last active December 24, 2015 01:39
Show Gist options
  • Save allan-simon/6724771 to your computer and use it in GitHub Desktop.
Save allan-simon/6724771 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""FileChooserThumbView
====================
The FileChooserThumbView widget is similar to FileChooserIconView,
but if possible it shows a thumbnail instead of a normal icon.
Usage
-----
You can set some properties in order to control its performance:
* **showthumbs:** Thumbnail limit. If set to a number > 0, it will show the
thumbnails only if the directory doesn't contain more files or directories.
If set to 0 it won't show any thumbnail. If set to a number < 0 it will always
show the thumbnails, regardless of how many items the current directory
contains. By default it is set to -1, so it will show all the thumbnails.
* **thumbdir:** Custom directory for the thumbnails. By default it uses
tempfile to generate it randomly.
* **thumbsize:** The size of the thumbnails. It defaults to 64d
"""
import os
import mimetypes
#(enable for debugging)
import traceback
import subprocess
from os.path import join, exists, dirname
from chardet import detect as chardetect
from tempfile import mktemp, mkdtemp
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import StringProperty
from kivy.properties import DictProperty
from kivy.properties import ObjectProperty
from kivy.properties import BooleanProperty
from kivy.properties import NumericProperty
from kivy.uix.filechooser import FileChooserController
Builder.load_string("""
<FileChooserThumbView>:
on_entry_added: stacklayout.add_widget(args[1])
on_entries_cleared: stacklayout.clear_widgets()
scrollview: scrollview
ScrollView:
id: scrollview
pos: root.pos
size: root.size
size_hint: None, None
do_scroll_x: False
Scatter:
do_rotation: False
do_scale: False
do_translation: False
size_hint_y: None
height: stacklayout.height
StackLayout:
id: stacklayout
width: scrollview.width
size_hint_y: None
height: self.minimum_height
spacing: '10dp'
padding: '10dp'
[FileThumbEntry@Widget]:
locked: False
path: ctx.path
selected: self.path in ctx.controller().selection
size_hint: None, None
on_touch_down: self.collide_point(*args[1].pos) and ctx.controller().entry_touched(self, args[1])
on_touch_up: self.collide_point(*args[1].pos) and ctx.controller().entry_released(self, args[1])
size: ctx.controller().thumbsize + dp(52), ctx.controller().thumbsize + dp(52)
canvas:
Color:
rgba: 1, 1, 1, 1 if self.selected else 0
BorderImage:
border: 8, 8, 8, 8
pos: root.pos
size: root.size
source: 'atlas://data/images/defaulttheme/filechooser_selected'
Image:
size: ctx.controller().thumbsize, ctx.controller().thumbsize
source: ctx.controller()._get_image(ctx)
pos: root.x + dp(24), root.y + dp(40)
Label:
text: ctx.name
text_size: (ctx.controller().thumbsize, self.height)
halign: 'center'
shorten: True
size: ctx.controller().thumbsize, '16dp'
pos: root.center_x - self.width / 2, root.y + dp(16)
Label:
text: ctx.controller()._gen_label(ctx)
font_size: '11sp'
color: .8, .8, .8, 1
size: ctx.controller().thumbsize, '16sp'
pos: root.center_x - self.width / 2, root.y
halign: 'center'
""")
DEFAULT_THEME = 'atlas://data/images/defaulttheme/'
FILE_ICON = DEFAULT_THEME + 'filechooser_file'
FOLDER_ICON = DEFAULT_THEME + 'filechooser_folder'
FLAC_MIME = "audio/flac"
MP3_MIME = "audio/mpeg"
AVCONV_BIN = 'avconv'
FFMPEG_BIN = 'ffmpeg'
class FileChooserThumbView(FileChooserController):
'''Implementation of :class:`FileChooserController` using an icon view
with thumbnails.
'''
_ENTRY_TEMPLATE = 'FileThumbEntry'
thumbdir = StringProperty(mkdtemp(prefix="kivy-", suffix="-thumbs"))
'''Custom directory for the thumbnails. By default it uses tempfile to
generate it randomly.
'''
showthumbs = NumericProperty(-1)
'''Thumbnail limit. If set to a number > 0, it will show the thumbnails
only if the directory doesn't contain more files or directories. If set
to 0 it won't show any thumbnail. If set to a number < 0 it will always
show the thumbnails, regardless of how many items the current directory
contains.
By default it is set to -1, so it will show all the thumbnails.
'''
thumbsize = NumericProperty(dp(64))
"""The size of the thumbnails. It defaults to 64dp.
"""
_thumbs = DictProperty({})
scrollview = ObjectProperty(None)
def __init__(self, **kwargs):
super(FileChooserThumbView, self).__init__(**kwargs)
if not exists(self.thumbdir):
os.mkdir(self.thumbdir)
def _dir_has_too_much_file(self, path):
if (self.showthumbs < 0):
return False
nbrFileInDir = len(
os.listdir(dirname(path))
)
return nbrFileInDir > self.showthumbs
def _get_image(self, ctx):
if ctx.isdir:
return FOLDER_ICON
# if the directory contains more files
# than what has been configurated
# we directly return a default file icon
if self._dir_has_too_much_file(ctx.path):
return FILE_ICON
try:
mime = get_mime(ctx.name)
# if we already have generated the thumb
# for this file, we get it directly from our
# cache
if ctx.path in self._thumbs.keys():
return self._thumbs[ctx.path]
# if it's a picture, we don't need to do
# any transormation
if is_picture(mime, ctx.name):
return ctx.path
# for mp3/flac an image can be embedded
# into the file, so we try to get it
if mime == MP3_MIME:
return self._generate_image_from_mp3(
ctx.path
)
if mime == FLAC_MIME:
return self._generate_image_from_flac(
ctx.path
)
# if it's a video we will extract a frame out of it
if "video/" in mime:
return self._generate_image_from_video(ctx.path)
except:
traceback.print_exc()
return FILE_ICON
return FILE_ICON
def _generate_image_from_flac(self, flacPath):
# if we don't have the python module to
# extract image from flac, we just return
# default file's icon
try:
from mutagen.flac import FLAC
except ImportError:
return FILE_ICON
try:
audio = FLAC(flacPath)
art = audio.pictures
return self._generate_image_from_art(
art,
flacPath
)
except IndexError, TypeError:
return FILE_ICON
except:
return FILE_ICON
def _generate_image_from_mp3(self, mp3Path):
# if we don't have the python module to
# extract image from mp3, we just return
# default file's icon
try:
from mutagen.id3 import ID3
except ImportError:
return FILE_ICON
try:
audio = ID3(mp3Path)
art = audio.getall("APIC")
return self._generate_image_from_art(
art,
mp3Path
)
except IndexError, TypeError:
return FILE_ICON
except:
return FILE_ICON
def _generate_image_from_art(self, art, path):
pix = pix_from_art(art)
ext = mimetypes.guess_extension(pix.mime)
if ext == 'jpe':
ext = 'jpg'
image = self._generate_image_from_data(
path,
ext,
pix.data
)
self._thumbs[path] = image
return image
def _gen_temp_file_name(self, extension):
return join(self.thumbdir, mktemp()) + extension
def _generate_image_from_data(self, path, extension, data):
# data contains the raw bytes
# we save it inside a file, and return this temporay
# file's parth
image = self._gen_temp_file_name(extension)
with open(image, "w") as img:
img.write(data)
return image
def _generate_image_from_video(self, videoPath):
# we try to use an external software to get a frame
# as an image, otherwise => default file icon
data = extract_image_from_video(videoPath)
if data:
image = self._generate_image_from_data(videoPath, ".png", data)
self._thumbs[videoPath] = image
return image
else:
return FILE_ICON
def _gen_label(self, ctx):
size = ctx.get_nice_size()
temp = ""
try:
temp = os.path.splitext(ctx.name)[1][1:].upper()
except IndexError:
pass
if ctx.name.endswith(".tar.gz"):
temp = "TAR.GZ"
if ctx.name.endswith(".tar.bz2"):
temp = "TAR.BZ2"
if temp == "":
label = size
else:
label = size + " - " + temp
return label
def _unicode_noerrs(self, string):
if not string:
return u""
return unicode(string, encoding=chardetect(string)["encoding"])
# test if the file is a supported picture
# file
def is_picture(mime, name):
if mime is None:
return False
return "image/" in mime and (
"jpeg" in mime or
"jpg" in mime or
"gif" in mime or
"png" in mime
) and not name.endswith(".jpe")
def pix_from_art(art):
pix = None
if len(art) == 1:
pix = art[0]
elif len(art) > 1:
for pic in art:
if pic.type == 3:
pix = pic
if not pix:
# This would raise an exception if no image is present,
# and the default one would be returned
pix = art[0]
return pix
def get_mime(fileName):
try:
mime = mimetypes.guess_type(fileName)[0]
if mime is None:
return ""
return mime
except TypeError:
return ""
return ""
def extract_image_from_video(path):
data = None
if exec_exists(AVCONV_BIN):
data = get_png_from_video(AVCONV_BIN, path)
elif exec_exists(FFMPEG_BIN):
data = get_png_from_video(FFMPEG_BIN, path)
return data
# generic function to call a software to extract a PNG
# from an video file, it return the raw bytes, not an
# image file
def get_png_from_video(software, videoPath):
return subprocess.Popen(
[
software,
'-i',
videoPath,
'-an',
'-vcodec',
'png',
'-vframes',
'1',
'-ss',
'00:00:01',
'-y',
'-f',
'rawvideo',
'-'
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
).communicate()[0]
def exec_exists(bin):
try:
subprocess.check_output(["which", bin])
return True
except subprocess.CalledProcessError:
return False
except:
return False
if __name__ == "__main__":
from kivy.base import runTouchApp
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
box = BoxLayout(orientation="vertical")
fileChooser = FileChooserThumbView(thumbsize=128)
label = Label(markup=True, size_hint_y=None)
fileChooser.mylabel = label
box.add_widget(fileChooser)
box.add_widget(label)
def setlabel(instance, value):
instance.mylabel.text = "[b]Selected:[/b] {0}".format(value)
fileChooser.bind(selection=setlabel)
runTouchApp(box)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment