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