You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

527 lines
22 KiB

# -*- coding: utf-8 -*-
from __future__ import division, unicode_literals, absolute_import
import shutil
from kivy import require
require('1.9.0')
import os, json
from sys import argv
from .meta import version as app_version
import urllib
import re
try:
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse
from lxml import html
import tempfile
from PIL import Image
import tarfile
from kivy.config import Config
from kivy.app import App
from kivy.core.window import Window
from kivy.properties import StringProperty, ListProperty, DictProperty, NumericProperty
from kivy.uix.screenmanager import ScreenManager, SlideTransition, FadeTransition
from kivy.uix.popup import Popup
from kivy.uix.label import Label
from kivy.factory import Factory
from kivy.uix.progressbar import ProgressBar
from .editor import Slide, SlideInfoDialog, DraggableSlide
from .viewer import SlideBox
from kivy.logger import Logger
from kivy.network.urlrequest import UrlRequest
from .search import ItemButton
from kivy.uix.filechooser import FileChooserIconView, FileChooserListView
class HadalyApp(App):
presentation = DictProperty({'app': ('hadaly', app_version), 'title': 'New Title', 'slides': []})
slides_list = ListProperty()
filename = StringProperty('')
dirname = StringProperty('')
use_kivy_settings = True
def build(self):
self.icon = str('data/icon.png')
self.title = str('Hadaly')
root = Manager(transition=FadeTransition(duration=.35))
self.searchscreen = Factory.SearchScreen(name='search')
self.viewerscreen = Factory.ViewerScreen(name='viewer')
self.editorscreen = Factory.EditorScreen(name='editor')
root.add_widget(self.editorscreen)
root.add_widget(self.viewerscreen)
root.add_widget(self.searchscreen)
return root
def build_config(self, config):
config.add_section('viewer')
config.add_section('editor')
config.add_section('search')
config.set('editor', 'autosave_time', '15')
config.set('editor', 'autosave', '0')
config.set('editor', 'font_size', '12')
config.set('viewer', 'thumb', '1')
config.set('viewer', 'thumb_pos', 'bottom left')
config.set('viewer', 'font_size', '15')
config.set('viewer', 'caption_pos', 'bottom')
config.set('search', 'search_rpp', '10')
def build_settings(self, settings):
settings.add_json_panel('Hadaly',
self.config,
'data/settings_panel.json')
def on_start(self):
self.tempdir = tempfile.mkdtemp()
try:
if argv[1].endswith('.opah'):
self.load_slides(os.path.dirname(argv[1]), [os.path.basename(argv[1])])
Logger.info('Application: file \'{file}\' loaded'.format(file=self.filename))
self.root.current = 'viewer'
except IndexError:
pass
def load_slides(self, path, filename):
try:
with tarfile.open(os.path.join(path, filename[0]), 'r:*') as tar:
tar.extractall(path=self.tempdir)
except ValueError as msg:
Logger.debug('Application: {msg}'.format(msg=msg))
self.show_popup(_('Error'), _('File is not valid.'))
except IndexError as msg:
Logger.debug('Application: {msg}'.format(msg=msg))
self.show_popup(_('Error'), _('No file selected'))
else:
if len(self.root.current_screen.slides_view.grid_layout.children) > 0:
self.clear()
try:
with open(os.path.join(self.tempdir, 'presentation.json'), 'r') as fd:
data = json.load(fd)
except ValueError as msg:
Logger.debug('Application (JSON Loading): {msg}'.format(msg=msg))
self.dirname = path
self.filename = filename[0]
self.presentation = data
self.presentation_title = data['title']
for slide in reversed(self.presentation['slides']):
# TODO: Remove tmp in filename on save
thumb_src = os.path.join(self.tempdir, slide['thumb_src'])
img_src = os.path.join(self.tempdir, slide['img_src'])
index = self.presentation['slides'].index(slide)
self.presentation['slides'][index]['thumb_src'] = thumb_src
self.presentation['slides'][index]['img_src'] = img_src
img_slide = Factory.Slide(img_src=str(img_src),
thumb_src=str(thumb_src),
artist=slide['artist'],
title=slide['title'],
year=slide['year']
)
drag_slide = Factory.DraggableSlide(img=img_slide, app=self)
self.root.current_screen.slides_view.grid_layout.add_widget(drag_slide)
def clear(self):
self.root.current_screen.slides_view.grid_layout.clear_widgets()
self.root.get_screen('viewer').carousel.clear_widgets()
self.presentation = {'app': ('hadaly', __main__.__version__), 'title': 'New Title', 'slides': []}
self.filename = self.dirname = ''
def update_presentation(self, type, old_index, new_index):
"""
:param type: type of update to do : 'mv' (move) or 'rm' (remove) or 'update'
:param old_index: previous index of item in grid layout as an integer.
:param new_index: new index of item in grid layout as an integer.
"""
if type == 'rm':
Logger.debug('Application: Removing presentation item.')
self.presentation['slides'].pop(old_index)
try:
self.root.get_screen('viewer').update_carousel()
except AttributeError:
Logger.exception('Application: Viewer dialog not yet added !')
elif type == 'mv':
Logger.debug('Application: Moving presentation item.')
try:
Logger.debug('Application: Moved {slide} at position (in grid layout) {index} to {new_index}'.format(
slide=self.presentation['slides'][old_index],
index=old_index, new_index=new_index))
item = self.presentation['slides'].pop(old_index)
self.presentation['slides'].insert(new_index, item)
try:
self.root.get_screen('viewer').update_carousel()
except AttributeError:
Logger.exception('Application: Viewer dialog not yet added !')
except TypeError:
Logger.exception('Application: Only one slide in presentation view. (BUG)')
elif type == 'update':
Logger.debug('Application: Updating presentation.')
self.presentation['slides'][old_index] = self.root.get_screen('editor').slides_view.grid_layout.children[
old_index].img.get_slide_info()
def show_open(self):
popup = Factory.OpenDialog()
popup.open()
def show_file_explorer(self):
popup = Popup(size_hint=(0.8, 0.8))
file_explorer = FileChooserListView(filters=['*.jpg', '*.png', '*.jpeg'])
file_explorer.bind(on_submit=self.show_add_slide)
file_explorer.popup = popup
popup.content = file_explorer
popup.title = _('File explorer')
popup.open()
def show_save(self, action):
"""Show save dialog.
If action == 'save', dialog is not shown and file is
saved based on self.dirname and self.filename.
"""
if len(self.presentation['slides']) == 0:
self.show_popup(_('Error'), _('Nothing to save...'))
else:
if self.filename and action == 'save':
self.save(self.dirname, self.filename)
else:
popup = Factory.SaveDialog()
popup.open()
def save(self, path, filename):
"""Save file.
:param path: path where to save to as string
:param filename: filename of file to save as string
"""
if not filename.endswith('.opah'):
filename = '.'.join((filename, 'opah'))
self.filename = filename
self.dirname = path
tar = tarfile.open(os.path.join(path, filename), 'w')
# Add image file to *.opah file
try:
for file in [slide for slide in self.presentation['slides']]:
if os.path.exists(file['img_src']):
tar.add(os.path.relpath(file['img_src']),
arcname=os.path.basename(file['img_src']))
else:
tar.add(os.path.relpath(os.path.join(self.tempdir, file['img_src'])),
arcname=os.path.basename(file['img_src']))
if os.path.exists(file['thumb_src']):
tar.add(os.path.relpath(os.path.join(self.tempdir, file['thumb_src'])),
arcname=os.path.basename(file['thumb_src']))
else:
tar.add(os.path.relpath(file['thumb_src']),
arcname=os.path.basename(file['thumb_src']))
except IndexError:
Logger.exception('Saving:')
except IOError:
Logger.exception('Saving:')
# Create json file with slides metadata
for slide in self.presentation['slides']:
Logger.debug('Save: thumb {thumb} from img {img}'.format(thumb=slide['thumb_src'],
img=slide['img_src']))
slide['img_src'] = os.path.basename(slide['img_src'])
slide['thumb_src'] = os.path.basename(slide['thumb_src'])
try:
with open(os.path.join(self.tempdir, 'presentation.json'), 'w') as fd:
json.dump(self.presentation, fd)
except IOError as msg:
Logger.debug('Application: {msg}'.format(msg=msg[1]))
self.show_popup('Error', msg[1])
# Add json file to *.opah file
tar.add(os.path.join(self.tempdir, 'presentation.json'), 'presentation.json')
tar.close()
def switch_slide(self, index):
"""Switch index of carousel to current index.
:param index:
"""
self.root.current_screen.carousel.index = index
def compare_slide(self, index=None, action='add'):
"""Add new SlideBox to ViewerScreen based on SlideButton index.
:param index: index of slide as int.
:param action: 'add' or 'rm'
"""
if action == 'add':
slide = SlideBox(slide=self.presentation['slides'][index])
self.root.current_screen.box.add_widget(slide, 0)
# Change scatter position to compensate screen split.
pos = self.root.current_screen.carousel.current_slide.viewer.pos
self.root.current_screen.carousel.current_slide.viewer.pos = (pos[0] / 2, pos[1])
elif action == 'rm':
self.root.current_screen.box.remove_widget(self.root.current_screen.box.children[0])
pos = self.root.current_screen.carousel.current_slide.viewer.pos
self.root.current_screen.carousel.current_slide.viewer.pos = (pos[0] * 2, pos[1])
def set_title(self):
popup = Factory.TitleDialog()
popup.open()
def show_add_slide(self, original_src, *args):
"""Show dialog to add a slide.
:param img_source: image path as string.
"""
original_src.popup.dismiss()
thumb_src = self.create_thumbnail(original_src.selection[0])
slide_popup = SlideInfoDialog(slide=Slide(img_src=original_src.selection[0],
thumb_src=thumb_src,
artist='',
title='',
year=''))
slide_popup.open()
def add_slide(self, slide):
"""Add slide to presentation.
:param slide: slide as object
"""
# Logger.debug('Application: Adding to presentation {slide}'.format(slide=slide.get_slide_info()))
img_slide = DraggableSlide(img=slide, app=self)
self.presentation['slides'].insert(0, slide.get_slide_info())
self.root.get_screen('editor').slides_view.grid_layout.add_widget(img_slide)
def create_thumbnail(self, img_src):
Logger.debug('Creating thumbnail from {src}'.format(src=img_src))
image = Image.open(img_src)
if Image.VERSION == '1.1.7':
image.thumbnail((200, 200))
else:
image.thumbnail((200, 200), Image.ANTIALIAS)
thumb_filename = ''.join(('thumb_', os.path.basename(img_src)))
thumb_src = os.path.join(self.tempdir, thumb_filename)
image.save(thumb_src)
Logger.debug('Application: Thumbnail created at {thumb}'.format(thumb=thumb_src))
return thumb_src
def show_popup(self, type, msg):
"""
:param type: popup title as string.
:param msg: content of the popup as string.
"""
popup = Popup(title=type, size_hint=(0.5, 0.2))
popup.content = Label(text=msg, id='label')
popup.open()
def search_flickr(self, term):
"""make a search request on flickr and return results
:param term: the term to search.
:return results: response in json
"""
api_key = '624d3a7086d14e85f1422430f0b889a1'
base_url = 'https://api.flickr.com/services/rest/?'
method = 'flickr.photos.search'
params = urllib.urlencode([('method', method),
('text', term),
('api_key', api_key),
('format', 'json'),
('nojsoncallback', 1),
('per_page', 20),
('content_type', 4)
])
url = ''.join((base_url, params))
result = self.request(url)
return result
def request(self, url):
url = urllib.quote(url, safe="%/:=&?~#+!$,;'@()*[]")
req = UrlRequest(url, debug=True)
req.wait()
return req.result
def download_img(self, url, slide, wait=False):
pb = ProgressBar(id='_pb')
self.progress_dialog = Popup(title='Downloading...',
size_hint=(0.5, 0.2), content=pb, auto_dismiss=False)
url = urllib.quote(url, safe="%/:=&?~#+!$,;'@()*[]")
path = os.path.join(self.tempdir,
os.path.basename(urlparse(url).path))
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0"}
req = UrlRequest(url, self.reload_slide, file_path=path, on_progress=self.show_download_progress,
req_headers=headers, debug=True)
self.progress_dialog.open()
def show_download_progress(self, req, down_size, end_size):
self.progress_dialog.content.max = end_size
self.progress_dialog.content.value = down_size
if down_size == end_size:
self.progress_dialog.dismiss()
def reload_slide(self, request, data):
Logger.debug('Application: Download finished, reloading slide source.')
slide_path = urlparse(request.url)
filename = os.path.basename(slide_path[2])
slides = [slide for slide in self.root.get_screen('editor').slides_view.grid_layout.children]
for slide in slides:
img_src = urlparse(slide.img.img_src)
if img_src[0] == 'http' and img_src[2].endswith(filename):
Logger.debug('Application: Found slide ! Updating image source...')
slide.img.img_src = os.path.join(self.tempdir, filename)
Logger.debug('Application: {src}'.format(src=slide.img.img_src))
slide.img.thumb_src = self.create_thumbnail(slide.img.img_src)
slide.img.update_texture_size()
self.update_presentation('update', 0, 0)
self.root.current = 'editor'
def get_flickr_url(self, photo, size):
"""construct a source url to a flickr photo.
:param photo: photo information in json format.
:param size: desired size of the photo :
s small square 75x75
q large square 150x150
t thumbnail, 100 on longest side
m small, 240 on longest side
n small, 320 on longest side
- medium, 500 on longest side
z medium 640, 640 on longest side
c medium 800, 800 on longest side†
b large, 1024 on longest side*
o original image, either a jpg, gif or png, depending on source format
:return url:
"""
url = 'https://farm{farm_id}.staticflickr.com/{server_id}/{id}_{secret}_{size}.jpg'
url = url.format(farm_id=photo['farm'], server_id=photo['server'], id=photo['id'],
secret=photo['secret'], size=size)
return url
def search_term(self, term, engine, page):
engines = {
'met': ('http://www.metmuseum.org/'
'collection/the-collection-online/search'
'?ft={term}&ao=on&rpp={rpp}&pg={page}'),
'getty': ('http://search.getty.edu/'
'gateway/search'
'?q={term}&cat=highlight&f="Open+Content+Images"&rows={rpp}&srt=&dir=s&dsp=0&img=0&pg={page}')
}
url = engines[engine].format(term=term,
rpp=self.config.get('search', 'search_rpp'),
page=page)
clean_url = urllib.quote(url, safe="%/:=&?~#+!$,;'@()*[]")
UrlRequest(clean_url, on_success=self.parse_results, debug=True)
def parse_results(self, request, data):
tree = html.fromstring(data)
search_screen = self.root.get_screen('search')
results = []
if 'metmuseum' in urlparse(request.url).hostname:
try:
total_pages = tree.xpath(
'//li[starts-with(@id, "phcontent_0_phfullwidthcontent_0_paginationWidget_rptPagination_paginationLineItem_")]//a/text()')
search_screen.box.total_pages = max([int(n) for n in total_pages])
except ValueError:
self.show_popup(_('Error'), _('No results found.'))
return
objects = tree.xpath('//div[starts-with(@class, "list-view-object ")]')
for object in objects:
result = {}
result['title'] = object.xpath('./div[@class="list-view-object-info"]/a/div[@class="objtitle"]/text()')[
0]
try:
result['artist'] = \
object.xpath('./div[@class="list-view-object-info"]/div[@class="artist"]/text()')[::2][0]
except IndexError:
result['artist'] = 'Unknown'
result['thumb'] = urllib.quote(object.xpath('./div[@class="list-view-thumbnail"]//img/@src')[0],
safe="%/:=&?~#+!$,;'@()*[]")
object_info = object.xpath('./div[@class="list-view-object-info"]/div[@class="objectinfo"]/text()')
result['year'] = object_info[0].strip('Dates: ')
results.append(result)
elif 'getty' in urlparse(request.url).hostname:
try:
search_screen.box.total_pages = tree.xpath('//td[@class="cs-page"]//input/@count')[0]
if int(re.sub("[^0-9]", "", tree.xpath('//strong[@id="cs-results-count"]//text()')[0])) == 0:
raise ValueError
except ValueError:
self.show_popup(_('Error'), _('No results found.'))
return
artists = tree.xpath(
'//div[@class="cs-result-data-brief"]//td[* = "Creator:" or * = "Maker Name:"]/following-sibling::td[1]/p/text()')
titles = tree.xpath(
'//div[@class="cs-result-data-brief"]//td[* = "Title:" or * = "Primary Title:"]/following-sibling::td[1]/p[@class="cs-record-link"]/a/strong/text()')
dates = tree.xpath(
'//div[@class="cs-result-data-brief"]//td[* = "Date:"]/following-sibling::td[1]/p/text()')
thumb = tree.xpath('//img[@class="cs-result-thumbnail"]/@src')
obj_link = tree.xpath(
'//div[@class="cs-result-data-brief"]//td[* = "Title:" or * = "Primary Title:"]/following-sibling::td[1]/p[@class="cs-record-link"]/a/@href')
results = [{'title': a, 'artist': b, 'year': c, 'thumb': urllib.quote(d, safe="%/:=&?~#+!$,;'@()*[]"),
'obj_link': e}
for a, b, c, d, e in zip(titles, artists, dates, thumb, obj_link)]
for photo in results:
Logger.debug('Search (MET): Loading {url}'.format(url=photo['thumb']))
image = ItemButton(photo=photo, source=photo['thumb'], keep_ratio=True)
search_screen.box.grid.add_widget(image)
search_screen.box.status = "Page {page} on {total_page}".format(page=search_screen.box.current_page,
total_page=search_screen.box.total_pages)
def on_stop(self):
try:
shutil.rmtree(self.tempdir, ignore_errors=True)
except:
Logger.exception('Application: Removing temp dir failed.')
# # TODO: Check changes and ask user to save.
pass
class Manager(ScreenManager):
def __init__(self, **kwargs):
super(Manager, self).__init__(**kwargs)
Window.bind(on_key_down=self._on_keyboard_down)
def _on_keyboard_down(self, instance, key, scancode, codepoint, modifier, **kwargs):
if key == 275 and self.current == 'viewer':
self.get_screen('viewer').carousel.load_next(mode='next')
elif key == 276 and self.current == 'viewer':
self.get_screen('viewer').carousel.load_previous()
elif key == 108 and modifier == ['ctrl'] and self.current == 'viewer':
self.get_screen('viewer').carousel.current_slide.viewer.lock()
elif key == 100 and modifier == ['ctrl'] and self.current == 'viewer':
self.get_screen('viewer').carousel.current_slide.viewer.painter.canvas.clear()