Merge lp:~tomasgroth/openlp/mupdf into lp:openlp

Proposed by Tomas Groth
Status: Superseded
Proposed branch: lp:~tomasgroth/openlp/mupdf
Merge into: lp:openlp
Diff against target: 652 lines (+487/-23) (has conflicts)
6 files modified
openlp/plugins/presentations/lib/mediaitem.py (+72/-21)
openlp/plugins/presentations/lib/messagelistener.py (+17/-1)
openlp/plugins/presentations/lib/pdfcontroller.py (+290/-0)
openlp/plugins/presentations/lib/presentationtab.py (+103/-0)
openlp/plugins/presentations/presentationplugin.py (+3/-0)
resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py (+2/-1)
Text conflict in openlp/plugins/presentations/lib/mediaitem.py
Text conflict in openlp/plugins/presentations/lib/presentationtab.py
To merge this branch: bzr merge lp:~tomasgroth/openlp/mupdf
Reviewer Review Type Date Requested Status
Tim Bentley Needs Resubmitting
Review via email: mp+182009@code.launchpad.net

This proposal supersedes a proposal from 2013-07-15.

This proposal has been superseded by a proposal from 2013-11-14.

Description of the change

Support for presenting PDF using mupdf or ghostscript.

To post a comment you must log in.
Revision history for this message
Dmitriy Marmyshev (marmyshev) wrote : Posted in a previous version of this proposal

Looks good!
Really expecting function for us! Keep develop it!

1Q: Does it works on OS X?
2Q: I didnt get the point with PdfViewer class. Dont you use slide's system in OpenLP? Do I correctly understand that you just convert PDF to images and then show them like images? May be it will be easier to use image plugin for showing images after conversion.

And 1 more suggestion: may be it is better to use this:

os.path.isfile(os.path.join(self.get_temp_folder(), u'mainslide001.png'))

instead of this:

os.path.isfile(self.get_temp_folder() + u'/mainslide001.png')

Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

>And 1 more suggestion: may be it is better to use this:
>
>os.path.isfile(os.path.join(self.get_temp_folder(),
>u'mainslide001.png'))
>
>instead of this:
>
>os.path.isfile(self.get_temp_folder() + u'/mainslide001.png')

Good catch Dmitriy!

--
Sent from my Android phone with K-9 Mail. Please excuse my brevity.

Revision history for this message
Tomas Groth (tomasgroth) wrote : Posted in a previous version of this proposal

> 1Q: Does it works on OS X?
Yea, provided that either ghostscript or mupdf is available.

> 2Q: I didnt get the point with PdfViewer class. Dont you use slide's system in
> OpenLP? Do I correctly understand that you just convert PDF to images and then
> show them like images? May be it will be easier to use image plugin for
> showing images after conversion.
I've thought of the same thing, but I have to admit that I'm yet to find a way to bend the system to my need. I'd like the service-item to be a pdf-presentation, but presenting that using the same aproach as the image plugin isn't easy. But maybe I just don't understand the structure of OpenLP yet.

> And 1 more suggestion: may be it is better to use this:
> os.path.isfile(os.path.join(self.get_temp_folder(), u'mainslide001.png'))
> instead of this:
> os.path.isfile(self.get_temp_folder() + u'/mainslide001.png')
I'll change this in the next push.

Revision history for this message
Tim Bentley (trb143) wrote : Posted in a previous version of this proposal

Hum
The conversion to images is done on importing the PDF so all the images are
in the file system.
You could then add the images as image items to the service instead of
presentation file.

That would be a clean cross over Services would work as images but added as
a PDF.

On 21 July 2013 15:16, Tomas Groth <email address hidden> wrote:

> > 1Q: Does it works on OS X?
> Yea, provided that either ghostscript or mupdf is available.
>
> > 2Q: I didnt get the point with PdfViewer class. Dont you use slide's
> system in
> > OpenLP? Do I correctly understand that you just convert PDF to images
> and then
> > show them like images? May be it will be easier to use image plugin for
> > showing images after conversion.
> I've thought of the same thing, but I have to admit that I'm yet to find a
> way to bend the system to my need. I'd like the service-item to be a
> pdf-presentation, but presenting that using the same aproach as the image
> plugin isn't easy. But maybe I just don't understand the structure of
> OpenLP yet.
>
> > And 1 more suggestion: may be it is better to use this:
> > os.path.isfile(os.path.join(self.get_temp_folder(), u'mainslide001.png'))
> > instead of this:
> > os.path.isfile(self.get_temp_folder() + u'/mainslide001.png')
> I'll change this in the next push.
> --
> https://code.launchpad.net/~tomasgroth/openlp/mupdf/+merge/174849
> Your team OpenLP Core is requested to review the proposed merge of
> lp:~tomasgroth/openlp/mupdf into lp:openlp.
>
> _______________________________________________
> Mailing list: https://launchpad.net/~openlp-core
> Post to : <email address hidden>
> Unsubscribe : https://launchpad.net/~openlp-core
> More help : https://help.launchpad.net/ListHelp
>

--
Tim and Alison Bentley
<email address hidden>

Revision history for this message
Andreas Preikschat (googol-deactivatedaccount) wrote : Posted in a previous version of this proposal

And please add docstrings to *all* methods/functions

Revision history for this message
Dmitriy Marmyshev (marmyshev) wrote : Posted in a previous version of this proposal

> Hum
> The conversion to images is done on importing the PDF so all the images are
> in the file system.
> You could then add the images as image items to the service instead of
> presentation file.
>
> That would be a clean cross over Services would work as images but added as
> a PDF.
>

Let me not to agree with this. We should not to make life of OpenLP operator (user) harder by changing visually a "PDF file" to a "group of imageses". If I (as user) add PDF presentation to OpenLP - i'd like to see PDF in the list.
Upper, I mean that create new class (PdfViewer) just to show images may be is not the easiest way, may be it is better simply to use classes from ImagePlugin to show images from PDF controller.

Revision history for this message
Jonathan Corwin (j-corwin) wrote : Posted in a previous version of this proposal

