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

Proposed by Tomas Groth
Status: Merged
Merged at revision: 2328
Proposed branch: lp:~tomasgroth/openlp/mupdf
Merge into: lp:openlp
Diff against target: 934 lines (+663/-42)
10 files modified
openlp/core/ui/slidecontroller.py (+3/-3)
openlp/plugins/presentations/lib/ghostscript_get_resolution.ps (+10/-0)
openlp/plugins/presentations/lib/mediaitem.py (+86/-35)
openlp/plugins/presentations/lib/messagelistener.py (+31/-1)
openlp/plugins/presentations/lib/pdfcontroller.py (+316/-0)
openlp/plugins/presentations/lib/presentationtab.py (+93/-1)
openlp/plugins/presentations/presentationplugin.py (+3/-0)
resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py (+2/-1)
tests/functional/openlp_plugins/presentations/test_mediaitem.py (+10/-1)
tests/functional/openlp_plugins/presentations/test_pdfcontroller.py (+109/-0)
To merge this branch: bzr merge lp:~tomasgroth/openlp/mupdf
Reviewer Review Type Date Requested Status
Raoul Snyman Approve
Tim Bentley Pending
Review via email: mp+208230@code.launchpad.net

This proposal supersedes a proposal from 2014-02-21.

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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
Revision history for this message
Tomas Groth (tomasgroth) wrote : Posted in a previous version of this proposal

I need some feedback/guidance/help to finish this implementation...
The basic idea is to use mupdf or ghostscript to create a picture for each page in the PDF, and when going to live/preview mode, then replace the service_item with an image-based service_item. I've made it work when going to live/preview from the mediamanager, but I can't figure out how/where to do it from the servicemanager - any hints would be appreciated! As an alternative it can be implemented as the other presentation-backends, with a "player".

I'd also like some directions as to what tests to include? I couldn't find any tests for the other presentation backends...

And finally, I've added some extra fields to the presentation-settings-tab, and for some reason now all fields has moved a bit to the right instead for being to the far left - any hints as to why?

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

Hi Tomas, in it's current state your merge proposal has some conflicts. Can you merge from trunk and fix those please?

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

Merged with trunk to resolve conflicts. Last set of questions still applies.

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

I think I've resolved most problems now and even added some simple tests.
The basic idea is to (still) use mupdf or ghostscript to create a picture for each page in the PDF, and when going to live/preview mode the service_item is replaced with an image-based service_item on the fly.

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

Conflicts with head.

lines 8-13 why. This needs a good explanation as it looks like a botch.
lines 52-53 - fix for PEP8 - have a look at other places as well.
line 77 - why not service?

Linn 200+ Confused. If generated from plugin we have a image service item. Good. If started from Service Manager we need to convert. Why? How did it get to the service manager cos the plugin would have created an image service item not a presentation one. Also not sure about exposing the innards of the serviceitem in this could. could it be a new method on the service item?

tmpFile = tmp_file

484 - 498 Line lengths.
611, 668, 675 etc tabbing
650 space

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

Fixed for PEP8 compliance and trunk-conflicts.
Changed the way the serviceitem was copied/converted in messengerlistener.startup() to make it simpler and removed changes from service_item.
The serviceitem of PDF is a presentation-serviceitem containing a PDF file (in the same way a presentation-serviceitem contains ppt-file), and it remains like this when in the mediamanager or the servicemanager. When going to preview/live-mode the serviceitem is converted to a image-serviceitem and displayed as such. Where in the code the serviceitem-conversion should be done is open for debate. Since it (at the moment) is tied to the Pdf-presentation, maybe it should be a method in the PdfDocument class

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

Looks good but not sure about the postscript.
We hard code it (looks complex and messey) and then write it to a temp file.
Could we not have it as a file and just load it?

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

> Looks good but not sure about the postscript.
> We hard code it (looks complex and messey) and then write it to a temp file.
> Could we not have it as a file and just load it?
I see your point about the postscript. The reason I kept it 'hardcoded' was that I couldn't (and still can't) figure out how to reliable get the path to the file if I placed it in resources/misc/ghostscript_get_resolution.ps.

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

It needs to be in the same directory as the python code or a resources dir under the plugin.

The main resources directories are not in the builds

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

Ok, moved the postscript into a separate file.

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

Looks great but in final testing came up with a problem

Settings dialog - the tick box seems to use all three states.

Tick box and type rubbish in to the file box.
Press cancel and you get an error message - Not Correct.
When the dialog reappears the light grey tick is present - Not correct.

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

> Looks great but in final testing came up with a problem
>
> Settings dialog - the tick box seems to use all three states.
>
> Tick box and type rubbish in to the file box.
> Press cancel and you get an error message - Not Correct.
> When the dialog reappears the light grey tick is present - Not correct.

I can't see any easy way around this if I want to validate input in the settingstab, and display an error without saving.
As I see it we need to add a "validate" method to the SettingsTab class, which would then be called before "save" is called. That way my onchange-validation isn't needed.

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

Have a look at the Advanced Tab how it handles the data directory. This is read only and can only be accessed from the button.

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

Changed to only allow pdf-program selection using filedialog as suggested.

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

Looks good to go.

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

