Snit widget which wrapps the fast pdf viewer mupdf on X-Windows systems edit
- 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
#
# 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
}
}