SnitXMupdf

Snit widget which wrapps the fast pdf viewer mupdf on X-Windows systems

  • provides an additional toolbar for easier usage of mupdf
  • additional features added to mupdf
    • toolbar
    • possibility to open new files in place
    • file dialog for opening pdf, epub, comic book and fiction book files
    • reopen accidentally closed files
    • see: http://www.mupdf.com
    • requires mupdf, xprop, TkXext
    • if mupdf-gl is available it will be used, has TOC (o) and help (F1) keys

WikiDBImage SnitXMupdf.png

##############################################################################
#
#  Created By    : Dr. Detlef Groth
#  Created       : Mon Feb 12 16:55:44 2018
#  Last Modified : <180213.1216>
#
#  Description         : A PDF viewer widget for X-Windows systems using the Tcl/Tk extension TkXext
#                  for mupdf see  http://www.mupdf.com
#                   
#
#  Requirements  : mupdf and xprop 
#                  on fedora: dnf install mupdf xprop
#
#  History       : 0.1 initial release 2018-02-15
#                : 0.2 some fixes and additions 2018-02-17
#                     * support for mupdf-gl
#                     * check for window status, 
#                       avoiding steeling window from other SnitXMupdf widget
#                      * reloading does now work even after file open
#                     * memorize last directory after opening new file
##############################################################################
#
#  Copyright (c) 2018 Dr. Detlef Groth.
#   
#  License GNU AFFERO GENERAL PUBLIC LICENSE  Version 3, 19 November 2007
#  See: http://git.ghostscript.com/?p=mupdf.git;a=blob_plain;f=COPYING;hb=HEAD
#  See for background: http://artifex.com/licensing/
#  As mupdf code is not included we might chose an other license, but I am not sure
 
package require Tk
package require TkXext
package require snit
package provide SnitXMupdf 0.2

