############################################################################## # # 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 } }