Traceback (most recent call last):
  File "/home/raoul/Projects/OpenLP/mupdf/openlp/core/common/registry.py", line 164, in execute
    result = function(*args, **kwargs)
  File "/home/raoul/Projects/OpenLP/mupdf/openlp/core/common/openlpmixin.py", line 66, in wrapped
    raise e
  File "/home/raoul/Projects/OpenLP/mupdf/openlp/core/common/openlpmixin.py", line 62, in wrapped
    return func(*args, **kwargs)
  File "/home/raoul/Projects/OpenLP/mupdf/openlp/core/lib/pluginmanager.py", line 61, in bootstrap_initialise
    self.find_plugins()
  File "/home/raoul/Projects/OpenLP/mupdf/openlp/core/common/openlpmixin.py", line 66, in wrapped
    raise e
  File "/home/raoul/Projects/OpenLP/mupdf/openlp/core/common/openlpmixin.py", line 62, in wrapped
    return func(*args, **kwargs)
  File "/home/raoul/Projects/OpenLP/mupdf/openlp/core/lib/pluginmanager.py", line 121, in find_plugins
    if plugin.check_pre_conditions():
  File "/home/raoul/Projects/OpenLP/mupdf/openlp/plugins/presentations/presentationplugin.py", line 141, in check_pre_conditions
    controller = controller_class(self)
  File "/home/raoul/Projects/OpenLP/mupdf/openlp/plugins/presentations/lib/pdfcontroller.py", line 62, in __init__
    self.check_installed()
  File "/home/raoul/Projects/OpenLP/mupdf/openlp/plugins/presentations/lib/pdfcontroller.py", line 140, in check_installed
    self.gsbin = check_output(['which', 'gs']).rstrip('\n')
TypeError: Type str doesn't support the buffer API

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

