Welcome, guest | Sign In | My Account | Store | Cart
#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
Created on Sun Jun 07 06:57:08 2015

@author: Jorj McKie
Copyright (c) 2015 Jorj X. McKie

The license of this program is governed by the GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007. See the "
COPYING" file of this repository.

This is an example for using the Python binding PyMuPDF for MuPDF.

This program joins PDF files into one output file. Its features include:
* Selection of page ranges
* Optional rotation in steps of 90 degrees
* Copy any table of contents to the output (default - can be switched off)
* Editable PDF metadata

Dependencies:
wxPython 3.x, PyPDF2, PyMuPDF
"""


import os, sys
import wx
import wx.grid as gridlib
import wx.lib.gridmovers as gridmovers
import PyPDF2                          # only used for output (make_pdf)
import fitz

# some abbreviations
DefPos  = wx.DefaultPosition
DefSize = wx.DefaultSize

class PDFTable(gridlib.PyGridTableBase):
   
def __init__(self):
        gridlib
.PyGridTableBase.__init__(self)

       
self.colLabels = ['File','Pages','from','to','rotate']
       
self.dataTypes = [gridlib.GRID_VALUE_STRING,
                          gridlib
.GRID_VALUE_NUMBER,
                          gridlib
.GRID_VALUE_NUMBER,
                          gridlib
.GRID_VALUE_NUMBER,
                          gridlib
.GRID_VALUE_CHOICE + ':0, 90, 180, 270',
                         
]

       
self.data = []

#==============================================================================
# Methods for the wxPyGridTableBase interface (mostly mandatory)
#==============================================================================

   
def GetNumberRows(self):
       
return len(self.data)

   
def GetNumberCols(self):
       
return len(self.colLabels)

   
def IsEmptyCell(self, row, col):
       
try:
           
return not self.data[row][col]
       
except IndexError:
           
return True

   
def GetValue(self, row, col):
       
return self.data[row][col]

   
def SetValue(self, row, col, value):
       
self.data[row][col] = value

#==============================================================================
# Provide column header names
#==============================================================================
   
def GetColLabelValue(self, col):
       
return self.colLabels[col]

#==============================================================================
# Provide row header names (just the line numbers in our case)
#==============================================================================
   
def GetRowLabelValue(self,row):
       
return str(row +1)

#==============================================================================
# Provide type of a cell value
#==============================================================================
   
def GetTypeName(self, row, col):
       
return self.dataTypes[col]

#==============================================================================
# Move a row
#==============================================================================
   
def MoveRow(self,frm,to):
        grid
= self.GetView()

       
if grid:
           
# Move the rowLabels and data rows
            oldData
= self.data[frm]
           
del self.data[frm]

           
if to > frm:
               
self.data.insert(to-1,oldData)
           
else:
               
self.data.insert(to,oldData)
#==============================================================================
#           inform the grid about our doing
#==============================================================================
            grid
.BeginBatch()
            msg
= gridlib.GridTableMessage(
                   
self, gridlib.GRIDTABLE_NOTIFY_ROWS_DELETED, frm, 1)
            grid
.ProcessTableMessage(msg)
            msg
= gridlib.GridTableMessage(
                   
self, gridlib.GRIDTABLE_NOTIFY_ROWS_INSERTED, to, 1)
            grid
.ProcessTableMessage(msg)
            grid
.EndBatch()

#==============================================================================
# Insert a row
#==============================================================================
   
def NewRow(self, zeile):
        grid
= self.GetView()
       
if grid:
           
self.data.append(zeile)
            grid
.BeginBatch()
            msg
= gridlib.GridTableMessage(
                   
self, gridlib.GRIDTABLE_NOTIFY_ROWS_APPENDED, 1)
            grid
.ProcessTableMessage(msg)
            grid
.EndBatch()

#==============================================================================
# Duplicate a row
#==============================================================================
   
def DuplicateRow(self, row):
        grid
= self.GetView()
       
if grid:
            zeile
= [self.data[row][0], self.data[row][1],
                     
self.data[row][2], self.data[row][3],
                     
self.data[row][4]]
           
self.data.insert(row, zeile)
            grid
.BeginBatch()
            msg
= gridlib.GridTableMessage(
                   
self, gridlib.GRIDTABLE_NOTIFY_ROWS_INSERTED, row, 1)
            grid
.ProcessTableMessage(msg)
            grid
.EndBatch()

#==============================================================================
# Remove a row
#==============================================================================
   
def DeleteRow(self, row):
        grid
= self.GetView()
       
if grid:
           
del self.data[row]
            grid
.BeginBatch()
            msg
= gridlib.GridTableMessage(self,
                    gridlib
.GRIDTABLE_NOTIFY_ROWS_DELETED, row, 1)
            grid
.ProcessTableMessage(msg)
            grid
.EndBatch()

#==============================================================================
# Define the grid
#==============================================================================
class MyGrid(gridlib.Grid):
   
def __init__(self, parent):
        gridlib
.Grid.__init__(self, parent, -1)

        table
= PDFTable()      # create PDFTable object

#==============================================================================
# Announce our table to the grid and let it manage it ('True')
#==============================================================================
       
self.SetTable(table, True)

#==============================================================================
# do some cell attribute setting
#==============================================================================
        align1
= gridlib.GridCellAttr()
        align1
.SetAlignment(wx.ALIGN_RIGHT, wx.ALIGN_CENTER)
       
self.SetColAttr(2, align1)
       
self.SetColAttr(3, align1)
       
self.SetColAttr(4, align1)
        align2
= gridlib.GridCellAttr()
        align2
.SetAlignment(wx.ALIGN_CENTER, wx.ALIGN_CENTER)
       
self.SetColAttr(5, align2)

#==============================================================================
# Enable Row moving
#==============================================================================
        gridmovers
.GridRowMover(self)

#==============================================================================
# Bind: move a row
#==============================================================================
       
self.Bind(gridmovers.EVT_GRID_ROW_MOVE, self.OnRowMove, self)
#==============================================================================
# Bind: delete a row
#==============================================================================
       
self.Bind(gridlib.EVT_GRID_LABEL_RIGHT_DCLICK, self.OnRowDel, self)
#==============================================================================
# Bind: duplicate a row
#==============================================================================
       
self.Bind(gridlib.EVT_GRID_LABEL_LEFT_DCLICK, self.OnRowDup, self)

#==============================================================================
# Event Method: move a row
#==============================================================================
   
def OnRowMove(self,evt):
        frm
= evt.GetMoveRow()          # Row being moved
        to
= evt.GetBeforeRow()         # Before which row to insert
       
self.GetTable().MoveRow(frm,to)

#==============================================================================
# Event Method: delete a row
#==============================================================================
   
def OnRowDel(self, evt):
        row
= evt.GetRow()
       
self.GetTable().DeleteRow(row)

#==============================================================================
# Event Method: duplicate a row
#==============================================================================
   
def OnRowDup(self, evt):
        row
= evt.GetRow()
        col
= evt.GetCol()
       
if col < 0 and row >= 0:       # else it is not a row duplication!
           
self.GetTable().DuplicateRow(row)
        evt
.Skip()

#==============================================================================
#
# Define the dialog
#
#==============================================================================
class PDFDialog (wx.Dialog):
   
def __init__(self, parent):
        wx
.Dialog.__init__ (self, parent, id = wx.ID_ANY,
                             title
= u"Join PDF files",
                             pos
= DefPos,
                             size
= wx.Size(900,710),
                             style
= wx.CAPTION|
                                     wx
.CLOSE_BOX|
                                     wx
.DEFAULT_DIALOG_STYLE|
                                     wx
.MAXIMIZE_BOX|
                                     wx
.MINIMIZE_BOX|
                                     wx
.RESIZE_BORDER)

       
self.SetSizeHintsSz(DefSize, DefSize)
       
self.FileList = {}

#==============================================================================
# Create Sizer 01 (browse button and explaining text)
#==============================================================================
        szr01
= wx.BoxSizer(wx.HORIZONTAL)

       
self.btn_neu = wx.FilePickerCtrl(self, wx.ID_ANY,
                        wx
.EmptyString,
                        u
"Select a PDF file",
                        u
"*.pdf",
                       
DefPos, DefSize,
                        wx
.FLP_CHANGE_DIR|wx.FLP_FILE_MUST_EXIST|wx.FLP_SMALL,
                       
)
        szr01
.Add(self.btn_neu, 0, wx.ALIGN_TOP|wx.ALL, 5)

        msg_txt
="""ADD files with this button. Path and total page number will be appended to the table below.\nDUPLICATE row: double-click its number. MOVE row: drag its number with the mouse. DELETE row: right-double-click its number."""
        msg
= wx.StaticText(self, wx.ID_ANY, msg_txt,
                   
DefPos, wx.Size(-1, 50), wx.ALIGN_LEFT)
        msg
.Wrap(-1)
        msg
.SetFont(wx.Font(10, 74, 90, 90, False, "Arial"))

        szr01
.Add(msg, 0, wx.ALIGN_TOP|wx.ALL, 5)

#==============================================================================
# Create Sizer 02 (contains the grid)
#==============================================================================
       
self.szr02 = MyGrid(self)
       
self.szr02.AutoSizeColumn(0)
       
self.szr02.AutoSizeColumn(1)
       
self.szr02.SetColSize(2, 45)
       
self.szr02.SetColSize(3, 45)
       
self.szr02.SetColSize(4, 45)
       
self.szr02.SetRowLabelSize(30)
       
# Columns 1 and 2 are read only
        attr_ro
= gridlib.GridCellAttr()
        attr_ro
.SetReadOnly(True)
       
self.szr02.SetColAttr(0, attr_ro)
       
self.szr02.SetColAttr(1, attr_ro)

#==============================================================================
# Create Sizer 03 (output parameters)
#==============================================================================
        szr03
= wx.FlexGridSizer( 5, 2, 0, 0 )   # 4 rows, 2 cols, gap sizes 0
        szr03
.SetFlexibleDirection( wx.BOTH )
        szr03
.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_SPECIFIED )

        tx_ausdat
= wx.StaticText(self, wx.ID_ANY, u"Output:",
                           
DefPos, DefSize, 0)
        tx_ausdat
.Wrap(-1)
        szr03
.Add(tx_ausdat, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)

       
self.btn_aus = wx.FilePickerCtrl(self, wx.ID_ANY,
                        os
.path.join(os.path.expanduser('~'), "joined.pdf"),
                        u
"Specify output file",
                        u
"*.pdf",
                       
DefPos, wx.Size(480,-1),
                        wx
.FLP_OVERWRITE_PROMPT|
                        wx
.FLP_SAVE|wx.FLP_SMALL|
                        wx
.FLP_USE_TEXTCTRL)
        szr03
.Add(self.btn_aus, 0, wx.ALL, 5)
        tx_autor
= wx.StaticText( self, wx.ID_ANY, u"Author:",
                         
DefPos, DefSize, 0 )
        tx_autor
.Wrap( -1 )
        szr03
.Add( tx_autor, 0, wx.ALL, 5 )

       
self.ausaut = wx.TextCtrl( self, wx.ID_ANY,
                       os
.path.basename(os.path.expanduser('~')),
                       
DefPos, wx.Size(480, -1), 0)
        szr03
.Add( self.ausaut, 0, wx.ALL, 5 )

        pdf_titel
= wx.StaticText( self, wx.ID_ANY, u"Title:",
                         
DefPos, DefSize, 0 )
        pdf_titel
.Wrap( -1 )
        szr03
.Add( pdf_titel, 0, wx.ALL, 5 )

       
self.austit = wx.TextCtrl( self, wx.ID_ANY,
                       u
"Joined PDF files",
                       
DefPos, wx.Size(480, -1), 0 )
        szr03
.Add( self.austit, 0, wx.ALL, 5 )

        tx_subject
= wx.StaticText( self, wx.ID_ANY, u"Subject:",
                           
DefPos, DefSize, wx.ALIGN_RIGHT)
        tx_subject
.Wrap( -1 )
        szr03
.Add( tx_subject, 0, wx.ALL, 5 )

       
self.aussub = wx.TextCtrl( self, wx.ID_ANY,
                       u
"Joined PDF files",
                       
DefPos, wx.Size(480, -1), 0 )
        szr03
.Add( self.aussub, 0, wx.ALL, 5 )
        tx_blank
= wx.StaticText( self, wx.ID_ANY, u" ",
                           
DefPos, DefSize, wx.ALIGN_RIGHT)
        tx_blank
.Wrap( -1 )
        szr03
.Add( tx_blank, 0, wx.ALL, 5 )
       
self.noToC = wx.CheckBox( self, wx.ID_ANY,
                           u
"check if no table of contents wanted",
                           
DefPos, DefSize, wx.ALIGN_LEFT)
        szr03
.Add( self.noToC, 0, wx.ALL, 5 )


#==============================================================================
# Create Sizer 04 (OK / Cancel buttons)
#==============================================================================
        szr04
= wx.StdDialogButtonSizer()
        szr04OK
= wx.Button(self, wx.ID_OK)
        szr04
.AddButton(szr04OK)
        szr04Cancel
= wx.Button(self, wx.ID_CANCEL)
        szr04
.AddButton(szr04Cancel)
        szr04
.Realize();

#==============================================================================
# 3 horizontal lines (decoration only)
#==============================================================================
        linie1
= wx.StaticLine(self, wx.ID_ANY,
                       
DefPos, DefSize, wx.LI_HORIZONTAL)
        linie2
= wx.StaticLine(self, wx.ID_ANY,
                       
DefPos, DefSize, wx.LI_HORIZONTAL)
        linie3
= wx.StaticLine(self, wx.ID_ANY,
                       
DefPos, DefSize, wx.LI_HORIZONTAL)

        mainszr
= wx.BoxSizer(wx.VERTICAL)
        mainszr
.Add(szr01, 0, wx.EXPAND, 5)
        mainszr
.Add(linie1, 0, wx.EXPAND |wx.ALL, 5)
        mainszr
.Add(self.szr02, 1, wx.EXPAND, 5)
        mainszr
.Add(linie2, 0, wx.EXPAND|wx.ALL, 5)
        mainszr
.Add(szr03, 0, wx.EXPAND, 5)
        mainszr
.Add(linie3, 0, wx.EXPAND |wx.ALL, 5)
        mainszr
.Add(szr04, 0, wx.ALIGN_TOP|wx.ALIGN_CENTER_HORIZONTAL, 5)

       
self.SetSizer(mainszr)
       
self.Layout()

       
self.Centre(wx.BOTH)

#==============================================================================
# Define event handlers for the buttons
#==============================================================================
       
self.btn_neu.Bind(wx.EVT_FILEPICKER_CHANGED, self.NewFile)
       
self.btn_aus.Bind(wx.EVT_FILEPICKER_CHANGED, self.AusgabeDatei)

   
def __del__(self):
       
pass

#==============================================================================
# "NewFile" - Event Handler for including new files
#==============================================================================
   
def NewFile(self, event):
        dat
= event.GetPath()
       
if dat not in self.FileList:
            doc
= fitz.Document(dat)
           
if doc.needsPass:
                wx
.MessageBox("Cannot read encrypted file\n" + dat,
                     
"Encrypted File Error")
               
event.Skip()
               
return
           
self.FileList[dat] = doc
       
else:
            doc
= self.FileList[dat]
        seiten
= doc.pageCount
        zeile
= [dat, str(seiten), 1, str(seiten), 0]
       
self.szr02.Table.NewRow(zeile)
       
self.szr02.AutoSizeColumn(0)
       
self.Layout()
       
event.Skip()

#==============================================================================
# "AusgabeDatei" - Event Handler for out file
#==============================================================================
   
def AusgabeDatei(self, event):
       
event.Skip()

#==============================================================================
# Create the joined PDF
#==============================================================================
def make_pdf(dlg):
   
# no file selected: treat like "Cancel"
   
if not len(dlg.szr02.Table.data):       # no files there
       
return None

    cdate
= wx.DateTime.Now().Format("D:%Y%m%d%H%M%S-04'30'")
    ausgabe
= dlg.btn_aus.GetPath()
    pdf_fle_out
= open(ausgabe,"wb")
    pdf_out
= PyPDF2.PdfFileWriter()
    aus_nr
= 0                              # current page number in output
    pdf_dict
= {"/Creator":"PDF-Joiner",
               
"/Producer":"PyMuPDF, PyPDF2",
               
"/CreationDate": cdate,
               
"/ModDate": cdate,
               
"/Title": dlg.austit.Value,
               
"/Author": dlg.ausaut.Value,
               
"/Subject": dlg.aussub.Value}
    pdf_out
.addMetadata(pdf_dict)
    parents
= {}
#==============================================================================
# process one input file
#==============================================================================
   
for zeile in dlg.szr02.Table.data:
        dateiname
= zeile[0]
        doc
= dlg.FileList[dateiname]
        max_seiten
= int(zeile[1])
#==============================================================================
# user input minus 1, PDF pages count from zero
# also correct any inconsistent input
#==============================================================================
        von
= int(zeile[2]) - 1
        bis
= int(zeile[3]) - 1

        von
= max(0, von)                   # "from" must not be < 0
        bis
= min(max_seiten - 1, bis)      # "to" must not be > max pages - 1
        bis
= max(von, bis)                 # "to" cannot be < "from"
        rot
= int(zeile[4])                 # get rotation angle

        pdfin
= PyPDF2.PdfFileReader(dateiname)
       
for p in range(von, bis + 1):       # read pages from input file
            pdf_page
= pdfin.getPage(p)
           
if rot > 0:
                pdf_page
.rotateClockwise(rot)  # rotate the page
            pdf_out
.addPage(pdf_page)          # output the page

       
# title = "infile [from-to (max.pages)]"
       
if dlg.noToC.Value:                # no ToC wanted
           
continue
        bm_main_title
= "%s [%s-%s (%s)]" % \
             
(os.path.basename(dateiname[:-4]).encode("latin-1"), von + 1,
               bis
+ 1, max_seiten)

        bm_main
= pdf_out.addBookmark(bm_main_title, aus_nr,
               
None, None, False, False, "/Fit")
       
print 1, bm_main_title, aus_nr

        parents
[1] = bm_main           # lvl 1 bookmark is infile's title

        toc
= fitz.GetToC(doc)         # get infile's table of contents
        bm_lst
= []                    # prepare the relevant sub-ToC
       
for t in toc:
           
if t[2] > von and t[2] <= bis + 1:   # relevant page range only
                bm_lst
.append([t[0] + 1,         # indent increased 1 level
                               t
[1],             # the title
                               t
[2] + aus_nr - von - 1])  # new page number

        aus_nr
+= (bis - von + 1)      # increase output counter

       
if bm_lst == []:               # do we have a sub-ToC?
           
continue                   # no, next infile
       
# while indent gap is too large, prepend "filler" bookmarks to bm_lst
       
while bm_lst[0][0] > 2:
            zeile
= [bm_lst[0][0] - 1, "<>", bm_lst[0][2]]
            bm_lst
.insert(0, zeile)
       
# now add infile's bookmarks
       
for b in bm_lst:
            bm
= pdf_out.addBookmark(b[1].encode("latin-1"), b[2],
                    parents
[b[0]-1], None, False, False, "/Fit")
            parents
[b[0]] = bm

#==============================================================================
# all input files processed
#==============================================================================
    pdf_out
.write(pdf_fle_out)
    pdf_fle_out
.close()
   
return ausgabe

#==============================================================================
#
# Main program
#
#==============================================================================
if wx.VERSION[0] >= 3:
   
pass
else:
   
print "wx Version needs to be at least 3"
    sys
.exit(1)
app
= None
app
= wx.App()
this_dir
= os.getcwd()

#==============================================================================
# create dialog
#==============================================================================
dlg
= PDFDialog(None)

#==============================================================================
# Show dialog and wait ...
#==============================================================================
rc
= dlg.ShowModal()

#==============================================================================
# if OK pressed, create output PDF
#==============================================================================
if rc == wx.ID_OK:
    ausgabe
= make_pdf(dlg)
dlg
.Destroy()
app
= None

History