Canvas postscript - text rendering bug

IDG Feb. 26 2003 The code to generate postscript from canvases appears to contain a bug.

[Please remember to report bugs to the author/maintainer of a package to ensure the problem is addressed appropriately.]

Unless your value of tk scaling is exactly 1.0, text is drawn too small by the postscript output. The following script illustrates the problem

 puts "tk scaling is [tk scaling]"
 pack [canvas .c -width 300p -height 100p]
 .c create rectangle 10p  10p  270p 30p
 .c create text 10p 20p -anchor w -font {courier 18}\
    -text "This fills the rectangle"
 update idletasks
 .c postscript -file

The Tk display will show a text string that just about fills the rectangle (modulo system-dependent font mojo). If you print the generated postscript file, or view it with Ghostscript, you will see that the text is too small by a factor [tk scaling].

To see what is happening, here are selections from the ps file, generated on a system where tk scaling is 1.87281105991. (uninteresting bits snipped)

  0.534 0.534 scale

All subsequent ps drawing is scaled by this factor. This number is the reciprocal of tk scaling

  18.7281105990783 172.271889400922 moveto 486.930875576037 0 rlineto

Code that draws the top of the rectangle. The ps line is nominally drawn 486.9 points long, but because of the scaling, the real length is 260 points, which is correct as specified by the tcl script.

 /Courier findfont 18 scalefont ISOEncode setfont

Code that selects the font to draw the text. Note that 18pt is used, unchanged from the Tk font size. Because of the scale, this is actually drawn as a 9.6pt font, and appears much too small. If this scaling method is used, a 33.7pt font should have been specified here.

This seems like a generic problem. It happens with 8.0, 8.3 and 8.4 on windows, 8.3 on linux and 8.4 on solaris, which are the systems I have been able to test.

You might think you can avoid the problem by drawing rectangle and text with pixel sizes instead of points; it doesn't help. Nor does mixing pixels and points or centimeters.

So it looks to me like a bug that needs to be fixed. (8.5 anyone ?)

For now, how can we work around it?

  • Set tk scaling to 1. Problem goes away. This is ok if you are in control of the whole app. Disastrous if e.g. you are writing a package.
  • Use a -fontmap parameter to the canvas postscript command. Not fun if you have a lot of fonts.
  • Capture the postscript output and massage all the scalefont commands before you let it loose. - Any hidden gotchas here?

MGS From what I can see:

  • the postscript output is not dependent on the value of tk scaling at the time of output.
  • canvas coords of items are stored in pixel values.
  • font properties (sizes) are stored in points (if you created them that way).

So it looks like the only way to output postscript where fonts and other items are scaled correctly relative to each other, is to scale all canvas items by the reciprocal of the tk scaling value. With me so far? i.e.

 proc canvas:dump {c file} {
   set sf [expr {1.0 / [tk scaling]}]
   $c scale all 0 0 $sf $sf

   if { [catch {
     $c postscript -file $file
   } error] } {
     puts stderr "\[$::errorCode\] $error"
   } else {
     puts "dumped [file size $file] bytes to file $file"

   set sf [tk scaling]
   $c scale all 0 0 $sf $sf

 # demo code (from above)
   pack [canvas .c -width 300p -height 100p]
   .c create rectangle 10p  10p  270p 30p \
     -outline red
   set font [font create -family courier -size 18]
   set id [.c create text 10p 20p \
     -anchor w \
     -font {courier 18} \
     -text "This fills the rectangle" \
   .c create rectangle [.c bbox $id]
   update idletasks
   canvas:dump .c

IDG Feb. 27 2003. Cool! Yet slightly yucky. I had not realized the canvas scale command doesn't scale text. A careful lawyerly reading of the manual does seem to indicate that it doesn't.

Your suggestion shows another way: use named fonts on the canvas, and scale them up before generating the postscript:

 foreach f $canvas_font_list {
    font configure $f -size [expr round([font configure $f -size] * [tk scaling])]

Then generate the ps and put the fonts back to normal. This has the advantage that lines etc. in the ps will be the size you expect, and the disadvantage that font requires integer sizes, so you can't guarantee to get the scaling exactly right.

MGS Or else play with the -fontmap option. I haven't looked into this much, but ...

 set font [font create -family courier -size 18 -weight bold]
 global fontmap ; # not sure if this needs to be global or not
 set fontmap($font) [list Courier-Bold [expr 18 * [tk scaling]]]
 parray fontmap ; # just for info
 .c postscript -file -fontmap fontmap

Combine this with the example(s) above.

Even though the fontmap array can have non-integer values for sizes, it looks like Tk truncates to an integer in the ps file. This looks like a bug. If you hand-edit the ps file, you can put in decimal values.

A more generic way to map all fonts used in the canvas would be a good idea. Maybe I'll look into this later ...

CSB: "Capture the postscript output and massage all the scalefont commands before you let it loose. - Any hidden gotchas here?"

Thought I'd just pipe in that (in a perltk app) we've apparently 'solved' the problem by redefining 'scalefont' in the postscript output:

   /scalefont {1.54 mul scalefont} bind def

Where, in this case, 1.54 is the the tk scaling return value of tk scaling for the canvas.

This is a single line solution to the whole problem which, thus far, hasn't caused any problems.

Beware 1/3/2011 - Different bug: If I use the font "Dax-Light" (or, I think, any font with a dash in it) on a canvas and then postscript the canvas, the PS output says ...font required... Dax-light (with a lowercase 'l'). This causes GS to not find the font file. Any tips?