snit::widget SnitXMupdf {
    variable app ""
    variable pid ""
    option -infile ""
    option -standalone false
    option -page 1
    option -commandargs ""
    option -appname ""
    option -statusbar true
    variable WinTitle ""
    variable DynTitle ""
    variable numPages 0
    variable pnr 5
    variable dpi 96
    variable Closed true
    variable mupdf mupdf
    variable LastDir [file dirname [info script]]
    constructor {args} {
        $self configurelist $args
        foreach tool [list mupdf xprop] {
            set ok [auto_execok $tool]
            if {$ok eq ""} {
                return -code error "please install $tool"
            }
        }
        if {[auto_execok mupdf-gl] ne ""} {
            set mupdf mupdf-gl
        } else {
            set mupdf mupdf
        }

        pack [frame $win.controls] -padx 5 -pady 5
        pack [button $win.controls.fo -image fileopen-16 -command [mymethod ReopenWithNewFile] -relief groove -borderwidth 2] -side left -padx 2        
        pack [button $win.controls.rel -image reload-16 -command [mymethod reload] -relief groove -borderwidth 2] -side left -padx 2        
        pack [button $win.controls.bl -image playstart16 -command [mymethod goFirst] -relief groove -borderwidth 2] -side left -padx 2
        pack [button $win.controls.bb -image nav1leftarrow16 -command [mymethod goBackward] -relief groove -borderwidth 2] -side left -padx 2
        pack [button $win.controls.bf -image nav1rightarrow16 -command [mymethod goForward] -relief groove -borderwidth 2] -side left -padx 2
        pack [button $win.controls.b1 -image playend16 -command [mymethod goLast] -relief groove -borderwidth 2] -side left -padx 2
        pack [entry $win.controls.entry -textvariable [myvar pnr] -width 5] -side left -padx 5 
        bind $win.controls.entry <Return> [mymethod setPageBind]
        bind all <Enter> [mymethod bindFocus %W]
        pack [button $win.controls.plus -image viewmag+16 -command [mymethod doPlus] -relief groove -borderwidth 2] -side left -padx 5
        pack [button $win.controls.minus -image viewmag-16 -command [mymethod doMinus] -relief groove -borderwidth 2] -side left -padx 3
        # install check
        pack [frame $win.status] -side bottom -fill x -expand false
        pack [ttk::label $win.status.lb -textvariable [myvar DynTitle] -width 50 -anchor nw] -side left -fill x -expand true -padx 5 -pady 3

        $self LoadApp 
    }
    typeconstructor {
        image create photo nav1downarrow16 -data {
            R0lGODlhEAAQAIAAAPwCBAQCBCH5BAEAAAAALAAAAAAQABAAAAIYhI+py+0P
            UZi0zmTtypflV0VdRJbm6fgFACH+aENyZWF0ZWQgYnkgQk1QVG9HSUYgUHJv
            IHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4gQWxsIHJpZ2h0
            cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29tADs=
        }
        image create photo nav1uparrow16 -data {
            R0lGODlhEAAQAIAAAPwCBAQCBCH5BAEAAAAALAAAAAAQABAAAAIYhI+py+0P
            WwhxzmetzFpxnnxfRJbmufgFACH+aENyZWF0ZWQgYnkgQk1QVG9HSUYgUHJv
            IHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4gQWxsIHJpZ2h0
            cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29tADs=
        }
        image create photo nav1leftarrow16 -data {
            R0lGODlhEAAQAIAAAP///wAAACH5BAEAAAAALAAAAAAQABAAAAIdhI+pyxqd
            woNGTmgvy9px/IEWBWRkKZ2oWrKu4hcAIf5oQ3JlYXRlZCBieSBCTVBUb0dJ
            RiBQcm8gdmVyc2lvbiAyLjUNCqkgRGV2ZWxDb3IgMTk5NywxOTk4LiBBbGwg
            cmlnaHRzIHJlc2VydmVkLg0KaHR0cDovL3d3dy5kZXZlbGNvci5jb20AOw==
        }
        image create photo nav1rightarrow16 -data {
            R0lGODlhEAAQAIAAAPwCBAQCBCH5BAEAAAAALAAAAAAQABAAAAIdhI+pyxCt
            woNHTmpvy3rxnnwQh1mUI52o6rCu6hcAIf5oQ3JlYXRlZCBieSBCTVBUb0dJ
            RiBQcm8gdmVyc2lvbiAyLjUNCqkgRGV2ZWxDb3IgMTk5NywxOTk4LiBBbGwg
            cmlnaHRzIHJlc2VydmVkLg0KaHR0cDovL3d3dy5kZXZlbGNvci5jb20AOw==
        }

        image create photo playend16 -data {
            R0lGODlhEAAQAIAAAPwCBAQCBCH5BAEAAAAALAAAAAAQABAAAAIjhI+py8Eb
            3ENRggrxjRnrVIWcIoYd91FaenysMU6wTNeLXwAAIf5oQ3JlYXRlZCBieSBC
            TVBUb0dJRiBQcm8gdmVyc2lvbiAyLjUNCqkgRGV2ZWxDb3IgMTk5NywxOTk4
            LiBBbGwgcmlnaHRzIHJlc2VydmVkLg0KaHR0cDovL3d3dy5kZXZlbGNvci5j
            b20AOw==
        }
        image create photo playstart16 -data {
            R0lGODlhEAAQAIAAAPwCBAQCBCH5BAEAAAAALAAAAAAQABAAAAIjhI+pyxud
            wlNyguqkqRZh3h0gl43hpoElqlHt9UKw7NG27BcAIf5oQ3JlYXRlZCBieSBC
            TVBUb0dJRiBQcm8gdmVyc2lvbiAyLjUNCqkgRGV2ZWxDb3IgMTk5NywxOTk4
            LiBBbGwgcmlnaHRzIHJlc2VydmVkLg0KaHR0cDovL3d3dy5kZXZlbGNvci5j
            b20AOw==
        }
        image create photo viewmag+16 -data {
            R0lGODlhEAAQAIUAAPwCBCQmJDw+PAwODAQCBMza3NTm5MTW1HyChOTy9Mzq
            7Kze5Kzm7OT29Oz6/Nzy9Lzu7JTW3GTCzLza3NTy9Nz29Ize7HTGzHzK1AwK
            DMTq7Kzq9JTi7HTW5HzGzMzu9KzS1IzW5Iza5FTK1ESyvLTa3HTK1GzGzGzG
            1DyqtIzK1AT+/AQGBATCxHRydMTCxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAaB
            QIAQEBAMhkikgFAwHAiC5FCASCQUCwYiKiU0HA9IRAIhSAcTSuXBsFwwk0wy
            YNBANpyOxPMxIzMgCyEiHSMkGCV+SAQQJicoJCllUgBUECEeKhAIBCuUSxMK
            IFArBIpJBCxmLQQuL6cAsLECrqeys7WxpqZdtK9Ct8C0fsHAZn5BACH+aENy
            ZWF0ZWQgYnkgQk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29y
            IDE5OTcsMTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cu
            ZGV2ZWxjb3IuY29tADs=
        }
        image create photo viewmag-16 -data {
            R0lGODlhEAAQAIUAAPwCBCQmJDw+PAwODAQCBMza3NTm5MTW1HyChOTy9Mzq
            7Kze5Kzm7OT29Oz6/Nzy9Lzu7JTW3GTCzLza3NTy9Nz29Ize7HTGzHzK1AwK
            DMTq7Kzq9JTi7HTW5HzGzMzu9KzS1IzW5Iza5FTK1ESyvLTa3HTK1GzGzGzG
            1DyqtIzK1AT+/AQGBATCxHRydMTCxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAZ+
            QIAQEBAMhkikgFAwHAiC5FCASCQUCwYiKiU0HA9IRAIhSAcTSuXBsFwwk0wy
            YNBANpyOxPMxIzMgCyEiHSMkGCV+SAQQJicoJCllUgBUECEeKhAIBCuUSxMK
            IFArBIpJBCxmLQQuL6eUAFCusJSzr7Kmpl0CtLGLvbW2Zn5BACH+aENyZWF0
            ZWQgYnkgQk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5
            OTcsMTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2
            ZWxjb3IuY29tADs=
        }
        image create photo fileopen-16 -data {
            R0lGODlhEAAQAIUAAPwCBAQCBOSmZPzSnPzChPzGhPyuZEwyHExOTFROTFxa
            VFRSTMSGTPT29Ozu7Nze3NTS1MzKzMTGxLy6vLS2tLSytDQyNOTm5OTi5Ly+
            vKyqrKSmpIyOjLR+RNTW1MzOzJyenGxqZBweHKSinJSWlExKTMTCxKyurGxu
            bBQSFAwKDJyanERCRERGRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAaR
            QIBwGCgGhkhkEWA8HpNPojFJFU6ryitTiw0IBgRBkxsYFAiGtDodDZwPCERC
            EV8sEk0CI9FoOB4BEBESExQVFgEEBw8PFxcYEBIZGhscCEwdCxAPGA8eHxkU
            GyAhIkwHEREQqxEZExUjJCVWCBAZJhEmGRUnoygpQioZGxsnxsQrHByzQiJx
            z3EsLSwWpkJ+QQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9u
            IDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2
            ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7
        }
        image create photo reload-16 -data {
            R0lGODlhEAAQAIUAAPwCBCRaJBxWJBxOHBRGBCxeLLTatCSKFCymJBQ6BAwm
            BNzu3AQCBAQOBCRSJKzWrGy+ZDy+NBxSHFSmTBxWHLTWtCyaHCSSFCx6PETK
            NBQ+FBwaHCRKJMTixLy6vExOTKyqrFxaXDQyNDw+PBQSFHx6fCwuLJyenDQ2
            NISChLSytJSSlFxeXAwODCQmJBweHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
            AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAaB
            QIBQGBAMBALCcCksGA4IQkJBUDIDC6gVwGhshY5HlMn9DiCRL1MyYE8iiapa
            SKlALBdMRiPckDkdeXt9HgxkGhWDXB4fH4ZMGnxcICEiI45kQiQkDCUmJZsk
            mUIiJyiPQgyoQwwpH35LqqgMKiEjq5obqh8rLCMtowAkLqovuH5BACH+aENy
            ZWF0ZWQgYnkgQk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29y
            IDE5OTcsMTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cu
            ZGV2ZWxjb3IuY29tADs=
        }

        image create photo actcross16 -data {
            R0lGODlhEAAQAIIAAASC/PwCBMQCBEQCBIQCBAAAAAAAAAAAACH5BAEAAAAA
            LAAAAAAQABAAAAMuCLrc/hCGFyYLQjQsquLDQ2ScEEJjZkYfyQKlJa2j7AQn
            MM7NfucLze1FLD78CQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJz
            aW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVz
            ZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7
        }
        image create photo acthelp16 -data {
            R0lGODlhEAAQAIMAAPwCBAQ6XAQCBCyCvARSjAQ+ZGSm1ARCbEyWzESOxIy6
            3ARalAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAQ/EEgQqhUz00GE
            Jx2WFUY3BZw5HYh4cu6mSkEy06B72LHkiYFST0NRLIaa4I0oQyZhTKInSq2e
            AlaaMAuYEv0RACH+aENyZWF0ZWQgYnkgQk1QVG9HSUYgUHJvIHZlcnNpb24g
            Mi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZl
            ZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29tADs=
        }
    }
    method ReadPipe {chan} {
        set d [read $chan]
        foreach line [split $d \n] {
            if {[regexp {^WM_NAME.STRING. = "(.+)"} $line m title] } {
                set DynTitle $title
                if {[regexp { - ([0-9]+) */ *([0-9]+) \(([0-9]+) dpi} $title m pnr numPages dpi]} {
                     set options(-page) $pnr
                 }  elseif {[regexp { - ([0-9]+) */ *([0-9]+)} $title m pnr numPages]} {
                     set options(-page) $pnr
                 }

                 if {$options(-standalone)} {
                     if {$options(-appname) ne ""} {
                         wm title . "$options(-appname) - $DynTitle"
                     } else {
                         wm title . "$DynTitle"
                     }
                 }
                                               
            } 
        }
        if {[eof $chan]} {
            fileevent $chan readable {}
            close $chan
            set Closed true
        }
    }
    method bindFocus {mwin} {
        catch {focus -force $mwin}
    }
    method getWinState {} {
        set state Normal
        set res [exec xprop -id 0x$app]
        foreach line [split $res "\n"] {
            if {[regexp {window state: ([^ ]+)} $line -> state]} {
                break
            }
        }
        return $state
    }
                 
    method getWinProperties {} {
        set xpin [open "|xprop -id 0x$app -spy" r]
        fconfigure $xpin -blocking 0 -buffering line
        fileevent $xpin readable [mymethod ReadPipe $xpin]
    }
    method ReopenWithNewFile {} {
        set types {
            {{Pdf Files}              {.pdf}  }
            {{Epub Files}             {.epub} }
            {{Comic Book Files}       {.cbz}  }            
            {{Fiction Book Files}     {.fb2}  }            
            {{All Files}        *             }
        }
        set filename [tk_getOpenFile -filetypes $types -initialdir $LastDir]
        if {$filename != ""} {
            set options(-infile) [file nativename $filename]
            set options(-page) 1
            if {[winfo exists $win.f]} {
                destroy $win.f
            }
            after 1000
            $self LoadApp
            after 1000
            set Closed false
        }
    }
    method LoadApp {} {
        if {![winfo exists $win.f]} {
            frame $win.f -width 200 -height 200 -container 1
            bind $win.f <Control-r> [mymethod LoadApp]
            bind $win.f <Control-o> [mymethod ReopenWithNewFile]   
        }
        catch {
            pack forget $win.status
        }
        if {$pid ne ""} {
            catch { exec kill -9 $pid }
        }
        set pid [exec $mupdf  $options(-infile) $options(-page) &]
        set searchtitle [file tail $options(-infile)]
        if {[string length $searchtitle] > 30} {
            set searchtitle [string range $searchtitle [expr {[string length $searchtitle] -30}] end]
        }
        after 200
        set app ""
        while {$app eq ""} {
            after 100
            set app [TkXext.find.window "*${searchtitle}*"];
            if {[$self getWinState] eq "Withdrawn"} {
                # window was already catched
                # wait for the fresh one!!
                # don't steal
                set app ""
            }
            if {[incr x] > 200} {
                break
            }
        }
        if {$app eq ""} {
            destroy $win.f
            return -code error "unable to find the mupdf window"
        }
        TkXext.reparent.window $app [winfo id $win.f]
        bind $win.f <Configure> [list TkXext.resize.window $app %w %h]
        pack $win.f -fill both -expand 1 
        set LastDir [file dirname $options(-infile)]
        $self getWinProperties
        if {$options(-statusbar)} {
            catch {
                pack $win.status -side bottom -fill x -expand false
            }
        } else {
            catch {
                pack forget $win.status
            }

        }
        after 300
        set Closed false
                
    }
    method reload {} {
        if {[winfo exists $win.f]} {
            catch {
                TkXext.focus $app ; 
                after 200
                TkXext.send.string "r"
                after 200
                TkXext.send.string w
            } 
        } else {
            $self LoadApp
        }
    }
    method getPage {} {
        return $pnr
    }
    method getPages {} {
        return $numPages
    }
    method getZoom {} {
        return $dpi
    }
    method setPageBind {} {
        $self setPage $pnr
    }
    method setPage {page} {
        TkXext.focus $app ; 
        after 200
        TkXext.send.string "${page}g"
        after 200
        TkXext.send.string w
    }
    method LoadFile {filename {page 1}} {
        $self loadFile $filename $page
    }

    method loadFile {filename {page 1}} {
        set options(-infile) $filename
        set options(-page) $page
        if {[winfo exists $win.f]} {
            destroy $win.f
        }
        $self LoadApp
    }
    method goForward {} {
        TkXext.focus $app ; 
        TkXext.send.string .
    }
    method goLast {} {
        TkXext.focus $app ; 
        TkXext.send.string G
    }
    method goFirst {} {
        TkXext.focus $app ; 
        TkXext.send.string 1g
    }
    method goBackward {} {
        TkXext.focus $app ; 
        TkXext.send.string b
    }
    method doPlus {} {
        TkXext.focus $app ; 
        TkXext.send.string "+"
        TkXext.send.string w
    }
    method doMinus {} {
        TkXext.focus $app ; 
        TkXext.send.string "-"
        TkXext.send.string w
    }

    destructor {
        try {
            exec kill -9 $pid
        } finally {
            try {
                #TkXext.delete.or.kill $app
                # did not work
            }
        }
        destroy $win
    }
}

if {$argv0 eq [info script]} {
    if {[llength $argv] == 0 } {
        puts "Usage SnitXMupdf.tcl filename"
        exit 0
    } elseif {[llength $argv] == 1 } {
        SnitXMupdf .mpdf -infile  [lindex $argv 0] -standalone true -appname SnitXMupdf -statusbar true
        pack .mpdf -side top -fill both -expand true
    } elseif {[llength $argv] == 2 } { 
        SnitXMupdf .mpdf -infile  [lindex $argv 0] -page [lindex $argv 1] -standalone true -appname SnitXMupdf
        pack .mpdf -side top -fill both -expand true
    }

    
}