(Note comment only, I've not looked at the code)

I too think it is nice to keep it in the Presentation plugin. The user is dealing with a PDF file as far as they are concerned and transporting the PDF in the service file rather than lots of images is in my view the correct way of doing this. Importing a PDF in Presentations and having to look for it in the image plugin would confuse many. Also by moving the PDF around it protects us from adding it on one computer with a 640x480 resolution moving to a 1900x1080 system and ending up having to deal with images in an resolution that don't scale nicely.

In the future some other app (e.g. Impress) might support the opening of PDF files and in this case the user would then be able to choose between mupdf or something else. If we're only storing the images then that wouldn't be so easy.

Yes it would be nice if we could just hook into the already existing image handling functionality of OpenLP for the display, but I wouldn't want us to tear the code base to pieces to get this to work. (I don't know the difficulties of this). I don't see any harm in making it a two phase task anyway and to do this later if we find a nice way to do it.

Finally an idea has just come to mind that in the future we could also support an "Export to Images" function on the presentation whereby we could copy the thumbnail images (created at output resolution) to the an Image Plugin group, to at least cater for those situations where the user knows the presentation application isn't installed at the destination system. However this would be outside the scope of this particular branch and would be a whole new feature.

Revision history for this message
Dave Warnock (dave-warnock) wrote : Posted in a previous version of this proposal

Just wanted to add my support to what Jonathon suggests.

On 22 July 2013 10:18, Jonathan Corwin <email address hidden> wrote:

