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 | |
---|
29 | import os |
---|
30 | import shutil # for file operations like whole directory deletion |
---|
31 | import sys # for proccessing of command line args |
---|
32 | import urllib # for parsing filename information passed by DnD |
---|
33 | import threading |
---|
34 | import tempfile |
---|
35 | from copy import copy |
---|
36 | |
---|
37 | import locale #for multilanguage support |
---|
38 | import gettext |
---|
39 | gettext.install('pdfshuffler', unicode=1) |
---|
40 | |
---|
41 | |
---|
42 | APPNAME = 'PdfShuffler' # PDF-Shuffler, PDFShuffler, pdfshuffler |
---|
43 | VERSION = '0.6.0' |
---|
44 | WEBSITE = 'http://pdfshuffler.sourceforge.net/' |
---|
45 | LICENSE = 'GNU General Public License (GPL) Version 3.' |
---|
46 | |
---|
47 | try: |
---|
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) |
---|
53 | except 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) |
---|
63 | except: |
---|
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 | |
---|
68 | import gobject # for using custom signals |
---|
69 | import pango # for adjusting the text alignment in CellRendererText |
---|
70 | import gio # for inquiring mime types information |
---|
71 | import cairo |
---|
72 | |
---|
73 | import poppler #for the rendering of pdf pages |
---|
74 | from pyPdf import PdfFileWriter, PdfFileReader |
---|
75 | |
---|
76 | from pdfshuffler_iconview import CellRendererImage |
---|
77 | gobject.type_register(CellRendererImage) |
---|
78 | |
---|
79 | import time |
---|
80 | |
---|
81 | class 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 | |
---|
1018 | class 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 | |
---|
1044 | class 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 | |
---|
1080 | def main(): |
---|
1081 | """This function starts PdfShuffler""" |
---|
1082 | gtk.gdk.threads_init() |
---|
1083 | gobject.threads_init() |
---|
1084 | PdfShuffler() |
---|
1085 | gtk.main() |
---|
1086 | |
---|
1087 | if __name__ == '__main__': |
---|
1088 | main() |
---|
1089 | |
---|