Capturing Multiple Screens

Keith Vetter 2006-02-13 : Here's a package I recently wrote that solves the problem of capturing the contents of a canvas into an image. If the canvas is small enough to fit entirely in the current window then you can use the Img package (see Capture a window into an image).

This package handles the situation when the canvas is too large. It first displays a dialog letting the user select which portions of the canvas to capture, then for each section it scrolls the canvas to that section, does a screen capture and joins them together into one large image.

There are some really nasty corner situations with partial sections at the edges.

NB. this package doesn't handle two cases: first, if you have embedded widgets they show up as blank (a limitation of the Img package), and second, it assumes highlightthickness is set to 0.


 ##+##########################################################################
 #
 # snap.tcl -- Takes snapshot of current or multiple connected screens
 # by Keith Vetter, February 2006
 #
 # Revisions:
 # KPV Feb 13, 2006 - initial revision
 #
 ##+##########################################################################
 #############################################################################
 
 package require Tk
 package require Img
 package require tile
 namespace import -force ::ttk::button
 
 image create photo ::img::camera -data {
    R0lGODlhVQBbALMAAAQCBMzO/LT6/PwCBPz+/OkAABIAAAAAALgB394A2hgA1AAAd8CgAN7qABgS
    AAAAACH5BAEAAAMALAAAAABVAFsAAwT/cMhJq72Ygsy7/2AIbGFpWuN5jaTqiuw7sK1sr3QN03eP
    5joP0EeUAGOfY7DoogmGHeWSeWIJrlDc6Mmj2qzXcO63xXZ3XiM4bD47xefMMa0GsO9SOz6ukdJn
    ZXd7b4J8eSl/gIKLXHqMPId8XoGMlY+HcIh/awIEnpZhnp+ZhGySk46iopWqq3CWY5trrapstK2g
    pFR5obe+v4wEpkgykYS/yL69o21TSZjBydLJw5oqOblX09vAzTfYrNzit7rfpcvj6cyNzlWc2urx
    1Tp5z5Sd8erzxu0V5+M5xmVr1k+LI3zTjEnLlcXEMWn8jii7dKrELGQR85AbZK1YKoxA/yAGpLWv
    oEU9ySRyG7GRncmTZUCODECzpk1VLHG5fAniCMJbSjzZHErTU85amYj4/AlUpdAAh3Tu8wiOqS+n
    T2tiFVaoYc9/CUOKIjqS5CIl9u5Z/eUUqsaJXdEaLCUOK1G3LLrFlTv3osiyWqXALVkPw9K1rdoG
    poGMox+HQBCLsrs4b8udPNOio6X4boDBxHb5bSr2V9HLoZWOJl02pVlvRehuy2hZKrvY7wDyG3y7
    xzl4dUs33mtOra3ZQYc7ztx3qzoAyknFuvZYQspI4vxVFDL9AtvW6iaFuJrc8+JWiT5wFkxTymno
    BLymH8CaNov38AOnZ7wefvzkrbQHQP9RR+mXBoD18edLewLe1yBzIEwW0mdGvUWAeTblUBluFeKU
    E4GCGYWhefK9cNSHUKlSWYMSukfWHKpdiBdR8a1Y01g0kBgJbgPidV+OPvZY1Ih3HcJjj3k8SOSM
    nhXmg4YuFjlCZQ76mCFfvmm4JBBUTqlgf9tRd4SUXHYJnowU/hemO/YBGaSGY3nWIhNtavnmfXEq
    kWZHxdUppJ6fmengf6KNaYyZQx1KKJ16Htogkw8mueiRUEaipFtXChlpThByZ6ilkGqK6Ys5xlco
    l45WKaqoXQZwYKOgqmoempu+WmaqUxKpYqS2aokrqzTu+mCvQP4KFYm0bIjKrbHm2uRkR2iuSVWx
    zSI5Kl7oDEsHs4coOaCzbwib2qkDCEqtl6paEeBnfC5b7qbclingHWPVON8ME8Abr51qZeXqvfnW
    scSfYwYmRic2AcxjplX+q7BqmgL7MKPOEjXxsiVeXKjGHBMRAQA7
 }
 namespace eval ::Snapshot {
    variable WHO
    variable W .snap
    variable S
    variable BOX
    variable saveName "snapshot"
    variable msg "Snapshot Demo"                ;# For use as -texvariable
    variable tops                               ;# For hiding other toplevels
    
    array set S {w 200 h 200}
 }
 
 ##+##########################################################################
 # 
 # ::Snapshot::Dialog -- puts up the snapshot dialog
 # 
 proc ::Snapshot::Dialog {who} {
    variable WHO $who
    variable S
    variable W
    variable BOX
 
    destroy $W
    toplevel $W
    wm title $W "Snapshot"
    scan [wm geom .] "%dx%d+%d+%d" w h x y
    wm geom $W +[expr {$x+$w+30}]+$y
    wm resizable $W 0 0
 
    label $W.icon -image ::img::camera -width 100 -height 100
    frame $W.btns -bd 2 -relief ridge
    button $W.snap -text "Take Snapshot" -command ::Snapshot::_Snap
    button $W.dismiss -text Dismiss -command [list destroy $W]
    frame $W.f -bd 5 -relief ridge -pady 5 -padx 5
    canvas $W.c -highlightthickness 0 -width $S(w) -height $S(h) -bg red
    label $W.l1 -text "Click on cells to select" -font {Helvetica 10 bold}
    label $W.l2 -textvariable ::Snapshot::BOX(size) -font {Helvetica 10 bold}
 
    grid $W.icon $W.f -sticky n -pady 10
    grid configure $W.f -sticky news -padx 10
    grid x $W.l1 
    grid x $W.l2 
    grid $W.btns - -sticky ew
    
    grid rowconfigure $W 0 -weight 1
    grid columnconfigure $W 1 -weight 1
    
    pack $W.c -side top -in $W.f -fill both -expand 1
    pack $W.snap $W.dismiss -in $W.btns -side left -expand 1 -pady 10
 
    ::Snapshot::_MakeGrid
    ::Snapshot::_click $BOX(me,row) $BOX(me,col)
 
    grab $W  ;# Use this if you want to freeze the display
 }
 
 ##+##########################################################################
 # 
 # ::Snapshot::_MakeGrid -- draws the grid to select cells on
 # 
 proc ::Snapshot::_MakeGrid {} {
    variable W
    variable BOX
    variable S
 
    $W.c delete all
    ::Snapshot::_GetDimensions
    foreach arr [array names BOX xy,*] {
        scan $arr "xy,%d,%d" row col
        set tag box$row,$col
        
        foreach {x0 y0 x1 y1} $BOX($arr) break
        set x0 [expr {$x0 * $S(w)}]
        set y0 [expr {$y0 * $S(h)}]
        set x1 [expr {$x1 * $S(w)}]
        set y1 [expr {$y1 * $S(h)}]
 
        $W.c create rect $x0 $y0 $x1 $y1 -tag [list box $tag] -width 2 \
            -outline black -fill black -stipple gray50
        $W.c bind $tag <1> [list ::Snapshot::_click $row $col]
        set BOX(val,$row,$col) 0
    }
 }
 ##+##########################################################################
 # 
 # ::Snapshot::_click -- handles clicking on a grid cell
 # 
 proc ::Snapshot::_click {row col} {
    variable W
    variable BOX
    
    set tag box$row,$col
    if {$BOX(val,$row,$col) == 0} {
        set BOX(val,$row,$col) 1
        $W.c itemconfig $tag -fill red -stipple gray75
    } else {
        set BOX(val,$row,$col) 0
        $W.c itemconfig $tag -fill black -stipple gray50
    }
    ::Snapshot::_GetISize
 }
 ##+##########################################################################
 # 
 # ::Snapshot::_GetISize -- Figures outs image size based on clicked cells
 # 
 proc ::Snapshot::_GetISize {} {
    variable BOX
    variable W
 
    set cnt 0
    for {set row 0} {$row < $BOX(rows)} {incr row} {
        for {set col 0} {$col < $BOX(cols)} {incr col} {
            if {$BOX(val,$row,$col) == 0} continue
            if {[incr cnt] == 1} {
                set rmin $row
                set rmax $row
                set cmin $col
                set cmax $col
            } else {
                if {$row < $rmin} {set rmin $row}
                if {$row > $rmax} {set rmax $row}
                if {$col < $cmin} {set cmin $col}
                if {$col > $cmax} {set cmax $col}
            }
        }
    }
    if {$cnt == 0} {
        set BOX(size) "? x ?"
        $W.snap config -state disabled
        return
    }
 
    set w [expr {$cmax - $cmin + 1}]
    set h [expr {$rmax - $rmin + 1}]
    $W.snap config -state [expr {$cnt == ($w*$h) ? "normal" : "disabled"}]
 
    foreach var {rmin rmax cmin cmax} {
        set BOX($var) [set $var]
    }
    foreach {. . w h} [::Snapshot::_GetToCoords $BOX(rmax) $BOX(cmax)] break
    set BOX(size) "[comma $w] x [comma $h]"
    set BOX(cnt) $cnt
 }
 ##+##########################################################################
 # 
 # ::Snapshot::_GetDimensions -- get scroll percentages for all cells
 # 
 proc ::Snapshot::_GetDimensions {} {
    variable WHO
    variable BOX
 
    unset -nocomplain BOX
 
    # Get size of whole screen
    set bbox [$WHO cget -scrollregion]
    if {$bbox eq ""} { set bbox [$WHO bbox all]}
    if {$bbox eq ""} {error problem}
    set ::bbox $bbox
 
    foreach {l t r b} $bbox break
    set BOX(s,w) [expr {$r - $l}]
    set BOX(s,h) [expr {$b - $t}]
    
    foreach {xlo xxhi} [$WHO xview] break
    foreach {ylo yyhi} [$WHO yview] break
    set wx [expr {$xxhi - $xlo}]
    set wy [expr {$yyhi - $ylo}]
    
 
    for {set x $xlo} {$x > $wx} {set x [expr {$x - $wx}]} {}
    set xx 0
    if {$x == 0} {set x $wx}
    while {$x < 1} {
        lappend xx $x
        set x [expr {$x + $wx}]
    }
    lappend xx 1
    set BOX(cols) [expr {[llength $xx]-1}]
 
    for {set y $ylo} {$y > $wy} {set y [expr {$y - $wy}]} {}
    set yy 0
    if {$y == 0} {set y $wy}
    while {$y < 1} {
        lappend yy $y
        set y [expr {$y + $wy}]
    }
    lappend yy 1
    set BOX(rows) [expr {[llength $yy]-1}]
 
    # Now we can compute coordinates for all the boxes
    for {set col0 0; set col1 1} {$col1 < [llength $xx]} {incr col0; incr col1} {
        set xlo [lindex $xx $col0]
        set xhi [lindex $xx $col1]
        if {abs($xhi - $xxhi) < .0001} { set BOX(me,col) $col0 }
 
        for {set row0 0; set row1 1} {$row1 < [llength $yy]} {incr row0; incr row1} {
            set ylo [lindex $yy $row0]
            set yhi [lindex $yy $row1]
            if {abs($yhi - $yyhi) < .0001} { set BOX(me,row) $row0 } 
            set BOX(xy,$row0,$col0) [list $xlo $ylo $xhi $yhi]
        }
    }
 }
 ##+##########################################################################
 # 
 # ::Snapshot::_Snap -- High level handler for capturing image
 # 
 proc ::Snapshot::_Snap {} {
    variable W
    variable msg "Taking Snapshot..."
    
    destroy $W
    ::Snapshot::HideToplevels 1
    set iname [::Snapshot::_MakeImage]
    ::Snapshot::HideToplevels 0
 
    ::Snapshot::_Save $iname
    image delete $iname
 }
 ##+##########################################################################
 # 
 # ::Snapshot::_MakeImage -- middle level handler for creating image
 # 
 proc ::Snapshot::_MakeImage {} {
    variable BOX
    variable msg
 
    set iname ::snap::img
    set tname ::snap::tmp
    foreach cmd [info commands ::snap::*] {image delete $cmd}
 
    foreach {. . w h} [::Snapshot::_GetToCoords $BOX(rmax) $BOX(cmax)] break
    image create photo $iname -width $w -height $h
 
    set cnt 0
    for {set row $BOX(rmin)} {$row <= $BOX(rmax)} {incr row} {
        for {set col $BOX(cmin)} {$col <= $BOX(cmax)} {incr col} {
            incr cnt
            set msg "Taking snapshot $cnt/$BOX(cnt)"
            ::Snapshot::_MakeOneImage $tname $row $col
            set to [::Snapshot::_GetToCoords $row $col]
            set from [::Snapshot::_GetFromCoords $row $col $tname]
            eval $iname copy $tname -to $to -from $from
        }
    }
    image delete $tname
    ::Snapshot::_MoveTo $BOX(me,row) $BOX(me,col)
    return $iname
 }
 ##+##########################################################################
 # 
 # ::Snapshot::_MakeOneImage -- lowest level handler for creating image
 # 
 proc ::Snapshot::_MakeOneImage {iname row col} {
    variable WHO
    ::Snapshot::_MoveTo $row $col
    if {[info commands $iname] ne ""} {image delete $iname}
    image create photo $iname -data $WHO
    return $iname
 }
 ##+##########################################################################
 # 
 # ::Snapshot::_GetToCoords -- returns coords where this cell goes in
 # the final image
 # 
 proc ::Snapshot::_GetToCoords {row col} {
    variable BOX
 
    # Get top left corner of image in percentages
    foreach {xx yy} $BOX(xy,$BOX(rmin),$BOX(cmin)) break
    
    foreach {x0 y0 x1 y1} $BOX(xy,$row,$col) break
    set x0 [expr {round(($x0-$xx) * $BOX(s,w))}]
    set x1 [expr {round(($x1-$xx) * $BOX(s,w))}]
    set y0 [expr {round(($y0-$yy) * $BOX(s,h))}]
    set y1 [expr {round(($y1-$yy) * $BOX(s,h))}]
    return [list $x0 $y0 $x1 $y1]
 }
 ##+##########################################################################
 # 
 # ::Snapshot::_GetFromCoords -- for the extreme right column
 # and bottom row we might have to grab from within the image
 # 
 proc ::Snapshot::_GetFromCoords {row col img} {
    variable BOX
 
    set fromX 0
    set fromY 0
    
    foreach {x0 y0 x1 y1} $BOX(xy,$row,$col) break
    if {$x1 >= 1 && $x0 > 0} {
        set needX [expr {round($BOX(s,w) * ($x1-$x0))}]
        set fromX [expr {[image width $img] - $needX}]
    }
    if {$y1 >= 1 && $y0 > 0} {
        set needY [expr {round($BOX(s,h) * ($y1-$y0))}]
        set fromY [expr {[image height $img] - $needY}]
    }
    return [list $fromX $fromY]
 }
 ##+##########################################################################
 # 
 # ::Snapshot::_MoveTo -- Moves screen so a given cell is visible
 # 
 proc ::Snapshot::_MoveTo {row col} {
    variable WHO
    variable BOX
    foreach {xmin ymin} $BOX(xy,$row,$col) break
    $WHO xview moveto $xmin
    $WHO yview moveto $ymin
    update
 }
 ##+##########################################################################
 # 
 # ::Snapshot::_Save -- Saves our image in a file
 # 
 proc ::Snapshot::_Save {iname} {
    variable saveName
    variable msg
 
    set types {}
    lappend types {"JPEG Files" ".jpg"}
    lappend types {"PNG Files" ".png"}
    set ext "jpg"
    
    set fname [tk_getSaveFile -defaultextension $ext \
                   -title "Save Snapshot" \
                   -filetypes $types \
                   -initialfile $saveName]
    if {$fname eq ""} {
        set msg "Cancelled snapshot"
        return
    }
    set saveName $fname
 
    set fmt [expr {[file extension $saveName] eq ".png" ? "png" : "jpeg"}]
    $iname write $saveName -format $fmt
    set msg "Saved snapshot as $saveName"
 }
 ##+##########################################################################
 # 
 # ::Snapshot::HideToplevels -- withdraws all top levels so the
 # main window is not obscured
 # 
 proc ::Snapshot::HideToplevels {hide} {
    variable tops
 
    if {$hide} {
        set tops {}
        foreach w [winfo child .] {
            if {[winfo class $w] ne "Toplevel"} continue
            lappend tops $w [wm state $w]
            if {[wm state $w] eq "normal"} {
                wm withdraw $w
            }
        }
        raise .
        update
    } else {
        foreach {w wstate} $tops {
            if {$wstate eq "normal"} { wm deiconify $w }
        }
    }
 }
 proc comma { num {sep ,} } {
    while {[regsub {^([-+]?\d+)(\d\d\d)} $num "\\1$sep\\2" num]} {}
    return $num
 }
 ################################################################
 #
 # Demo code 
 #
 scrollbar .sb_x -command {.c xview} -orient horizontal
 scrollbar .sb_y -command {.c yview} -orient vertical
 canvas .c -highlightthickness 0
 bind .c <2>              [bind Text <2>]         ;# Enable dragging w/ <2>
 bind .c <B2-Motion>      [bind Text <B2-Motion>]
 .c config -xscrollcommand {.sb_x set}
 .c config -yscrollcommand {.sb_y set}
 label .l -textvariable ::Snapshot::msg -font {Times 12 bold} -bd 2 -relief ridge
 button .go -text "Snapshot" -command {::Snapshot::Dialog .c}
 ::ttk::separator .sep
 
 grid .c .sb_y -sticky news
 grid .sb_x -sticky news
 #grid .sep - -sticky ew -pady 20 -padx 10
 grid .l - -sticky news
 grid .go - -pady 10
 grid rowconfigure . 0 -weight 1
 grid columnconfigure . 0 -weight 1
 
 for {set i 0} {$i < 200} {incr i} {
    set x0 [expr {rand() * 1000 - 500}]
    set y0 [expr {rand() * 1000 - 500}]
    set x1 [expr {$x0 + rand()*200}]
    set y1 [expr {$y0 + rand()*200}]
    set clr [format "\#%06x" [expr {int(rand()*0xFFFFFF)}]]
    set clr2 [format "\#%06x" [expr {int(rand()*0xFFFFFF)}]]
    set width [expr {rand() * 5}]
    set type [expr {rand() < .5 ? "oval" : "rect"}]
    .c create $type $x0 $y0 $x1 $y1 -fill $clr -width $width -outline $clr2
 }
 .c config -scrollregion [.c bbox all]
 return