Also, if I start up without a configuration, I get the following exception (which I don't get in trunk):

Traceback (most recent call last):
  File "openlp.py", line 45, in <module>
    main()
  File "/home/raoul/Projects/OpenLP/mupdf/openlp/core/__init__.py", line 329, in main
    sys.exit(application.run(qt_args))
  File "/home/raoul/Projects/OpenLP/mupdf/openlp/core/__init__.py", line 150, in run
    self.main_window.first_time()
  File "/home/raoul/Projects/OpenLP/mupdf/openlp/core/ui/mainwindow.py", line 636, in first_time
    plugin.first_time()
  File "/home/raoul/Projects/OpenLP/mupdf/openlp/plugins/songs/songsplugin.py", line 264, in first_time
    self.on_tools_reindex_item_triggered()
  File "/home/raoul/Projects/OpenLP/mupdf/openlp/plugins/songs/songsplugin.py", line 180, in on_tools_reindex_item_triggered
    self.media_item.on_search_text_button_clicked()
AttributeError: 'NoneType' object has no attribute 'on_search_text_button_clicked'

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

OK, found and fixed the first traceback. Still can't reproduce the second one, it doesn't look related to the pdf-changes.

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Looks good to me.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openlp/core/ui/slidecontroller.py'
2--- openlp/core/ui/slidecontroller.py 2014-01-11 17:52:01 +0000
3+++ openlp/core/ui/slidecontroller.py 2014-02-25 21:06:14 +0000
4@@ -444,7 +444,7 @@
5 # "V1" was the slide we wanted to go.
6 self.preview_widget.change_slide(self.slide_list[self.current_shortcut])
7 self.slide_selected()
8- # Reset the shortcut.
9+ # Reset the shortcut.
10 self.current_shortcut = ''
11
12 def set_live_hot_keys(self, parent=None):
13@@ -774,7 +774,7 @@
14 self._reset_blank()
15 if service_item.is_command():
16 Registry().execute(
17- '%s_start' % service_item.name.lower(), [service_item, self.is_live, self.hide_mode(), slide_no])
18+ '%s_start' % service_item.name.lower(), [self.service_item, self.is_live, self.hide_mode(), slide_no])
19 self.slide_list = {}
20 if self.is_live:
21 self.song_menu.menu().clear()
22@@ -1440,4 +1440,4 @@
23 """
24 process the bootstrap post setup request
25 """
26- self.post_set_up()
27\ No newline at end of file
28+ self.post_set_up()
29
30=== added file 'openlp/plugins/presentations/lib/ghostscript_get_resolution.ps'
31--- openlp/plugins/presentations/lib/ghostscript_get_resolution.ps 1970-01-01 00:00:00 +0000
32+++ openlp/plugins/presentations/lib/ghostscript_get_resolution.ps 2014-02-25 21:06:14 +0000
33@@ -0,0 +1,10 @@
34+%!PS
35+() =
36+File dup (r) file runpdfbegin
37+1 pdfgetpage dup
38+/MediaBox pget {
39+aload pop exch 4 1 roll exch sub 3 1 roll sub
40+( Size: x: ) print =print (, y: ) print =print (\n) print
41+} if
42+flush
43+quit
44
45=== modified file 'openlp/plugins/presentations/lib/mediaitem.py'
46--- openlp/plugins/presentations/lib/mediaitem.py 2014-02-11 20:29:57 +0000
47+++ openlp/plugins/presentations/lib/mediaitem.py 2014-02-25 21:06:14 +0000
48@@ -116,7 +116,7 @@
49 self.display_type_label = QtGui.QLabel(self.presentation_widget)
50 self.display_type_label.setObjectName('display_type_label')
51 self.display_type_combo_box = create_horizontal_adjusting_combo_box(self.presentation_widget,
52- 'display_type_combo_box')
53+ 'display_type_combo_box')
54 self.display_type_label.setBuddy(self.display_type_combo_box)
55 self.display_layout.addRow(self.display_type_label, self.display_type_combo_box)
56 # Add the Presentation widget to the page layout.
57@@ -138,6 +138,9 @@
58 """
59 self.display_type_combo_box.clear()
60 for item in self.controllers:
61+ # For PDF reload backend, since it can have changed
62+ if self.controllers[item].name == 'Pdf':
63+ self.controllers[item].check_available()
64 # load the drop down selection
65 if self.controllers[item].enabled():
66 self.display_type_combo_box.addItem(item)
67@@ -177,9 +180,8 @@
68 if titles.count(filename) > 0:
69 if not initial_load:
70 critical_error_message_box(translate('PresentationPlugin.MediaItem', 'File Exists'),
71- translate('PresentationPlugin.MediaItem',
72- 'A presentation with that filename already exists.')
73- )
74+ translate('PresentationPlugin.MediaItem',
75+ 'A presentation with that filename already exists.'))
76 continue
77 controller_name = self.findControllerByType(filename)
78 if controller_name:
79@@ -203,7 +205,8 @@
80 icon = build_icon(':/general/general_delete.png')
81 else:
82 critical_error_message_box(UiStrings().UnsupportedFile,
83- translate('PresentationPlugin.MediaItem', 'This type of presentation is not supported.'))
84+ translate('PresentationPlugin.MediaItem',
85+ 'This type of presentation is not supported.'))
86 continue
87 item_name = QtGui.QListWidgetItem(filename)
88 item_name.setData(QtCore.Qt.UserRole, file)
89@@ -238,7 +241,7 @@
90 Settings().setValue(self.settings_section + '/presentations files', self.get_file_list())
91
92 def generate_slide_data(self, service_item, item=None, xml_version=False,
93- remote=False, context=ServiceItemContext.Service):
94+ remote=False, context=ServiceItemContext.Service, presentation_file=None):
95 """
96 Load the relevant information for displaying the presentation in the slidecontroller. In the case of
97 powerpoints, an image for each slide.
98@@ -249,45 +252,93 @@
99 items = self.list_view.selectedItems()
100 if len(items) > 1:
101 return False
102- service_item.processor = self.display_type_combo_box.currentText()
103- service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
104+ filename = presentation_file
105+ if filename is None:
106+ filename = items[0].data(QtCore.Qt.UserRole)
107+ file_type = os.path.splitext(filename)[1][1:]
108 if not self.display_type_combo_box.currentText():
109 return False
110- for bitem in items:
111- filename = bitem.data(QtCore.Qt.UserRole)
112- (path, name) = os.path.split(filename)
113- service_item.title = name
114- if os.path.exists(filename):
115- if service_item.processor == self.automatic:
116- service_item.processor = self.findControllerByType(filename)
117- if not service_item.processor:
118+ if (file_type == 'pdf' or file_type == 'xps') and context != ServiceItemContext.Service:
119+ service_item.add_capability(ItemCapabilities.CanMaintain)
120+ service_item.add_capability(ItemCapabilities.CanPreview)
121+ service_item.add_capability(ItemCapabilities.CanLoop)
122+ service_item.add_capability(ItemCapabilities.CanAppend)
123+ # force a nonexistent theme
124+ service_item.theme = -1
125+ for bitem in items:
126+ filename = presentation_file
127+ if filename is None:
128+ filename = bitem.data(QtCore.Qt.UserRole)
129+ (path, name) = os.path.split(filename)
130+ service_item.title = name
131+ if os.path.exists(filename):
132+ processor = self.findControllerByType(filename)
133+ if not processor:
134 return False
135- controller = self.controllers[service_item.processor]
136- doc = controller.add_document(filename)
137- if doc.get_thumbnail_path(1, True) is None:
138- doc.load_presentation()
139- i = 1
140- img = doc.get_thumbnail_path(i, True)
141- if img:
142- while img:
143- service_item.add_from_command(path, name, img)
144+ controller = self.controllers[processor]
145+ service_item.processor = None
146+ doc = controller.add_document(filename)
147+ if doc.get_thumbnail_path(1, True) is None or not os.path.isfile(
148+ os.path.join(doc.get_temp_folder(), 'mainslide001.png')):
149+ doc.load_presentation()
150+ i = 1
151+ imagefile = 'mainslide%03d.png' % i
152+ image = os.path.join(doc.get_temp_folder(), imagefile)
153+ while os.path.isfile(image):
154+ service_item.add_from_image(image, name)
155 i += 1
156- img = doc.get_thumbnail_path(i, True)
157+ imagefile = 'mainslide%03d.png' % i
158+ image = os.path.join(doc.get_temp_folder(), imagefile)
159 doc.close_presentation()
160 return True
161 else:
162 # File is no longer present
163 if not remote:
164 critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
165- translate('PresentationPlugin.MediaItem',
166- 'The presentation %s is incomplete, please reload.') % filename)
167- return False
168- else:
169- # File is no longer present
170- if not remote:
171- critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
172- translate('PresentationPlugin.MediaItem', 'The presentation %s no longer exists.') % filename)
173- return False
174+ translate('PresentationPlugin.MediaItem',
175+ 'The presentation %s no longer exists.') % filename)
176+ return False
177+ else:
178+ service_item.processor = self.display_type_combo_box.currentText()
179+ service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
180+ for bitem in items:
181+ filename = bitem.data(QtCore.Qt.UserRole)
182+ (path, name) = os.path.split(filename)
183+ service_item.title = name
184+ if os.path.exists(filename):
185+ if service_item.processor == self.automatic:
186+ service_item.processor = self.findControllerByType(filename)
187+ if not service_item.processor:
188+ return False
189+ controller = self.controllers[service_item.processor]
190+ doc = controller.add_document(filename)
191+ if doc.get_thumbnail_path(1, True) is None:
192+ doc.load_presentation()
193+ i = 1
194+ img = doc.get_thumbnail_path(i, True)
195+ if img:
196+ while img:
197+ service_item.add_from_command(path, name, img)
198+ i += 1
199+ img = doc.get_thumbnail_path(i, True)
200+ doc.close_presentation()
201+ return True
202+ else:
203+ # File is no longer present
204+ if not remote:
205+ critical_error_message_box(translate('PresentationPlugin.MediaItem',
206+ 'Missing Presentation'),
207+ translate('PresentationPlugin.MediaItem',
208+ 'The presentation %s is incomplete, please reload.')
209+ % filename)
210+ return False
211+ else:
212+ # File is no longer present
213+ if not remote:
214+ critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
215+ translate('PresentationPlugin.MediaItem',
216+ 'The presentation %s no longer exists.') % filename)
217+ return False
218
219 def findControllerByType(self, filename):
220 """
221
222=== modified file 'openlp/plugins/presentations/lib/messagelistener.py'
223--- openlp/plugins/presentations/lib/messagelistener.py 2013-12-28 21:33:38 +0000
224+++ openlp/plugins/presentations/lib/messagelistener.py 2014-02-25 21:06:14 +0000
225@@ -28,11 +28,13 @@
226 ###############################################################################
227
228 import logging
229+import copy
230
231 from PyQt4 import QtCore
232
233 from openlp.core.common import Registry
234 from openlp.core.ui import HideMode
235+from openlp.core.lib import ServiceItemContext, ServiceItem
236
237 log = logging.getLogger(__name__)
238
239@@ -69,6 +71,7 @@
240 return
241 self.doc.slidenumber = slide_no
242 self.hide_mode = hide_mode
243+ log.debug('add_handler, slidenumber: %d' % slide_no)
244 if self.is_live:
245 if hide_mode == HideMode.Screen:
246 Registry().execute('live_display_hide', HideMode.Screen)
247@@ -316,6 +319,28 @@
248 hide_mode = message[2]
249 file = item.get_frame_path()
250 self.handler = item.processor
251+ # When starting presentation from the servicemanager we convert
252+ # PDF/XPS-serviceitems into image-serviceitems. When started from the mediamanager
253+ # the conversion has already been done at this point.
254+ if file.endswith('.pdf') or file.endswith('.xps'):
255+ log.debug('Converting from pdf/xps to images for serviceitem with file %s', file)
256+ # Create a copy of the original item, and then clear the original item so it can be filled with images
257+ item_cpy = copy.copy(item)
258+ item.__init__(None)
259+ if is_live:
260+ self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Live, file)
261+ else:
262+ self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Preview, file)
263+ # Some of the original serviceitem attributes is needed in the new serviceitem
264+ item.footer = item_cpy.footer
265+ item.from_service = item_cpy.from_service
266+ item.iconic_representation = item_cpy.iconic_representation
267+ item.image_border = item_cpy.image_border
268+ item.main = item_cpy.main
269+ item.theme_data = item_cpy.theme_data
270+ # When presenting PDF or XPS, we are using the image presentation code,
271+ # so handler & processor is set to None, and we skip adding the handler.
272+ self.handler = None
273 if self.handler == self.media_item.automatic:
274 self.handler = self.media_item.findControllerByType(file)
275 if not self.handler:
276@@ -324,7 +349,12 @@
277 controller = self.live_handler
278 else:
279 controller = self.preview_handler
280- controller.add_handler(self.controllers[self.handler], file, hide_mode, message[3])
281+ # When presenting PDF or XPS, we are using the image presentation code,
282+ # so handler & processor is set to None, and we skip adding the handler.
283+ if self.handler is None:
284+ self.controller = controller
285+ else:
286+ controller.add_handler(self.controllers[self.handler], file, hide_mode, message[3])
287
288 def slide(self, message):
289 """
290
291=== added file 'openlp/plugins/presentations/lib/pdfcontroller.py'
292--- openlp/plugins/presentations/lib/pdfcontroller.py 1970-01-01 00:00:00 +0000
293+++ openlp/plugins/presentations/lib/pdfcontroller.py 2014-02-25 21:06:14 +0000
294@@ -0,0 +1,316 @@
295+# -*- coding: utf-8 -*-
296+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
297+
298+###############################################################################
299+# OpenLP - Open Source Lyrics Projection #
300+# --------------------------------------------------------------------------- #
301+# Copyright (c) 2008-2014 Raoul Snyman #
302+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
303+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
304+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
305+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
306+# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
307+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
308+# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
309+# --------------------------------------------------------------------------- #
310+# This program is free software; you can redistribute it and/or modify it #
311+# under the terms of the GNU General Public License as published by the Free #
312+# Software Foundation; version 2 of the License. #
313+# #
314+# This program is distributed in the hope that it will be useful, but WITHOUT #
315+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
316+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
317+# more details. #
318+# #
319+# You should have received a copy of the GNU General Public License along #
320+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
321+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
322+###############################################################################
323+
324+import os
325+import logging
326+from tempfile import NamedTemporaryFile
327+import re
328+from subprocess import check_output, CalledProcessError, STDOUT
329+
330+from openlp.core.utils import AppLocation
331+from openlp.core.common import Settings
332+from openlp.core.lib import ScreenList
333+from .presentationcontroller import PresentationController, PresentationDocument
334+
335+log = logging.getLogger(__name__)
336+
337+
338+class PdfController(PresentationController):
339+ """
340+ Class to control PDF presentations
341+ """
342+ log.info('PdfController loaded')
343+
344+ def __init__(self, plugin):
345+ """
346+ Initialise the class
347+
348+ :param plugin: The plugin that creates the controller.
349+ """
350+ log.debug('Initialising')
351+ self.process = None
352+ PresentationController.__init__(self, plugin, 'Pdf', PdfDocument)
353+ self.supports = ['pdf']
354+ self.also_supports = []
355+ # Determine whether mudraw or ghostscript is used
356+ self.check_installed()
357+
358+ @staticmethod
359+ def check_binary(program_path):
360+ """
361+ Function that checks whether a binary is either ghostscript or mudraw or neither.
362+ Is also used from presentationtab.py
363+
364+ :param program_path:The full path to the binary to check.
365+ :return: Type of the binary, 'gs' if ghostscript, 'mudraw' if mudraw, None if invalid.
366+ """
367+ program_type = None
368+ runlog = ''
369+ log.debug('testing program_path: %s', program_path)
370+ try:
371+ runlog = check_output([program_path, '--help'], stderr=STDOUT)
372+ except CalledProcessError as e:
373+ runlog = e.output
374+ except Exception:
375+ runlog = ''
376+ # Analyse the output to see it the program is mudraw, ghostscript or neither
377+ for line in runlog.splitlines():
378+ decoded_line = line.decode()
379+ found_mudraw = re.search('usage: mudraw.*', decoded_line)
380+ if found_mudraw:
381+ program_type = 'mudraw'
382+ break
383+ found_gs = re.search('GPL Ghostscript.*', decoded_line)
384+ if found_gs:
385+ program_type = 'gs'
386+ break
387+ log.debug('in check_binary, found: %s', program_type)
388+ return program_type
389+
390+ def check_available(self):
391+ """
392+ PdfController is able to run on this machine.
393+
394+ :return: True if program to open PDF-files was found, otherwise False.
395+ """
396+ log.debug('check_available Pdf')
397+ return self.check_installed()
398+
399+ def check_installed(self):
400+ """
401+ Check the viewer is installed.
402+
403+ :return: True if program to open PDF-files was found, otherwise False.
404+ """
405+ log.debug('check_installed Pdf')
406+ self.mudrawbin = ''
407+ self.gsbin = ''
408+ self.also_supports = []
409+ # Use the user defined program if given
410+ if (Settings().value('presentations/enable_pdf_program')):
411+ pdf_program = Settings().value('presentations/pdf_program')
412+ program_type = self.check_binary(pdf_program)
413+ if program_type == 'gs':
414+ self.gsbin = pdf_program
415+ elif program_type == 'mudraw':
416+ self.mudrawbin = pdf_program
417+ else:
418+ # Fallback to autodetection
419+ application_path = AppLocation.get_directory(AppLocation.AppDir)
420+ if os.name == 'nt':
421+ # for windows we only accept mudraw.exe in the base folder
422+ application_path = AppLocation.get_directory(AppLocation.AppDir)
423+ if os.path.isfile(application_path + '/../mudraw.exe'):
424+ self.mudrawbin = application_path + '/../mudraw.exe'
425+ else:
426+ DEVNULL = open(os.devnull, 'wb')
427+ # First try to find mupdf
428+ try:
429+ self.mudrawbin = check_output(['which', 'mudraw'], stderr=DEVNULL).decode(encoding='UTF-8').rstrip('\n')
430+ except CalledProcessError:
431+ self.mudrawbin = ''
432+ # if mupdf isn't installed, fallback to ghostscript
433+ if not self.mudrawbin:
434+ try:
435+ self.gsbin = check_output(['which', 'gs'], stderr=DEVNULL).decode(encoding='UTF-8').rstrip('\n')
436+ except CalledProcessError:
437+ self.gsbin = ''
438+ # Last option: check if mudraw is placed in OpenLP base folder
439+ if not self.mudrawbin and not self.gsbin:
440+ application_path = AppLocation.get_directory(AppLocation.AppDir)
441+ if os.path.isfile(application_path + '/../mudraw'):
442+ self.mudrawbin = application_path + '/../mudraw'
443+ if self.mudrawbin:
444+ self.also_supports = ['xps']
445+ return True
446+ elif self.gsbin:
447+ return True
448+ else:
449+ return False
450+
451+ def kill(self):
452+ """
453+ Called at system exit to clean up any running presentations
454+ """
455+ log.debug('Kill pdfviewer')
456+ while self.docs:
457+ self.docs[0].close_presentation()
458+
459+
460+class PdfDocument(PresentationDocument):
461+ """
462+ Class which holds information of a single presentation.
463+ This class is not actually used to present the PDF, instead we convert to
464+ image-serviceitem on the fly and present as such. Therefore some of the 'playback'
465+ functions is not implemented.
466+ """
467+ def __init__(self, controller, presentation):
468+ """
469+ Constructor, store information about the file and initialise.
470+ """
471+ log.debug('Init Presentation Pdf')
472+ PresentationDocument.__init__(self, controller, presentation)
473+ self.presentation = None
474+ self.blanked = False
475+ self.hidden = False
476+ self.image_files = []
477+ self.num_pages = -1
478+
479+ def gs_get_resolution(self, size):
480+ """
481+ Only used when using ghostscript
482+ Ghostscript can't scale automatically while keeping aspect like mupdf, so we need
483+ to get the ratio between the screen size and the PDF to scale
484+
485+ :param size: Size struct containing the screen size.
486+ :return: The resolution dpi to be used.
487+ """
488+ # Use a postscript script to get size of the pdf. It is assumed that all pages have same size
489+ gs_resolution_script = AppLocation.get_directory(AppLocation.PluginsDir) + '/presentations/lib/ghostscript_get_resolution.ps'
490+ # Run the script on the pdf to get the size
491+ runlog = []
492+ try:
493+ runlog = check_output([self.controller.gsbin, '-dNOPAUSE', '-dNODISPLAY', '-dBATCH',
494+ '-sFile=' + self.filepath, gs_resolution_script])
495+ except CalledProcessError as e:
496+ log.debug(' '.join(e.cmd))
497+ log.debug(e.output)
498+ # Extract the pdf resolution from output, the format is " Size: x: <width>, y: <height>"
499+ width = 0
500+ height = 0
501+ for line in runlog.splitlines():
502+ try:
503+ width = int(re.search('.*Size: x: (\d+\.?\d*), y: \d+.*', line.decode()).group(1))
504+ height = int(re.search('.*Size: x: \d+\.?\d*, y: (\d+\.?\d*).*', line.decode()).group(1))
505+ break
506+ except AttributeError:
507+ pass
508+ # Calculate the ratio from pdf to screen
509+ if width > 0 and height > 0:
510+ width_ratio = size.right() / float(width)
511+ height_ratio = size.bottom() / float(height)
512+ # return the resolution that should be used. 72 is default.
513+ if width_ratio > height_ratio:
514+ return int(height_ratio * 72)
515+ else:
516+ return int(width_ratio * 72)
517+ else:
518+ return 72
519+
520+ def load_presentation(self):
521+ """
522+ Called when a presentation is added to the SlideController. It generates images from the PDF.
523+
524+ :return: True is loading succeeded, otherwise False.
525+ """
526+ log.debug('load_presentation pdf')
527+ # Check if the images has already been created, and if yes load them
528+ if os.path.isfile(os.path.join(self.get_temp_folder(), 'mainslide001.png')):
529+ created_files = sorted(os.listdir(self.get_temp_folder()))
530+ for fn in created_files:
531+ if os.path.isfile(os.path.join(self.get_temp_folder(), fn)):
532+ self.image_files.append(os.path.join(self.get_temp_folder(), fn))
533+ self.num_pages = len(self.image_files)
534+ return True
535+ size = ScreenList().current['size']
536+ # Generate images from PDF that will fit the frame.
537+ runlog = ''
538+ try:
539+ if not os.path.isdir(self.get_temp_folder()):
540+ os.makedirs(self.get_temp_folder())
541+ if self.controller.mudrawbin:
542+ runlog = check_output([self.controller.mudrawbin, '-w', str(size.right()), '-h', str(size.bottom()),
543+ '-o', os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), self.filepath])
544+ elif self.controller.gsbin:
545+ resolution = self.gs_get_resolution(size)
546+ runlog = check_output([self.controller.gsbin, '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m',
547+ '-r' + str(resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
548+ '-sOutputFile=' + os.path.join(self.get_temp_folder(), 'mainslide%03d.png'),
549+ self.filepath])
550+ created_files = sorted(os.listdir(self.get_temp_folder()))
551+ for fn in created_files:
552+ if os.path.isfile(os.path.join(self.get_temp_folder(), fn)):
553+ self.image_files.append(os.path.join(self.get_temp_folder(), fn))
554+ except Exception as e:
555+ log.debug(e)
556+ log.debug(runlog)
557+ return False
558+ self.num_pages = len(self.image_files)
559+ # Create thumbnails
560+ self.create_thumbnails()
561+ return True
562+
563+ def create_thumbnails(self):
564+ """
565+ Generates thumbnails
566+ """
567+ log.debug('create_thumbnails pdf')
568+ if self.check_thumbnails():
569+ return
570+ # use builtin function to create thumbnails from generated images
571+ index = 1
572+ for image in self.image_files:
573+ self.convert_thumbnail(image, index)
574+ index += 1
575+
576+ def close_presentation(self):
577+ """
578+ Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being
579+ shut down.
580+ """
581+ log.debug('close_presentation pdf')
582+ self.controller.remove_doc(self)
583+
584+ def is_loaded(self):
585+ """
586+ Returns true if a presentation is loaded.
587+
588+ :return: True if loaded, False if not.
589+ """
590+ log.debug('is_loaded pdf')
591+ if self.num_pages < 0:
592+ return False
593+ return True
594+
595+ def is_active(self):
596+ """
597+ Returns true if a presentation is currently active.
598+
599+ :return: True if active, False if not.
600+ """
601+ log.debug('is_active pdf')
602+ return self.is_loaded() and not self.hidden
603+
604+ def get_slide_count(self):
605+ """
606+ Returns total number of slides
607+
608+ :return: The number of pages in the presentation..
609+ """
610+ return self.num_pages
611
612=== modified file 'openlp/plugins/presentations/lib/presentationtab.py'
613--- openlp/plugins/presentations/lib/presentationtab.py 2013-12-24 08:56:50 +0000
614+++ openlp/plugins/presentations/lib/presentationtab.py 2014-02-25 21:06:14 +0000
615@@ -30,7 +30,9 @@
616 from PyQt4 import QtGui
617
618 from openlp.core.common import Settings, UiStrings, translate
619-from openlp.core.lib import SettingsTab
620+from openlp.core.lib import SettingsTab, build_icon
621+from openlp.core.lib.ui import critical_error_message_box
622+from .pdfcontroller import PdfController
623
624
625 class PresentationTab(SettingsTab):
626@@ -64,6 +66,7 @@
627 self.presenter_check_boxes[controller.name] = checkbox
628 self.controllers_layout.addWidget(checkbox)
629 self.left_layout.addWidget(self.controllers_group_box)
630+ # Advanced
631 self.advanced_group_box = QtGui.QGroupBox(self.left_column)
632 self.advanced_group_box.setObjectName('advanced_group_box')
633 self.advanced_layout = QtGui.QVBoxLayout(self.advanced_group_box)
634@@ -72,8 +75,34 @@
635 self.override_app_check_box.setObjectName('override_app_check_box')
636 self.advanced_layout.addWidget(self.override_app_check_box)
637 self.left_layout.addWidget(self.advanced_group_box)
638+ # Pdf options
639+ self.pdf_group_box = QtGui.QGroupBox(self.left_column)
640+ self.pdf_group_box.setObjectName('pdf_group_box')
641+ self.pdf_layout = QtGui.QFormLayout(self.pdf_group_box)
642+ self.pdf_layout.setObjectName('pdf_layout')
643+ self.pdf_program_check_box = QtGui.QCheckBox(self.pdf_group_box)
644+ self.pdf_program_check_box.setObjectName('pdf_program_check_box')
645+ self.pdf_layout.addRow(self.pdf_program_check_box)
646+ self.pdf_program_path_layout = QtGui.QHBoxLayout()
647+ self.pdf_program_path_layout.setObjectName('pdf_program_path_layout')
648+ self.pdf_program_path = QtGui.QLineEdit(self.pdf_group_box)
649+ self.pdf_program_path.setObjectName('pdf_program_path')
650+ self.pdf_program_path.setReadOnly(True)
651+ self.pdf_program_path.setPalette(self.get_grey_text_palette(True))
652+ self.pdf_program_path_layout.addWidget(self.pdf_program_path)
653+ self.pdf_program_browse_button = QtGui.QToolButton(self.pdf_group_box)
654+ self.pdf_program_browse_button.setObjectName('pdf_program_browse_button')
655+ self.pdf_program_browse_button.setIcon(build_icon(':/general/general_open.png'))
656+ self.pdf_program_browse_button.setEnabled(False)
657+ self.pdf_program_path_layout.addWidget(self.pdf_program_browse_button)
658+ self.pdf_layout.addRow(self.pdf_program_path_layout)
659+ self.left_layout.addWidget(self.pdf_group_box)
660 self.left_layout.addStretch()
661+ self.right_column.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred)
662 self.right_layout.addStretch()
663+ # Signals and slots
664+ self.pdf_program_browse_button.clicked.connect(self.on_pdf_program_browse_button_clicked)
665+ self.pdf_program_check_box.clicked.connect(self.on_pdf_program_check_box_clicked)
666
667 def retranslateUi(self):
668 """
669@@ -85,8 +114,11 @@
670 checkbox = self.presenter_check_boxes[controller.name]
671 self.set_controller_text(checkbox, controller)
672 self.advanced_group_box.setTitle(UiStrings().Advanced)
673+ self.pdf_group_box.setTitle(translate('PresentationPlugin.PresentationTab', 'PDF options'))
674 self.override_app_check_box.setText(
675 translate('PresentationPlugin.PresentationTab', 'Allow presentation application to be overridden'))
676+ self.pdf_program_check_box.setText(
677+ translate('PresentationPlugin.PresentationTab', 'Use given full path for mudraw or ghostscript binary:'))
678
679 def set_controller_text(self, checkbox, controller):
680 if checkbox.isEnabled():
681@@ -103,6 +135,14 @@
682 checkbox = self.presenter_check_boxes[controller.name]
683 checkbox.setChecked(Settings().value(self.settings_section + '/' + controller.name))
684 self.override_app_check_box.setChecked(Settings().value(self.settings_section + '/override app'))
685+ # load pdf-program settings
686+ enable_pdf_program = Settings().value(self.settings_section + '/enable_pdf_program')
687+ self.pdf_program_check_box.setChecked(enable_pdf_program)
688+ self.pdf_program_path.setPalette(self.get_grey_text_palette(not enable_pdf_program))
689+ self.pdf_program_browse_button.setEnabled(enable_pdf_program)
690+ pdf_program = Settings().value(self.settings_section + '/pdf_program')
691+ if pdf_program:
692+ self.pdf_program_path.setText(pdf_program)
693
694 def save(self):
695 """
696@@ -128,6 +168,18 @@
697 if Settings().value(setting_key) != self.override_app_check_box.checkState():
698 Settings().setValue(setting_key, self.override_app_check_box.checkState())
699 changed = True
700+ # Save pdf-settings
701+ pdf_program = self.pdf_program_path.text()
702+ enable_pdf_program = self.pdf_program_check_box.checkState()
703+ # If the given program is blank disable using the program
704+ if pdf_program == '':
705+ enable_pdf_program = 0
706+ if pdf_program != Settings().value(self.settings_section + '/pdf_program'):
707+ Settings().setValue(self.settings_section + '/pdf_program', pdf_program)
708+ changed = True
709+ if enable_pdf_program != Settings().value(self.settings_section + '/enable_pdf_program'):
710+ Settings().setValue(self.settings_section + '/enable_pdf_program', enable_pdf_program)
711+ changed = True
712 if changed:
713 self.settings_form.register_post_process('mediaitem_suffix_reset')
714 self.settings_form.register_post_process('mediaitem_presentation_rebuild')
715@@ -143,3 +195,43 @@
716 checkbox = self.presenter_check_boxes[controller.name]
717 checkbox.setEnabled(controller.is_available())
718 self.set_controller_text(checkbox, controller)
719+
720+ def on_pdf_program_browse_button_clicked(self):
721+ """
722+ Select the mudraw or ghostscript binary that should be used.
723+ """
724+ filename = QtGui.QFileDialog.getOpenFileName(self, translate('PresentationPlugin.PresentationTab',
725+ 'Select mudraw or ghostscript binary.'),
726+ self.pdf_program_path.text())
727+ if filename:
728+ program_type = PdfController.check_binary(filename)
729+ if not program_type:
730+ critical_error_message_box(UiStrings().Error,
731+ translate('PresentationPlugin.PresentationTab',
732+ 'The program is not ghostscript or mudraw which is required.'))
733+ else:
734+ self.pdf_program_path.setText(filename)
735+
736+ def on_pdf_program_check_box_clicked(self, checked):
737+ """
738+ When checkbox for manual entering pdf-program is clicked,
739+ enable or disable the textbox for the programpath and the browse-button.
740+
741+ :param checked: If the box is checked or not.
742+ """
743+ self.pdf_program_path.setPalette(self.get_grey_text_palette(not checked))
744+ self.pdf_program_browse_button.setEnabled(checked)
745+
746+ def get_grey_text_palette(self, greyed):
747+ """
748+ Returns a QPalette with greyed out text as used for placeholderText.
749+
750+ :param greyed: Determines whether the palette should be grayed.
751+ :return: The created palette.
752+ """
753+ palette = QtGui.QPalette()
754+ color = self.palette().color(QtGui.QPalette.Active, QtGui.QPalette.Text)
755+ if greyed:
756+ color.setAlpha(128)
757+ palette.setColor(QtGui.QPalette.Active, QtGui.QPalette.Text, color)
758+ return palette
759
760=== modified file 'openlp/plugins/presentations/presentationplugin.py'
761--- openlp/plugins/presentations/presentationplugin.py 2013-12-24 08:56:50 +0000
762+++ openlp/plugins/presentations/presentationplugin.py 2014-02-25 21:06:14 +0000
763@@ -45,9 +45,12 @@
764
765 __default_settings__ = {
766 'presentations/override app': QtCore.Qt.Unchecked,
767+ 'presentations/enable_pdf_program': QtCore.Qt.Unchecked,
768+ 'presentations/pdf_program': '',
769 'presentations/Impress': QtCore.Qt.Checked,
770 'presentations/Powerpoint': QtCore.Qt.Checked,
771 'presentations/Powerpoint Viewer': QtCore.Qt.Checked,
772+ 'presentations/Pdf': QtCore.Qt.Checked,
773 'presentations/presentations files': []
774 }
775
776
777=== modified file 'resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py'
778--- resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py 2013-12-24 08:56:50 +0000
779+++ resources/pyinstaller/hook-openlp.plugins.presentations.presentationplugin.py 2014-02-25 21:06:14 +0000
780@@ -29,4 +29,5 @@
781
782 hiddenimports = ['openlp.plugins.presentations.lib.impresscontroller',
783 'openlp.plugins.presentations.lib.powerpointcontroller',
784- 'openlp.plugins.presentations.lib.pptviewcontroller']
785+ 'openlp.plugins.presentations.lib.pptviewcontroller',
786+ 'openlp.plugins.presentations.lib.pdfcontroller']
787
788=== modified file 'tests/functional/openlp_plugins/presentations/test_mediaitem.py'
789--- tests/functional/openlp_plugins/presentations/test_mediaitem.py 2013-12-28 21:33:38 +0000
790+++ tests/functional/openlp_plugins/presentations/test_mediaitem.py 2014-02-25 21:06:14 +0000
791@@ -75,11 +75,16 @@
792 presentation_controller.also_supports = []
793 presentation_viewer_controller = MagicMock()
794 presentation_viewer_controller.enabled.return_value = False
795+ pdf_controller = MagicMock()
796+ pdf_controller.enabled.return_value = True
797+ pdf_controller.supports = ['pdf']
798+ pdf_controller.also_supports = ['xps']
799 # Mock the controllers.
800 self.media_item.controllers = {
801 'Impress': impress_controller,
802 'Powerpoint': presentation_controller,
803- 'Powerpoint Viewer': presentation_viewer_controller
804+ 'Powerpoint Viewer': presentation_viewer_controller,
805+ 'Pdf': pdf_controller
806 }
807
808 # WHEN: Build the file mask.
809@@ -92,3 +97,7 @@
810 'The file mask should contain the odp extension')
811 self.assertIn('*.ppt', self.media_item.on_new_file_masks,
812 'The file mask should contain the ppt extension')
813+ self.assertIn('*.pdf', self.media_item.on_new_file_masks,
814+ 'The file mask should contain the pdf extension')
815+ self.assertIn('*.xps', self.media_item.on_new_file_masks,
816+ 'The file mask should contain the xps extension')
817
818=== added file 'tests/functional/openlp_plugins/presentations/test_pdfcontroller.py'
819--- tests/functional/openlp_plugins/presentations/test_pdfcontroller.py 1970-01-01 00:00:00 +0000
820+++ tests/functional/openlp_plugins/presentations/test_pdfcontroller.py 2014-02-25 21:06:14 +0000
821@@ -0,0 +1,109 @@
822+# -*- coding: utf-8 -*-
823+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
824+
825+###############################################################################
826+# OpenLP - Open Source Lyrics Projection #
827+# --------------------------------------------------------------------------- #
828+# Copyright (c) 2008-2014 Raoul Snyman #
829+# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
830+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
831+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
832+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
833+# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
834+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
835+# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
836+# --------------------------------------------------------------------------- #
837+# This program is free software; you can redistribute it and/or modify it #
838+# under the terms of the GNU General Public License as published by the Free #
839+# Software Foundation; version 2 of the License. #
840+# #
841+# This program is distributed in the hope that it will be useful, but WITHOUT #
842+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
843+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
844+# more details. #
845+# #
846+# You should have received a copy of the GNU General Public License along #
847+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
848+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
849+###############################################################################
850+"""
851+This module contains tests for the PdfController
852+"""
853+import os
854+import shutil
855+from unittest import TestCase, SkipTest
856+from tempfile import mkstemp, mkdtemp
857+
858+from PyQt4 import QtGui
859+
860+from openlp.plugins.presentations.lib.pdfcontroller import PdfController, PdfDocument
861+from tests.functional import MagicMock
862+from openlp.core.common import Settings
863+from openlp.core.lib import ScreenList
864+from tests.utils.constants import TEST_RESOURCES_PATH
865+
866+__default_settings__ = {
867+ 'presentations/enable_pdf_program': False
868+}
869+
870+
871+class TestPdfController(TestCase):
872+ """
873+ Test the PdfController.
874+ """
875+ def setUp(self):
876+ """
877+ Set up the components need for all tests.
878+ """
879+ self.fd, self.ini_file = mkstemp('.ini')
880+ Settings().set_filename(self.ini_file)
881+ self.application = QtGui.QApplication.instance()
882+ ScreenList.create(self.application.desktop())
883+ Settings().extend_default_settings(__default_settings__)
884+ self.temp_folder = mkdtemp()
885+ self.thumbnail_folder = mkdtemp()
886+
887+ def tearDown(self):
888+ """
889+ Delete all the C++ objects at the end so that we don't have a segfault
890+ """
891+ del self.application
892+ try:
893+ os.unlink(self.ini_file)
894+ shutil.rmtree(self.thumbnail_folder)
895+ shutil.rmtree(self.temp_folder)
896+ except OSError:
897+ pass
898+
899+ def constructor_test(self):
900+ """
901+ Test the Constructor
902+ """
903+ # GIVEN: No presentation controller
904+ controller = None
905+
906+ # WHEN: The presentation controller object is created
907+ controller = PdfController(plugin=MagicMock())
908+
909+ # THEN: The name of the presentation controller should be correct
910+ self.assertEqual('Pdf', controller.name, 'The name of the presentation controller should be correct')
911+
912+ def load_pdf_test(self):
913+ """
914+ Test loading of a Pdf
915+ """
916+ # GIVEN: A Pdf-file
917+ test_file = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf')
918+
919+ # WHEN: The Pdf is loaded
920+ controller = PdfController(plugin=MagicMock())
921+ if not controller.check_available():
922+ raise SkipTest('Could not detect mudraw or ghostscript, so skipping PDF test')
923+ controller.temp_folder = self.temp_folder
924+ controller.thumbnail_folder = self.thumbnail_folder
925+ document = PdfDocument(controller, test_file)
926+ loaded = document.load_presentation()
927+
928+ # THEN: The load should succeed and we should be able to get a pagecount
929+ self.assertTrue(loaded, 'The loading of the PDF should succeed.')
930+ self.assertEqual(3, document.get_slide_count(), 'The pagecount of the PDF should be 3.')
931
932=== added directory 'tests/resources/presentations'
933=== added file 'tests/resources/presentations/pdf_test1.pdf'
934Binary files tests/resources/presentations/pdf_test1.pdf 1970-01-01 00:00:00 +0000 and tests/resources/presentations/pdf_test1.pdf 2014-02-25 21:06:14 +0000 differ