source: pdfshuffler/trunk/fuentes/.pc/fix-threading/pdfshuffler/pdfshuffler.py @ 337

Last change on this file since 337 was 337, checked in by jrpelegrina, 4 years ago

Firs release to xenial

File size: 43.8 KB
Line 
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4"""
5
6 PdfShuffler 0.6.0 - GTK+ based utility for splitting, rearrangement and
7 modification of PDF documents.
8 Copyright (C) 2008-2012 Konstantinos Poulios
9 <https://sourceforge.net/projects/pdfshuffler>
10
11 This file is part of PdfShuffler.
12
13 PdfShuffler is free software; you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation; either version 3 of the License, or
16 (at your option) any later version.
17
18 This program is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 GNU General Public License for more details.
22
23 You should have received a copy of the GNU General Public License along
24 with this program; if not, write to the Free Software Foundation, Inc.,
25 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26
27"""
28
29import os
30import shutil       # for file operations like whole directory deletion
31import sys          # for proccessing of command line args
32import urllib       # for parsing filename information passed by DnD
33import threading
34import tempfile
35from copy import copy
36
37import locale       #for multilanguage support
38import gettext
39gettext.install('pdfshuffler', unicode=1)
40
41
42APPNAME = 'PdfShuffler' # PDF-Shuffler, PDFShuffler, pdfshuffler
43VERSION = '0.6.0'
44WEBSITE = 'http://pdfshuffler.sourceforge.net/'
45LICENSE = 'GNU General Public License (GPL) Version 3.'
46
47try:
48    import pygtk
49    pygtk.require('2.0')
50    import gtk
51    assert gtk.gtk_version >= (2, 10, 0)
52    assert gtk.pygtk_version >= (2, 10, 0)
53except AssertionError:
54    print('You do not have the required versions of GTK+ and PyGTK ' +
55          'installed.\n\n' +
56          'Installed GTK+ version is ' +
57          '.'.join([str(n) for n in gtk.gtk_version]) + '\n' +
58          'Required GTK+ version is 2.10.0 or higher\n\n'
59          'Installed PyGTK version is ' +
60          '.'.join([str(n) for n in gtk.pygtk_version]) + '\n' +
61          'Required PyGTK version is 2.10.0 or higher')
62    sys.exit(1)
63except:
64    print('PyGTK version 2.10.0 or higher is required to run this program.')
65    print('No version of PyGTK was found on your system.')
66    sys.exit(1)
67
68import gobject      # for using custom signals
69import pango        # for adjusting the text alignment in CellRendererText
70import gio          # for inquiring mime types information
71import cairo
72
73import poppler      #for the rendering of pdf pages
74from pyPdf import PdfFileWriter, PdfFileReader
75
76from pdfshuffler_iconview import CellRendererImage
77gobject.type_register(CellRendererImage)
78
79import time
80
81class PdfShuffler:
82    prefs = {
83        'window width': min(700, gtk.gdk.screen_get_default().get_width() / 2),
84        'window height': min(600, gtk.gdk.screen_get_default().get_height() - 50),
85        'window x': 0,
86        'window y': 0,
87        'initial thumbnail size': 300,
88        'initial zoom level': -14,
89    }
90
91    MODEL_ROW_INTERN = 1001
92    MODEL_ROW_EXTERN = 1002
93    TEXT_URI_LIST = 1003
94    MODEL_ROW_MOTION = 1004
95    TARGETS_IV = [('MODEL_ROW_INTERN', gtk.TARGET_SAME_WIDGET, MODEL_ROW_INTERN),
96                  ('MODEL_ROW_EXTERN', gtk.TARGET_OTHER_APP, MODEL_ROW_EXTERN),
97                  ('MODEL_ROW_MOTION', 0, MODEL_ROW_MOTION)]
98    TARGETS_SW = [('text/uri-list', 0, TEXT_URI_LIST),
99                  ('MODEL_ROW_EXTERN', gtk.TARGET_OTHER_APP, MODEL_ROW_EXTERN)]
100
101    def __init__(self):
102        # Create the temporary directory
103        self.tmp_dir = tempfile.mkdtemp("pdfshuffler")
104        os.chmod(self.tmp_dir, 0700)
105
106        icon_theme = gtk.icon_theme_get_default()
107        try:
108            gtk.window_set_default_icon(icon_theme.load_icon("pdfshuffler", 64, 0))
109        except:
110            print(_("Can't load icon. Application is not installed correctly."))
111
112        # Import the user interface file, trying different possible locations
113        ui_path = '/usr/share/pdfshuffler/pdfshuffler.ui'
114        if not os.path.exists(ui_path):
115            ui_path = '/usr/local/share/pdfshuffler/pdfshuffler.ui'
116
117        if not os.path.exists(ui_path):
118            parent_dir = os.path.dirname( \
119                         os.path.dirname(os.path.realpath(__file__)))
120            ui_path = os.path.join(parent_dir, 'data', 'pdfshuffler.ui')
121
122        if not os.path.exists(ui_path):
123            head, tail = os.path.split(parent_dir)
124            while tail != 'lib' and tail != '':
125                head, tail = os.path.split(head)
126            if tail == 'lib':
127                ui_path = os.path.join(head, 'share', 'pdfshuffler', \
128                                       'pdfshuffler.ui')
129
130        self.uiXML = gtk.Builder()
131        self.uiXML.add_from_file(ui_path)
132        self.uiXML.connect_signals(self)
133
134        # Create the main window, and attach delete_event signal to terminating
135        # the application
136        self.window = self.uiXML.get_object('main_window')
137        self.window.set_title(APPNAME)
138        self.window.set_border_width(0)
139        self.window.move(self.prefs['window x'], self.prefs['window y'])
140        self.window.set_default_size(self.prefs['window width'],
141                                     self.prefs['window height'])
142        self.window.connect('delete_event', self.close_application)
143
144        # Create a scrolled window to hold the thumbnails-container
145        self.sw = self.uiXML.get_object('scrolledwindow')
146        self.sw.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
147                              gtk.DEST_DEFAULT_HIGHLIGHT |
148                              gtk.DEST_DEFAULT_DROP |
149                              gtk.DEST_DEFAULT_MOTION,
150                              self.TARGETS_SW,
151                              gtk.gdk.ACTION_COPY |
152                              gtk.gdk.ACTION_MOVE)
153        self.sw.connect('drag_data_received', self.sw_dnd_received_data)
154        self.sw.connect('button_press_event', self.sw_button_press_event)
155        self.sw.connect('scroll_event', self.sw_scroll_event)
156
157        # Create an alignment to keep the thumbnails center-aligned
158        align = gtk.Alignment(0.5, 0.5, 0, 0)
159        self.sw.add_with_viewport(align)
160
161        # Create ListStore model and IconView
162        self.model = gtk.ListStore(str,         # 0.Text descriptor
163                                   gobject.TYPE_PYOBJECT,
164                                                # 1.Cached page image
165                                   int,         # 2.Document number
166                                   int,         # 3.Page number
167                                   float,       # 4.Scale
168                                   str,         # 5.Document filename
169                                   int,         # 6.Rotation angle
170                                   float,       # 7.Crop left
171                                   float,       # 8.Crop right
172                                   float,       # 9.Crop top
173                                   float,       # 10.Crop bottom
174                                   int,         # 11.Page width
175                                   int,         # 12.Page height
176                                   float)       # 13.Resampling factor
177
178        self.zoom_set(self.prefs['initial zoom level'])
179        self.iv_col_width = self.prefs['initial thumbnail size']
180
181        self.iconview = gtk.IconView(self.model)
182        self.iconview.set_item_width(self.iv_col_width + 12)
183
184        self.cellthmb = CellRendererImage()
185        self.iconview.pack_start(self.cellthmb, False)
186        self.iconview.set_attributes(self.cellthmb, image=1,
187            scale=4, rotation=6, cropL=7, cropR=8, cropT=9, cropB=10,
188            width=11, height=12, resample=13)
189
190        self.celltxt = gtk.CellRendererText()
191        self.celltxt.set_property('width', self.iv_col_width)
192        self.celltxt.set_property('wrap-width', self.iv_col_width)
193        self.celltxt.set_property('alignment', pango.ALIGN_CENTER)
194        self.iconview.pack_start(self.celltxt, False)
195        self.iconview.set_attributes(self.celltxt, text=0)
196
197        self.iconview.set_selection_mode(gtk.SELECTION_MULTIPLE)
198        self.iconview.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,
199                                               self.TARGETS_IV,
200                                               gtk.gdk.ACTION_COPY |
201                                               gtk.gdk.ACTION_MOVE)
202        self.iconview.enable_model_drag_dest(self.TARGETS_IV,
203                                             gtk.gdk.ACTION_DEFAULT)
204        self.iconview.connect('drag_begin', self.iv_drag_begin)
205        self.iconview.connect('drag_data_get', self.iv_dnd_get_data)
206        self.iconview.connect('drag_data_received', self.iv_dnd_received_data)
207        self.iconview.connect('drag_data_delete', self.iv_dnd_data_delete)
208        self.iconview.connect('drag_motion', self.iv_dnd_motion)
209        self.iconview.connect('drag_leave', self.iv_dnd_leave_end)
210        self.iconview.connect('drag_end', self.iv_dnd_leave_end)
211        self.iconview.connect('button_press_event', self.iv_button_press_event)
212
213        align.add(self.iconview)
214
215        # Progress bar
216        self.progress_bar = self.uiXML.get_object('progressbar')
217        self.progress_bar_timeout_id = 0
218
219        # Define window callback function and show window
220        self.window.connect('size_allocate', self.on_window_size_request)        # resize
221        self.window.connect('key_press_event', self.on_keypress_event ) # keypress
222        self.window.show_all()
223        self.progress_bar.hide_all()
224
225        # Change iconview color background
226        style = self.sw.get_style().copy()
227        for state in (gtk.STATE_NORMAL, gtk.STATE_PRELIGHT, gtk.STATE_ACTIVE):
228            style.base[state] = style.bg[gtk.STATE_NORMAL]
229        self.iconview.set_style(style)
230
231        # Creating the popup menu
232        self.popup = gtk.Menu()
233        popup_rotate_right = gtk.ImageMenuItem(_('_Rotate Right'))
234        popup_rotate_left = gtk.ImageMenuItem(_('Rotate _Left'))
235        popup_crop = gtk.MenuItem(_('C_rop...'))
236        popup_delete = gtk.ImageMenuItem(gtk.STOCK_DELETE)
237        popup_saveselection = gtk.MenuItem(_('_Export selection...'))
238        popup_rotate_right.connect('activate', self.rotate_page_right)
239        popup_rotate_left.connect('activate', self.rotate_page_left)
240        popup_crop.connect('activate', self.crop_page_dialog)
241        popup_delete.connect('activate', self.clear_selected)
242        popup_saveselection.connect('activate', self.choose_export_pdf_name, True)
243        popup_rotate_right.show()
244        popup_rotate_left.show()
245        popup_crop.show()
246        popup_delete.show()
247        popup_saveselection.show()
248        self.popup.append(popup_rotate_right)
249        self.popup.append(popup_rotate_left)
250        self.popup.append(popup_crop)
251        self.popup.append(popup_delete)
252        self.popup.append(popup_saveselection)
253
254        # Initializing variables
255        self.export_directory = os.getenv('HOME')
256        self.import_directory = self.export_directory
257        self.nfile = 0
258        self.iv_auto_scroll_direction = 0
259        self.iv_auto_scroll_timer = None
260        self.pdfqueue = []
261
262        gobject.type_register(PDF_Renderer)
263        gobject.signal_new('update_thumbnail', PDF_Renderer,
264                           gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
265                           [gobject.TYPE_INT, gobject.TYPE_PYOBJECT,
266                            gobject.TYPE_FLOAT])
267        self.rendering_thread = 0
268
269        self.set_unsaved(False)
270
271        # Importing documents passed as command line arguments
272        for filename in sys.argv[1:]:
273            self.add_pdf_pages(filename)
274
275    def render(self):
276        if self.rendering_thread:
277            self.rendering_thread.quit = True
278            self.rendering_thread.join()
279        #FIXME: the resample=2. factor has to be dynamic when lazy rendering
280        #       is implemented
281        self.rendering_thread = PDF_Renderer(self.model, self.pdfqueue, 2)
282        self.rendering_thread.connect('update_thumbnail', self.update_thumbnail)
283        self.rendering_thread.start()
284
285        if self.progress_bar_timeout_id:
286            gobject.source_remove(self.progress_bar_timeout_id)
287        self.progress_bar_timout_id = \
288            gobject.timeout_add(50, self.progress_bar_timeout)
289
290    def set_unsaved(self, flag):
291        self.is_unsaved = flag
292        gobject.idle_add(self.retitle)
293
294    def retitle(self):
295        title = ''
296        if len(self.pdfqueue) == 1:
297            title += self.pdfqueue[0].filename
298        elif len(self.pdfqueue) == 0:
299            title += _("No document")
300        else:
301            title += _("Several documents")
302        if self.is_unsaved:
303            title += '*'
304        title += ' - ' + APPNAME
305        self.window.set_title(title)
306
307    def progress_bar_timeout(self):
308        cnt_finished = 0
309        cnt_all = 0
310        for row in self.model:
311            cnt_all += 1
312            if row[1]:
313                cnt_finished += 1
314        fraction = float(cnt_finished)/float(cnt_all)
315
316        self.progress_bar.set_fraction(fraction)
317        self.progress_bar.set_text(_('Rendering thumbnails... [%(i1)s/%(i2)s]')
318                                   % {'i1' : cnt_finished, 'i2' : cnt_all})
319        if fraction >= 0.999:
320            self.progress_bar.hide_all()
321            return False
322        elif not self.progress_bar.flags() & gtk.VISIBLE:
323            self.progress_bar.show_all()
324
325        return True
326 
327    def update_thumbnail(self, object, num, thumbnail, resample):
328        row = self.model[num]
329        gtk.gdk.threads_enter()
330        row[13] = resample
331        row[4] = self.zoom_scale
332        row[1] = thumbnail
333        gtk.gdk.threads_leave()
334
335    def on_window_size_request(self, window, event):
336        """Main Window resize - workaround for autosetting of
337           iconview cols no."""
338
339        #add 12 because of: http://bugzilla.gnome.org/show_bug.cgi?id=570152
340        col_num = 9 * window.get_size()[0] \
341            / (10 * (self.iv_col_width + self.iconview.get_column_spacing() * 2))
342        self.iconview.set_columns(col_num)
343
344    def update_geometry(self, iter):
345        """Recomputes the width and height of the rotated page and saves
346           the result in the ListStore"""
347
348        if not self.model.iter_is_valid(iter):
349            return
350
351        nfile, npage, rotation = self.model.get(iter, 2, 3, 6)
352        crop = self.model.get(iter, 7, 8, 9, 10)
353        page = self.pdfqueue[nfile-1].document.get_page(npage-1)
354        w0, h0 = page.get_size()
355
356        rotation = int(rotation) % 360
357        rotation = ((rotation + 45) / 90) * 90
358        if rotation == 90 or rotation == 270:
359            w1, h1 = h0, w0
360        else:
361            w1, h1 = w0, h0
362
363        self.model.set(iter, 11, w1, 12, h1)
364
365    def reset_iv_width(self, renderer=None):
366        """Reconfigures the width of the iconview columns"""
367
368        if not self.model.get_iter_first(): #just checking if model is empty
369            return
370
371        max_w = 10 + int(max(row[4]*row[11]*(1.-row[7]-row[8]) \
372                             for row in self.model))
373        if max_w != self.iv_col_width:
374            self.iv_col_width = max_w
375            self.celltxt.set_property('width', self.iv_col_width)
376            self.celltxt.set_property('wrap-width', self.iv_col_width)
377            self.iconview.set_item_width(self.iv_col_width + 12) #-1)
378            self.on_window_size_request(self.window, None)
379
380    def on_keypress_event(self, widget, event):
381        """Keypress events in Main Window"""
382
383        #keyname = gtk.gdk.keyval_name(event.keyval)
384        if event.keyval == 65535:   # Delete keystroke
385            self.clear_selected()
386
387    def close_application(self, widget, event=None, data=None):
388        """Termination"""
389
390        if self.rendering_thread:
391            self.rendering_thread.quit = True
392            self.rendering_thread.join()
393
394        if os.path.isdir(self.tmp_dir):
395            shutil.rmtree(self.tmp_dir)
396        if gtk.main_level():
397            gtk.main_quit()
398        else:
399            sys.exit(0)
400        return False
401
402    def add_pdf_pages(self, filename,
403                            firstpage=None, lastpage=None,
404                            angle=0, crop=[0.,0.,0.,0.]):
405        """Add pages of a pdf document to the model"""
406
407        res = False
408        # Check if the document has already been loaded
409        pdfdoc = None
410        for it_pdfdoc in self.pdfqueue:
411            if os.path.isfile(it_pdfdoc.filename) and \
412               os.path.samefile(filename, it_pdfdoc.filename) and \
413               os.path.getmtime(filename) is it_pdfdoc.mtime:
414                pdfdoc = it_pdfdoc
415                break
416
417        if not pdfdoc:
418            pdfdoc = PDF_Doc(filename, self.nfile, self.tmp_dir)
419            self.import_directory = os.path.split(filename)[0]
420            self.export_directory = self.import_directory
421            if pdfdoc.nfile != 0 and pdfdoc != []:
422                self.nfile = pdfdoc.nfile
423                self.pdfqueue.append(pdfdoc)
424            else:
425                return res
426
427        n_start = 1
428        n_end = pdfdoc.npage
429        if firstpage:
430           n_start = min(n_end, max(1, firstpage))
431        if lastpage:
432           n_end = max(n_start, min(n_end, lastpage))
433
434        for npage in range(n_start, n_end + 1):
435            descriptor = ''.join([pdfdoc.shortname, '\n', _('page'), ' ', str(npage)])
436            page = pdfdoc.document.get_page(npage-1)
437            w, h = page.get_size()
438            iter = self.model.append((descriptor,         # 0
439                                      None,               # 1
440                                      pdfdoc.nfile,       # 2
441                                      npage,              # 3
442                                      self.zoom_scale,    # 4
443                                      pdfdoc.filename,    # 5
444                                      angle,              # 6
445                                      crop[0],crop[1],    # 7-8
446                                      crop[2],crop[3],    # 9-10
447                                      w,h,                # 11-12
448                                      2.              ))  # 13 FIXME
449            self.update_geometry(iter)
450            res = True
451
452        self.reset_iv_width()
453        gobject.idle_add(self.retitle)
454        if res:
455            gobject.idle_add(self.render)
456        return res
457
458    def choose_export_pdf_name(self, widget=None, only_selected=False):
459        """Handles choosing a name for exporting """
460
461        chooser = gtk.FileChooserDialog(title=_('Export ...'),
462                                        action=gtk.FILE_CHOOSER_ACTION_SAVE,
463                                        buttons=(gtk.STOCK_CANCEL,
464                                                 gtk.RESPONSE_CANCEL,
465                                                 gtk.STOCK_SAVE,
466                                                 gtk.RESPONSE_OK))
467        chooser.set_do_overwrite_confirmation(True)
468        chooser.set_current_folder(self.export_directory)
469        filter_pdf = gtk.FileFilter()
470        filter_pdf.set_name(_('PDF files'))
471        filter_pdf.add_mime_type('application/pdf')
472        chooser.add_filter(filter_pdf)
473
474        filter_all = gtk.FileFilter()
475        filter_all.set_name(_('All files'))
476        filter_all.add_pattern('*')
477        chooser.add_filter(filter_all)
478
479        while True:
480            response = chooser.run()
481            if response == gtk.RESPONSE_OK:
482                file_out = chooser.get_filename()
483                (path, shortname) = os.path.split(file_out)
484                (shortname, ext) = os.path.splitext(shortname)
485                if ext.lower() != '.pdf':
486                    file_out = file_out + '.pdf'
487                try:
488                    self.export_to_file(file_out, only_selected)
489                    self.export_directory = path
490                    self.set_unsaved(False)
491                except Exception, e:
492                    chooser.destroy()
493                    error_msg_dlg = gtk.MessageDialog(flags=gtk.DIALOG_MODAL,
494                                                      type=gtk.MESSAGE_ERROR,
495                                                      message_format=str(e),
496                                                      buttons=gtk.BUTTONS_OK)
497                    response = error_msg_dlg.run()
498                    if response == gtk.RESPONSE_OK:
499                        error_msg_dlg.destroy()
500                    return
501            break
502        chooser.destroy()
503
504    def export_to_file(self, file_out, only_selected=False):
505        """Export to file"""
506
507        selection = self.iconview.get_selected_items()
508        pdf_output = PdfFileWriter()
509        pdf_input = []
510        for pdfdoc in self.pdfqueue:
511            pdfdoc_inp = PdfFileReader(file(pdfdoc.copyname, 'rb'))
512            if pdfdoc_inp.getIsEncrypted():
513                try: # Workaround for lp:#355479
514                    stat = pdfdoc_inp.decrypt('')
515                except:
516                    stat = 0
517                if (stat!=1):
518                    errmsg = _('File %s is encrypted.\n'
519                               'Support for encrypted files has not been implemented yet.\n'
520                               'File export failed.') % pdfdoc.filename
521                    raise Exception, errmsg
522                #FIXME
523                #else
524                #   ask for password and decrypt file
525            pdf_input.append(pdfdoc_inp)
526
527        for row in self.model:
528
529            if only_selected and row.path not in selection:
530                continue
531
532            # add pages from input to output document
533            nfile = row[2]
534            npage = row[3]
535            current_page = copy(pdf_input[nfile-1].getPage(npage-1))
536            angle = row[6]
537            angle0 = current_page.get("/Rotate",0)
538            crop = [row[7],row[8],row[9],row[10]]
539            if angle != 0:
540                current_page.rotateClockwise(angle)
541            if crop != [0.,0.,0.,0.]:
542                rotate_times = (((angle + angle0) % 360 + 45) / 90) % 4
543                crop_init = crop
544                if rotate_times != 0:
545                    perm = [0,2,1,3]
546                    for it in range(rotate_times):
547                        perm.append(perm.pop(0))
548                    perm.insert(1,perm.pop(2))
549                    crop = [crop_init[perm[side]] for side in range(4)]
550                #(x1, y1) = current_page.cropBox.lowerLeft
551                #(x2, y2) = current_page.cropBox.upperRight
552                (x1, y1) = [float(xy) for xy in current_page.mediaBox.lowerLeft]
553                (x2, y2) = [float(xy) for xy in current_page.mediaBox.upperRight]
554                x1_new = int(x1 + (x2-x1) * crop[0])
555                x2_new = int(x2 - (x2-x1) * crop[1])
556                y1_new = int(y1 + (y2-y1) * crop[3])
557                y2_new = int(y2 - (y2-y1) * crop[2])
558                #current_page.cropBox.lowerLeft = (x1_new, y1_new)
559                #current_page.cropBox.upperRight = (x2_new, y2_new)
560                current_page.mediaBox.lowerLeft = (x1_new, y1_new)
561                current_page.mediaBox.upperRight = (x2_new, y2_new)
562
563            pdf_output.addPage(current_page)
564
565        # finally, write "output" to document-output.pdf
566        pdf_output.write(file(file_out, 'wb'))
567
568    def on_action_add_doc_activate(self, widget, data=None):
569        """Import doc"""
570
571        chooser = gtk.FileChooserDialog(title=_('Import...'),
572                                        action=gtk.FILE_CHOOSER_ACTION_OPEN,
573                                        buttons=(gtk.STOCK_CANCEL,
574                                                  gtk.RESPONSE_CANCEL,
575                                                  gtk.STOCK_OPEN,
576                                                  gtk.RESPONSE_OK))
577        chooser.set_current_folder(self.import_directory)
578        chooser.set_select_multiple(True)
579
580        filter_all = gtk.FileFilter()
581        filter_all.set_name(_('All files'))
582        filter_all.add_pattern('*')
583        chooser.add_filter(filter_all)
584
585        filter_pdf = gtk.FileFilter()
586        filter_pdf.set_name(_('PDF files'))
587        filter_pdf.add_mime_type('application/pdf')
588        chooser.add_filter(filter_pdf)
589        chooser.set_filter(filter_pdf)
590
591        response = chooser.run()
592        if response == gtk.RESPONSE_OK:
593            for filename in chooser.get_filenames():
594                if os.path.isfile(filename):
595                    # FIXME
596                    f = gio.File(filename)
597                    f_info = f.query_info('standard::content-type')
598                    mime_type = f_info.get_content_type()
599                    expected_mime_type = 'application/pdf'
600
601                    if mime_type == expected_mime_type:
602                        self.add_pdf_pages(filename)
603                    elif mime_type[:34] == 'application/vnd.oasis.opendocument':
604                        print(_('OpenDocument not supported yet!'))
605                    elif mime_type[:5] == 'image':
606                        print(_('Image file not supported yet!'))
607                    else:
608                        print(_('File type not supported!'))
609                else:
610                    print(_('File %s does not exist') % filename)
611        elif response == gtk.RESPONSE_CANCEL:
612            print(_('Closed, no files selected'))
613        chooser.destroy()
614        gobject.idle_add(self.retitle)
615
616    def clear_selected(self, button=None):
617        """Removes the selected elements in the IconView"""
618
619        model = self.iconview.get_model()
620        selection = self.iconview.get_selected_items()
621        if selection:
622            selection.sort(reverse=True)
623            self.set_unsaved(True)
624            for path in selection:
625                iter = model.get_iter(path)
626                model.remove(iter)
627            path = selection[-1]
628            self.iconview.select_path(path)
629            if not self.iconview.path_is_selected(path):
630                if len(model) > 0:      # select the last row
631                    row = model[-1]
632                    path = row.path
633                    self.iconview.select_path(path)
634            self.iconview.grab_focus()
635
636    def iv_drag_begin(self, iconview, context):
637        """Sets custom icon on drag begin for multiple items selected"""
638
639        if len(iconview.get_selected_items()) > 1:
640            iconview.stop_emission('drag_begin')
641            context.set_icon_stock(gtk.STOCK_DND_MULTIPLE, 0, 0)
642
643    def iv_dnd_get_data(self, iconview, context,
644                        selection_data, target_id, etime):
645        """Handles requests for data by drag and drop in iconview"""
646
647        model = iconview.get_model()
648        selection = self.iconview.get_selected_items()
649        selection.sort(key=lambda x: x[0])
650        data = []
651        for path in selection:
652            if selection_data.target == 'MODEL_ROW_INTERN':
653                data.append(str(path[0]))
654            elif selection_data.target == 'MODEL_ROW_EXTERN':
655                iter = model.get_iter(path)
656                nfile, npage, angle = model.get(iter, 2, 3, 6)
657                crop = model.get(iter, 7, 8, 9, 10)
658                pdfdoc = self.pdfqueue[nfile - 1]
659                data.append('\n'.join([pdfdoc.filename,
660                                       str(npage),
661                                       str(angle)] +
662                                       [str(side) for side in crop]))
663        if data:
664            data = '\n;\n'.join(data)
665            selection_data.set(selection_data.target, 8, data)
666
667    def iv_dnd_received_data(self, iconview, context, x, y,
668                             selection_data, target_id, etime):
669        """Handles received data by drag and drop in iconview"""
670
671        model = iconview.get_model()
672        data = selection_data.data
673        if data:
674            data = data.split('\n;\n')
675            drop_info = iconview.get_dest_item_at_pos(x, y)
676            iter_to = None
677            if drop_info:
678                path, position = drop_info
679                ref_to = gtk.TreeRowReference(model,path)
680            else:
681                position = gtk.ICON_VIEW_DROP_RIGHT
682                if len(model) > 0:  #find the iterator of the last row
683                    row = model[-1]
684                    path = row.path
685                    ref_to = gtk.TreeRowReference(model,path)
686            if ref_to:
687                before = (position == gtk.ICON_VIEW_DROP_LEFT
688                          or position == gtk.ICON_VIEW_DROP_ABOVE)
689                #if target_id == self.MODEL_ROW_INTERN:
690                if selection_data.target == 'MODEL_ROW_INTERN':
691                    if before:
692                        data.sort(key=int)
693                    else:
694                        data.sort(key=int,reverse=True)
695                    ref_from_list = [gtk.TreeRowReference(model,path)
696                                     for path in data]
697                    for ref_from in ref_from_list:
698                        path = ref_to.get_path()
699                        iter_to = model.get_iter(path)
700                        path = ref_from.get_path()
701                        iter_from = model.get_iter(path)
702                        row = model[iter_from]
703                        if before:
704                            model.insert_before(iter_to, row)
705                        else:
706                            model.insert_after(iter_to, row)
707                    if context.action == gtk.gdk.ACTION_MOVE:
708                        for ref_from in ref_from_list:
709                            path = ref_from.get_path()
710                            iter_from = model.get_iter(path)
711                            model.remove(iter_from)
712
713                #elif target_id == self.MODEL_ROW_EXTERN:
714                elif selection_data.target == 'MODEL_ROW_EXTERN':
715                    if not before:
716                        data.reverse()
717                    while data:
718                        tmp = data.pop(0).split('\n')
719                        filename = tmp[0]
720                        npage, angle = [int(k) for k in tmp[1:3]]
721                        crop = [float(side) for side in tmp[3:7]]
722                        if self.add_pdf_pages(filename, npage, npage,
723                                                        angle, crop):
724                            if len(model) > 0:
725                                path = ref_to.get_path()
726                                iter_to = model.get_iter(path)
727                                row = model[-1] #the last row
728                                path = row.path
729                                iter_from = model.get_iter(path)
730                                if before:
731                                    model.move_before(iter_from, iter_to)
732                                else:
733                                    model.move_after(iter_from, iter_to)
734                                if context.action == gtk.gdk.ACTION_MOVE:
735                                    context.finish(True, True, etime)
736
737    def iv_dnd_data_delete(self, widget, context):
738        """Deletes dnd items after a successful move operation"""
739
740        model = self.iconview.get_model()
741        selection = self.iconview.get_selected_items()
742        ref_del_list = [gtk.TreeRowReference(model,path) for path in selection]
743        for ref_del in ref_del_list:
744            path = ref_del.get_path()
745            iter = model.get_iter(path)
746            model.remove(iter)
747
748    def iv_dnd_motion(self, iconview, context, x, y, etime):
749        """Handles the drag-motion signal in order to auto-scroll the view"""
750
751        autoscroll_area = 40
752        sw_vadj = self.sw.get_vadjustment()
753        sw_height = self.sw.get_allocation().height
754        if y -sw_vadj.get_value() < autoscroll_area:
755            if not self.iv_auto_scroll_timer:
756                self.iv_auto_scroll_direction = gtk.DIR_UP
757                self.iv_auto_scroll_timer = gobject.timeout_add(150,
758                                                                self.iv_auto_scroll)
759        elif y -sw_vadj.get_value() > sw_height - autoscroll_area:
760            if not self.iv_auto_scroll_timer:
761                self.iv_auto_scroll_direction = gtk.DIR_DOWN
762                self.iv_auto_scroll_timer = gobject.timeout_add(150,
763                                                                self.iv_auto_scroll)
764        elif self.iv_auto_scroll_timer:
765            gobject.source_remove(self.iv_auto_scroll_timer)
766            self.iv_auto_scroll_timer = None
767
768    def iv_dnd_leave_end(self, widget, context, ignored=None):
769        """Ends the auto-scroll during DND"""
770
771        if self.iv_auto_scroll_timer:
772            gobject.source_remove(self.iv_auto_scroll_timer)
773            self.iv_auto_scroll_timer = None
774
775    def iv_auto_scroll(self):
776        """Timeout routine for auto-scroll"""
777
778        sw_vadj = self.sw.get_vadjustment()
779        sw_vpos = sw_vadj.get_value()
780        if self.iv_auto_scroll_direction == gtk.DIR_UP:
781            sw_vpos -= sw_vadj.step_increment
782            sw_vadj.set_value(max(sw_vpos, sw_vadj.lower))
783        elif self.iv_auto_scroll_direction == gtk.DIR_DOWN:
784            sw_vpos += sw_vadj.step_increment
785            sw_vadj.set_value(min(sw_vpos, sw_vadj.upper - sw_vadj.page_size))
786        return True  #call me again
787
788    def iv_button_press_event(self, iconview, event):
789        """Manages mouse clicks on the iconview"""
790
791        if event.button == 3:
792            x = int(event.x)
793            y = int(event.y)
794            time = event.time
795            path = iconview.get_path_at_pos(x, y)
796            selection = iconview.get_selected_items()
797            if path:
798                if path not in selection:
799                    iconview.unselect_all()
800                iconview.select_path(path)
801                iconview.grab_focus()
802                self.popup.popup(None, None, None, event.button, time)
803            return 1
804
805    def sw_dnd_received_data(self, scrolledwindow, context, x, y,
806                             selection_data, target_id, etime):
807        """Handles received data by drag and drop in scrolledwindow"""
808
809        data = selection_data.data
810        if target_id == self.MODEL_ROW_EXTERN:
811            self.model
812            if data:
813                data = data.split('\n;\n')
814            while data:
815                tmp = data.pop(0).split('\n')
816                filename = tmp[0]
817                npage, angle = [int(k) for k in tmp[1:3]]
818                crop = [float(side) for side in tmp[3:7]]
819                if self.add_pdf_pages(filename, npage, npage, angle, crop):
820                    if context.action == gtk.gdk.ACTION_MOVE:
821                        context.finish(True, True, etime)
822        elif target_id == self.TEXT_URI_LIST:
823            uri = data.strip()
824            uri_splitted = uri.split() # we may have more than one file dropped
825            for uri in uri_splitted:
826                filename = self.get_file_path_from_dnd_dropped_uri(uri)
827                if os.path.isfile(filename): # is it file?
828                    self.add_pdf_pages(filename)
829
830    def sw_button_press_event(self, scrolledwindow, event):
831        """Unselects all items in iconview on mouse click in scrolledwindow"""
832
833        if event.button == 1:
834            self.iconview.unselect_all()
835
836    def sw_scroll_event(self, scrolledwindow, event):
837        """Manages mouse scroll events in scrolledwindow"""
838
839        if event.state & gtk.gdk.CONTROL_MASK:
840            if event.direction == gtk.gdk.SCROLL_UP:
841                self.zoom_change(1)
842                return 1
843            elif event.direction == gtk.gdk.SCROLL_DOWN:
844                self.zoom_change(-1)
845                return 1
846
847    def zoom_set(self, level):
848        """Sets the zoom level"""
849        self.zoom_level = max(min(level, 5), -24)
850        self.zoom_scale = 1.1 ** self.zoom_level
851        for row in self.model:
852            row[4] = self.zoom_scale
853        self.reset_iv_width()
854
855    def zoom_change(self, step=5):
856        """Modifies the zoom level"""
857        self.zoom_set(self.zoom_level + step)
858
859    def zoom_in(self, widget=None):
860        """Increases the zoom level by 5 steps"""
861        self.zoom_change(5)
862
863    def zoom_out(self, widget=None, step=5):
864        """Reduces the zoom level by 5 steps"""
865        self.zoom_change(-5)
866
867    def get_file_path_from_dnd_dropped_uri(self, uri):
868        """Extracts the path from an uri"""
869
870        path = urllib.url2pathname(uri) # escape special chars
871        path = path.strip('\r\n\x00')   # remove \r\n and NULL
872
873        # get the path to file
874        if path.startswith('file:\\\\\\'): # windows
875            path = path[8:]  # 8 is len('file:///')
876        elif path.startswith('file://'):   # nautilus, rox
877            path = path[7:]  # 7 is len('file://')
878        elif path.startswith('file:'):     # xffm
879            path = path[5:]  # 5 is len('file:')
880        return path
881
882    def rotate_page_right(self, widget, data=None):
883        self.rotate_page(90)
884
885    def rotate_page_left(self, widget, data=None):
886        self.rotate_page(-90)
887
888    def rotate_page(self, angle):
889        """Rotates the selected page in the IconView"""
890
891        model = self.iconview.get_model()
892        selection = self.iconview.get_selected_items()
893        if len(selection) > 0:
894            self.set_unsaved(True)
895        rotate_times = (((-angle) % 360 + 45) / 90) % 4
896        if rotate_times is not 0:
897            for path in selection:
898                iter = model.get_iter(path)
899                nfile = model.get_value(iter, 2)
900                npage = model.get_value(iter, 3)
901
902                crop = [0.,0.,0.,0.]
903                perm = [0,2,1,3]
904                for it in range(rotate_times):
905                    perm.append(perm.pop(0))
906                perm.insert(1,perm.pop(2))
907                crop = [model.get_value(iter, 7 + perm[side]) for side in range(4)]
908                for side in range(4):
909                    model.set_value(iter, 7 + side, crop[side])
910
911                new_angle = model.get_value(iter, 6) + int(angle)
912                new_angle = new_angle % 360
913                model.set_value(iter, 6, new_angle)
914                self.update_geometry(iter)
915        self.reset_iv_width()
916
917    def crop_page_dialog(self, widget):
918        """Opens a dialog box to define margins for page cropping"""
919
920        sides = ('L', 'R', 'T', 'B')
921        side_names = {'L':_('Left'), 'R':_('Right'),
922                      'T':_('Top'), 'B':_('Bottom') }
923        opposite_sides = {'L':'R', 'R':'L', 'T':'B', 'B':'T' }
924
925        def set_crop_value(spinbutton, side):
926           opp_side = opposite_sides[side]
927           pos = sides.index(opp_side)
928           adj = spin_list[pos].get_adjustment()
929           adj.set_upper(99.0 - spinbutton.get_value())
930
931        model = self.iconview.get_model()
932        selection = self.iconview.get_selected_items()
933
934        crop = [0.,0.,0.,0.]
935        if selection:
936            path = selection[0]
937            pos = model.get_iter(path)
938            crop = [model.get_value(pos, 7 + side) for side in range(4)]
939
940        dialog = gtk.Dialog(title=(_('Crop Selected Pages')),
941                            parent=self.window,
942                            flags=gtk.DIALOG_MODAL,
943                            buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
944                                     gtk.STOCK_OK, gtk.RESPONSE_OK))
945        dialog.set_size_request(340, 250)
946        dialog.set_default_response(gtk.RESPONSE_OK)
947
948        frame = gtk.Frame(_('Crop Margins'))
949        dialog.vbox.pack_start(frame, False, False, 20)
950
951        vbox = gtk.VBox(False, 0)
952        frame.add(vbox)
953
954        spin_list = []
955        units = 2 * [_('% of width')] + 2 * [_('% of height')]
956        for side in sides:
957            hbox = gtk.HBox(True, 0)
958            vbox.pack_start(hbox, False, False, 5)
959
960            label = gtk.Label(side_names[side])
961            label.set_alignment(0, 0.0)
962            hbox.pack_start(label, True, True, 20)
963
964            adj = gtk.Adjustment(100.*crop.pop(0), 0.0, 99.0, 1.0, 5.0, 0.0)
965            spin = gtk.SpinButton(adj, 0, 1)
966            spin.set_activates_default(True)
967            spin.connect('value-changed', set_crop_value, side)
968            spin_list.append(spin)
969            hbox.pack_start(spin, False, False, 30)
970
971            label = gtk.Label(units.pop(0))
972            label.set_alignment(0, 0.0)
973            hbox.pack_start(label, True, True, 0)
974
975        dialog.show_all()
976        result = dialog.run()
977
978        if result == gtk.RESPONSE_OK:
979            modified = False
980            crop = [spin.get_value()/100. for spin in spin_list]
981            for path in selection:
982                pos = model.get_iter(path)
983                for it in range(4):
984                    old_val = model.get_value(pos, 7 + it)
985                    model.set_value(pos, 7 + it, crop[it])
986                    if crop[it] != old_val:
987                        modified = True
988                self.update_geometry(pos)
989            if modified:
990                self.set_unsaved(True)
991            self.reset_iv_width()
992        elif result == gtk.RESPONSE_CANCEL:
993            print(_('Dialog closed'))
994        dialog.destroy()
995
996    def about_dialog(self, widget, data=None):
997        about_dialog = gtk.AboutDialog()
998        try:
999            about_dialog.set_transient_for(self.window)
1000            about_dialog.set_modal(True)
1001        except:
1002            pass
1003        # FIXME
1004        about_dialog.set_name(APPNAME)
1005        about_dialog.set_version(VERSION)
1006        about_dialog.set_comments(_(
1007            '%s is a tool for rearranging and modifying PDF files. ' \
1008            'Developed using GTK+ and Python') % APPNAME)
1009        about_dialog.set_authors(['Konstantinos Poulios',])
1010        about_dialog.set_website_label(WEBSITE)
1011        about_dialog.set_logo_icon_name('pdfshuffler')
1012        about_dialog.set_license(LICENSE)
1013        about_dialog.connect('response', lambda w, *args: w.destroy())
1014        about_dialog.connect('delete_event', lambda w, *args: w.destroy())
1015        about_dialog.show_all()
1016
1017
1018class PDF_Doc:
1019    """Class handling PDF documents"""
1020
1021    def __init__(self, filename, nfile, tmp_dir):
1022
1023        self.filename = os.path.abspath(filename)
1024        (self.path, self.shortname) = os.path.split(self.filename)
1025        (self.shortname, self.ext) = os.path.splitext(self.shortname)
1026        f = gio.File(filename)
1027        mime_type = f.query_info('standard::content-type').get_content_type()
1028        expected_mime_type = 'application/pdf'
1029        file_prefix = 'file://'
1030
1031        if mime_type == expected_mime_type:
1032            self.nfile = nfile + 1
1033            self.mtime = os.path.getmtime(filename)
1034            self.copyname = os.path.join(tmp_dir, '%02d_' % self.nfile +
1035                                                  self.shortname + '.pdf')
1036            shutil.copy(self.filename, self.copyname)
1037            self.document = poppler.document_new_from_file (file_prefix + self.copyname, None)
1038            self.npage = self.document.get_n_pages()
1039        else:
1040            self.nfile = 0
1041            self.npage = 0
1042
1043
1044class PDF_Renderer(threading.Thread,gobject.GObject):
1045
1046    def __init__(self, model, pdfqueue, resample=1.):
1047        threading.Thread.__init__(self)
1048        gobject.GObject.__init__(self)
1049        self.model = model
1050        self.pdfqueue = pdfqueue
1051        self.resample = resample
1052        self.quit = False
1053
1054    def run(self):
1055        for idx, row in enumerate(self.model):
1056            if self.quit:
1057                return
1058            if not row[1]:
1059                try:
1060                    nfile = row[2]
1061                    npage = row[3]
1062                    pdfdoc = self.pdfqueue[nfile - 1]
1063                    page = pdfdoc.document.get_page(npage-1)
1064                    w, h = page.get_size()
1065                    thumbnail = cairo.ImageSurface(cairo.FORMAT_ARGB32,
1066                                                   int(w/self.resample),
1067                                                   int(h/self.resample))
1068                    cr = cairo.Context(thumbnail)
1069                    if self.resample != 1.:
1070                        cr.scale(1./self.resample, 1./self.resample)
1071                    page.render(cr)
1072                    time.sleep(0.003)
1073                    gobject.idle_add(self.emit,'update_thumbnail',
1074                                     idx, thumbnail, self.resample,
1075                                     priority=gobject.PRIORITY_LOW)
1076                except Exception,e:
1077                    print e
1078
1079
1080def main():
1081    """This function starts PdfShuffler"""
1082    gtk.gdk.threads_init()
1083    gobject.threads_init()
1084    PdfShuffler()
1085    gtk.main()
1086
1087if __name__ == '__main__':
1088    main()
1089
Note: See TracBrowser for help on using the repository browser.