# print.tcl --
# This file defines the 'tk print' command for printing of the canvas
# widget and text on X11, Windows, and macOS. It implements an abstraction
# layer that presents a consistent API across the three platforms.
# Copyright © 2009 Michael I. Schwartz.
# Copyright © 2021 Kevin Walzer/WordTech Communications LLC.
# Copyright © 2021 Harald Oehlmann, Elmicron GmbH
# Copyright © 2022 Emiliano Gavilan
#
# See the file "license.terms" for information on usage and redistribution
# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
namespace eval ::tk::print {
namespace import -force ::tk::msgcat::*
# makeTempFile:
# Create a temporary file and populate its contents
# Arguments:
# filename - base of the name of the file to create
# contents - what to put in the file; defaults to empty
# Returns:
# Full filename for created file
#
proc makeTempFile {filename {contents ""}} {
set dumpfile [file join /tmp rawprint.txt]
set tmpfile [file join /tmp $filename]
set f [open $dumpfile w]
try {
puts -nonewline $f $contents
} finally {
close $f
if {[file extension $filename] == ".ps"} {
#don't apply formatting to PostScript
file rename -force $dumpfile $tmpfile
} else {
#Make text fixed width for improved printed output
exec fmt -w 75 $dumpfile > $tmpfile
}
return $tmpfile
}
}
if {[tk windowingsystem] eq "win32"} {
variable printer_name
variable copies
variable dpi_x
variable dpi_y
variable paper_width
variable paper_height
variable margin_left
variable margin_top
variable printargs
array set printargs {}
# Multiple utility procedures for printing text based on the
# C printer primitives.
# _set_dc:
# Select printer and set device context and other parameters
# for print job.
#
proc _set_dc {} {
variable printargs
variable printer_name
variable paper_width
variable paper_height
variable dpi_x
variable dpi_y
variable copies
#First, we select the printer.
_selectprinter
#Next, set values. Some are taken from the printer,
#some are sane defaults.
if {[info exists printer_name] && $printer_name ne ""} {
set printargs(hDC) $printer_name
set printargs(pw) $paper_width
set printargs(pl) $paper_height
set printargs(lm) 1000
set printargs(tm) 1000
set printargs(rm) 1000
set printargs(bm) 1000
set printargs(resx) $dpi_x
set printargs(resy) $dpi_y
set printargs(copies) $copies
set printargs(resolution) [list $dpi_x $dpi_y]
}
}
# _print_data
# This function prints multiple-page files, using a line-oriented
# function, taking advantage of knowing the character widths.
# Arguments:
# data - Text data for printing
# breaklines - If non-zero, keep newlines in the string as
# newlines in the output.
# font - Font for printing
proc _print_data {data {breaklines 1} {font ""}} {
variable printargs
variable printer_name
_set_dc
if {![info exists printer_name]} {
return
}
if {$font eq ""} {
_gdi characters $printargs(hDC) -array printcharwid
} else {
_gdi characters $printargs(hDC) -font $font -array printcharwid
}
set pagewid [expr {($printargs(pw) - $printargs(rm) ) / 1000 * $printargs(resx)}]
set pagehgt [expr {($printargs(pl) - $printargs(bm) ) / 1000 * $printargs(resy)}]
set totallen [string length $data]
set curlen 0
set curhgt [expr {$printargs(tm) * $printargs(resy) / 1000}]
_opendoc
_openpage
while {$curlen < $totallen} {
set linestring [string range $data $curlen end]
if {$breaklines} {
set endind [string first "\n" $linestring]
if {$endind >= 0} {
set linestring [string range $linestring 0 $endind]
# handle blank lines....
if {$linestring eq ""} {
set linestring " "
}
}
}
set result [_print_page_nextline $linestring \
printcharwid printargs $curhgt $font]
incr curlen [lindex $result 0]
incr curhgt [lindex $result 1]
if {$curhgt + [lindex $result 1] > $pagehgt} {
_closepage
_openpage
set curhgt [expr {$printargs(tm) * $printargs(resy) / 1000}]
}
}
_closepage
_closedoc
}
# _print_file
# This function prints multiple-page files
# It will either break lines or just let them run over the
# margins (and thus truncate).
# The font argument is JUST the font name, not any additional
# arguments.
# Arguments:
# filename - File to open for printing
# breaklines - 1 to break lines as done on input, 0 to ignore newlines
# font - Optional arguments to supply to the text command
proc _print_file {filename {breaklines 1} {font ""}} {
set fn [open $filename r]
set data [read $fn]
close $fn
_print_data $data $breaklines $font
}
# _print_page_nextline
# Returns the pair "chars y"
# where chars is the number of characters printed on the line
# and y is the height of the line printed
# Arguments:
# string - Data to print
# pdata - Array of values for printer characteristics
# cdata - Array of values for character widths
# y - Y value to begin printing at
# font - if non-empty specifies a font to draw the line in
proc _print_page_nextline {string carray parray y font} {
upvar #0 $carray charwidths
upvar #0 $parray printargs
variable printargs
set endindex 0
set totwidth 0
set maxwidth [expr {
(($printargs(pw) - $printargs(rm)) / 1000) * $printargs(resx)
}]
set maxstring [string length $string]
set lm [expr {$printargs(lm) * $printargs(resx) / 1000}]
for {set i 0} {($i < $maxstring) && ($totwidth < $maxwidth)} {incr i} {
incr totwidth $charwidths([string index $string $i])
# set width($i) $totwidth
}
set endindex $i
set startindex $endindex
if {$i < $maxstring} {
# In this case, the whole data string is not used up, and we
# wish to break on a word. Since we have all the partial
# widths calculated, this should be easy.
set endindex [expr {[string wordstart $string $endindex] - 1}]
set startindex [expr {$endindex + 1}]
# If the line is just too long (no word breaks), print as much
# as you can....
if {$endindex <= 1} {
set endindex $i
set startindex $i
}
}
set txt [string trim [string range $string 0 $endindex] "\r\n"]
if {$font ne ""} {
set result [_gdi text $printargs(hDC) $lm $y \
-anchor nw -justify left \
-text $txt -font $font]
} else {
set result [_gdi text $printargs(hDC) $lm $y \
-anchor nw -justify left -text $txt]
}
return "$startindex $result"
}
# These procedures read in the canvas widget, and write all of
# its contents out to the Windows printer.
variable option
variable vtgPrint
proc _init_print_canvas {} {
variable option
variable vtgPrint
variable printargs
set vtgPrint(printer.bg) white
}
proc _is_win {} {
variable printargs
return [info exist tk_patchLevel]
}
# _print_widget
# Main procedure for printing a widget. Currently supports
# canvas widgets. Handles opening and closing of printer.
# Arguments:
# wid - The widget to be printed.
# printer - Flag whether to use the default printer.
# name - App name to pass to printer.
proc _print_widget {wid {printer default} {name "Tk Print Output"}} {
variable printargs
variable printer_name
_set_dc
if {![info exists printer_name]} {
return
}
_opendoc
_openpage
# Here is where any scaling/gdi mapping should take place
# For now, scale so the dimensions of the window are sized to the
# width of the page. Scale evenly.
# For normal windows, this may be fine--but for a canvas, one
# wants the canvas dimensions, and not the WINDOW dimensions.
if {[winfo class $wid] eq "Canvas"} {
set sc [$wid cget -scrollregion]
# if there is no scrollregion, use width and height.
if {$sc eq ""} {
set window_x [winfo pixels $wid [$wid cget -width]]
set window_y [winfo pixels $wid [$wid cget -height]]
} else {
set window_x [lindex $sc 2]
set window_y [lindex $sc 3]
}
} else {
set window_x [winfo width $wid]
set window_y [winfo height $wid]
}
set printer_x [expr {
( $printargs(pw) - $printargs(lm) - $printargs(rm) ) *
$printargs(resx) / 1000.0
}]
set printer_y [expr {
( $printargs(pl) - $printargs(tm) - $printargs(bm) ) *
$printargs(resy) / 1000.0
}]
set factor_x [expr {$window_x / $printer_x}]
set factor_y [expr {$window_y / $printer_y}]
if {$factor_x < $factor_y} {
set lo $window_y
set ph $printer_y
} else {
set lo $window_x
set ph $printer_x
}
_gdi map $printargs(hDC) -logical $lo -physical $ph \
-offset $printargs(resolution)
# Handling of canvas widgets.
switch [winfo class $wid] {
Canvas {
_print_canvas $printargs(hDC) $wid
}
default {
puts "Can't print items of type [winfo class $wid]. No handler registered"
}
}
# End printing process.
_closepage
_closedoc
}
# _print_canvas
# Main procedure for writing canvas widget items to printer.
# Arguments:
# hdc - The printer handle.
# cw - The canvas widget.
proc _print_canvas {hdc cw} {
variable vtgPrint
variable printargs
# Get information about page being printed to
# print_canvas.CalcSizing $cw
set vtgPrint(canvas.bg) [string tolower [$cw cget -background]]
# Re-write each widget from cw to printer
foreach id [$cw find all] {
set type [$cw type $id]
if {[info commands _print_canvas.$type] eq "_print_canvas.$type"} {
_print_canvas.[$cw type $id] $printargs(hDC) $cw $id
} else {
puts "Omitting canvas item of type $type since there is no handler registered for it"
}
}
}
# These procedures support the various canvas item types, reading the
# information about the item on the real canvas and then writing a
# similar item to the printer.
# _print_canvas.line
# Description:
# Prints a line item.
# Arguments:
# hdc - The printer handle.
# cw - The canvas widget.
# id - The id of the canvas item.
proc _print_canvas.line {hdc cw id} {
variable vtgPrint
variable printargs
set color [_print_canvas.TransColor [$cw itemcget $id -fill]]
if {[string match $vtgPrint(printer.bg) $color]} {
return
}
set coords [$cw coords $id]
set wdth [$cw itemcget $id -width]
set arrow [$cw itemcget $id -arrow]
set arwshp [$cw itemcget $id -arrowshape]
set dash [$cw itemcget $id -dash]
set smooth [$cw itemcget $id -smooth]
set splinesteps [$cw itemcget $id -splinesteps]
set cmdargs {}
if {$wdth > 1} {
lappend cmdargs -width $wdth
}
if {$dash ne ""} {
lappend cmdargs -dash $dash
}
if {$smooth ne ""} {
lappend cmdargs -smooth $smooth
}
if {$splinesteps ne ""} {
lappend cmdargs -splinesteps $splinesteps
}
set result [_gdi line $hdc {*}$coords \
-fill $color -arrow $arrow -arrowshape $arwshp \
{*}$cmdargs]
if {$result ne ""} {
puts $result
}
}
# _print_canvas.arc
# Prints a arc item.
# Args:
# hdc - The printer handle.
# cw - The canvas widget.
# id - The id of the canvas item.
proc _print_canvas.arc {hdc cw id} {
variable vtgPrint
variable printargs
set color [_print_canvas.TransColor [$cw itemcget $id -outline]]
if {[string match $vtgPrint(printer.bg) $color]} {
return
}
set coords [$cw coords $id]
set wdth [$cw itemcget $id -width]
set style [$cw itemcget $id -style]
set start [$cw itemcget $id -start]
set extent [$cw itemcget $id -extent]
set fill [$cw itemcget $id -fill]
set cmdargs {}
if {$wdth > 1} {
lappend cmdargs -width $wdth
}
if {$fill ne ""} {
lappend cmdargs -fill $fill
}
_gdi arc $hdc {*}$coords \
-outline $color -style $style -start $start -extent $extent \
{*}$cmdargs
}
# _print_canvas.polygon
# Prints a polygon item.
# Arguments:
# hdc - The printer handle.
# cw - The canvas widget.
# id - The id of the canvas item.
proc _print_canvas.polygon {hdc cw id} {
variable vtgPrint
variable printargs
set fcolor [_print_canvas.TransColor [$cw itemcget $id -fill]]
if {$fcolor eq ""} {
set fcolor $vtgPrint(printer.bg)
}
set ocolor [_print_canvas.TransColor [$cw itemcget $id -outline]]
if {$ocolor eq ""} {
set ocolor $vtgPrint(printer.bg)
}
set coords [$cw coords $id]
set wdth [$cw itemcget $id -width]
set smooth [$cw itemcget $id -smooth]
set splinesteps [$cw itemcget $id -splinesteps]
set cmdargs {}
if {$smooth ne ""} {
lappend cmdargs -smooth $smooth
}
if {$splinesteps ne ""} {
lappend cmdargs -splinesteps $splinesteps
}
_gdi polygon $hdc {*}$coords \
-width $wdth -fill $fcolor -outline $ocolor {*}$cmdargs
}
# _print_canvas.oval
# Prints an oval item.
# Arguments:
# hdc - The printer handle.
# cw - The canvas widget.
# id - The id of the canvas item.
proc _print_canvas.oval {hdc cw id} {
variable vtgPrint
set fcolor [_print_canvas.TransColor [$cw itemcget $id -fill]]
if {$fcolor eq ""} {
set fcolor $vtgPrint(printer.bg)
}
set ocolor [_print_canvas.TransColor [$cw itemcget $id -outline]]
if {$ocolor eq ""} {
set ocolor $vtgPrint(printer.bg)
}
set coords [$cw coords $id]
set wdth [$cw itemcget $id -width]
_gdi oval $hdc {*}$coords \
-width $wdth -fill $fcolor -outline $ocolor
}
# _print_canvas.rectangle
# Prints a rectangle item.
# Arguments:
# hdc - The printer handle.
# cw - The canvas widget.
# id - The id of the canvas item.
proc _print_canvas.rectangle {hdc cw id} {
variable vtgPrint
set fcolor [_print_canvas.TransColor [$cw itemcget $id -fill]]
if {$fcolor eq ""} {
set fcolor $vtgPrint(printer.bg)
}
set ocolor [_print_canvas.TransColor [$cw itemcget $id -outline]]
if {$ocolor eq ""} {
set ocolor $vtgPrint(printer.bg)
}
set coords [$cw coords $id]
set wdth [$cw itemcget $id -width]
_gdi rectangle $hdc {*}$coords \
-width $wdth -fill $fcolor -outline $ocolor
}
# _print_canvas.text
# Prints a text item.
# Arguments:
# hdc - The printer handle.
# cw - The canvas widget.
# id - The id of the canvas item.
proc _print_canvas.text {hdc cw id} {
variable vtgPrint
variable printargs
set color [_print_canvas.TransColor [$cw itemcget $id -fill]]
# if {"white" eq [string tolower $color]} {return}
# set color black
set txt [$cw itemcget $id -text]
if {$txt eq ""} {
return
}
set coords [$cw coords $id]
set anchr [$cw itemcget $id -anchor]
set bbox [$cw bbox $id]
set wdth [expr {[lindex $bbox 2] - [lindex $bbox 0]}]
set just [$cw itemcget $id -justify]
# Get the real canvas font info and create a compatible font,
# suitable for printer name extraction.
set font [font create {*}[font actual [$cw itemcget $id -font]]]
# Just get the name and family, or some of the _gdi commands will
# fail.
set font [list [font configure $font -family] \
-[font configure $font -size]]
_gdi text $hdc {*}$coords \
-fill $color -text $txt -font $font \
-anchor $anchr -width $wdth -justify $just
}
# _print_canvas.image
# Prints an image item.
# Arguments:
# hdc - The printer handle.
# cw - The canvas widget.
# id - The id of the canvas item.
proc _print_canvas.image {hdc cw id} {
# First, we have to get the image name.
set imagename [$cw itemcget $id -image]
# Now we get the size.
set wid [image width $imagename]
set hgt [image height $imagename]
# Next, we get the location and anchor
set anchor [$cw itemcget $id -anchor]
set coords [$cw coords $id]
_gdi photo $hdc -destination $coords -photo $imagename
}
# _print_canvas.bitmap
# Prints a bitmap item.
# Arguments:
# hdc - The printer handle.
# cw - The canvas widget.
# id - The id of the canvas item.
proc _print_canvas.bitmap {hdc cw id} {
variable option
variable vtgPrint
# First, we have to get the bitmap name.
set imagename [$cw itemcget $id -image]
# Now we get the size.
set wid [image width $imagename]
set hgt [image height $imagename]
#Next, we get the location and anchor.
set anchor [$cw itemcget $id -anchor]
set coords [$cw coords $id]
# Since the GDI commands don't yet support images and bitmaps,
# and since this represents a rendered bitmap, we CAN use
# copybits IF we create a new temporary toplevel to hold the beast.
# If this is too ugly, change the option!
if {[info exist option(use_copybits)]} {
set firstcase $option(use_copybits)
} else {
set firstcase 0
}
if {$firstcase > 0} {
set tl [toplevel .tmptop[expr {int( rand() * 65535 )}] \
-height $hgt -width $wid \
-background $vtgPrint(canvas.bg)]
canvas $tl.canvas -width $wid -height $hgt
$tl.canvas create image 0 0 -image $imagename -anchor nw
pack $tl.canvas -side left -expand false -fill none
tkwait visibility $tl.canvas
update
set srccoords [list 0 0 [expr {$wid - 1}] [expr {$hgt - 1}]]
set dstcoords [list [lindex $coords 0] [lindex $coords 1] [expr {$wid - 1}] [expr {$hgt - 1}]]
_gdi copybits $hdc -window $tl -client \
-source $srccoords -destination $dstcoords
destroy $tl
} else {
_gdi bitmap $hdc {*}$coords \
-anchor $anchor -bitmap $imagename
}
}
# These procedures transform attribute setting from the real
# canvas to the appropriate setting for printing to paper.
# _print_canvas.TransColor
# Does the actual transformation of colors from the
# canvas widget to paper.
# Arguments:
# color - The color value to be transformed.
proc _print_canvas.TransColor {color} {
variable vtgPrint
variable printargs
switch [string toupper $color] {
$vtgPrint(canvas.bg) {return $vtgPrint(printer.bg)}
}
return $color
}
# Initialize all the variables once.
_init_print_canvas
}
#end win32 procedures
}
# Begin X11 procedures. They depends on Cups being installed.
# X11 procedures abstracts print management with a "cups" ensemble command
# cups defaultprinter returns the default printer
# cups getprinters returns a dictionary of printers along
# with printer info
# cups print $printer $data ?$options?
# print the data (binary) on a given printer
# with the provided (supported) options:
# -colormode -copies -format -margins
# -media -nup -orientation
# -prettyprint -title -tzoom
# Some output configuration that on other platforms is managed through
# the printer driver/dialog is configured through the canvas postscript command.
if {[tk windowingsystem] eq "x11"} {
if {[info commands ::tk::print::cups] eq ""} {
namespace eval ::tk::print::cups {
# Pure Tcl cups ensemble command implementation
variable pcache
}
proc ::tk::print::cups::defaultprinter {} {
set default {}
regexp {: ([^[:space:]]+)$} [exec lpstat -d] _ default
return $default
}
proc ::tk::print::cups::getprinters {} {
variable pcache
# Test for existence of lpstat command to obtain the list of
# printers.
# Return an error if not found.
set res {}
try {
set printers [lsort -unique [split [exec lpstat -e] \n]]
foreach printer $printers {
set options [Parseoptions [exec lpoptions -p $printer]]
dict set res $printer $options
}
} trap {POSIX ENOENT} {e o} {
# no such command in PATH
set cmd [lindex [dict get $o -errorstack ] 1 2]
return -code error "Unable to obtain the list of printers.\
Command \"$cmd\" not found.\
Please install the CUPS package for your system."
} trap {CHILDSTATUS} {} {
# command returns a non-0 exit status. Wrong print system?
set cmd [lindex [dict get $o -errorstack ] 1 2]
return -code error "Command \"$cmd\" return with errors"
}
return [set pcache $res]
}
# Parseoptions
# Parse lpoptions -d output. It has three forms
# option-key
# option-key=option-value
# option-key='option value with spaces'
# Arguments:
# data - data to process.
#
proc ::tk::print::cups::Parseoptions {data} {
set res {}
set re {[^ =]+|[^ ]+='[^']+'|[^ ]+=[^ ']+}
foreach tok [regexp -inline -all $re $data] {
lassign [split $tok "="] k v
dict set res $k [string trim $v "'"]
}
return $res
}
proc ::tk::print::cups::print {printer data args} {
variable pcache
if {$printer ni [dict keys $pcache]} {
return -code error "unknown printer or class \"$printer\""
}
set title "Tk print job"
set options {
-colormode -copies -format -margins -media -nup -orientation
-prettyprint -title -tzoom
}
while {[llength $args]} {
set opt [tcl::prefix match $options [lpop args 0]]
switch $opt {
-colormode {
set opts {auto monochrome color}
set val [tcl::prefix match $opts [lpop args 0]]
lappend printargs -o print-color-mode=$val
}
-copies {
set val [lpop args 0]
if {![string is integer -strict $val] ||
$val < 0 || $val > 100
} {
# save paper !!
return -code error "copies must be an integer\
between 0 and 100"
}
lappend printargs -o copies=$val
}
-format {
set opts {auto pdf postscript text}
set val [tcl::prefix match $opts [lpop args 0]]
# lpr uses auto always
}
-margins {
set val [lpop args 0]
if {[llength $val] != 4 ||
![string is integer -strict [lindex $val 0]] ||
![string is integer -strict [lindex $val 1]] ||
![string is integer -strict [lindex $val 2]] ||
![string is integer -strict [lindex $val 3]]
} {
return -code error "margins must be a list of 4\
integers: top left bottom right"
}
lappend printargs -o page-top=[lindex $val 0]
lappend printargs -o page-left=[lindex $val 1]
lappend printargs -o page-bottom=[lindex $val 2]
lappend printargs -o page-right=[lindex $val 3]
}
-media {
set opts {a4 legal letter}
set val [tcl::prefix match $opts [lpop args 0]]
lappend printargs -o media=$val
}
-nup {
set val [lpop args 0]
if {$val ni {1 2 4 6 9 16}} {
return -code error "number-up must be 1, 2, 4, 6, 9 or\
16"
}
lappend printargs -o number-up=$val
}
-orientation {
set opts {portrait landscape}
set val [tcl::prefix match $opts [lpop args 0]]
if {$val eq "landscape"}
lappend printargs -o landscape=true
}
-prettyprint {
lappend printargs -o prettyprint=true
# prettyprint mess with these default values if set
# so we force them.
# these will be overriden if set after this point
if {[lsearch $printargs {cpi=*}] == -1} {
lappend printargs -o cpi=10.0
lappend printargs -o lpi=6.0
}
}
-title {
set title [lpop args 0]
}
-tzoom {
set val [lpop args 0]
if {![string is double -strict $val] ||
$val < 0.5 || $val > 2.0
} {
return -code error "text zoom must be a number between\
0.5 and 2.0"
}
# CUPS text filter defaults to lpi=6 and cpi=10
lappend printargs -o cpi=[expr {10.0 / $val}]
lappend printargs -o lpi=[expr {6.0 / $val}]
}
default {
# shouldn't happen
}
}
}
# build our options
lappend printargs -T $title
lappend printargs -P $printer
# open temp file
set fd [file tempfile fname tk_print]
chan configure $fd -translation binary
chan puts $fd $data
chan close $fd
# add -r to automatically delete temp files
exec lpr {*}$printargs -r $fname &
}
namespace eval ::tk::print::cups {
namespace export defaultprinter getprinters print
namespace ensemble create
}
};# ::tk::print::cups
namespace eval ::tk::print {
variable mcmap
set mcmap(media) [dict create \
[mc "Letter"] letter \
[mc "Legal"] legal \
[mc "A4"] a4]
set mcmap(orient) [dict create \
[mc "Portrait"] portrait \
[mc "Landscape"] landscape]
set mcmap(color) [dict create \
[mc "RGB"] color \
[mc "Grayscale"] gray]
# available print options
variable optlist
set optlist(printer) {}
set optlist(media) [dict keys $mcmap(media)]
set optlist(orient) [dict keys $mcmap(orient)]
set optlist(color) [dict keys $mcmap(color)]
set optlist(number-up) {1 2 4 6 9 16}
# selected options
variable option
set option(printer) {}
# Initialize with sane defaults.
set option(copies) 1
set option(media) [mc "A4"]
# Canvas options
set option(orient) [mc "Portrait"]
set option(color) [mc "RGB"]
set option(czoom) 100
# Text options.
# See libcupsfilter's cfFilterTextToPDF() and cups-filters's texttopdf
# known options:
# prettyprint, wrap, columns, lpi, cpi
set option(number-up) 1
set option(tzoom) 100; # we derive lpi and cpi from this value
set option(pprint) 0 ; # pretty print
set option(margin-top) 20 ; # ~ 7mm (~ 1/4")
set option(margin-left) 20 ; # ~ 7mm (~ 1/4")
set option(margin-right) 20 ; # ~ 7mm (~ 1/4")
set option(margin-bottom) 20 ; # ~ 7mm (~ 1/4")
# array to collect printer information
variable pinfo
array set pinfo {}
# a map for printer state -> human readable message
variable statemap
dict set statemap 3 [mc "Idle"]
dict set statemap 4 [mc "Printing"]
dict set statemap 5 [mc "Printer stopped"]
}
# ttk version of [tk_optionMenu]
# var should be a full qualified varname
proc ::tk::print::ttk_optionMenu {w var args} {
ttk::menubutton $w -textvariable $var -menu $w.menu
menu $w.menu
foreach option $args {
$w.menu add command \
-label $option \
-command [list set $var $option]
}
# return the same value as tk_optionMenu
return $w.menu
}
# _setprintenv
# Set the print environtment - list of printers, state and options.
# Arguments:
# none.
#
proc ::tk::print::_setprintenv {} {
variable option
variable optlist
variable pinfo
set optlist(printer) {}
dict for {printer options} [cups getprinters] {
lappend optlist(printer) $printer
set pinfo($printer) $options
}
# It's an error to not have any printer configured
if {[llength $optlist(printer)] == 0} {
return -code error "No installed printers found.\
Please check or update your CUPS installation."
}
# If no printer is selected, check for the default one
# If none found, use the first one from the list
if {$option(printer) eq ""} {
set option(printer) [cups defaultprinter]
if {$option(printer) eq ""} {
set option(printer) [lindex $optlist(printer) 0]
}
}
}
# _print
# Main printer dialog.
# Select printer, set options, and fire print command.
# Arguments:
# w - widget with contents to print.
#
proc ::tk::print::_print {w} {
variable optlist
variable option
variable pinfo
variable statemap
# default values for dialog widgets
option add *Printdialog*TLabel.anchor e
option add *Printdialog*TMenubutton.Menu.tearOff 0
option add *Printdialog*TMenubutton.width 12
option add *Printdialog*TSpinbox.width 12
# this is tempting to add, but it's better to leave it to
# user's taste.
# option add *Printdialog*Menu.background snow
set class [winfo class $w]
if {$class ni {Text Canvas}} {
return -code error "printing windows of class \"$class\"\
is not supported"
}
# Should this be called with every invocaton?
# Yes. It allows dynamic discovery of newly added printers
# whithout having to restart the app
_setprintenv
set p ._print
destroy $p
# Copy the current values to a dialog's temporary variable.
# This allow us to cancel the dialog discarding any changes
# made to the options
namespace eval dlg {variable option}
array set dlg::option [array get option]
set var [namespace which -variable dlg::option]
# The toplevel of our dialog
toplevel $p -class Printdialog
place [ttk::frame $p.background] -x 0 -y 0 -relwidth 1.0 -relheight 1.0
wm title $p [mc "Print"]
wm resizable $p 0 0
wm attributes $p -type dialog
wm transient $p [winfo toplevel $w]
# The printer to use
set pf [ttk::frame $p.printerf]
pack $pf -side top -fill x -expand no -padx 9p -pady 9p
ttk::label $pf.printerl -text "[mc "Printer"]"
set tv [ttk::treeview $pf.prlist -height 5 \
-columns {printer location state} \
-show headings \
-selectmode browse]
$tv configure \
-yscrollcommand [namespace code [list _scroll $pf.sy]] \
-xscrollcommand [namespace code [list _scroll $pf.sx]]
ttk::scrollbar $pf.sy -command [list $tv yview]
ttk::scrollbar $pf.sx -command [list $tv xview] -orient horizontal
$tv heading printer -text [mc "Printer"]
$tv heading location -text [mc "Location"]
$tv heading state -text [mc "State"]
$tv column printer -width 200 -stretch 0
$tv column location -width 100 -stretch 0
$tv column state -width 250 -stretch 0
foreach printer $optlist(printer) {
set location [dict getdef $pinfo($printer) printer-location ""]
set nstate [dict getdef $pinfo($printer) printer-state 0]
set state [dict getdef $statemap $nstate ""]
switch -- $nstate {
3 - 4 {
set accepting [dict getdef $pinfo($printer) \
printer-is-accepting-jobs ""]
if {$accepting ne ""} {
append state ". " [mc "Printer is accepting jobs"]
}
}
5 {
set reason [dict getdef $pinfo($printer) \
printer-state-reasons ""]
if {$reason ne ""} {
append state ". (" $reason ")"
}
}
}
set id [$tv insert {} end \
-values [list $printer $location $state]]
if {$option(printer) eq $printer} {
$tv selection set $id
}
}
grid $pf.printerl -sticky w
grid $pf.prlist $pf.sy -sticky news
grid $pf.sx -sticky ew
grid remove $pf.sy $pf.sx
bind $tv <<TreeviewSelect>> [namespace code {_onselect %W}]
# Start of printing options
set of [ttk::labelframe $p.optionsframe -text [mc "Options"]]
pack $of -fill x -padx 9p -pady {0 9p} -ipadx 2p -ipady 2p
# COPIES
ttk::label $of.copiesl -text "[mc "Copies"] :"
ttk::spinbox $of.copies -textvariable ${var}(copies) \
-from 1 -to 1000
grid $of.copiesl $of.copies -sticky ew -padx 2p -pady 2p
$of.copies state readonly
# PAPER SIZE
ttk::label $of.medial -text "[mc "Paper"] :"
ttk_optionMenu $of.media ${var}(media) {*}$optlist(media)
grid $of.medial $of.media -sticky ew -padx 2p -pady 2p
if {$class eq "Canvas"} {
# additional options for Canvas output
# SCALE
ttk::label $of.percentl -text "[mc "Scale"] :"
ttk::spinbox $of.percent -textvariable ${var}(czoom) \
-from 5 -to 500 -increment 5
grid $of.percentl $of.percent -sticky ew -padx 2p -pady 2p
$of.percent state readonly
# ORIENT
ttk::label $of.orientl -text "[mc "Orientation"] :"
ttk_optionMenu $of.orient ${var}(orient) {*}$optlist(orient)
grid $of.orientl $of.orient -sticky ew -padx 2p -pady 2p
# COLOR
ttk::label $of.colorl -text "[mc "Output"] :"
ttk_optionMenu $of.color ${var}(color) {*}$optlist(color)
grid $of.colorl $of.color -sticky ew -padx 2p -pady 2p
} elseif {$class eq "Text"} {
# additional options for Text output
# NUMBER-UP
ttk::label $of.nupl -text "[mc "Pages per sheet"] :"
ttk_optionMenu $of.nup ${var}(number-up) {*}$optlist(number-up)
grid $of.nupl $of.nup -sticky ew -padx 2p -pady 2p
# TEXT SCALE
ttk::label $of.tzooml -text "[mc "Text scale"] :"
ttk::spinbox $of.tzoom -textvariable ${var}(tzoom) \
-from 50 -to 200 -increment 5
grid $of.tzooml $of.tzoom -sticky ew -padx 2p -pady 2p
$of.tzoom state readonly
# PRETTY PRINT (banner on top)
ttk::checkbutton $of.pprint -onvalue 1 -offvalue 0 \
-text [mc "Pretty print"] \
-variable ${var}(pprint)
grid $of.pprint - -sticky ew -padx 2p -pady 2p
}
# The buttons frame.
set bf [ttk::frame $p.buttonf]
pack $bf -fill x -expand no -side bottom -padx 9p -pady {0 9p}
ttk::button $bf.print -text [mc "Print"] \
-command [namespace code [list _runprint $w $class $p]]
ttk::button $bf.cancel -text [mc "Cancel"] \
-command [list destroy $p]
pack $bf.print -side right
pack $bf.cancel -side right -padx {0 4.5p}
# cleanup binding
bind $bf <Destroy> [namespace code [list _cleanup $p]]
# Center the window as a dialog.
::tk::PlaceWindow $p
}
# _onselect
# Updates the selected printer when treeview selection changes.
# Arguments:
# tv - treeview pathname.
#
proc ::tk::print::_onselect {tv} {
variable dlg::option
set id [$tv selection]
if {$id eq ""} {
# is this even possible?
set option(printer) ""
} else {
set option(printer) [$tv set $id printer]
}
}
# _scroll
# Implements autoscroll for the printers view
#
proc ::tk::print::_scroll {sbar from to} {
if {$from == 0.0 && $to == 1.0} {
grid remove $sbar
} else {
grid $sbar
$sbar set $from $to
}
}
# _cleanup
# Perform cleanup when the dialog is destroyed.
# Arguments:
# p - print dialog pathname (not used).
#
proc ::tk::print::_cleanup {p} {
namespace delete dlg
}
# _runprint -
# Execute the print command--print the file.
# Arguments:
# w - widget with contents to print.
# class - class of the widget to print (Canvas or Text).
# p - print dialog pathname.
#
proc ::tk::print::_runprint {w class p} {
variable option
variable mcmap
# copy the values back from the dialog
array set option [array get dlg::option]
# get (back) name of media from the translated one
set media [dict get $mcmap(media) $option(media)]
set printargs {}
lappend printargs -title "[tk appname]: Tk window $w"
lappend printargs -copies $option(copies)
lappend printargs -media $media
if {$class eq "Canvas"} {
set colormode [dict get $mcmap(color) $option(color)]
set rotate 0
if {[dict get $mcmap(orient) $option(orient)] eq "landscape"} {
set rotate 1
}
# Scale based on size of widget, not size of paper.
# TODO: is this correct??
set printwidth [expr {
$option(czoom) / 100.0 * [winfo width $w]
}]
set data [encoding convertto iso8859-1 [$w postscript \
-colormode $colormode -rotate $rotate -pagewidth $printwidth]]
} elseif {$class eq "Text"} {
set tzoom [expr {$option(tzoom) / 100.0}]
if {$option(tzoom) != 100} {
lappend printargs -tzoom $tzoom
}
if {$option(pprint)} {
lappend printargs -prettyprint
}
if {$option(number-up) != 1} {
lappend printargs -nup $option(number-up)
}
# these are hardcoded. Should we allow the user to control
# margins?
lappend printargs -margins [list \
$option(margin-top) $option(margin-left) \
$option(margin-bottom) $option(margin-right) ]
# get the data in shape. Cupsfilter's text filter wraps lines
# at character level, not words, so we do it by ourselves.
# compute usable page width in inches
set pw [dict get {a4 8.27 legal 8.5 letter 8.5} $media]
set pw [expr {
$pw - ($option(margin-left) + $option(margin-right)) / 72.0
}]
# set the wrap length at 98% of computed page width in chars
# the 9.8 constant is the product 10.0 (default cpi) * 0.98
set wl [expr {int( 9.8 * $pw / $tzoom )}]
set data [encoding convertto utf-8 [_wrapLines [$w get 1.0 end] $wl]]
}
# launch the job in the background
after idle [namespace code \
[list cups print $option(printer) $data {*}$printargs]]
destroy $p
}
# _wrapLines -
# wrap long lines into lines of at most length wl at word boundaries
# Arguments:
# str - string to be wrapped
# wl - wrap length
#
proc ::tk::print::_wrapLines {str wl} {
# This is a really simple algorithm: it breaks a line on space or tab
# character, collapsing them only at the breaking point.
# Leading space is left as-is.
# For a full fledged line breaking algorithm see
# Unicode® Standard Annex #14 "Unicode Line Breaking Algorithm"
set res {}
incr wl -1
set re [format {((?:^|[^[:blank:]]).{0,%d})(?:[[:blank:]]|$)} $wl]
foreach line [split $str \n] {
lappend res {*}[lmap {_ l} [regexp -all -inline -- $re $line] {
set l
}]
}
return [join $res \n]
}
}
#end X11 procedures
namespace eval ::tk::print {
#begin macOS Aqua procedures
if {[tk windowingsystem] eq "aqua"} {
# makePDF -
# Convert a file to PDF
# Arguments:
# inFilename - file containing the data to convert; format is
# autodetected.
# outFilename - base for filename to write to; conventionally should
# have .pdf as suffix
# Returns:
# The full pathname of the generated PDF.
#
proc makePDF {inFilename outFilename} {
set out [::tk::print::makeTempFile $outFilename]
try {
exec /usr/sbin/cupsfilter $inFilename > $out
} trap NONE {msg} {
# cupsfilter produces a lot of debugging output, which we
# don't want.
regsub -all -line {^(?:DEBUG|INFO):.*$} $msg "" msg
set msg [string trimleft [regsub -all {\n+} $msg "\n"] "\n"]
if {$msg ne ""} {
# Lines should be prefixed with WARN or ERROR now
puts $msg
}
}
return $out
}
}
#end macOS Aqua procedures
namespace export canvas text
namespace ensemble create
}
# tk print --
# This procedure prints the canvas and text widgets using platform-
# native API's.
# Arguments:
# w: Widget to print.
proc ::tk::print {w} {
switch [winfo class $w],[tk windowingsystem] {
"Canvas,win32" {
tailcall ::tk::print::_print_widget $w 0 "Tk Print Output"
}
"Canvas,x11" {
tailcall ::tk::print::_print $w
}
"Canvas,aqua" {
::tk::print::_printcanvas $w
set printfile /tmp/tk_canvas.pdf
::tk::print::_print $printfile
}
"Text,win32" {
tailcall ::tk::print::_print_data [$w get 1.0 end] 1 {Arial 12}
}
"Text,x11" {
tailcall ::tk::print::_print $w
}
"Text,aqua" {
set txtfile [::tk::print::makeTempFile tk_text.txt [$w get 1.0 end]]
try {
set printfile [::tk::print::makePDF $txtfile [file join /tmp tk_text.pdf]]
::tk::print::_print $printfile
} finally {
file delete $txtfile
}
}
default {
return -code error -errorcode {TK PRINT CLASS_UNSUPPORTED} \
"widgets of class [winfo class $w] are not supported on\
this platform"
}
}
}
#Add this command to the tk command ensemble: tk print
#Thanks to Christian Gollwitzer for the guidance here
namespace ensemble configure tk -map \
[dict merge [namespace ensemble configure tk -map] \
{print ::tk::print}]
return
# Local Variables:
# mode: tcl
# fill-column: 78
# End: