High-level wrapper for pdf4tcl

Arjen Markus (26 October 2009) Having experimented a bit with the pdf4tcl package I found it a very useful package for generating PDF files, but there were too many details to take care of: the commands it offers are somewhat low-level. Luckily it is easy to wrap them into commands that hide the details.

(14 December 2009) Since the very first, incomplete version I have worked out some more ideas. The text I use in the example is simply nonsense and it does not cover all current possibilities, but you get the idea anyway.

Note: I have used Snit to create the wrapper interface, as pdf4tcl itself uses Snit too. As it is my first serious use of Snit, I can probably improve on it.

Second note: Things that are not quite satisfactory at the moment are the positioning of page breaks and the occurrence of empty lines at the start of a page. This makes the layout of the pages somewhat sloppy. But I am working on it.

Third note: I am using this to convert the text files I have for my "young programmers' project" - Teach programming to children

Code

::snit::type document {
    variable pdf
    variable yCoord
    variable lineHeight

    variable header_even
    variable header_odd
    variable header_font

    variable footer_even
    variable footer_odd
    variable footer_font

    variable yHeader
    variable yFooter
    variable page_number
    variable doc_font

    constructor {args} {

        #set pdf [::pdf4tcl::new %AUTO% {*}$args]
        set pdf [::pdf4tcl::new %AUTO%]
        $pdf configure -margin {10m 10m 10m 10m}

        set lineHeight [expr {1.3*12}]
        set yCoord     0

        set doc_font "12 Times-Roman"
        $pdf setFont 12 Times-Roman

        set header_font "8 Times-Roman"
        set footer_font "8 Times-Roman"

        set header_even   {"" "" ""}
        set header_odd    {"" "" ""}
        set footer_even   {"" "" "#"}
        set footer_odd    {"#" "" ""}

        foreach {pageWidth pageHeight} [$pdf getDrawableArea] {break}
        set yHeader            0
        set yFooter            [expr {$pageHeight - $lineHeight}]

        set page_number        1
    }

    method chapter {title} {

        $self startPage 0

        $pdf setFont 24 Helvetica-Bold
        set yCoord [$pdf getFontMetric ascend]
        $pdf setTextPosition 0 $yCoord
        $pdf text $title

        $pdf setFont 12 Times-Roman

        set yCoord 36
    }

    method section {title} {

        $pdf setFont 18 Times-Bold
        set lineHeight [expr {1.3*18}]

        $self paragraph $title

        $pdf setFont 12 Times-Roman
        set lineHeight [expr {1.3*12}]
    }

    method subsection {title} {

        $pdf setFont 14 Times-Italic
        set lineHeight [expr {1.3*14}]

        $self paragraph $title

        $pdf setFont 12 Times-Roman
        set lineHeight [expr {1.3*12}]
    }

    method paragraph {text {margin 0}} {

        set text [string map {\n " "} $text]

        foreach {pageWidth pageHeight} [$pdf getDrawableArea] {break}

        while { $text != "" } {
            set text [$pdf drawTextBox $margin $yCoord [expr {$pageWidth-$margin}] $lineHeight $text -align justify]
            set yCoord [expr {$yCoord + $lineHeight}]

            if { $yCoord > $pageHeight-$lineHeight } {
                $self startPage 1
                set yCoord [expr {2*$lineHeight}]
            }
        }
        $pdf newLine
    }

    method newLine {} {

        set yCoord [expr {$yCoord + $lineHeight}]
    }

    method bullet {text} {

        set xcentre [expr {0.5 * $lineHeight}]
        set ycentre [expr {$yCoord + 0.3 * $lineHeight}]

        $pdf circle $xcentre $ycentre 2p -filled 1

        $self paragraph $text $lineHeight
    }

    method bullet2 {text} {

        set xcentre [expr {1.5 * $lineHeight}]
        set ycentre [expr {$yCoord + 0.3 * $lineHeight}]

        $pdf circle $xcentre $ycentre 1.2p -filled 1

        $self paragraph $text [expr {2.0*$lineHeight}]
    }

    method code {text} {

        set doc_font "10 Courier"
        $pdf setFont 10 Courier
        set lineHeight [expr {1.3 * 10}]
        set charWidth  [$pdf getCharWidth " "]

        foreach {pageWidth pageHeight} [$pdf getDrawableArea] {break}

        foreach line [split $text \n] {
            set margin [expr {$charWidth * ([string length [regexp -inline {^ *} $line]] - 2)}]
            $pdf drawTextBox $margin $yCoord [expr {$pageWidth-$margin}] $lineHeight $line
            set yCoord [expr {$yCoord + $lineHeight}]

            if { $yCoord > $pageHeight } {
                $self startPage 1
                set yCoord $lineHeight
            }
        }
        $pdf newLine

        set doc_font "12 Times-Roman"
        $pdf setFont 12 Times-Roman
        set lineHeight [expr {1.3 * 12}]
    }

    method setFont {size font} {

        $pdf setFont $size $font
        set lineHeight [expr {1.3*$size}]
    }

    method canvas {cnv width} {

        tkwait visibility $cnv
        set cwidth  [$cnv cget -width]
        set cheight [$cnv cget -height]

        set width  [expr {25.4*$width}] ;# Unit in pdf4tcl is unclear
        set height [expr {$width * $cheight / double($cwidth)}]

        # -sticky not honoured?
        set paperwidth [lindex [$pdf getDrawableArea] 0]
        $pdf canvas $cnv -sticky n -x [expr {0.5*($paperwidth-$width)}] -y $yCoord -width $width
        set yCoord [expr {$yCoord + $height}]

        destroy $cnv
    }

    method getpdf {} {

        return $pdf
    }

    method write {filename} {

        $pdf write -file $filename
        $pdf destroy
    }

    method {header font} {font} {
        set header_font $font
    }

    method {header both} {left middle right} {
        set header_even [list $left $middle $right]
        set header_odd  $header_even
    }

    method {header even} {left middle right} {
        set header_even [list $left $middle $right]
    }

    method {header odd} {left middle right} {
        set header_odd  [list $left $middle $right]
    }

    method {footer font} {font} {
        set footer_font $font
    }

    method {footer both} {left middle right} {
        set footer_even [list $left $middle $right]
        set footer_odd  $footer_even
    }

    method {footer even} {left middle right} {
        set footer_even [list $left $middle $right]
    }

    method {footer odd} {left middle right} {
        set footer_odd  [list $left $middle $right]
    }

    method startPage {addHeader} {
        $pdf startPage

        set margin 0
        foreach {pageWidth pageHeight} [$pdf getDrawableArea] {break}

        if { $addHeader } {
            $pdf setFont {*}$header_font
            set yCoord $yHeader
            if { $page_number %2 == 0 } {
                set texts $header_even
            } else {
                set texts $header_odd
            }
            foreach text $texts align {left center right} {
                set text [string map [list # $page_number] $text]
                $pdf drawTextBox $margin $yCoord [expr {$pageWidth-$margin}] $lineHeight $text -align $align
            }
        }

        $pdf setFont {*}$footer_font
        set yCoord $yFooter
        if { $page_number %2 == 0 } {
            set texts $footer_even
        } else {
            set texts $footer_odd
        }
        foreach text $texts align {left center right} {
            set text [string map [list # $page_number] $text]
            $pdf drawTextBox $margin $yCoord [expr {$pageWidth-$margin}] $lineHeight $text -align $align
        }

        $pdf setFont {*}$doc_font

        incr page_number
    }
}

document doc

doc chapter "Chapter 1"

doc paragraph "Some longish text that clearly wraps around the right margin of the
page, so that we can demonstrate the aligning behaviour of this command. And of course
how you put in a paragraph of text into the document, using a simple, high-level method."

doc section "Demonstration of a section"
doc subsection "Demonstration of a subsection"

doc paragraph "Does it do pages?"

for { set i 0 } { $i < 50 } { incr i } {
    doc paragraph "Line $i"
}

doc chapter "Chapter 2"
doc paragraph "Again some text - mostly for fun"
doc paragraph "What about two paragraphs?"

doc bullet "Bullet 1, but let's demonstrate the wrapping for this type of
text entity too. After all we do want it to be useful in a fairly general context"
doc newLine

doc bullet "Bullet 2"
doc bullet "Bullet 3"
doc bullet2 "Second level bullet - no more levels"

doc code {
What about writing some text in the style of "code"?
proc putTitle {string} {
    global currentY

    book setFont 24 Times-BoldItalic
    book text [string range $string 3 end-3]
    book setFont 12 Times-Roman

    set currentY [expr {$currentY + 24}]
}
}  ;# End of "doc code"

doc write "mydocument.pdf"

TclOO version

drkoru - 2017-06-18 19:09:12

I agree that the above can be a useful wrapper for those who would like to have some high level functions above those provided by pdf4tcl. Below, I provide the same script with TclOO.

::oo::class create document {
    variable pdf
    variable yCoord
    variable lineHeight

    variable header_even
    variable header_odd
    variable header_font

    variable footer_even
    variable footer_odd
    variable footer_font

    variable yHeader
    variable yFooter
    variable page_number
    variable doc_font

   constructor {args} {

        #set pdf [::pdf4tcl::new %AUTO% {*}$args]
        set pdf [::pdf4tcl::new %AUTO%]
        $pdf configure -margin {10m 10m 10m 10m}

        set lineHeight [expr {1.3*12}]
        set yCoord     0

        set doc_font "12 Times-Roman"
        $pdf setFont 12 Times-Roman

        set header_font "8 Times-Roman"
        set footer_font "8 Times-Roman"

        set header_even   {"" "" ""}
        set header_odd    {"" "" ""}
        set footer_even   {"" "" "#"}
        set footer_odd    {"#" "" ""}

        foreach {pageWidth pageHeight} [$pdf getDrawableArea] {break}
        set yHeader            0
        set yFooter            [expr {$pageHeight - $lineHeight}]

        set page_number        1
    }

    method chapter {title} {

        my startPage 0

        $pdf setFont 24 Helvetica-Bold
        set yCoord [$pdf getFontMetric ascend]
        $pdf setTextPosition 0 $yCoord
        $pdf text $title

        $pdf setFont 12 Times-Roman

        set yCoord 36
    }

    method section {title} {

        $pdf setFont 18 Times-Bold
        set lineHeight [expr {1.3*18}]

        my paragraph $title

        $pdf setFont 12 Times-Roman
        set lineHeight [expr {1.3*12}]
    }

    method subsection {title} {

        $pdf setFont 14 Times-Italic
        set lineHeight [expr {1.3*14}]

        my paragraph $title

        $pdf setFont 12 Times-Roman
        set lineHeight [expr {1.3*12}]
    }

    method paragraph {text {margin 0}} {

        set text [string map {\n " "} $text]

        foreach {pageWidth pageHeight} [$pdf getDrawableArea] {break}

        while { $text != "" } {
            set text [$pdf drawTextBox $margin $yCoord [expr {$pageWidth-$margin}] $lineHeight $text -align justify]
            set yCoord [expr {$yCoord + $lineHeight}]

            if { $yCoord > $pageHeight-$lineHeight } {
                my startPage 1
                set yCoord [expr {2*$lineHeight}]
            }
        }
        $pdf newLine
    }

    method newLine {} {

        set yCoord [expr {$yCoord + $lineHeight}]
    }

    method bullet {text} {

        set xcentre [expr {0.5 * $lineHeight}]
        set ycentre [expr {$yCoord + 0.3 * $lineHeight}]

        $pdf circle $xcentre $ycentre 2p -filled 1

        my paragraph $text $lineHeight
    }

    method bullet2 {text} {

        set xcentre [expr {1.5 * $lineHeight}]
        set ycentre [expr {$yCoord + 0.3 * $lineHeight}]

        $pdf circle $xcentre $ycentre 1.2p -filled 1

        my paragraph $text [expr {2.0*$lineHeight}]
    }

    method code {text} {

        set doc_font "10 Courier"
        $pdf setFont 10 Courier
        set lineHeight [expr {1.3 * 10}]
        set charWidth  [$pdf getCharWidth " "]

        foreach {pageWidth pageHeight} [$pdf getDrawableArea] {break}

        foreach line [split $text \n] {
            set margin [expr {$charWidth * ([string length [regexp -inline {^ *} $line]] - 2)}]
            $pdf drawTextBox $margin $yCoord [expr {$pageWidth-$margin}] $lineHeight $line
            set yCoord [expr {$yCoord + $lineHeight}]

            if { $yCoord > $pageHeight } {
                my startPage 1
                set yCoord $lineHeight
            }
        }
        $pdf newLine

        set doc_font "12 Times-Roman"
        $pdf setFont 12 Times-Roman
        set lineHeight [expr {1.3 * 12}]
    }

    method setFont {size font} {

        $pdf setFont $size $font
        set lineHeight [expr {1.3*$size}]
    }

    method canvas {cnv width} {

        tkwait visibility $cnv
        set cwidth  [$cnv cget -width]
        set cheight [$cnv cget -height]

        set width  [expr {25.4*$width}] ;# Unit in pdf4tcl is unclear
        set height [expr {$width * $cheight / double($cwidth)}]

        # -sticky not honoured?
        set paperwidth [lindex [$pdf getDrawableArea] 0]
        $pdf canvas $cnv -sticky n -x [expr {0.5*($paperwidth-$width)}] -y $yCoord -width $width
        set yCoord [expr {$yCoord + $height}]

        destroy $cnv
    }

    method getpdf {} {

        return $pdf
    }

    method write {filename} {

        $pdf write -file $filename
        $pdf destroy
    }

    method {header font} {font} {
        set header_font $font
    }

    method {header both} {left middle right} {
        set header_even [list $left $middle $right]
        set header_odd  $header_even
    }

    method {header even} {left middle right} {
        set header_even [list $left $middle $right]
    }

    method {header odd} {left middle right} {
        set header_odd  [list $left $middle $right]
    }

    method {footer font} {font} {
        set footer_font $font
    }

    method {footer both} {left middle right} {
        set footer_even [list $left $middle $right]
        set footer_odd  $footer_even
    }

    method {footer even} {left middle right} {
        set footer_even [list $left $middle $right]
    }

    method {footer odd} {left middle right} {
        set footer_odd  [list $left $middle $right]
    }

    method startPage {addHeader} {
        $pdf startPage

        set margin 0
        foreach {pageWidth pageHeight} [$pdf getDrawableArea] {break}

        if { $addHeader } {
            $pdf setFont {*}$header_font
            set yCoord $yHeader
            if { $page_number %2 == 0 } {
                set texts $header_even
            } else {
                set texts $header_odd
            }
            foreach text $texts align {left center right} {
                set text [string map [list # $page_number] $text]
                $pdf drawTextBox $margin $yCoord [expr {$pageWidth-$margin}] $lineHeight $text -align $align
            }
        }

        $pdf setFont {*}$footer_font
        set yCoord $yFooter
        if { $page_number %2 == 0 } {
            set texts $footer_even
        } else {
            set texts $footer_odd
        }
        foreach text $texts align {left center right} {
            set text [string map [list # $page_number] $text]
            $pdf drawTextBox $margin $yCoord [expr {$pageWidth-$margin}] $lineHeight $text -align $align
        }

        $pdf setFont {*}$doc_font

        incr page_number
    }
}

document create doc

doc chapter "Chapter 1"

doc paragraph "Some longish text that clearly wraps around the right margin of the
page, so that we can demonstrate the aligning behaviour of this command. And of course
how you put in a paragraph of text into the document, using a simple, high-level method."

doc section "Demonstration of a section"
doc subsection "Demonstration of a subsection"

doc paragraph "Does it do pages?"

for { set i 0 } { $i < 50 } { incr i } {
    doc paragraph "Line $i"
}

doc chapter "Chapter 2"
doc paragraph "Again some text - mostly for fun"
doc paragraph "What about two paragraphs?"

doc bullet "Bullet 1, but let's demonstrate the wrapping for this type of
text entity too. After all we do want it to be useful in a fairly general context"
doc newLine

doc bullet "Bullet 2"
doc bullet "Bullet 3"
doc bullet2 "Second level bullet - no more levels"

doc code {
What about writing some text in the style of "code"?
proc putTitle {string} {
    global currentY

    book setFont 24 Times-BoldItalic
    book text [string range $string 3 end-3]
    book setFont 12 Times-Roman

    set currentY [expr {$currentY + 24}]
}
}  ;# End of "doc code"

doc write "mydocument.pdf"

APE (24 July 2023) Add a method to add an image (no error checking on path by now)

    method image {filePath} {
        image create photo img -file $filePath
        set imgdata [img data]
        set id [$pdf addRawImage $imgdata]
  
        set iwidth  [$pdf getImageWidth $id]
        set iheight [$pdf getImageHeight $id]
        lassign [$pdf getDrawableArea] pageWidth pageHeight

        # take the minimum width between image width and paper width
        set width [expr min($iwidth,$pageWidth)]
        set height [expr {$width * $iheight / double($iwidth)}]

        # check height space, start a new page if necessary
        set yImgCoord [expr {$yCoord + $height}]
        if { $yImgCoord > $pageHeight } {
            my startPage 0
            set yCoord 0
        }

        $pdf putImage $id 0 $yCoord -width $width
        set yCoord [expr {$yCoord + $height}]
    }