> (Note comment only, I've not looked at the code)
>
> I too think it is nice to keep it in the Presentation plugin. The user is
> dealing with a PDF file as far as they are concerned and transporting the
> PDF in the service file rather than lots of images is in my view the
> correct way of doing this. Importing a PDF in Presentations and having to
> look for it in the image plugin would confuse many. Also by moving the PDF
> around it protects us from adding it on one computer with a 640x480
> resolution moving to a 1900x1080 system and ending up having to deal with
> images in an resolution that don't scale nicely.
>
> In the future some other app (e.g. Impress) might support the opening of
> PDF files and in this case the user would then be able to choose between
> mupdf or something else. If we're only storing the images then that
> wouldn't be so easy.
>
> Yes it would be nice if we could just hook into the already existing image
> handling functionality of OpenLP for the display, but I wouldn't want us to
> tear the code base to pieces to get this to work. (I don't know the
> difficulties of this). I don't see any harm in making it a two phase task
> anyway and to do this later if we find a nice way to do it.
>
> Finally an idea has just come to mind that in the future we could also
> support an "Export to Images" function on the presentation whereby we could
> copy the thumbnail images (created at output resolution) to the an Image
> Plugin group, to at least cater for those situations where the user knows
> the presentation application isn't installed at the destination system.
> However this would be outside the scope of this particular branch and would
> be a whole new feature.
> --
> https://code.launchpad.net/~tomasgroth/openlp/mupdf/+merge/174849
> You are subscribed to branch lp:openlp.
>

--
Dave Warnock: http://42.blogs.warnock.me.uk
Cycling Blog: http://42bikes.warnock.me.uk

Revision history for this message
Tim Bentley (trb143) wrote : Posted in a previous version of this proposal
Download full text (3.5 KiB)

Ok so I worded it in a clumsy manner.

PDF gets added to the presentation plugin no problems. It generates images
of the pages like the presentation plugins do.

The difference suggested and echoed by Jonathan is we use the image
processing part of the serviceitem to do the displays instead of writing
and managing an 8th player within the architecture.

It will look like a PDF, quack like a PDF display like a set of images.

The changes suggested will be constrained to the Presentations plugin so
easier to implement and test with less disruption.

Adding a new player is not a small job and with the issues we have had in
the past (4 months to sort out media after 5 months getting it in) and the
migrations going on it will be a challenge.

BTW are the PDF libraries fully python3 compliant and have versions
available for all the distros we support. That would also be a merge
requirement.

Tim

On 22 July 2013 11:35, Dave Warnock <email address hidden> wrote:

> Just wanted to add my support to what Jonathon suggests.
>
> On 22 July 2013 10:18, Jonathan Corwin <email address hidden> wrote:
>
> > (Note comment only, I've not looked at the code)
> >
> > I too think it is nice to keep it in the Presentation plugin. The user is
> > dealing with a PDF file as far as they are concerned and transporting the
> > PDF in the service file rather than lots of images is in my view the
> > correct way of doing this. Importing a PDF in Presentations and having to
> > look for it in the image plugin would confuse many. Also by moving the
> PDF
> > around it protects us from adding it on one computer with a 640x480
> > resolution moving to a 1900x1080 system and ending up having to deal with
> > images in an resolution that don't scale nicely.
> >
> > In the future some other app (e.g. Impress) might support the opening of
> > PDF files and in this case the user would then be able to choose between
> > mupdf or something else. If we're only storing the images then that
> > wouldn't be so easy.
> >
> > Yes it would be nice if we could just hook into the already existing
> image
> > handling functionality of OpenLP for the display, but I wouldn't want us
> to
> > tear the code base to pieces to get this to work. (I don't know the
> > difficulties of this). I don't see any harm in making it a two phase task
> > anyway and to do this later if we find a nice way to do it.
> >
> > Finally an idea has just come to mind that in the future we could also
> > support an "Export to Images" function on the presentation whereby we
> could
> > copy the thumbnail images (created at output resolution) to the an Image
> > Plugin group, to at least cater for those situations where the user knows
> > the presentation application isn't installed at the destination system.
> > However this would be outside the scope of this particular branch and
> would
> > be a whole new feature.
> > --
> > https://code.launchpad.net/~tomasgroth/openlp/mupdf/+merge/174849
> > You are subscribed to branch lp:openlp.
> >
>
>
>
> --
> Dave Warnock: http://42.blogs.warnock.me.uk
> Cycling Blog: http://42bikes.warnock.me.uk
>
> https://code.launchpad.net/~tomasgroth/openlp/mupdf/+merge/174849
> Your team OpenL...

Read more...

Revision history for this message
Tomas Groth (tomasgroth) wrote : Posted in a previous version of this proposal

Ok, here is some more thoughts...
The challenge as I see it is that the presentation plugin has been build around the fact the PowerPoint, PowerPoint Viewer and Impress all have their own "players", which means that since this pdf-presentation extensions is inside the presentation plugin, it is expected to provide a player, which is what I implemented in this first revision by adding the simple PdfViewer class. As some of you noted it might not be a great idea to add another "player", but the problem then is how to bend OpenLP into presenting images, even though it is a presentation...
I've looked around a bit and for now my best idea is to remove my PdfViewer class, and instead use my own instance of the MainDisplay class, which can present images. This way I reuse a "player", and avoid having to hack various parts too much. I haven't tried it yet, but I think it could work.
About python3, that shouldn't be an issue since I call the ghostscript or mupdf programs directly, not via python libs.

Revision history for this message
Jonathan Corwin (j-corwin) wrote : Posted in a previous version of this proposal

Tim, I don't understand why adding a new player /here/ will be a big problem. Yes it was a problem for media because that was all integrated with songs, the toolbar, etc and there was a lot of overlap across plugins.

However the Presentations plugin is completely designed to deal with third party apps. All they need to be able to do is blank, and the plugin will deal with the rest. Having a simple Window that can go full screen is not going to have the problems that we had with media. In fact I personally think trying to tie this in to use the existing display would make the code more complicated and error prone that keeping it standalone.

Revision history for this message
Tomas Groth (tomasgroth) wrote : Posted in a previous version of this proposal

Hi guys,
I've now also made a version which uses the approach Tim mentioned. It works quite well, but I haven't pushed it yet.
So how do we figure out which version should be used?

Revision history for this message
Jonathan Corwin (j-corwin) wrote : Posted in a previous version of this proposal

Push it and get it reviewed :)

Revision history for this message
Tomas Groth (tomasgroth) wrote : Posted in a previous version of this proposal

Ok, so I've just pushed the player-less version, where the image-presentation functionality is used instead. There is still a few issues. For example it tries to load the first image added to the serviceitem as a pdf-presentation when going live/preview. Haven't figured out why.

Revision history for this message
Tim Bentley (trb143) wrote : Posted in a previous version of this proposal

That is because you have set the ServiceItem.processor.
You have code in the pdf route which is not needed.

On 27 July 2013 10:52, Tomas Groth <email address hidden> wrote:

> Ok, so I've just pushed the player-less version, where the
> image-presentation functionality is used instead. There is still a few
> issues. For example it tries to load the first image added to the
> serviceitem as a pdf-presentation when going live/preview. Haven't figured
> out why.
> --
> https://code.launchpad.net/~tomasgroth/openlp/mupdf/+merge/174849
> Your team OpenLP Core is requested to review the proposed merge of
> lp:~tomasgroth/openlp/mupdf into lp:openlp.
>
> _______________________________________________
> Mailing list: https://launchpad.net/~openlp-core
> Post to : <email address hidden>
> Unsubscribe : https://launchpad.net/~openlp-core
> More help : https://help.launchpad.net/ListHelp
>

--
Tim and Alison Bentley
<email address hidden>

Revision history for this message
Tomas Groth (tomasgroth) wrote : Posted in a previous version of this proposal

I've now added the possibility for pointing to ghostscript or mudraw if autodetection isn't working. Also minor fixes for the pdf-presentation code.

Revision history for this message
Tomas Groth (tomasgroth) wrote : Posted in a previous version of this proposal

Hi guys, I was wondering if you think something more should be added before this is ready to be merged into trunk? Does it make sense to make tests for presentation plugins?

Revision history for this message
Tim Bentley (trb143) wrote :

Looks good but these need to be fixed.

* settings screen the grouping has moved from the left side to the middle.
* settings screen "binary" has the "y" half chopped off.
* showing an image live / preview does not work when loaded from the service manager. Fine from the media manager. Have not tried to save / reload but guess that is broken.
* blank lines in functions please remove (general)
* questions in comments please removed.
* check_binary why is this outside the class as a standalone function?
* if os.name != u'nt': swap logic round so is positive and less chance of a miss read.
* 320 would this be better to have in the code a file instead of writing to a file each run?
* if self.pdf_program_path.text() != u'': should be if not self.pdf_program_path.text(): - General point.
* setting do not need "given" in the string.
* img should be image (old bad code copied!
* no tests.
* line 53 far too long.
* lines 52-64 do we need all this. if the images are generated then it would be a case of just loading them not processing them again?

review: Needs Fixing
Revision history for this message
Tim Bentley (trb143) wrote :

Please resubmit due to the age of this request and additionally it will need converting to Python 3 as on 1/9/2013 truck will be converted.

review: Needs Resubmitting
lp:~tomasgroth/openlp/mupdf updated
2282. By Tomas Groth

Merge with python3 changes.

2283. By Tomas Groth

Made branch work with python3.

2284. By Tomas Groth

Mostly code cleanup of PDF support code.

2285. By Tomas Groth

Merged with trunk to resolve conflicts.

2286. By Tomas Groth

Made presenting PDF from the servicemanager work

2287. By Tomas Groth

Merged with trunk

2288. By Tomas Groth

Fixes for the presentation setting tab

2289. By Tomas Groth

Added tests for the PdfController.

2290. By Tomas Groth

Merged with trunk.

2291. By Tomas Groth

Changed the way a serviceitem from the servicemanager is copied and converted. Also fixed for PEP8.

2292. By Tomas Groth

merged with trunk

2293. By Tomas Groth

Added some docstring documentation.

2294. By Tomas Groth

Moved PS script into separate file.

2295. By Tomas Groth

Make the PdfController reload backend if setting changes.

2296. By Tomas Groth

Only allow pdf-program selection using filedialog.

2297. By Tomas Groth

merged with trunk

2298. By Tomas Groth

fixed an exception

2299. By Tomas Groth

merged with head

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'openlp/plugins/presentations/lib/mediaitem.py'
--- openlp/plugins/presentations/lib/mediaitem.py 2013-10-13 21:07:28 +0000
+++ openlp/plugins/presentations/lib/mediaitem.py 2013-11-14 16:44:06 +0000
@@ -249,10 +249,11 @@
249 items = self.list_view.selectedItems()249 items = self.list_view.selectedItems()
250 if len(items) > 1:250 if len(items) > 1:
251 return False251 return False
252 service_item.processor = self.display_type_combo_box.currentText()252 filename = items[0].data(QtCore.Qt.UserRole)
253 service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)253 file_type = os.path.splitext(filename)[1][1:]
254 if not self.display_type_combo_box.currentText():254 if not self.display_type_combo_box.currentText():
255 return False255 return False
256<<<<<<< TREE
256 for bitem in items:257 for bitem in items:
257 filename = bitem.data(QtCore.Qt.UserRole)258 filename = bitem.data(QtCore.Qt.UserRole)
258 (path, name) = os.path.split(filename)259 (path, name) = os.path.split(filename)
@@ -261,33 +262,83 @@
261 if service_item.processor == self.automatic:262 if service_item.processor == self.automatic:
262 service_item.processor = self.findControllerByType(filename)263 service_item.processor = self.findControllerByType(filename)
263 if not service_item.processor:264 if not service_item.processor:
265=======
266 if (file_type == 'pdf' or file_type == 'xps') and context != ServiceItemContext.Service:
267 service_item.add_capability(ItemCapabilities.CanMaintain)
268 service_item.add_capability(ItemCapabilities.CanPreview)
269 service_item.add_capability(ItemCapabilities.CanLoop)
270 service_item.add_capability(ItemCapabilities.CanAppend)
271 # force a nonexistent theme
272 service_item.theme = -1
273 for bitem in items:
274 filename = bitem.data(QtCore.Qt.UserRole)
275 (path, name) = os.path.split(filename)
276 service_item.title = name
277 if os.path.exists(filename):
278 processor = self.findControllerByType(filename)
279 if not processor:
280>>>>>>> MERGE-SOURCE
264 return False281 return False
265 controller = self.controllers[service_item.processor]282 controller = self.controllers[processor]
266 doc = controller.add_document(filename)283 service_item.processor = None
267 if doc.get_thumbnail_path(1, True) is None:284 doc = controller.add_document(filename)
268 doc.load_presentation()285 if doc.get_thumbnail_path(1, True) is None or not os.path.isfile(
269 i = 1286 os.path.join(doc.get_temp_folder(), 'mainslide001.png')):
270 img = doc.get_thumbnail_path(i, True)287 doc.load_presentation()
271 if img:288 i = 1
272 while img:289 imagefile = 'mainslide%03d.png' % i
273 service_item.add_from_command(path, name, img)290 image = os.path.join(doc.get_temp_folder(), imagefile)
291 while os.path.isfile(image):
292 service_item.add_from_image(image, name)
274 i += 1293 i += 1
275 img = doc.get_thumbnail_path(i, True)294 imagefile = 'mainslide%03d.png' % i
295 image = os.path.join(doc.get_temp_folder(), imagefile)
276 doc.close_presentation()296 doc.close_presentation()
277 return True297 return True
278 else:298 else:
279 # File is no longer present299 # File is no longer present
280 if not remote:300 if not remote:
281 critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),301 critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
282 translate('PresentationPlugin.MediaItem',302 translate('PresentationPlugin.MediaItem', 'The presentation %s no longer exists.') % filename)
283 'The presentation %s is incomplete, please reload.') % filename)303 return False
284 return False304 else:
285 else:305 service_item.processor = self.display_type_combo_box.currentText()
286 # File is no longer present306 service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
287 if not remote:307 for bitem in items:
288 critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),308 filename = bitem.data(QtCore.Qt.UserRole)
289 translate('PresentationPlugin.MediaItem', 'The presentation %s no longer exists.') % filename)309 (path, name) = os.path.split(filename)
290 return False310 service_item.title = name
311 if os.path.exists(filename):
312 if service_item.processor == self.Automatic:
313 service_item.processor = self.findControllerByType(filename)
314 if not service_item.processor:
315 return False
316 controller = self.controllers[service_item.processor]
317 doc = controller.add_document(filename)
318 if doc.get_thumbnail_path(1, True) is None:
319 doc.load_presentation()
320 i = 1
321 img = doc.get_thumbnail_path(i, True)
322 if img:
323 while img:
324 service_item.add_from_command(path, name, img)
325 i += 1
326 img = doc.get_thumbnail_path(i, True)
327 doc.close_presentation()
328 return True
329 else:
330 # File is no longer present
331 if not remote:
332 critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
333 translate('PresentationPlugin.MediaItem',
334 'The presentation %s is incomplete, please reload.') % filename)
335 return False
336 else:
337 # File is no longer present
338 if not remote:
339 critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
340 translate('PresentationPlugin.MediaItem', 'The presentation %s no longer exists.') % filename)
341 return False
291342
292 def findControllerByType(self, filename):343 def findControllerByType(self, filename):
293 """344 """
294345
=== modified file 'openlp/plugins/presentations/lib/messagelistener.py'
--- openlp/plugins/presentations/lib/messagelistener.py 2013-08-31 18:17:38 +0000
+++ openlp/plugins/presentations/lib/messagelistener.py 2013-11-14 16:44:06 +0000
@@ -33,6 +33,7 @@
3333
34from openlp.core.lib import Registry34from openlp.core.lib import Registry
35from openlp.core.ui import HideMode35from openlp.core.ui import HideMode
36from openlp.core.lib import ServiceItemContext
3637
37log = logging.getLogger(__name__)38log = logging.getLogger(__name__)
3839
@@ -69,6 +70,7 @@
69 return70 return
70 self.doc.slidenumber = slide_no71 self.doc.slidenumber = slide_no
71 self.hide_mode = hide_mode72 self.hide_mode = hide_mode
73 log.debug('add_handler, slidenumber: %d' % slide_no)
72 if self.is_live:74 if self.is_live:
73 if hide_mode == HideMode.Screen:75 if hide_mode == HideMode.Screen:
74 Registry().execute('live_display_hide', HideMode.Screen)76 Registry().execute('live_display_hide', HideMode.Screen)
@@ -316,6 +318,14 @@
316 hide_mode = message[2]318 hide_mode = message[2]
317 file = item.get_frame_path()319 file = item.get_frame_path()
318 self.handler = item.processor320 self.handler = item.processor
321 #self.media_item.generate_slide_data(self, service_item, item=None, xml_version=False,remote=False, context=ServiceItemContext.Service)
322 if file.endswith('.pdf') or file.endswith('.xps'):
323 log.debug('Trying to convert from pdf to images for service-item')
324 if is_live:
325 self.media_item.generate_slide_data(self, item, None, False, False, ServiceItemContext.Live)
326 else:
327 self.media_item.generate_slide_data(self, item, None, False, False, ServiceItemContext.Preview)
328
319 if self.handler == self.media_item.Automatic:329 if self.handler == self.media_item.Automatic:
320 self.handler = self.media_item.findControllerByType(file)330 self.handler = self.media_item.findControllerByType(file)
321 if not self.handler:331 if not self.handler:
@@ -324,7 +334,13 @@
324 controller = self.live_handler334 controller = self.live_handler
325 else:335 else:
326 controller = self.preview_handler336 controller = self.preview_handler
327 controller.add_handler(self.controllers[self.handler], file, hide_mode, message[3])337 # when presenting PDF, we're using the image presentation code,
338 # so handler & processor is set to None, and we skip adding the handler.
339 log.debug('file: %s' % file)
340 if self.handler == None:
341 self.controller = controller
342 else:
343 controller.add_handler(self.controllers[self.handler], file, hide_mode, message[3])
328344
329 def slide(self, message):345 def slide(self, message):
330 """346 """
331347
=== added file 'openlp/plugins/presentations/lib/pdfcontroller.py'
--- openlp/plugins/presentations/lib/pdfcontroller.py 1970-01-01 00:00:00 +0000
+++ openlp/plugins/presentations/lib/pdfcontroller.py 2013-11-14 16:44:06 +0000
@@ -0,0 +1,290 @@
1# -*- coding: utf-8 -*-
2# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
3
4###############################################################################
5# OpenLP - Open Source Lyrics Projection #
6# --------------------------------------------------------------------------- #
7# Copyright (c) 2008-2013 Raoul Snyman #
8# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
9# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
10# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
11# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
12# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
13# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
14# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
15# --------------------------------------------------------------------------- #
16# This program is free software; you can redistribute it and/or modify it #
17# under the terms of the GNU General Public License as published by the Free #
18# Software Foundation; version 2 of the License. #
19# #
20# This program is distributed in the hope that it will be useful, but WITHOUT #
21# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
22# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
23# more details. #
24# #
25# You should have received a copy of the GNU General Public License along #
26# with this program; if not, write to the Free Software Foundation, Inc., 59 #
27# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
28###############################################################################
29
30import os
31import logging
32from tempfile import NamedTemporaryFile
33import re
34from subprocess import check_output, CalledProcessError, STDOUT
35#from PyQt4 import QtCore, QtGui
36
37from openlp.core.utils import AppLocation
38from openlp.core.lib import ScreenList, Settings
39from .presentationcontroller import PresentationController, PresentationDocument
40
41
42log = logging.getLogger(__name__)
43
44
45class PdfController(PresentationController):
46 """
47 Class to control PDF presentations
48 """
49 log.info('PdfController loaded')
50
51 def __init__(self, plugin):
52 """
53 Initialise the class
54 """
55 log.debug('Initialising')
56 self.process = None
57 PresentationController.__init__(self, plugin, 'Pdf', PdfDocument)
58 self.supports = ['pdf']
59 self.mudrawbin = ''
60 self.gsbin = ''
61 if self.check_installed() and self.mudrawbin:
62 self.also_supports = ['xps']
63
64 def check_binary(program_path):
65 """
66 Function that checks whether a binary is either ghostscript or mudraw or neither.
67 """
68 program_type = None
69 runlog = ''
70 try:
71 runlog = check_output([program_path, '--help'], stderr=STDOUT)
72 except CalledProcessError as e:
73 runlog = e.output
74 except Exception:
75 runlog = ''
76 # Analyse the output to see it the program is mudraw, ghostscript or neither
77 for line in runlog.splitlines():
78 found_mudraw = re.search('usage: mudraw.*', line)
79 if found_mudraw:
80 program_type = 'mudraw'
81 break
82 found_gs = re.search('GPL Ghostscript.*', line)
83 if found_gs:
84 program_type = 'gs'
85 break
86 log.info('in check_binary, found: ' + program_type)
87 return program_type
88
89 def check_available(self):
90 """
91 PdfController is able to run on this machine.
92 """
93 log.debug('check_available Pdf')
94 return self.check_installed()
95
96 def check_installed(self):
97 """
98 Check the viewer is installed.
99 """
100 log.debug('check_installed Pdf')
101 # Use the user defined program if given
102 if (Settings().value('presentations/enable_pdf_program')):
103 pdf_program = Settings().value('presentations/pdf_program')
104 type = self.check_binary(pdf_program)
105 if type == 'gs':
106 self.gsbin = pdf_program
107 return True
108 elif type == 'mudraw':
109 self.mudrawbin = pdf_program
110 return True
111 # Fallback to autodetection
112 application_path = AppLocation.get_directory(AppLocation.AppDir)
113 if os.name == 'nt':
114 # for windows we only accept mudraw.exe in the base folder
115 application_path = AppLocation.get_directory(AppLocation.AppDir)
116 if os.path.isfile(application_path + '/../mudraw.exe'):
117 self.mudrawbin = application_path + '/../mudraw.exe'
118 else:
119 # First try to find mupdf
120 try:
121 self.mudrawbin = check_output(['which', 'mudraw']).decode(encoding='UTF-8').rstrip('\n')
122 except CalledProcessError:
123 self.mudrawbin = ''
124 # if mupdf isn't installed, fallback to ghostscript
125 if not self.mudrawbin:
126 try:
127 self.gsbin = check_output(['which', 'gs']).rstrip('\n')
128 except CalledProcessError:
129 self.gsbin = ''
130 # Last option: check if mudraw is placed in OpenLP base folder
131 if not self.mudrawbin and not self.gsbin:
132 application_path = AppLocation.get_directory(AppLocation.AppDir)
133 if os.path.isfile(application_path + '/../mudraw'):
134 self.mudrawbin = application_path + '/../mudraw'
135 if not self.mudrawbin and not self.gsbin:
136 return False
137 else:
138 return True
139
140 def kill(self):
141 """
142 Called at system exit to clean up any running presentations
143 """
144 log.debug('Kill pdfviewer')
145 while self.docs:
146 self.docs[0].close_presentation()
147
148
149class PdfDocument(PresentationDocument):
150 """
151 Class which holds information of a single presentation.
152 """
153 def __init__(self, controller, presentation):
154 """
155 Constructor, store information about the file and initialise.
156 """
157 log.debug('Init Presentation Pdf')
158 PresentationDocument.__init__(self, controller, presentation)
159 self.presentation = None
160 self.blanked = False
161 self.hidden = False
162 self.image_files = []
163 self.num_pages = -1
164
165 def gs_get_resolution(self, size):
166 """
167 Only used when using ghostscript
168 Ghostscript can't scale automaticly while keeping aspect like mupdf, so we need
169 to get the ratio bewteen the screen size and the PDF to scale
170 """
171 # Use a postscript script to get size of the pdf. It is assumed that all pages have same size
172 postscript = '%!PS \n\
173() = \n\
174File dup (r) file runpdfbegin \n\
1751 pdfgetpage dup \n\
176/MediaBox pget { \n\
177aload pop exch 4 1 roll exch sub 3 1 roll sub \n\
178( Size: x: ) print =print (, y: ) print =print (\n) print \n\
179} if \n\
180flush \n\
181quit \n\
182'
183 # Put postscript into tempfile
184 tmpfile = NamedTemporaryFile(delete=False)
185 tmpfile.write(postscript)
186 tmpfile.close()
187 # Run the script on the pdf to get the size
188 runlog = []
189 try:
190 runlog = check_output([self.controller.gsbin, '-dNOPAUSE', '-dNODISPLAY', '-dBATCH', '-sFile=' + self.filepath, tmpfile.name])
191 except CalledProcessError as e:
192 log.debug(' '.join(e.cmd))
193 log.debug(e.output)
194 os.unlink(tmpfile.name)
195 # Extract the pdf resolution from output, the format is " Size: x: <width>, y: <height>"
196 width = 0
197 height = 0
198 for line in runlog.splitlines():
199 try:
200 width = re.search('.*Size: x: (\d+\.?\d*), y: \d+.*', line).group(1)
201 height = re.search('.*Size: x: \d+\.?\d*, y: (\d+\.?\d*).*', line).group(1)
202 break;
203 except AttributeError:
204 pass
205 # Calculate the ratio from pdf to screen
206 if width > 0 and height > 0:
207 width_ratio = size.right() / float(width)
208 height_ratio = size.bottom() / float(height)
209 # return the resolution that should be used. 72 is default.
210 if width_ratio > height_ratio:
211 return int(height_ratio * 72)
212 else:
213 return int(width_ratio * 72)
214 else:
215 return 72
216
217 def load_presentation(self):
218 """
219 Called when a presentation is added to the SlideController. It generates images from the PDF.
220 """
221 log.debug('load_presentation pdf')
222 # Check if the images has already been created, and if yes load them
223 if os.path.isfile(os.path.join(self.get_temp_folder(), 'mainslide001.png')):
224 created_files = sorted(os.listdir(self.get_temp_folder()))
225 for fn in created_files:
226 if os.path.isfile(os.path.join(self.get_temp_folder(), fn)):
227 self.image_files.append(os.path.join(self.get_temp_folder(), fn))
228 self.num_pages = len(self.image_files)
229 return True
230 size = ScreenList().current['size']
231 # Generate images from PDF that will fit the frame.
232 runlog = ''
233 try:
234 if not os.path.isdir(self.get_temp_folder()):
235 os.makedirs(self.get_temp_folder())
236 if self.controller.mudrawbin:
237 runlog = check_output([self.controller.mudrawbin, '-w', str(size.right()), '-h', str(size.bottom()), '-o', os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), self.filepath])
238 elif self.controller.gsbin:
239 resolution = self.gs_get_resolution(size)
240 runlog = check_output([self.controller.gsbin, '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m', '-r' + str(resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', '-sOutputFile=' + os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), self.filepath])
241 created_files = sorted(os.listdir(self.get_temp_folder()))
242 for fn in created_files:
243 if os.path.isfile(os.path.join(self.get_temp_folder(), fn)):
244 self.image_files.append(os.path.join(self.get_temp_folder(), fn))
245 except Exception as e:
246 log.debug(e)
247 log.debug(runlog)
248 return False
249 self.num_pages = len(self.image_files)
250 # Create thumbnails
251 self.create_thumbnails()
252 return True
253
254 def create_thumbnails(self):
255 """
256 Generates thumbnails
257 """
258 log.debug('create_thumbnails pdf')
259 if self.check_thumbnails():
260 return
261 # use builtin function to create thumbnails from generated images
262 index = 1
263 for image in self.image_files:
264 self.convert_thumbnail(image, index)
265 index += 1
266
267 def close_presentation(self):
268 """
269 Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being
270 shut down.
271 """
272 log.debug('close_presentation pdf')
273 self.controller.remove_doc(self)
274
275 def is_loaded(self):
276 """
277 Returns true if a presentation is loaded.
278 """
279 log.debug('is_loaded pdf')
280 if self.num_pages < 0:
281 return False
282 return True
283
284 def is_active(self):
285 """
286 Returns true if a presentation is currently active.
287 """
288 log.debug('is_active pdf')
289 return self.is_loaded() and not self.hidden
290
0291
=== modified file 'openlp/plugins/presentations/lib/presentationtab.py'
--- openlp/plugins/presentations/lib/presentationtab.py 2013-10-13 20:36:42 +0000
+++ openlp/plugins/presentations/lib/presentationtab.py 2013-11-14 16:44:06 +0000
@@ -29,9 +29,17 @@
2929
30from PyQt4 import QtGui30from PyQt4 import QtGui
3131
32<<<<<<< TREE
32from openlp.core.common import Settings, UiStrings, translate33from openlp.core.common import Settings, UiStrings, translate
33from openlp.core.lib import SettingsTab34from openlp.core.lib import SettingsTab
3435
36=======
37from openlp.core.lib import Settings, SettingsTab, UiStrings, translate, build_icon
38from openlp.core.lib.ui import critical_error_message_box
39from .pdfcontroller import PdfController
40#from openlp.plugins.presentations.lib.pdfcontroller import PdfController
41#from pdfcontroller import check_binary
42>>>>>>> MERGE-SOURCE
3543
36class PresentationTab(SettingsTab):44class PresentationTab(SettingsTab):
37 """45 """
@@ -64,6 +72,7 @@
64 self.presenter_check_boxes[controller.name] = checkbox72 self.presenter_check_boxes[controller.name] = checkbox
65 self.controllers_layout.addWidget(checkbox)73 self.controllers_layout.addWidget(checkbox)
66 self.left_layout.addWidget(self.controllers_group_box)74 self.left_layout.addWidget(self.controllers_group_box)
75 # Advanced
67 self.advanced_group_box = QtGui.QGroupBox(self.left_column)76 self.advanced_group_box = QtGui.QGroupBox(self.left_column)
68 self.advanced_group_box.setObjectName('advanced_group_box')77 self.advanced_group_box.setObjectName('advanced_group_box')
69 self.advanced_layout = QtGui.QVBoxLayout(self.advanced_group_box)78 self.advanced_layout = QtGui.QVBoxLayout(self.advanced_group_box)
@@ -71,7 +80,33 @@
71 self.override_app_check_box = QtGui.QCheckBox(self.advanced_group_box)80 self.override_app_check_box = QtGui.QCheckBox(self.advanced_group_box)
72 self.override_app_check_box.setObjectName('override_app_check_box')81 self.override_app_check_box.setObjectName('override_app_check_box')
73 self.advanced_layout.addWidget(self.override_app_check_box)82 self.advanced_layout.addWidget(self.override_app_check_box)
83
84 # Pdf options
85 self.pdf_group_box = QtGui.QGroupBox(self.left_column)
86 self.pdf_group_box.setObjectName('pdf_group_box')
87 self.pdf_layout = QtGui.QFormLayout(self.pdf_group_box)
88 self.pdf_layout.setObjectName('pdf_layout')
89 self.pdf_program_check_box = QtGui.QCheckBox(self.pdf_group_box)
90 self.pdf_program_check_box.setObjectName('pdf_program_check_box')
91 self.pdf_layout.addWidget(self.pdf_program_check_box)
92 self.pdf_program_path_layout = QtGui.QHBoxLayout()
93 self.pdf_program_path_layout.setObjectName('pdf_program_path_layout')
94 self.pdf_program_path = QtGui.QLineEdit(self.pdf_group_box)
95 self.pdf_program_path.setObjectName('pdf_program_path')
96 self.pdf_program_path.setReadOnly(True)
97 self.pdf_program_path.setPalette(self.get_grey_text_palette(True))
98 self.pdf_program_path_layout.addWidget(self.pdf_program_path)
99 self.pdf_program_browse_button = QtGui.QToolButton(self.pdf_group_box)
100 self.pdf_program_browse_button.setObjectName('pdf_program_browse_button')
101 self.pdf_program_browse_button.setIcon(build_icon(':/general/general_open.png'))
102 self.pdf_program_browse_button.setEnabled(False)
103 self.pdf_program_path_layout.addWidget(self.pdf_program_browse_button)
104 self.pdf_layout.addRow(self.pdf_program_path_layout)
105 self.pdf_program_path.editingFinished.connect(self.on_pdf_program_path_edit_finished)
106 self.pdf_program_browse_button.clicked.connect(self.on_pdf_program_browse_button_clicked)
107 self.pdf_program_check_box.clicked.connect(self.on_pdf_program_check_box_clicked)
74 self.left_layout.addWidget(self.advanced_group_box)108 self.left_layout.addWidget(self.advanced_group_box)
109 self.left_layout.addWidget(self.pdf_group_box)
75 self.left_layout.addStretch()110 self.left_layout.addStretch()
76 self.right_layout.addStretch()111 self.right_layout.addStretch()
77112
@@ -85,8 +120,12 @@
85 checkbox = self.presenter_check_boxes[controller.name]120 checkbox = self.presenter_check_boxes[controller.name]
86 self.set_controller_text(checkbox, controller)121 self.set_controller_text(checkbox, controller)
87 self.advanced_group_box.setTitle(UiStrings().Advanced)122 self.advanced_group_box.setTitle(UiStrings().Advanced)
123 self.pdf_group_box.setTitle(translate('PresentationPlugin.PresentationTab', 'PDF options'))
88 self.override_app_check_box.setText(124 self.override_app_check_box.setText(
89 translate('PresentationPlugin.PresentationTab', 'Allow presentation application to be overridden'))125 translate('PresentationPlugin.PresentationTab', 'Allow presentation application to be overridden'))
126 self.pdf_program_check_box.setText(
127 translate('PresentationPlugin.PresentationTab', 'Use given full path for mudraw or ghostscript binary:'))
128
90129
91 def set_controller_text(self, checkbox, controller):130 def set_controller_text(self, checkbox, controller):
92 if checkbox.isEnabled():131 if checkbox.isEnabled():
@@ -103,6 +142,15 @@
103 checkbox = self.presenter_check_boxes[controller.name]142 checkbox = self.presenter_check_boxes[controller.name]
104 checkbox.setChecked(Settings().value(self.settings_section + '/' + controller.name))143 checkbox.setChecked(Settings().value(self.settings_section + '/' + controller.name))
105 self.override_app_check_box.setChecked(Settings().value(self.settings_section + '/override app'))144 self.override_app_check_box.setChecked(Settings().value(self.settings_section + '/override app'))
145 # load pdf-program settings
146 enable_pdf_program = Settings().value(self.settings_section + '/enable_pdf_program')
147 self.pdf_program_check_box.setChecked(enable_pdf_program)
148 self.pdf_program_path.setReadOnly(not enable_pdf_program)
149 self.pdf_program_path.setPalette(self.get_grey_text_palette(not enable_pdf_program))
150 self.pdf_program_browse_button.setEnabled(enable_pdf_program)
151 pdf_program = Settings().value(self.settings_section + '/pdf_program')
152 if pdf_program:
153 self.pdf_program_path.setText(pdf_program)
106154
107 def save(self):155 def save(self):
108 """156 """
@@ -128,6 +176,20 @@
128 if Settings().value(setting_key) != self.override_app_check_box.checkState():176 if Settings().value(setting_key) != self.override_app_check_box.checkState():
129 Settings().setValue(setting_key, self.override_app_check_box.checkState())177 Settings().setValue(setting_key, self.override_app_check_box.checkState())
130 changed = True178 changed = True
179
180 # Save pdf-settings
181 pdf_program = self.pdf_program_path.text()
182 enable_pdf_program = self.pdf_program_check_box.checkState()
183 # If the given program is blank disable using the program
184 if pdf_program == '':
185 enable_pdf_program = 0
186 if pdf_program != Settings().value(self.settings_section + '/pdf_program'):
187 Settings().setValue(self.settings_section + '/pdf_program', pdf_program)
188 changed = True
189 if enable_pdf_program != Settings().value(self.settings_section + '/enable_pdf_program'):
190 Settings().setValue(self.settings_section + '/enable_pdf_program', enable_pdf_program)
191 changed = True
192
131 if changed:193 if changed:
132 self.settings_form.register_post_process('mediaitem_suffix_reset')194 self.settings_form.register_post_process('mediaitem_suffix_reset')
133 self.settings_form.register_post_process('mediaitem_presentation_rebuild')195 self.settings_form.register_post_process('mediaitem_presentation_rebuild')
@@ -143,3 +205,44 @@
143 checkbox = self.presenter_check_boxes[controller.name]205 checkbox = self.presenter_check_boxes[controller.name]
144 checkbox.setEnabled(controller.is_available())206 checkbox.setEnabled(controller.is_available())
145 self.set_controller_text(checkbox, controller)207 self.set_controller_text(checkbox, controller)
208
209 def on_pdf_program_path_edit_finished(self):
210 """
211 After selecting/typing in a program it is validated that it is a actually ghostscript or mudraw
212 """
213 type = None
214 if self.pdf_program_path.text() != '':
215 type = PdfController.check_binary(self.pdf_program_path.text())
216 if not type:
217 critical_error_message_box(UiStrings().Error,
218 translate('PresentationPlugin.PresentationTab', 'The program is not ghostscript or mudraw which is required.'))
219 self.pdf_program_path.setFocus()
220
221 def on_pdf_program_browse_button_clicked(self):
222 """
223 Select the mudraw or ghostscript binary that should be used.
224 """
225 filename = QtGui.QFileDialog.getOpenFileName(self, translate('PresentationPlugin.PresentationTab', 'Select mudraw or ghostscript binary.'))
226 if filename:
227 self.pdf_program_path.setText(filename)
228 self.pdf_program_path.setFocus()
229
230 def on_pdf_program_check_box_clicked(self, checked):
231 """
232 When checkbox for manual entering pdf-program is clicked,
233 enable or disable the textbox for the programpath and the browse-button.
234 """
235 self.pdf_program_path.setReadOnly(not checked)
236 self.pdf_program_path.setPalette(self.get_grey_text_palette(not checked))
237 self.pdf_program_browse_button.setEnabled(checked)
238
239 def get_grey_text_palette(self, greyed):
240 """
241 Returns a QPalette with greyed out text as used for placeholderText.
242 """
243 palette = QtGui.QPalette()
244 color = self.palette().color(QtGui.QPalette.Active, QtGui.QPalette.Text)
245 if greyed:
246 color.setAlpha(128)
247 palette.setColor(QtGui.QPalette.Active, QtGui.QPalette.Text, color)
248 return palette
146249
=== modified file 'openlp/plugins/presentations/presentationplugin.py'
--- openlp/plugins/presentations/presentationplugin.py 2013-10-13 21:07:28 +0000
+++ openlp/plugins/presentations/presentationplugin.py 2013-11-14 16:44:06 +0000
@@ -45,9 +45,12 @@
4545
46__default_settings__ = {46__default_settings__ = {
47 'presentations/override app': QtCore.Qt.Unchecked,47 'presentations/override app': QtCore.Qt.Unchecked,
48 'presentations/enable_pdf_program': QtCore.Qt.Unchecked,
49 'presentations/pdf_program': '',
48 'presentations/Impress': QtCore.Qt.Checked,50 'presentations/Impress': QtCore.Qt.Checked,
49 'presentations/Powerpoint': QtCore.Qt.Checked,51 'presentations/Powerpoint': QtCore.Qt.Checked,
50 'presentations/Powerpoint Viewer': QtCore.Qt.Checked,52 'presentations/Powerpoint Viewer': QtCore.Qt.Checked,
53 'presentations/Pdf': QtCore.Qt.Checked,
51 'presentations/presentations files': []54 'presentations/presentations files': []
52}55}
5356
5457
=== modified file 'resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py'
--- resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py 2013-07-18 19:28:35 +0000
+++ resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py 2013-11-14 16:44:06 +0000
@@ -29,4 +29,5 @@
2929
30hiddenimports = ['openlp.plugins.presentations.lib.impresscontroller',30hiddenimports = ['openlp.plugins.presentations.lib.impresscontroller',
31 'openlp.plugins.presentations.lib.powerpointcontroller',31 'openlp.plugins.presentations.lib.powerpointcontroller',
32 'openlp.plugins.presentations.lib.pptviewcontroller']32 'openlp.plugins.presentations.lib.pptviewcontroller',
33 'openlp.plugins.presentations.lib.pdfcontroller']