[NEM] 2009-03-30: Inspired by a question on [comp.lang.tcl] about producing paginated output from a canvas widget, here is an attempt at providing a paginated canvas widget that supports multiple pages and can create decent-ish paginated [PostScript] output. This is very much a work-in-progress, but I thought I'd put it here for others to use and fix. The widget uses the standard postscript capabilities of the underlying canvas to output each page as encapsulated postscript and then stitches them together into a full postscript file. It seems to work ok on my Mac, but I'm not a postscript expert, so YMMV. The widget also automatically sets the scrollregion and adds MouseWheel bindings, so that scrolling should work fairly automatically. [NEM] 2009-04-08: V0.2 Fixed some PostScript output bugs, and added some options for specifying the title, creator, etc in the resulting .ps file. ** Screenshots ** [A Paginated Canvas Widget - screenshot1.png] [A Paginated Canvas Widget - screenshot2.png] ** [A Paginated Canvas Widget - test.ps%|%Sample PS output] ** ** Code ** ====== # page.tcl -- # # A paginated canvas widget. # package require Tk 8.5 package require snit 2.0 package provide paged 0.2 namespace eval ::paged { namespace export canvas namespace ensemble create } snit::widgetadaptor paged::canvas { option -pagesize A4 option -pagefill white option -pageoutline #666666 option -pagepadding {20 20} delegate option * to hull delegate method * to hull # List of active pages variable pages [list] constructor args { installhull using ::canvas -background #cccccc $self configurelist $args } destructor { foreach page $pages { $page destroy } } typevariable pagesize [dict create] typemethod pagesizes {} { dict names $pagesize } typemethod pagesize {name args} { if {[llength $args] == 2} { dict set pagesize $name $args } elseif {[llength $args] == 0} { dict get $pagesize $name } else { usage "$type pagesize name ?width height?" } } method pixels {width} { winfo pixels $win $width } method pagepixelsize {name} { lassign [$type pagesize $name] width height set width [$self pixels $width] set height [$self pixels $height] return [list $width $height] } proc usage msg { return -level 2 -code error -errorcode [list USAGE $msg] $msg } typeconstructor { # Create a variety of standard sizes # Source: http://en.wikipedia.org/wiki/Paper_size # # ISO paper sizes # Calculate approximate sizes. Should be within tolerances specified in # standard, and most sizes should be exact. Only a couple of values in # the C series differ slightly from the standard values listed on # Wikipedia. foreach {series width height} { A 841 1189 B 1000 1414 C 917 1297 } { for {set i 0} {$i <= 10} {incr i} { $type pagesize $series$i ${width}m ${height}m lassign [list $width [expr {$height/2}]] height width #puts "$series$i = [$type pagesize $series$i]" } } # Extra German DIN 476 sizes $type pagesize 4A0 1682m 2378m $type pagesize 2A0 1189m 1682m # Standard US paper sizes $type pagesize letter 8.5i 11i $type pagesize legal 8.5i 14i $type pagesize juniorlegal 8i 5i $type pagesize ledger 17i 11i $type pagesize tabloid 11i 17i # Some UK writing paper sizes $type pagesize quarto 11i 9i $type pagesize imperial 9i 7i $type pagesize kings 8i 6.5i $type pagesize dukes 7i 5.5i # Adjust canvas bindings to support scrollwheel bind Canvas [list %W mousescroll %D] } method mousescroll {delta} { if {[tk windowingsystem] eq "aqua"} { $self yview scroll [expr {-$delta}] units } else { $self yview scroll [expr {-$delta/120}] units } } variable pages [list] method {page names} {} { return $pages } method {page create} {args} { set prev [lindex $pages end] if {$prev eq ""} { lassign {0 0 0 0} x1 y1 x2 y2 } else { lassign [$prev coords] x1 y1 x2 y2 } lassign [$self cget -pagepadding] left top set x $left set y [expr {$y2 + $top}] # Create the page item on the canvas set page [page create %AUTO% $self -xorigin $x -yorigin $y \ -outline [$self cget -pageoutline] \ -fill [$self cget -pagefill] \ -pagesize [$self cget -pagesize] {*}$args] #set offset [$page height] # Move all other pages down to accomodate for the change #foreach id [lrange $pages $index end] { # $id move 0 $offset #} lappend pages $page return $page } method {export postscript} args { set title [from args -title ""] set creator [from args -creator "Tk paged canvas"] set date [from args -creationdate [clock format [clock seconds]]] set size [from args -pagesize [$self cget -pagesize]] # Any remaining args will get passed to the individual page exporters. lassign [$self pagepixelsize [$self cget -pagesize]] w h set output "%!PS-Adobe-2.0\n" append output "%%Creator: $creator\n" append output "%%CreationDate: $date\n" append output "%%Title: $title\n" append output "%%Pages: [llength $pages]\n" append output "%%PageOrder: Ascend\n" #append output "%%DocumentMedia: Default $w $h 0 () ()\n" append output "%%EndComments\n" #append output "%%BeginFeature: *PageSize Default\n" #append output "<< /PageSize \[ $w $h \] /ImagingBBox null >> setpagedevice\n" #append output "%%EndFeature\n" append output "%%BeginSetup\n" append output "%%PaperSize: $size\n" append output "%%EndSetup\n" foreach page $pages i [range 1 [llength $pages]] { set eps [$page export postscript {*}$args] set label [$page cget -label] if {$label eq ""} { set label $i } append output "%%Page: $label $i\n" append output "%%BeginDocument: page$i.eps\n" append output $eps\n append output "%%EndDocument\n" } append output "%%Trailer\n" append output "%%EOF\n" return $output } proc range {start end} { set xs [list] for {set i $start} {$i <= $end} {incr i} { lappend xs $i } return $xs } method update {} { lassign [$self bbox all] _ _ w h lassign [$self cget -pagepadding] left top $self configure -scrollregion [list 0 0 $w [expr {$h+$top}]] } } snit::type paged::page { component canvas component item # General page configuration options option -pagesize -default A4 -configuremethod ChangePageSize option -margin -default {1i 1i 1i 1i} -configuremethod ChangeMargins option -xorigin -default 0 -configuremethod ChangeOffset option -yorigin -default 0 -configuremethod ChangeOffset option -marginoutline -default #efefef -configuremethod ChangeMarginView option -marginfill -default {} -configuremethod ChangeMarginView # Options delegated to the canvas item option -fill -default white -configuremethod ChangeOption option -outline -default #999999 -configuremethod ChangeOption option -state -default normal -configuremethod ChangeOption option -tags -default [list] -configuremethod ChangeOption option -label "" # Methods delegated to the canvas element method bbox {} { $canvas bbox $item } method bind args { $canvas bind $item {*}$args } method coords args { $canvas coords $item {*}$args } method move {xoffset yoffset} { # First move all items on this canvas foreach elem [$canvas find overlapping {*}[$self coords]] { $canvas move $elem $xoffset $yoffset } # Now move the page $canvas move $item $xoffset $yoffset } method create {itemtype args} { # Adjust each coordinate to be relative to the page + any margin offset set coords [list] lassign [$self margins] left top set xoff [expr {[$self cget -xorigin] + $left}] set yoff [expr {[$self cget -yorigin] + $top}] foreach {x y} [GetCoords args] { if {$x eq "center"} { set x [expr {[$self width] / 2}] } if {$y eq "center"} { set y [expr {[$self height] / 2}] } lappend coords [expr {round($x + $xoff)}] [expr {round($y + $yoff)}] } $canvas create $itemtype $coords {*}$args } method margins {} { lassign [$self cget -margin] left top right bottom list [$canvas pixels $left] [$canvas pixels $top] \ [$canvas pixels $right] [$canvas pixels $bottom] } proc GetCoords {argv} { upvar 1 $argv args set coord [lindex $args 0] if {[llength $coord] > 1} { return [pop args] } set coords [list] while {$coord eq "center" || [string is integer -strict $coord]} { lappend coords [pop args] set coord [lindex $args 0] } return $coords } proc pop {listVar} { upvar 1 $listVar list set list [lassign $list elem][set list ""] return $elem } # Constructor. Should only be called by paged canvas. constructor {_canvas args} { set canvas $_canvas install item using $canvas create rectangle 0 0 0 0 \ -tags page $self configurelist $args } method height {} { lassign [$self coords] _ y1 _ y2 lassign [$self margins] l t r b set h [expr {$t+$b}] expr {$y2-$y1-$h} } method width {} { lassign [$self coords] x1 _ x2 _ lassign [$self margins] l t r b set w [expr {$l+$r}] expr {$x2-$x1-$w} } # Export this page in a given format. method {export postscript} args { $canvas postscript {*}[$self pagebox] {*}$args } method pagebox {} { lassign [$self coords] x y x1 y1 # The "magic numbers" here were calculated using trial and error, # based on what it took for the postscript output to not include the # page border outline. list -x [expr {$x+2}] -y [expr {$y+2}] \ -width [expr {$x1-$x-4}] -height [expr {$y1-$y-4}] \ -pagex 0 -pagey 0 -pageanchor sw } method ChangePageSize {option value} { lassign [$canvas pagepixelsize $value] width height set x0 [$self cget -xorigin] set y0 [$self cget -yorigin] $self coords $x0 $y0 [expr {$x0+$width}] [expr {$y0+$height}] after idle [list $canvas update] set options($option) $value } method ChangeMargins {option value} { if {[llength $value] == 1} { set value [lrepeat 4 $value] } if {[llength $value] == 2} { set value [lrepeat 2 {*}$value] } set options($option) $value } method ChangeOffset {option value} { set options($option) $value set w [$self width] set h [$self height] set x0 [$self cget -xorigin] set y0 [$self cget -yorigin] set x1 [expr {$x0+$w}] set y1 [expr {$y0+$h}] $self coords $x0 $y0 $x1 $y1 } method ChangeOption {option value} { $canvas itemconfigure $item $option $value set options($option) $value } method ChangeMarginView {option value} { set options($option) $value } } ====== As an example of its use, this little program will create a small 2-page A4 document and then produce postscript output to the file "test.ps" in the current working directory: ====== paged canvas .c -background #efefef -yscrollcommand [list .vsb set] \ -xscrollcommand [list .hsb set] -pagesize A4 scrollbar .vsb -orient vertical -command [list .c yview] scrollbar .hsb -orient horizontal -command [list .c xview] grid .c .vsb -sticky nsew grid .hsb -sticky ew grid rowconfigure . 0 -weight 1 grid columnconfigure . 0 -weight 1 # Add some pages and text set p [.c page create] $p create text center 0 -text "Test Document" -tag title $p create text 0 40 -text "Introduction" -tag heading $p create text 0 70 -text {This is a sample piece of text to see how well it looks inside this wonderful canvas widget that I have created. It would be truly wonderful if we could get this widget to support justified text and all those other wonderful features...} -tags para set p2 [.c page create] $p2 create text 0 0 -text "Next Section" -tag heading $p2 create text 0 30 -text "Another little page..." -tag para .c itemconfigure title -font {{Lucida Grande} 34} -anchor n .c itemconfigure heading -font {{Lucida Grande} 24} -anchor nw .c itemconfigure para -anchor nw -font {Palatino 12} # Export as postscript. -file etc options not implemented yet. set ps [.c export postscript] set out [open test.ps w] puts $out $ps close $out ====== ** Discussion ** [Martyn Smith]: 09 April 2009 Is there something missing from this example, when I try it (tcl 8.5.2 or 8.6) I get an error message) ====== invalid command name "page" while executing page create %AUTO% $self -xorigin .... ====== I copied the whole page into a tcl file added an apropriate auto_path and ran it using tclkit. [NEM]: It works for me, on various platforms with various versions of Tcl/Tk. What version of snit are you using? [Martyn Smith]: as the code says 'package require snit 2.0', I only have one tcl file with all the code above plus, just in case a package require paged but always the same error. ---- greg 24 September 2018 I have a problem with font size in Postscript. I solved it with the two lines row 7 set faktor [expr round([tk scaling] * 100) / 100.0] row 24 append output "/scalefont \{$faktor mul scalefont\} bind def\n" ====== method {export postscript} args { set title [from args -title ""] set creator [from args -creator "Tk paged canvas"] set date [from args -creationdate [clock format [clock seconds]]] set size [from args -pagesize [$self cget -pagesize]] # Any remaining args will get passed to the individual page exporters. set faktor [expr round([tk scaling] * 100) / 100.0] lassign [$self pagepixelsize [$self cget -pagesize]] w h set output "%!PS-Adobe-2.0\n" append output "%%Creator: $creator\n" append output "%%CreationDate: $date\n" append output "%%Title: $title\n" append output "%%Pages: [llength $pages]\n" append output "%%PageOrder: Ascend\n" #append output "%%DocumentMedia: Default $w $h 0 () ()\n" append output "%%EndComments\n" #append output "%%BeginFeature: *PageSize Default\n" #append output "<< /PageSize \[ $w $h \] /ImagingBBox null >> setpagedevice\n" #append output "%%EndFeature\n" append output "%%BeginSetup\n" append output "%%PaperSize: $size\n" append output "%%EndSetup\n" append output "/scalefont \{$faktor mul scalefont\} bind def\n" foreach page $pages i [range 1 [llength $pages]] { set eps [$page export postscript {*}$args] set label [$page cget -label] if {$label eq ""} { set label $i } append output "%%Page: $label $i\n" append output "%%BeginDocument: page$i.eps\n" append output $eps\n append output "%%EndDocument\n" } append output "%%Trailer\n" append output "%%EOF\n" return $output } ====== ---- greg 24 September 2018 export in pdf with pdf4tcl ====== package require pdf4tcl ====== new method in snit::widgetadaptor paged::canvas { ... } ====== method {export pdf} args { lassign [$self pagemmsize [$self cget -pagesize]] w h set pagepdf [pdf4tcl::new ::paged::mypdf -paper [list $w $h]] foreach page $pages i [range 1 [llength $pages]] { $page export pdf {*}$args } return $pagepdf } ====== 3 new method in snit::type paged::page { ... } ====== method pagemmsize {name} { lassign [$type pagesize $name] width height return [list $width $height] } method {export pdf} args { mypdf startPage mypdf canvas $canvas {*}[$self pdfbox] mypdf endPage } method pdfbox {} { lassign [$self coords] x y x1 y1 list -bbox [list [expr {$x +2}] [expr {$y +2 }] [expr {$x1 -4}] [expr {$y1-4}]] } ====== in example ====== # Export as pdf set mypdf [.c export pdf] $mypdf write -file test.pdf $mypdf destroy ====== <> GUI | Widget | Printing