I updated the PDF Booklet project and removed Python 2 dependencies so that it will run under Ubuntu 22.04.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

1315 lines
51 KiB

#!/usr/bin/python3
# -*- coding: utf-8 -*-
from __future__ import print_function
from __future__ import unicode_literals
# PdfShuffler 0.6.0 Rev 82, modified for Windows compatibility
# See the Class PdfShuffler_Windows_cod" / class PdfShuffler_Linux_code :
# updated for python 3 and Gtk 3
# Version inside pdfBooklet : 3.0.2
"""
PdfShuffler 0.6.0 - GTK+ based utility for splitting, rearrangement and
modification of PDF documents.
Copyright (C) 2008-2012 Konstantinos Poulios
<https://sourceforge.net/projects/pdfshuffler>
This file is part of PdfShuffler.
PdfShuffler is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
import os
import shutil # for file operations like whole directory deletion
import sys # for proccessing of command line args
import stat
import urllib # for parsing filename information passed by DnD
import threading
import tempfile
import glob
from copy import copy
import locale #for multilanguage support
import gettext
import pdfbooklet.elib_intl3 as elib_intl3
# elib_intl does not work if the strings are unicode
domain = "pdfshuffler" # these lines have no effect in python 3
locale = "share/locale"
elib_intl3.install(domain, locale)
APPNAME = 'PdfShuffler' # PDF-Shuffler, PDFShuffler, pdfshuffler
VERSION = '0.6.0'
WEBSITE = 'http://pdfshuffler.sourceforge.net/'
LICENSE = 'GNU General Public License (GPL) Version 3.'
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Poppler', '0.18')
from gi.repository import Gtk, Gdk, Poppler
from gi.repository import Pango # for adjusting the text alignment in CellRendererText
from gi.repository import Gio # for inquiring mime types information
from gi.repository import GObject # for using custom signals
##from gi.repository import cairo # Raises the error : 'gi.repository.cairo' object has no attribute 'ImageSurface'
import cairo
from pdfbooklet.PyPDF2_G.pdf import PdfFileWriter, PdfFileReader
from pdfbooklet.pdfshuffler_iconview3 import CellRendererImage
GObject.type_register(CellRendererImage)
import time
Gtk.rc_parse("./gtkrc")
class PdfShuffler:
prefs = {
'window width': min(700, Gdk.Screen.get_default().get_width() / 2),
'window height': min(600, Gdk.Screen.get_default().get_height() - 50),
'window x': 0,
'window y': 0,
'initial thumbnail size': 800,
'initial zoom level': -3,
}
MODEL_ROW_INTERN = 1001
MODEL_ROW_EXTERN = 1002
TEXT_URI_LIST = 1003
MODEL_ROW_MOTION = 1004
TARGETS_IV = [Gtk.TargetEntry.new('MODEL_ROW_INTERN', Gtk.TargetFlags.SAME_WIDGET, MODEL_ROW_INTERN),
Gtk.TargetEntry.new('MODEL_ROW_EXTERN', Gtk.TargetFlags.OTHER_APP, MODEL_ROW_EXTERN),
Gtk.TargetEntry.new('MODEL_ROW_MOTION', 0, MODEL_ROW_MOTION)]
TARGETS_SW = [Gtk.TargetEntry.new('text/uri-list', 0, TEXT_URI_LIST),
Gtk.TargetEntry.new('MODEL_ROW_EXTERN', Gtk.TargetFlags.OTHER_APP, MODEL_ROW_EXTERN)]
def __init__(self):
if os.name == "nt" :
self.winux = PdfShuffler_Windows_code()
else :
self.winux = PdfShuffler_Linux_code()
# Create the temporary directory
self.tmp_dir = tempfile.mkdtemp("pdfshuffler")
self.selection_start = 0
os.chmod(self.tmp_dir, stat.S_IRWXO) # TODO il y avait 0700. RWXO est plutôt 777 ?
icon_theme = Gtk.IconTheme.get_default()
# TODO : icontheme
## try:
## Gtk.window_set_default_icon(icon_theme.load_icon("pdfshuffler", 64, 0))
## except:
## print(_("Can't load icon. Application is not installed correctly."))
# Import the user interface file, trying different possible locations
ui_path = '/usr/share/pdfbooklet/data/pdfshuffler_g.glade'
if not os.path.exists(ui_path):
ui_path = '/usr/local/share/pdfbooklet/data/pdfshuffler_g.glade'
# Windows standard path
if not os.path.exists(ui_path):
if getattr( sys, 'frozen', False ) : # running in a bundle (pyinstaller)
ui_path = os.path.join(sys._MEIPASS, "data/pdfshuffler_g.glade")
else : # running live
ui_path = './data/pdfshuffler_g.glade'
if not os.path.exists(ui_path):
parent_dir = os.path.dirname( \
os.path.dirname(os.path.realpath(__file__)))
ui_path = os.path.join(parent_dir, 'data', 'pdfshuffler_g.glade')
if not os.path.exists(ui_path):
head, tail = os.path.split(parent_dir)
while tail != 'lib' and tail != '':
head, tail = os.path.split(head)
if tail == 'lib':
ui_path = os.path.join(head, 'share', 'pdfbooklet', \
'data/pdfshuffler_g.glade')
self.uiXML = Gtk.Builder()
self.uiXML.add_from_file(ui_path)
self.uiXML.connect_signals(self)
# Create the main window, and attach delete_event signal to terminating
# the application
self.window = self.uiXML.get_object('main_window')
self.window.set_title(APPNAME)
self.window.set_border_width(0)
self.window.move(self.prefs['window x'], self.prefs['window y'])
self.window.set_default_size(self.prefs['window width'],
self.prefs['window height'])
self.window.connect('delete_event', self.close_application)
# Create a scrolled window to hold the thumbnails-container
self.sw = self.uiXML.get_object('scrolledwindow')
self.sw.drag_dest_set(Gtk.DestDefaults.MOTION |
Gtk.DestDefaults.HIGHLIGHT |
Gtk.DestDefaults.DROP |
Gtk.DestDefaults.MOTION,
self.TARGETS_SW,
Gdk.DragAction.COPY |
Gdk.DragAction.MOVE)
self.sw.connect('drag_data_received', self.sw_dnd_received_data)
self.sw.connect('button_press_event', self.sw_button_press_event)
self.sw.connect('scroll_event', self.sw_scroll_event)
# Create an alignment to keep the thumbnails center-aligned
align = Gtk.Alignment.new(0.5, 0.5, 0, 0) # python 3
self.sw.add_with_viewport(align)
# Create ListStore model and IconView
self.model = Gtk.ListStore(str, # 0.Text descriptor
GObject.TYPE_PYOBJECT,
# 1.Cached page image
int, # 2.Document number
int, # 3.Page number
float, # 4.Scale
str, # 5.Document filename
int, # 6.Rotation angle
float, # 7.Crop left
float, # 8.Crop right
float, # 9.Crop top
float, # 10.Crop bottom
int, # 11.Page width
int, # 12.Page height
float) # 13.Resampling factor
self.zoom_set(self.prefs['initial zoom level'])
bar = self.uiXML.get_object('hscale1')
bar.set_value(self.prefs['initial zoom level'])
self.iv_col_width = self.prefs['initial thumbnail size']
self.iconview = Gtk.IconView(self.model)
self.iconview.set_item_width(self.iv_col_width + 12)
self.cellthmb = CellRendererImage()
## self.cellthmb = IDRenderer()
self.iconview.pack_start(self.cellthmb, False)
self.iconview.add_attribute(self.cellthmb, "image", 1)
self.iconview.add_attribute(self.cellthmb, "scale", 4)
self.iconview.add_attribute(self.cellthmb, "rotation", 6)
self.iconview.add_attribute(self.cellthmb, "cropL", 7)
self.iconview.add_attribute(self.cellthmb, "cropR", 8)
self.iconview.add_attribute(self.cellthmb, "cropT", 9)
self.iconview.add_attribute(self.cellthmb, "cropB", 10)
self.iconview.add_attribute(self.cellthmb, "width", 11)
self.iconview.add_attribute(self.cellthmb, "height", 12)
self.iconview.add_attribute(self.cellthmb, "resample", 13)
self.celltxt = Gtk.CellRendererText()
self.celltxt.set_property('width', self.iv_col_width)
self.celltxt.set_property('wrap-width', self.iv_col_width)
self.celltxt.set_property('alignment', Pango.Alignment.CENTER)
self.iconview.pack_start(self.celltxt, False)
self.iconview.set_properties(self.celltxt, text_column=0)
self.iconview.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
self.iconview.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
self.TARGETS_IV,
Gdk.DragAction.COPY |
Gdk.DragAction.MOVE)
self.iconview.enable_model_drag_dest(self.TARGETS_IV,
Gdk.DragAction.DEFAULT)
self.iconview.connect('drag_begin', self.iv_drag_begin)
self.iconview.connect('drag_data_get', self.iv_dnd_get_data)
self.iconview.connect('drag_data_received', self.iv_dnd_received_data)
self.iconview.connect('drag_data_delete', self.iv_dnd_data_delete)
self.iconview.connect('drag_motion', self.iv_dnd_motion)
self.iconview.connect('drag_leave', self.iv_dnd_leave_end)
self.iconview.connect('drag_end', self.iv_dnd_leave_end)
self.iconview.connect('button_press_event', self.iv_button_press_event)
align.add(self.iconview)
# Progress bar
self.progress_bar = self.uiXML.get_object('progressbar')
self.progress_bar_timeout_id = 0
# Define window callback function and show window
self.window.connect('size_allocate', self.on_window_size_request) # resize
self.window.connect('key_press_event', self.on_keypress_event ) # keypress
self.window.show_all()
self.progress_bar.hide()
# Change iconview color background
## style = self.sw.get_style().copy()
## for state in (Gtk.StateType.NORMAL, Gtk.StateType.PRELIGHT, Gtk.StateType.ACTIVE):
## style.base[state] = style.bg[Gtk.StateType.NORMAL]
## self.iconview.set_style(style)
# Creating the popup menu
## self.popup = Gtk.Menu()
## popup_rotate_right = Gtk.ImageMenuItem(_('_Rotate Right'))
## popup_rotate_left = Gtk.ImageMenuItem(_('Rotate _Left'))
## popup_crop = Gtk.MenuItem(_('C_rop...'))
## popup_delete = Gtk.ImageMenuItem(Gtk.STOCK_DELETE)
## popup_rotate_right.connect('activate', self.rotate_page_right)
## popup_rotate_left.connect('activate', self.rotate_page_left)
## popup_crop.connect('activate', self.crop_page_dialog)
## popup_delete.connect('activate', self.clear_selected)
## popup_rotate_right.show()
## popup_rotate_left.show()
## popup_crop.show()
## popup_delete.show()
## self.popup.append(popup_rotate_right)
## self.popup.append(popup_rotate_left)
## self.popup.append(popup_crop)
## self.popup.append(popup_delete)
self.popup = self.uiXML.get_object('contextmenu1')
# Initializing variables
self.export_directory = self.winux.home_dir()
self.import_directory = self.export_directory
self.nfile = 0
self.iv_auto_scroll_direction = 0
self.iv_auto_scroll_timer = None
self.pdfqueue = []
GObject.type_register(PDF_Renderer)
GObject.signal_new('update_thumbnail', PDF_Renderer,
GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE,
[GObject.TYPE_INT, GObject.TYPE_PYOBJECT,
GObject.TYPE_FLOAT])
self.rendering_thread = 0
self.set_unsaved(False)
# Importing documents passed as command line arguments
for filename in sys.argv[1:]:
self.add_pdf_pages(filename)
def render(self):
if self.rendering_thread:
self.rendering_thread.quit = True
self.rendering_thread.join()
#FIXME: the resample=2. factor has to be dynamic when lazy rendering
# is implemented
self.rendering_thread = PDF_Renderer(self.model, self.pdfqueue, 2)
self.rendering_thread.connect('update_thumbnail', self.update_thumbnail)
self.rendering_thread.start()
if self.progress_bar_timeout_id:
GObject.source_remove(self.progress_bar_timeout_id)
self.progress_bar_timout_id = \
GObject.timeout_add(50, self.progress_bar_timeout)
def set_unsaved(self, flag):
self.is_unsaved = flag
GObject.idle_add(self.retitle)
def retitle(self):
title = ''
if len(self.pdfqueue) == 1:
title += self.pdfqueue[0].filename
elif len(self.pdfqueue) == 0:
title += _("No document")
else:
title += _("Several documents")
if self.is_unsaved:
title += '*'
title += ' - ' + APPNAME
self.window.set_title(title)
def progress_bar_timeout(self):
cnt_finished = 0
cnt_all = 0
for row in self.model:
cnt_all += 1
if row[1]:
cnt_finished += 1
fraction = float(cnt_finished)/float(cnt_all)
self.progress_bar.set_fraction(fraction)
self.progress_bar.set_text(_('Rendering thumbnails... [%(i1)s/%(i2)s]')
% {'i1' : cnt_finished, 'i2' : cnt_all})
if fraction >= 0.999:
self.progress_bar.hide()
return False
## elif not self.progress_bar.flags() & Gtk.VISIBLE: python 3 : flags unknown
else :
self.progress_bar.show_all()
return True
def update_thumbnail(self, object, num, thumbnail, resample):
row = self.model[num]
Gdk.threads_enter()
row[13] = resample
row[4] = self.zoom_scale
row[1] = thumbnail
Gdk.threads_leave()
def on_window_size_request(self, window, event):
"""Main Window resize - workaround for autosetting of
iconview cols no."""
#add 12 because of: http://bugzilla.gnome.org/show_bug.cgi?id=570152
col_num = 9 * window.get_size()[0] \
/ (10 * (self.iv_col_width + self.iconview.get_column_spacing() * 2))
self.iconview.set_columns(col_num)
def update_geometry(self, iter):
"""Recomputes the width and height of the rotated page and saves
the result in the ListStore"""
if not self.model.iter_is_valid(iter):
return
nfile, npage, rotation = self.model.get(iter, 2, 3, 6)
crop = self.model.get(iter, 7, 8, 9, 10)
page = self.pdfqueue[nfile-1].document.get_page(npage-1)
w0, h0 = page.get_size()
rotation = int(rotation) % 360
rotation = ((rotation + 45) / 90) * 90
if rotation == 90 or rotation == 270:
w1, h1 = h0, w0
else:
w1, h1 = w0, h0
self.model.set(iter, 11, w1, 12, h1)
def reset_iv_width(self, renderer=None):
"""Reconfigures the width of the iconview columns"""
if not self.model.get_iter_first(): #just checking if model is empty
return
max_w = 10 + int(max(row[4]*row[11]*(1.-row[7]-row[8]) \
for row in self.model))
if max_w != self.iv_col_width:
self.iv_col_width = max_w
self.celltxt.set_property('width', self.iv_col_width)
self.celltxt.set_property('wrap-width', self.iv_col_width)
self.iconview.set_item_width(self.iv_col_width + 12) #-1)
self.on_window_size_request(self.window, None)
def on_keypress_event(self, widget, event):
"""Keypress events in Main Window"""
#keyname = Gdk.keyval_name(event.keyval)
if event.keyval == 65535: # Delete keystroke
self.clear_selected()
def close_application(self, widget, event=None, data=None):
"""Termination"""
try :
if self.rendering_thread:
self.rendering_thread.quit = True
self.rendering_thread.join()
except :
pass # PdfShuffler may be already closed
if os.path.isdir(self.tmp_dir):
self.winux.remove_temp_dir(self.tmp_dir)
if Gtk.main_level():
Gtk.main_quit()
else:
sys.exit(0)
return False
def add_pdf_pages(self, filename,
firstpage=None, lastpage=None,
angle=0, crop=[0.,0.,0.,0.]):
"""Add pages of a pdf document to the model"""
res = False
# Check if the document has already been loaded
pdfdoc = None
for it_pdfdoc in self.pdfqueue:
if self.winux.check_same_file(filename, it_pdfdoc) == True :
pdfdoc = it_pdfdoc
break
if not pdfdoc:
pdfdoc = PDF_Doc(filename, self.nfile, self.tmp_dir)
self.import_directory = os.path.split(filename)[0]
self.export_directory = self.import_directory
if pdfdoc.nfile != 0 and pdfdoc != []:
self.nfile = pdfdoc.nfile
self.pdfqueue.append(pdfdoc)
else:
return res
n_start = 1
n_end = pdfdoc.npage
if firstpage:
n_start = min(n_end, max(1, firstpage))
if lastpage:
n_end = max(n_start, min(n_end, lastpage))
for npage in range(n_start, n_end + 1):
descriptor = ''.join([pdfdoc.shortname, '\n', _('page'), ' ', str(npage)])
page = pdfdoc.document.get_page(npage-1)
w, h = page.get_size()
iter = self.model.append((descriptor, # 0
None, # 1
pdfdoc.nfile, # 2
npage, # 3
self.zoom_scale, # 4
pdfdoc.filename, # 5
angle, # 6
crop[0],crop[1], # 7-8
crop[2],crop[3], # 9-10
w,h, # 11-12
2. )) # 13 FIXME
self.update_geometry(iter)
res = True
self.reset_iv_width()
GObject.idle_add(self.retitle)
if res:
GObject.idle_add(self.render)
return res
def choose_export_pdf_name(self, widget=None, only_selected=False):
"""Handles choosing a name for exporting """
chooser = Gtk.FileChooserDialog(title=_('Export ...'),
action=Gtk.FileChooserAction.SAVE,
buttons=(Gtk.STOCK_CANCEL,
Gtk.ResponseType.CANCEL,
Gtk.STOCK_SAVE,
Gtk.ResponseType.OK))
chooser.set_do_overwrite_confirmation(True)
chooser.set_current_folder(self.export_directory)
filter_pdf = Gtk.FileFilter()
filter_pdf.set_name(_('PDF files'))
filter_pdf.add_mime_type('application/pdf')
filter_pdf.add_pattern('*.pdf')
chooser.add_filter(filter_pdf)
filter_all = Gtk.FileFilter()
filter_all.set_name(_('All files'))
filter_all.add_pattern('*')
chooser.add_filter(filter_all)
while True:
response = chooser.run()
if response == Gtk.ResponseType.OK:
file_out = chooser.get_filename()
(path, shortname) = os.path.split(file_out)
(shortname, ext) = os.path.splitext(shortname)
if ext.lower() != '.pdf':
file_out = file_out + '.pdf'
try:
self.export_to_file(file_out, only_selected)
self.export_directory = path
self.set_unsaved(False)
except Exception as e:
chooser.destroy()
error_msg_dlg = Gtk.MessageDialog(None,
Gtk.DialogFlags.MODAL,
Gtk.MessageType.WARNING,
Gtk.ButtonsType.CLOSE,
str(e))
response = error_msg_dlg.run()
if response == Gtk.ResponseType.OK:
error_msg_dlg.destroy()
return
break
chooser.destroy()
def export_to_file(self, file_out, only_selected=False):
"""Export to file"""
selection = self.iconview.get_selected_items()
pdf_output = PdfFileWriter()
pdf_input = []
for pdfdoc in self.pdfqueue:
pdfdoc_inp = PdfFileReader(open(pdfdoc.copyname, 'rb'))
if pdfdoc_inp.getIsEncrypted():
try: # Workaround for lp:#355479
stat = pdfdoc_inp.decrypt('')
except:
stat = 0
if (stat!=1):
errmsg = _('File %s is encrypted.\n'
'Support for encrypted files has not been implemented yet.\n'
'File export failed.') % pdfdoc.filename
raise Exception(errmsg)
#FIXME
#else
# ask for password and decrypt file
pdf_input.append(pdfdoc_inp)
for row in self.model:
if only_selected and row.path not in selection:
continue
# add pages from input to output document
nfile = row[2]
npage = row[3]
if npage == -1 :
pdf_output.addBlankPage()
continue
current_page = copy(pdf_input[nfile-1].getPage(npage-1))
angle = row[6]
angle0 = current_page.get("/Rotate",0)
crop = [row[7],row[8],row[9],row[10]]
if angle != 0:
current_page.rotateClockwise(angle)
if crop != [0.,0.,0.,0.]:
rotate_times = (((angle + angle0) % 360 + 45) / 90) % 4
crop_init = crop
if rotate_times != 0:
perm = [0,2,1,3]
for it in range(rotate_times):
perm.append(perm.pop(0))
perm.insert(1,perm.pop(2))
crop = [crop_init[perm[side]] for side in range(4)]
#(x1, y1) = current_page.cropBox.lowerLeft
#(x2, y2) = current_page.cropBox.upperRight
(x1, y1) = [float(xy) for xy in current_page.mediaBox.lowerLeft]
(x2, y2) = [float(xy) for xy in current_page.mediaBox.upperRight]
x1_new = int(x1 + (x2-x1) * crop[0])
x2_new = int(x2 - (x2-x1) * crop[1])
y1_new = int(y1 + (y2-y1) * crop[3])
y2_new = int(y2 - (y2-y1) * crop[2])
#current_page.cropBox.lowerLeft = (x1_new, y1_new)
#current_page.cropBox.upperRight = (x2_new, y2_new)
current_page.mediaBox.lowerLeft = (x1_new, y1_new)
current_page.mediaBox.upperRight = (x2_new, y2_new)
pdf_output.addPage(current_page)
# finally, write "output" to document-output.pdf
pdf_output.write(open(file_out, 'wb'))
def on_action_add_doc_activate(self, widget, data=None):
"""Import doc"""
chooser = Gtk.FileChooserDialog(title=_('Import...'),
action=Gtk.FileChooserAction.OPEN,
buttons=(Gtk.STOCK_CANCEL,
Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN,
Gtk.ResponseType.OK))
if self.import_directory :
chooser.set_current_folder(self.import_directory)
chooser.set_select_multiple(True)
filter_all = Gtk.FileFilter()
filter_all.set_name(_('All files'))
filter_all.add_pattern('*')
chooser.add_filter(filter_all)
filter_pdf = Gtk.FileFilter()
filter_pdf.set_name(_('PDF files'))
filter_pdf.add_mime_type('application/pdf')
filter_pdf.add_pattern('*.pdf')
chooser.add_filter(filter_pdf)
chooser.set_filter(filter_pdf)
response = chooser.run()
if response == Gtk.ResponseType.OK:
for filename in chooser.get_filenames():
#filename = str(filename,"utf-8") Unsupported in python 3 # convert utf-8 to unicode for internal use
if os.path.isfile(filename):
# FIXME
## f = Gio.File(filename) # ££ python 3
## f_info = f.query_info('standard::content-type')
## mime_type = f_info.get_content_type()
## expected_mime_type = pdf_mime_type
##
## if mime_type == expected_mime_type :
self.add_pdf_pages(filename)
## elif mime_type[:34] == 'application/vnd.oasis.opendocument':
## print((_('OpenDocument not supported yet!')))
## elif mime_type[:5] == 'image':
## print((_('Image file not supported yet!')))
## else:
## print((_('File type not supported!')))
else:
print((_('File %s does not exist') % filename))
elif response == Gtk.RESPONSE_CANCEL:
print((_('Closed, no files selected')))
chooser.destroy()
GObject.idle_add(self.retitle)
def clear_selected(self, button=None):
"""Removes the selected elements in the IconView"""
model = self.iconview.get_model()
selection = self.iconview.get_selected_items()
if selection:
selection.sort(reverse=True)
self.set_unsaved(True)
for path in selection:
iter = model.get_iter(path)
model.remove(iter)
path = selection[-1]
self.iconview.select_path(path)
if not self.iconview.path_is_selected(path):
if len(model) > 0: # select the last row
row = model[-1]
path = row.path
self.iconview.select_path(path)
self.iconview.grab_focus()
def add_blank_page(self, menu=None, num_blank_pages=1):
action = ""
if menu != None :
name = Gtk.Buildable.get_name(menu)
num_blank_pages = int(name[-1])
action = name[0:5]
model = self.iconview.get_model()
selection = self.iconview.get_selected_items()
if selection:
selection.sort(reverse=True)
self.set_unsaved(True)
path = selection[0]
iter = model.get_iter(path)
descriptor = 'Blank'
w, h = self.model.get(iter, 11, 12)
row =(descriptor, # 0
None, # 1
1, # 2
-1, # 3
self.zoom_scale, # 4
"", # 5
0, # 6
0,0, # 7-8
0,0, # 9-10
w,h, # 11-12
2. ) # 13 FIXME
for i in range(num_blank_pages) :
if action == "befor" :
self.model.insert_before(iter, row)
else :
self.model.insert_after(iter, row)
def iv_drag_begin(self, iconview, context):
"""Sets custom icon on drag begin for multiple items selected"""
global shuffler_selection_a
#if len(iconview.get_selected_items()) > 1:
if len(shuffler_selection_a) > 0 :
iconview.stop_emission('drag_begin')
## context.set_icon_stock(Gtk.STOCK_DND_MULTIPLE, 0, 0)
for a in shuffler_selection_a :
iconview.select_path(a)
def iv_dnd_get_data(self, iconview, context,
selection_data, target_id, etime):
"""Handles requests for data by drag and drop in iconview"""
global shuffler_selection_a
model = iconview.get_model()
if len(shuffler_selection_a) > 1 :
selection = shuffler_selection_a
else :
selection = self.iconview.get_selected_items()
selection.sort(key=lambda x: x[0])
data = []
target_s = str(selection_data.get_target())
for path in selection:
if target_s == 'MODEL_ROW_INTERN':
data.append(str(path[0]))
elif target_s == 'MODEL_ROW_EXTERN':
iter = model.get_iter(path)
nfile, npage, angle = model.get(iter, 2, 3, 6)
crop = model.get(iter, 7, 8, 9, 10)
pdfdoc = self.pdfqueue[nfile - 1]
data.append('\n'.join([pdfdoc.filename,
str(npage),
str(angle)] +
[str(side) for side in crop]))
if data:
data = bytes('\n;\n'.join(data), "utf-8")
selection_data.set(selection_data.get_target(), 8, data)
def iv_dnd_received_data(self, iconview, context, x, y,
selection_data, target_id, etime):
"""Handles received data by drag and drop in iconview"""
model = iconview.get_model()
data = selection_data.get_data().decode("utf-8")
if data:
data = data.split('\n;\n')
drop_info = iconview.get_dest_item_at_pos(x, y)
iter_to = None
if drop_info:
path, position = drop_info
ref_to = Gtk.TreeRowReference.new(model,path)
else:
position = Gtk.IconViewDropPosition.DROP_RIGHT
if len(model) > 0: #find the iterator of the last row
row = model[-1]
path = row.path
ref_to = Gtk.TreeRowReference(model,path)
if ref_to:
before = (position == Gtk.IconViewDropPosition.DROP_LEFT
or position == Gtk.IconViewDropPosition.DROP_ABOVE)
#if target_id == self.MODEL_ROW_INTERN:
if str(selection_data.get_target()) == 'MODEL_ROW_INTERN':
if before:
data.sort(key=int)
else:
data.sort(key=int,reverse=True)
data2 = []
for data1 in data :
data2.append(Gtk.TreePath.new_from_string(data1))
ref_from_list = [Gtk.TreeRowReference.new(model,path)
for path in data2]
for ref_from in ref_from_list:
path = ref_to.get_path()
iter_to = model.get_iter(path)
path = ref_from.get_path()
iter_from = model.get_iter(path)
row = model[iter_from]
row_data = []
for a in row :
row_data.append(a)
if before:
iter_new = model.insert_before(iter_to, row_data)
else:
iter_new = model.insert_after(iter_to, row_data)
if context.get_selected_action() == Gdk.DragAction.MOVE:
for ref_from in ref_from_list:
path = ref_from.get_path()
iter_from = model.get_iter(path)
model.remove(iter_from)
#elif target_id == self.MODEL_ROW_EXTERN:
elif selection_data.target == 'MODEL_ROW_EXTERN':
if not before:
data.reverse()
while data:
tmp = data.pop(0).split('\n')
filename = tmp[0]
npage, angle = [int(k) for k in tmp[1:3]]
crop = [float(side) for side in tmp[3:7]]
if self.add_pdf_pages(filename, npage, npage,
angle, crop):
if len(model) > 0:
path = ref_to.get_path()
iter_to = model.get_iter(path)
row = model[-1] #the last row
path = row.path
iter_from = model.get_iter(path)
if before:
model.move_before(iter_from, iter_to)
else:
model.move_after(iter_from, iter_to)
if context.action == Gdk.ACTION_MOVE:
context.finish(True, True, etime)
def iv_dnd_data_delete(self, widget, context):
"""Deletes dnd items after a successful move operation"""
model = self.iconview.get_model()
selection = self.iconview.get_selected_items()
ref_del_list = [Gtk.TreeRowReference(model,path) for path in selection]
for ref_del in ref_del_list:
path = ref_del.get_path()
iter = model.get_iter(path)
model.remove(iter)
def iv_dnd_motion(self, iconview, context, x, y, etime):
"""Handles the drag-motion signal in order to auto-scroll the view"""
autoscroll_area = 40
sw_vadj = self.sw.get_vadjustment()
sw_height = self.sw.get_allocation().height
if y -sw_vadj.get_value() < autoscroll_area:
if not self.iv_auto_scroll_timer:
self.iv_auto_scroll_direction = Gtk.DIR_UP
self.iv_auto_scroll_timer = GObject.timeout_add(150,
self.iv_auto_scroll)
elif y -sw_vadj.get_value() > sw_height - autoscroll_area:
if not self.iv_auto_scroll_timer:
self.iv_auto_scroll_direction = Gtk.DIR_DOWN
self.iv_auto_scroll_timer = GObject.timeout_add(150,
self.iv_auto_scroll)
elif self.iv_auto_scroll_timer:
GObject.source_remove(self.iv_auto_scroll_timer)
self.iv_auto_scroll_timer = None
def iv_dnd_leave_end(self, widget, context, ignored=None):
"""Ends the auto-scroll during DND"""
if self.iv_auto_scroll_timer:
GObject.source_remove(self.iv_auto_scroll_timer)
self.iv_auto_scroll_timer = None
def iv_auto_scroll(self):
"""Timeout routine for auto-scroll"""
sw_vadj = self.sw.get_vadjustment()
sw_vpos = sw_vadj.get_value()
if self.iv_auto_scroll_direction == Gtk.DIR_UP:
sw_vpos -= sw_vadj.step_increment
sw_vadj.set_value(max(sw_vpos, sw_vadj.lower))
elif self.iv_auto_scroll_direction == Gtk.DIR_DOWN:
sw_vpos += sw_vadj.step_increment
sw_vadj.set_value(min(sw_vpos, sw_vadj.upper - sw_vadj.page_size))
return True #call me again
def iv_button_press_event(self, iconview, event):
"""Manages mouse clicks on the iconview"""
x = int(event.x)
y = int(event.y)
path = iconview.get_path_at_pos(x, y)
if path == None :
return
#print event.button
if event.button == 1: # Left button
global shuffler_selection_a
shuffler_selection_a = []
time = event.time
selection = iconview.get_selected_items()
if path:
if path in selection:
# Record the selection
shuffler_selection_a = selection
if event.button == 3:
time = event.time
selection = iconview.get_selected_items()
if path:
if path not in selection:
iconview.unselect_all()
iconview.select_path(path)
iconview.grab_focus()
self.popup.popup(None, None, None, None, event.button, time)
return 1
elif event.state & Gdk.ModifierType.SHIFT_MASK :
first_selection = self.selection_start
last_selection = path[0]
if last_selection > first_selection :
step = 1
last_selection += 1
else :
step = -1
last_selection -= 1
for a in range(first_selection,last_selection, step) :
iconview.select_path(Gtk.TreePath(a))
return True
else :
self.selection_start = path[0]
def sw_dnd_received_data(self, scrolledwindow, context, x, y,
selection_data, target_id, etime):
"""Handles received data by drag and drop in scrolledwindow"""
data = selection_data.data
if target_id == self.MODEL_ROW_EXTERN:
self.model
if data:
data = data.split('\n;\n')
while data:
tmp = data.pop(0).split('\n')
filename = tmp[0]
npage, angle = [int(k) for k in tmp[1:3]]
crop = [float(side) for side in tmp[3:7]]
if self.add_pdf_pages(filename, npage, npage, angle, crop):
if context.action == Gdk.ACTION_MOVE:
context.finish(True, True, etime)
elif target_id == self.TEXT_URI_LIST:
uri = data.strip()
uri_splitted = uri.split() # we may have more than one file dropped
for uri in uri_splitted:
filename = self.get_file_path_from_dnd_dropped_uri(uri)
if os.path.isfile(filename): # is it file?
self.add_pdf_pages(filename)
def sw_button_press_event(self, scrolledwindow, event):
"""Unselects all items in iconview on mouse click in scrolledwindow"""
if event.button == 1:
self.iconview.unselect_all()
def sw_scroll_event(self, scrolledwindow, event):
"""Manages mouse scroll events in scrolledwindow"""
if event.state & Gdk.CONTROL_MASK:
if event.direction == Gdk.SCROLL_UP:
self.zoom_change(1)
return 1
elif event.direction == Gdk.SCROLL_DOWN:
self.zoom_change(-1)
return 1
def zoom_set(self, level):
"""Sets the zoom level"""
self.zoom_level = max(min(level, 5), -24)
self.zoom_scale = 1.1 ** self.zoom_level
for row in self.model:
row[4] = self.zoom_scale
self.reset_iv_width()
def zoom_change(self, step=5):
"""Modifies the zoom level"""
bar = self.uiXML.get_object('hscale1')
bar.set_value(self.zoom_level + step)
self.zoom_set(self.zoom_level + step)
def zoom_in(self, widget=None):
"""Increases the zoom level by 5 steps"""
self.zoom_change(5)
def zoom_out(self, widget=None, step=5):
"""Reduces the zoom level by 5 steps"""
self.zoom_change(-5)
def zoom_bar(self,widget,a=None, b=None):
"""Modifies the zoom level with the slider"""
zoom_scale = widget.get_value()
self.zoom_set(zoom_scale)
def get_file_path_from_dnd_dropped_uri(self, uri):
"""Extracts the path from an uri"""
path = urllib.request.url2pathname(uri) # escape special chars
path = path.strip('\r\n\x00') # remove \r\n and NULL
# get the path to file
if path.startswith('file:\\\\\\'): # windows
path = path[8:] # 8 is len('file:///')
elif path.startswith('file://'): # nautilus, rox
path = path[7:] # 7 is len('file://')
elif path.startswith('file:'): # xffm
path = path[5:] # 5 is len('file:')
return path
def rotate_page_right(self, widget, data=None):
self.rotate_page(90)
def rotate_page_left(self, widget, data=None):
self.rotate_page(-90)
def rotate_page(self, angle):
"""Rotates the selected page in the IconView"""
model = self.iconview.get_model()
selection = self.iconview.get_selected_items()
if len(selection) > 0:
self.set_unsaved(True)
rotate_times = (((-angle) % 360 + 45) / 90) % 4
if rotate_times is not 0:
for path in selection:
iter = model.get_iter(path)
nfile = model.get_value(iter, 2)
npage = model.get_value(iter, 3)
crop = [0.,0.,0.,0.]
perm = [0,2,1,3]
for it in range(int(rotate_times)):
perm.append(perm.pop(0))
perm.insert(1,perm.pop(2))
crop = [model.get_value(iter, 7 + perm[side]) for side in range(4)]
for side in range(4):
model.set_value(iter, 7 + side, crop[side])
new_angle = model.get_value(iter, 6) + int(angle)
new_angle = new_angle % 360
model.set_value(iter, 6, new_angle)
self.update_geometry(iter)
self.reset_iv_width()
def crop_page_dialog(self, widget):
"""Opens a dialog box to define margins for page cropping"""
sides = ('L', 'R', 'T', 'B')
side_names = {'L':_('Left'), 'R':_('Right'),
'T':_('Top'), 'B':_('Bottom') }
opposite_sides = {'L':'R', 'R':'L', 'T':'B', 'B':'T' }
def set_crop_value(spinbutton, side):
opp_side = opposite_sides[side]
pos = sides.index(opp_side)
adj = spin_list[pos].get_adjustment()
adj.set_upper(99.0 - spinbutton.get_value())
model = self.iconview.get_model()
selection = self.iconview.get_selected_items()
crop = [0.,0.,0.,0.]
if selection:
path = selection[0]
pos = model.get_iter(path)
crop = [model.get_value(pos, 7 + side) for side in range(4)]
dialog = Gtk.Dialog(title=(_('Crop Selected Pages')),
parent=self.window,
flags=Gtk.DialogFlags.MODAL,
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OK, Gtk.ResponseType.OK))
dialog.set_size_request(340, 250)
dialog.set_default_response(Gtk.ResponseType.OK)
frame = Gtk.Frame()
frame.set_label(_('Crop Margins'))
dialog.vbox.pack_start(frame, False, False, 20)
vbox = Gtk.VBox(False, 0)
frame.add(vbox)
spin_list = []
units = 2 * [_('% of width')] + 2 * [_('% of height')]
for side in sides:
hbox = Gtk.HBox(True, 0)
vbox.pack_start(hbox, False, False, 5)
label = Gtk.Label(side_names[side])
label.set_alignment(0, 0.0)
hbox.pack_start(label, True, True, 20)
adj = Gtk.Adjustment(100.*crop.pop(0), 0.0, 99.0, 1.0, 5.0, 0.0)
## spin = Gtk.SpinButton(adj, 0, 1)
spin = Gtk.SpinButton()
spin.set_adjustment(adj)
spin.set_activates_default(True)
spin.connect('value-changed', set_crop_value, side)
spin_list.append(spin)
hbox.pack_start(spin, False, False, 30)
label = Gtk.Label(units.pop(0))
label.set_alignment(0, 0.0)
hbox.pack_start(label, True, True, 0)
dialog.show_all()
result = dialog.run()
if result == Gtk.ResponseType.OK:
modified = False
crop = [spin.get_value()/100. for spin in spin_list]
for path in selection:
pos = model.get_iter(path)
for it in range(4):
old_val = model.get_value(pos, 7 + it)
model.set_value(pos, 7 + it, crop[it])
if crop[it] != old_val:
modified = True
self.update_geometry(pos)
if modified:
self.set_unsaved(True)
self.reset_iv_width()
elif result == Gtk.ResponseType.CANCEL:
print((_('Dialog closed')))
dialog.destroy()
def about_dialog(self, widget, data=None):
about_dialog = Gtk.AboutDialog()
try:
about_dialog.set_transient_for(self.window)
about_dialog.set_modal(True)
except:
pass
# FIXME
about_dialog.set_name(APPNAME)
about_dialog.set_version(VERSION)
about_dialog.set_comments(_(
'%s is a tool for rearranging and modifying PDF files. ' \
'Developed using GTK+ and Python') % APPNAME)
about_dialog.set_authors(['Konstantinos Poulios',])
about_dialog.set_website_label(WEBSITE)
about_dialog.set_logo_icon_name('pdfshuffler')
about_dialog.set_license(LICENSE)
about_dialog.connect('response', lambda w, *args: w.destroy())
about_dialog.connect('delete_event', lambda w, *args: w.destroy())
about_dialog.show_all()
class PDF_Doc:
"""Class handling PDF documents"""
def __init__(self, filename, nfile, tmp_dir):
self.filename = os.path.abspath(filename)
(self.path, self.shortname) = os.path.split(self.filename)
(self.shortname, self.ext) = os.path.splitext(self.shortname)
## f = Gio.File
## mime_type = f.query_info('standard::content-type').get_content_type()
## expected_mime_type = pdf_mime_type
##
## if mime_type == expected_mime_type:
if 1 == 1 :
self.nfile = nfile + 1
self.mtime = os.path.getmtime(filename)
self.copyname = os.path.join(tmp_dir, '%02d_' % self.nfile +
self.shortname + '.pdf')
shutil.copy(self.filename, self.copyname)
self.document = Poppler.Document.new_from_file (file_prefix + self.copyname, None)
self.npage = self.document.get_n_pages()
else:
self.nfile = 0
self.npage = 0
class PDF_Renderer(threading.Thread, GObject.GObject):
def __init__(self, model, pdfqueue, resample=1.):
threading.Thread.__init__(self)
GObject.GObject.__init__(self)
self.model = model
self.pdfqueue = pdfqueue
self.resample = resample
self.quit = False
def run(self):
for idx, row in enumerate(self.model):
if self.quit:
return
if not row[1]:
try:
nfile = row[2]
npage = row[3]
pdfdoc = self.pdfqueue[nfile - 1]
page = pdfdoc.document.get_page(npage-1)
w, h = page.get_size()
thumbnail = cairo.ImageSurface(cairo.FORMAT_ARGB32,
int(w/self.resample),
int(h/self.resample))
## thumbnail1 = cairo.image_surface_create()
## thumbnail = thumbnail1(1,
## int(w/self.resample),
## int(h/self.resample))
cr = cairo.Context(thumbnail)
if self.resample != 1.:
cr.scale(1./self.resample, 1./self.resample)
page.render(cr)
time.sleep(0.003)
GObject.idle_add(self.emit,'update_thumbnail',
idx, thumbnail, self.resample,
priority=GObject.PRIORITY_LOW)
except Exception as e:
print(e)
class PdfShuffler_Linux_code :
def __init__(self):
global pdf_mime_type, file_prefix
pdf_mime_type = "application/pdf"
file_prefix = 'file://'
def home_dir(self):
return os.getenv('HOME')
def remove_temp_dir(self, tmp_dir):
shutil.rmtree(tmp_dir)
def check_same_file(self, filename, it_pdfdoc):
if os.path.isfile(it_pdfdoc.filename) and \
os.path.samefile(filename, it_pdfdoc.filename) and \
os.path.getmtime(filename) is it_pdfdoc.mtime:
return True
else :
return False
class PdfShuffler_Windows_code :
def __init__(self):
global _winreg, pdf_mime_type, file_prefix
## import winreg python 27
pdf_mime_type = ".pdf"
file_prefix = 'file:///'
def home_dir(self) :
return "" # python 3
global _winreg
regKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
'Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders')
myDocuments = _winreg.QueryValueEx(regKey, 'Personal')[0]
return myDocuments
def remove_temp_dir(self, tmp_dir):
# ============= Python-poppler for Windows bug workaround ============
# python-poppler does not "release" the file and only the files of previous sessions can be deleted
# Get the list of all pdf-shuffler temporary dirs
temp_dir_root = os.path.split(tmp_dir)[0]
shuffler_dirs = glob.glob(temp_dir_root + "/tmp??????pdfshuffler")
# delete if possible
for directory in shuffler_dirs :
try :
shutil.rmtree(directory)
except :
pass
def check_same_file(self, filename, it_pdfdoc) :
# The samefile method does not exist in Windows versions of Python
if os.path.isfile(it_pdfdoc.filename) and \
filename == it_pdfdoc.filename and \
os.path.getmtime(filename) is it_pdfdoc.mtime:
return True
else :
return False
def main():
"""This function starts PdfShuffler"""
#Gdk.threads_init() # This line hangs the program when the user tries to move the window
GObject.threads_init()
#Gdk.threads_enter() # This line was is necessary in Windows. Does not seem to be now
PdfShuffler()
Gtk.init()
Gtk.main()
if __name__ == '__main__':
main()