#  can2svg.tcl ---
#  
#      This file provides translation from canvas commands to XML/SVG format.
#      
#  Copyright (c) 2002-2007  Mats Bengtsson
#  
#  This file is distributed under BSD style license.
#
# $Id: can2svg.tcl,v 1.26 2008-01-27 08:19:36 matben Exp $
# 
# ########################### USAGE ############################################
#
#   NAME
#      can2svg - translate canvas command to SVG.
#      
#   SYNOPSIS
#      can2svg canvasCmd ?options?
#           canvasCmd is everything except the widget path name.
#           
#      can2svg::canvas2file widgetPath fileName ?options?
#           options:   -height
#                      -width
#      
#      can2svg::can2svg canvasCmd ?options?
#           options:    -httpbasedir        path
#                       -imagehandler       tclProc
#                       -ovalasellipse      0|1
#                       -reusedefs          0|1
#                       -uritype            file|http
#                       -usestyleattribute  0|1
#                       -usetags            0|all|first|last
#                       -windowitemhandler  tclProc
#                       
#      can2svg::config ?options?
#           options:        -allownewlines      0
#                        -filtertags         ""
#                        -httpaddr           localhost
#                        -ovalasellipse      0
#                        -reusedefs          1
#                        -uritype            file
#                        -usetags            all
#                        -usestyleattribute  1
#                       -windowitemhandler  tclProc
#
# ########################### CHANGES ##########################################
#
#   0.1      first release
#   0.2      URI encoded image file path
#   0.3      uses xmllists more, added svgasxmllist
#
# ########################### TODO #############################################
# 
#   handle units (m->mm etc.) 
#   better support for stipple patterns
#   how to handle tk editing? DOM?
#   
#   ...

# We need URN encoding for the file path in images. From my whiteboard code.

package require uriencode
package require tinyfileutils

package provide can2svg 0.3


namespace eval can2svg {

    namespace export can2svg canvas2file
    
    variable confopts
    array set confopts {
        -allownewlines        0
        -filtertags           ""
        -httpaddr             localhost
        -ovalasellipse        0
        -reusedefs            1
        -uritype              file
        -usetags              all
        -usestyleattribute    1
        -windowitemhandler    ""
    }
    set confopts(-httpbasedir) [info script]
    
    variable formatArrowMarker
    variable formatArrowMarkerLast
    
    # The key into this array is 'arrowMarkerDef_$col_$a_$b_$c', where
    # col is color, and a, b, c are the arrow's shape.
    variable defsArrowMarkerArr

    # Similarly for stipple patterns.
    variable defsStipplePatternArr

    # This shouldn't be hardcoded!
    variable defaultFont {Helvetica 12}

    variable pi 3.14159265359
    variable anglesToRadians [expr $pi/180.0]
    variable grayStipples {gray75 gray50 gray25 gray12}
        
    # Make 4x4 squares. Perhaps could be improved.
    variable stippleDataArr
    
    set stippleDataArr(gray75)  \
      {M 0 0 h3  M 0 1 h1 M 2 1 h2
       M 0 2 h3  M 0 3 h1 M 2 3 h1}
    set stippleDataArr(gray50)  \
      {M 0 0 h1 M 2 0 h1  M 1 1 h1 M 3 1 h1
       M 0 2 h1 M 2 2 h1  M 1 3 h1 M 3 3 h1}
    set stippleDataArr(gray25)  \
      {M 3 0 h1 M 1 1 h1 M 3 2 h1 M 1 3 h1}
    set stippleDataArr(gray12)  \
      {M 1 1 h1 M 3 3 h1}
    
}

proc can2svg::config {args} {
    variable confopts
    
    set options [lsort [array names confopts -*]]
    set usage [join $options ", "]
    if {[llength $args] == 0} {
        set result {}
        foreach name $options {
            lappend result $name $confopts($name)
        }
        return $result
    }
    regsub -all -- - $options {} options
    set pat ^-([join $options |])$
    if {[llength $args] == 1} {
        set flag [lindex $args 0]
        if {[regexp -- $pat $flag]} {
            return $confopts($flag)
        } else {
            return -code error "Unknown option $flag, must be: $usage"
        }
    } else {
        foreach {flag value} $args {
            if {[regexp -- $pat $flag]} {
                set confopts($flag) $value
            } else {
                return -code error "Unknown option $flag, must be: $usage"
            }
        }
    }
}

# can2svg::can2svg --
#
#       Make xml out of a canvas command, widgetPath removed.
#       
# Arguments:
#       cmd         canvas create commands without prepending widget path.
#       args    -httpbasedir        path
#               -imagehandler       tclProc
#               -ovalasellipse      0|1
#               -reusedefs          0|1
#               -uritype            file|http
#               -usestyleattribute  0|1
#               -usetags            0|all|first|last
#       
# Results:
#   xml data

proc can2svg::can2svg {cmd args} {

    set xml ""
    foreach xmllist [eval {svgasxmllist $cmd} $args] {
        append xml [MakeXML $xmllist]
    }
    return $xml
}

# can2svg::svgasxmllist --
#
#       Make a list of xmllists out of a canvas command, widgetPath removed.
#       
# Arguments:
#       cmd         canvas create command without prepending widget path.
#       args    -httpbasedir        path
#               -imagehandler       tclProc
#               -ovalasellipse      0|1
#               -reusedefs          0|1
#               -uritype            file|http
#               -usestyleattribute  0|1
#               -usetags            0|all|first|last
#       
# Results:
#       a list of xmllist = {tag attrlist isempty cdata {child1 child2 ...}}

proc can2svg::svgasxmllist {cmd args} {
    
    variable confopts
    variable defsArrowMarkerArr
    variable defsStipplePatternArr
    variable defaultFont
    variable grayStipples
        
    set nonum_ {[^0-9]}
    set wsp_ {[ ]+}
    set xmlLL [list]
    
    array set argsA [array get confopts]
    array set argsA $args
    set args [array get argsA]
    
    if {![string equal [lindex $cmd 0] "create"]} {
        return
    }
    
    set type [lindex $cmd 1]
    set rest [lrange $cmd 2 end]
    
    # Separate coords from options.
    set indopt [lsearch -regexp $rest "-${nonum_}"]
    if {$indopt < 0} {
        set ind end
        set opts [list]
    } else {
        set ind [expr $indopt - 1]
        set opts [lrange $rest $indopt end]
    }
    
    # Flatten coordinate list!
    set coo [lrange $rest 0 $ind]
    if {[llength $coo] == 1} {
        set coo [lindex $coo 0]
    }
    array set optA $opts
    
    # Is the item in normal state? If not, return.
    if {[info exists optA(-state)] && $optA(-state) != "normal"} {
        return
    }
    
    # Figure out if we've got a spline.
    set haveSpline 0
    if {[info exists optA(-smooth)] && ($optA(-smooth) != "0") &&  \
      [info exists optA(-splinesteps)] && ($optA(-splinesteps) > 2)} {
        set haveSpline 1
    }
    if {[info exists optA(-fill)]} {
        set fillValue $optA(-fill)
        if {![regexp {#[0-9]+} $fillValue]} {
            set fillValue [FormatColorName $fillValue]
        }
    } else {
        set fillValue black
    }
    if {[string length $argsA(-filtertags)] && [info exists optA(-tags)]} {
        set tag [uplevel #0 $argsA(-filtertags) [list $optA(-tags)]]
        set idAttr [list id $tag]
    } elseif {($argsA(-usetags) != "0") && [info exists optA(-tags)]} {
        
        # Remove any 'current' tag.
        set optA(-tags) \
          [lsearch -all -not -inline $optA(-tags) current]
        
        switch -- $argsA(-usetags) {
            all {                        
                set idAttr [list id $optA(-tags)]
            }
            first {
                set idAttr [list id [lindex $optA(-tags) 0]]
            }
            last {
                set idAttr [list id [lindex $optA(-tags) end]]
            }
        }
    } else {
        set idAttr ""
    }
    
    # If we need a marker (arrow head) need to make that first.
    if {[info exists optA(-arrow)] && ![string equal $optA(-arrow) "none"]} {
        if {[info exists optA(-arrowshape)]} {
            
            # Make a key of the arrowshape list into the array.
            regsub -all -- $wsp_ $optA(-arrowshape) _ shapeKey
            set arrowKey ${fillValue}_${shapeKey}
            set arrowShape $optA(-arrowshape)
        } else {
            set arrowKey ${fillValue}
            set arrowShape {8 10 3}
        }
        if {!$argsA(-reusedefs) || \
          ![info exists defsArrowMarkerArr($arrowKey)]} {
            set defsArrowMarkerArr($arrowKey)  \
              [eval {MakeArrowMarker} $arrowShape {$fillValue}]
            set xmlLL \
              [concat $xmlLL $defsArrowMarkerArr($arrowKey)]
        }
    }
    
    # If we need a stipple bitmap, need to make that first. Limited!!!
    # Only: gray12, gray25, gray50, gray75
    foreach key {-stipple -outlinestipple} {
        if {[info exists optA($key)] &&  \
          ([lsearch $grayStipples $optA($key)] >= 0)} {
            set stipple $optA($key)
            if {![info exists defsStipplePatternArr($stipple)]} {
                set defsStipplePatternArr($stipple)  \
                  [MakeGrayStippleDef $stipple]
            }
            lappend xmlLL $defsStipplePatternArr($stipple)
        }
    }
    puts "can2svg::svgasxmllist cmd=$cmd, args=$args"
    
    switch -- $type {
        
        arc {
            
            # Had to do it the hard way! (?)
            # "Wrong" coordinate system :-(
            set attr [CoordsToAttr $type $coo $opts elem]            
            if {[string length $idAttr] > 0} {
                set attr [concat $attr $idAttr]
            }
            set attr [concat $attr [MakeAttrList \
              $type $opts $argsA(-usestyleattribute)]]
            lappend xmlLL [MakeXMLList $elem -attrlist $attr]
        }
        bitmap - image {
            if {[info exists optA(-image)]} {
                set elem "image"
                set attr [eval {MakeImageAttr $coo $opts} $args]
                if {[string length $idAttr] > 0} {
                    set attr [concat $attr $idAttr]
                }
                set subEs [list]
                if {[info exists argsA(-imagehandler)]} {
                    set subE [uplevel #0 $argsA(-imagehandler) [list $cmd] $args]
                    if {[llength $subE]} {
                        set subEs [list $subE]
                    }
                }
                lappend xmlLL [MakeXMLList $elem -attrlist $attr -subtags $subEs]
            }
        }
        line {
            set attr [CoordsToAttr $type $coo $opts elem]            
            if {[string length $idAttr] > 0} {
                set attr [concat $attr $idAttr]
            }                    
            set attr [concat $attr [MakeAttrList \
              $type $opts $argsA(-usestyleattribute)]]
            lappend xmlLL [MakeXMLList $elem -attrlist $attr]
        }
        oval {
            set attr [CoordsToAttr $type $coo $opts elem]            
            foreach {x y w h} [NormalizeRectCoords $coo] break
            if {[expr $w == $h] && !$argsA(-ovalasellipse)} {
                # set elem "circle";# circle needs an r: not an rx & ry
                set elem "ellipse"
            } else {
                set elem "ellipse"
            }
            if {[string length $idAttr] > 0} {
                set attr [concat $attr $idAttr]
            }
            set attr [concat $attr [MakeAttrList \
              $type $opts $argsA(-usestyleattribute)]]
            lappend xmlLL [MakeXMLList $elem -attrlist $attr]
        }
        polygon {
            set attr [CoordsToAttr $type $coo $opts elem]            
            if {[string length $idAttr] > 0} {
                set attr [concat $attr $idAttr]
            }
            set attr [concat $attr [MakeAttrList \
              $type $opts $argsA(-usestyleattribute)]]
            lappend xmlLL [MakeXMLList $elem -attrlist $attr]
        }
        rectangle {
            set attr [CoordsToAttr $type $coo $opts elem]            
            if {[string length $idAttr] > 0} {
                set attr [concat $attr $idAttr]
            }
            set attr [concat $attr [MakeAttrList \
              $type $opts $argsA(-usestyleattribute)]]
            lappend xmlLL [MakeXMLList $elem -attrlist $attr]
        }
        text {
            set elem "text"
            set chdata ""
            set nlines 1
            if {[info exists optA(-font)]} {
                set theFont $optA(-font)
            } else {
                set theFont $defaultFont
            }
            set ascent [font metrics $theFont -ascent]
            set lineSpace [font metrics $theFont -linespace]
            if {[info exists optA(-text)]} {
                set chdata $optA(-text)
                
                if {[info exists optA(-width)]} {
                    
                    # MICK O'DONNELL: if the text is wrapped in the wgt, we need
                    # to simulate linebreaks
                    # 
                    # If the item has got -width != 0 then we must wrap it ourselves
                    # using newlines since the -text does not have extra newlines
                    # at these linebreaks.
                    set lines [split $chdata \n]
                    set newlines {}
                    foreach line $lines {
                        set lines2 [SplitWrappedLines $line $theFont $optA(-width)]
                        set newlines [concat $newlines $lines2]
                    }
                    set chdata [join $newlines \n]
                    if {!$argsA(-allownewlines) || \
                      ([llength $newlines] > [llength $lines])} {
                        set nlines [expr [regexp -all "\n" $chdata] + 1]
                    }
                } else {
                    if {!$argsA(-allownewlines)} {
                        set nlines [expr [regexp -all "\n" $chdata] + 1]
                    }
                }
            }
            
            # Figure out the coords of the first baseline.
            set anchor center
            if {[info exists optA(-anchor)]} {
                set anchor $optA(-anchor)
            }                                        
            
            foreach {xbase ybase}  \
              [GetTextSVGCoords $coo $anchor $chdata $theFont $nlines] {}
            
            set attr [list "x" $xbase "y" $ybase]
            # angle is negated in order to fit 
            # canvas coordinate system
            if {[info exists optA(-angle)]} {
                set ang [expr {-1.0*$optA(-angle)}]
                set val "rotate($ang $xbase,$ybase)"
                lappend attr "transform" $val
            }

            if {[string length $idAttr] > 0} {
                set attr [concat $attr $idAttr]
            }
            set attr [concat $attr [MakeAttrList \
              $type $opts $argsA(-usestyleattribute)]]
            set dy 0
            if {$nlines > 1} {
                
                # Use the 'tspan' trick here.
                set subList {}
                foreach line [split $chdata "\n"] {
                    lappend subList [MakeXMLList "tspan"  \
                      -attrlist [list "x" $xbase "dy" $dy] -chdata $line]
                    set dy $lineSpace
                }
                lappend xmlLL [MakeXMLList $elem -attrlist $attr \
                  -subtags $subList]
            } else {
                lappend xmlLL [MakeXMLList $elem -attrlist $attr \
                  -chdata $chdata]
            }
        }
        window {
            
            # There is no svg for this; must be handled by application layer.            
            #puts "window: $cmd"
            if {[string length $argsA(-windowitemhandler)]} {
                set xmllist \
                  [uplevel #0 $argsA(-windowitemhandler) [list $cmd] $args]
                if {[llength $xmllist]} {
                    lappend xmlLL $xmllist
                }
            }
        }
    }
    return $xmlLL
}

# can2svg::CoordsToAttr --
#
#       Makes a list of attributes corresponding to type and coords.
#       
# Arguments:
#
#       
# Results:
#       a list of attributes.

proc can2svg::CoordsToAttr {type coo opts svgElementVar} {
    upvar $svgElementVar elem 

    array set optA $opts
    
    # Figure out if we've got a spline.
    set haveSpline 0
    if {[info exists optA(-smooth)] && ($optA(-smooth) != "0") &&  \
      [info exists optA(-splinesteps)] && ($optA(-splinesteps) > 2)} {
        set haveSpline 1
    }
    set attr {}

    switch -- $type {
        arc {
            set elem "path"
            set data [MakeArcPath $coo $opts]
            set attr [list "d" $data]
        }
        bitmap - image {
            array set __optA $opts
            if {[info exists __optA(-image)]} {
                set elem "image"
                set attr [ImageCoordsToAttr $coo $opts]
            }
        }
        line {
            if {$haveSpline} {
                set elem "path"
                set data [ParseSplineToPath $type $coo]
                set attr [list "d" $data]
            } else {
                set elem "polyline"
                set attr [list "points" $coo]
            }   
        }
        oval {
            
            # Assume SVG ellipse.
            set elem "ellipse"
            foreach {x y w h} [NormalizeRectCoords $coo] break
            set attr [list  \
              "cx" [expr $x + $w/2.0] "cy" [expr $y + $h/2.0]  \
              "rx" [expr $w/2.0]      "ry" [expr $h/2.0]]
        }
        polygon {
            if {$haveSpline} {
                set elem "path"
                set data [ParseSplineToPath $type $coo]
                set attr [list "d" $data]
            } else {
                set elem "polygon"
                set attr [list "points" $coo]
            }
        }
        rectangle {
            set elem "rect"
            foreach {x y w h} [NormalizeRectCoords $coo] break
            set attr [list "x" $x "y" $y "width" $w "height" $h]
        }
        text {
            set elem "text"
            # ?
        }
    }
    return $attr
}

# can2svg::MakeArcPath --
# 
#       Makes a path using A commands from an arc.
#       Conversion from center to endpoint parameterization.
#       From: http://www.w3.org/TR/2003/REC-SVG11-20030114

proc can2svg::MakeArcPath {coo opts} {
    
    variable anglesToRadians
    variable pi

    # Canvas defaults.
    array set optA {
        -extent 90
        -start  0
        -style  pieslice
    }
    array set optA $opts

    # Extract center and radius from bounding box.
    foreach {x1 y1 x2 y2} $coo break
    set cx [expr ($x1 + $x2)/2.0]
    set cy [expr ($y1 + $y2)/2.0]
    set rx [expr abs($x1 - $x2)/2.0]
    set ry [expr abs($y1 - $y2)/2.0]

    set start  [expr $anglesToRadians * $optA(-start)]
    set extent [expr $anglesToRadians * $optA(-extent)]

    # NOTE: direction of angles are opposite for Tk and SVG!    
    set theta1 [expr -1*$start]
    set delta  [expr -1*$extent]
    set theta2 [expr $theta1 + $delta]
    set phi 0.0

    # F.6.4 Conversion from center to endpoint parameterization.
    set x1 [expr $cx + $rx * cos($theta1) * cos($phi) -  \
      $ry * sin($theta1) * sin($phi)]
    set y1 [expr $cy + $rx * cos($theta1) * sin($phi) +  \
      $ry * sin($theta1) * cos($phi)]
    set x2 [expr $cx + $rx * cos($theta2) * cos($phi) -  \
      $ry * sin($theta2) * sin($phi)]
    set y2 [expr $cy + $rx * cos($theta2) * sin($phi) +  \
      $ry * sin($theta2) * cos($phi)]
    
    set fa [expr {abs($delta) > $pi} ? 1 : 0]
    set fs [expr {$delta > 0.0} ? 1 : 0]
    
    set data [format "M %.1f %.1f A" $x1 $y1]
    append data [format " %.1f %.1f %.1f %1d %1d %.1f %.1f"  \
      $rx $ry $phi $fa $fs $x2 $y2]
    
    switch -- $optA(-style) {
        arc {
            # empty.
        }
        chord {
            append data " Z"
        }
        pieslice {
            append data [format " L %.1f %.1f Z" $cx $cy]
        }
    }
    return $data
}

# can2svg::MakeArcPathNonA --
# 
#       Makes a path without any A commands from an arc.

proc can2svg::MakeArcPathNonA {coo opts} {

    variable anglesToRadians

    array set optA $opts
    
    foreach {x1 y1 x2 y2} $coo break
    set cx [expr ($x1 + $x2)/2.0]
    set cy [expr ($y1 + $y2)/2.0]
    set rx [expr abs($x1 - $x2)/2.0]
    set ry [expr abs($y1 - $y2)/2.0]
    set rmin [expr $rx > $ry ? $ry : $rx]
    
    # This approximation gives a maximum half pixel error.
    set deltaPhi [expr 2.0/sqrt($rmin)]
    set extent   [expr $anglesToRadians * $optA(-extent)]
    set start    [expr $anglesToRadians * $optA(-start)]
    set nsteps   [expr int(abs($extent)/$deltaPhi) + 2]
    set delta    [expr $extent/$nsteps]
    set data [format "M %.1f %.1f L"  \
      [expr $cx + $rx*cos($start)] [expr $cy - $ry*sin($start)]]
    for {set i 0} {$i <= $nsteps} {incr i} {
        set phi [expr $start + $i * $delta]
        append data [format " %.1f %.1f"  \
          [expr $cx + $rx*cos($phi)] [expr $cy - $ry*sin($phi)]]
    }
    if {[info exists optA(-style)]} {
        
        switch -- $optA(-style) {
            chord {
                append data " Z"
            }
            pieslice {
                append data [format " %.1f %.1f Z" $cx $cy]
            }
        }
    } else {
        
        # Pieslice is the default.
        append data [format " %.1f %.1f Z" $cx $cy]
    }
    return $data
}

# can2svg::MakeAttrList --
# 
#       Handles the use of style attributes or presenetation attributes.

proc can2svg::MakeAttrList {type opts usestyleattribute} {
    
    if {$usestyleattribute} {
        set attrList [list style [MakeStyleAttr $type $opts]]
    } else {
        set attrList [MakeStyleList $type $opts]
    }
    return $attrList
}

# can2svg::MakeStyleAttr --
#
#       Produce the SVG style attribute from the canvas item options.
#
# Arguments:
#       type        tk canvas widget item type
#       opts
#       
# Results:
#       The SVG style attribute as a a string.

proc can2svg::MakeStyleAttr {type opts} {
    
    set style ""
    foreach {key value} [MakeStyleList $type $opts] {
        append style "${key}: ${value}; "
    }
    return [string trim $style]
}

proc can2svg::MakeStyleList {type opts args} {
    
    array set argsA {
        -setdefaults 1 
    }
    array set argsA $args
    
    # Defaults for everything except text.
    if {$argsA(-setdefaults) && ![string equal $type "text"]} {
        array set styleArr {fill none stroke black}
    }
    set fillCol black
    
    foreach {key value} $opts {
        
        switch -- $key {
            -arrow {
                set arrowValue $value
            }
            -arrowshape {
                set arrowShape $value
            }
            -capstyle {
                if {[string equal $value "projecting"]} {
                    set value "square"
                }
                if {![string equal $value "butt"]} {
                    set styleArr(stroke-linecap) $value
                }
            }
            -dash {
                set dashValue $value
            }
            -dashoffset {
                if {$value != 0} {
                    set styleArr(stroke-dashoffset) $value
                }
            }
            -extent {
                # empty
            }
            -fill {
                
                # Need to translate names to hex spec.
                if {![regexp {#[0-9]+} $value]} {
                    set value [FormatColorName $value]
                }
                set fillCol $value                
                if {[string equal $type "line"]} {
                    set styleArr(stroke) [MapEmptyToNone $value]
                } else {
                    set styleArr(fill) [MapEmptyToNone $value]
                }
            }
            -font {
                array set styleArr [MakeFontStyleList $value]
                
            }
            -joinstyle {
                set styleArr(stroke-linejoin) $value                
            }
            -outline {
                set styleArr(stroke) [MapEmptyToNone $value]
            }
            -outlinestipple {
                set outlineStippleValue $value
            }
            -start {
                # empty
            }
            -stipple {
                set stippleValue $value
            }
            -width {
                set styleArr(stroke-width) $value
            }
        }
    }
    
    # If any arrow specify its marker def url key.
    if {[info exists arrowValue]} {
        if {[info exists arrowShape]} {        
            foreach {a b c} $arrowShape break
            set arrowIdKey "arrowMarkerDef_${fillCol}_${a}_${b}_${c}"
            set arrowIdKeyLast "arrowMarkerLastDef_${fillCol}_${a}_${b}_${c}"
        } else {
            set arrowIdKey "arrowMarkerDef_${fillCol}"
            set arrowIdKeyLast $arrowIdKey
        }
        
        switch -- $arrowValue {
            first {
                set styleArr(marker-start) "url(#$arrowIdKey)"
            }
            last {
                set styleArr(marker-end) "url(#$arrowIdKeyLast)"
            }
            both {
                set styleArr(marker-start) "url(#$arrowIdKey)"
                set styleArr(marker-end) "url(#$arrowIdKeyLast)"
            }
        }
    }
    
    if {[info exists stippleValue]} {
        
        # Overwrite any existing.
        set styleArr(fill) "url(#tile[string trimleft $stippleValue @])"
    }
    if {[info exists outlineStippleValue]} {
        
        # Overwrite any existing.
        set styleArr(stroke) "url(#tile[string trimleft $stippleValue @])"
    }
    
    # Transform dash value.
    if {[info exists dashValue]} {
                
        # Two different syntax here.                
        if {[regexp {[\.,\-_]} $dashValue]} {
            
            # .=2 ,=4 -=6 space=4    times stroke width.
            # A space enlarges the... space.
            # Not foolproof!
            regsub -all -- {[^ ]} $dashValue "& " dash
            regsub -all -- "   "  $dash  "12 " dash
            regsub -all -- "  "   $dash  "8 " dash
            regsub -all -- " "    $dash  "4 " dash
            regsub -all -- {\.}   $dash  "2 " dash
            regsub -all -- {,}    $dash  "4 " dash
            regsub -all -- {-}    $dash  "6 " dash                    
        
            # Multiply with stroke width if > 1.
            if {[info exists styleArr(stroke-width)] &&  \
              ($styleArr(stroke-width) > 1)} {
                set width $styleArr(stroke-width)
                set dashOrig $dash
                set dash {}
                foreach num $dashOrig {
                    lappend dash [expr int($width * $num)]
                }
            }
            set styleArr(stroke-dasharray) [string trim $dash]
        } else {
            set dashValue [string trim $dashValue]
            if {$dashValue ne ""} {
                set styleArr(stroke-dasharray) $dashValue
            }
        }
    }
    if {[string equal $type "polygon"]} {
        set styleArr(fill-rule) "evenodd"
    }        
    return [array get styleArr]
}

proc can2svg::FormatColorName {value} {

    if {[string length $value] == 0} {
        return $value
    }

    switch -- $value {
        black - white - red - green - blue {
            set col $value
        }
        default {
        
            # winfo rgb . white -> 65535 65535 65535
            foreach rgb [winfo rgb . $value] {
                lappend rgbx [expr $rgb >> 8]
            }
            set col [eval {format "#%02x%02x%02x"} $rgbx]
        }
    }
    return $col
}

# can2svg::MakeFontStyleList --
# 
#       Takes a tk font description and returns a flat style array.
#       
# Arguments:
#       fontDesc    a tk font description 
#       
# Results:
#       flat style array

proc can2svg::MakeFontStyleList {fontDesc} {    

    # MICK Modify - break a named font into its component fields
    set font [lindex $fontDesc 0]
    if {[lsearch -exact [font names] $font] > -1} {
        
        # This is a font name
        set styleArr(font-family) [font config $font -family]
        set fsize [font config $font -size]
        if {$fsize > 0} {
            # points
            set funit pt
        } else {
            # pixels (actually user units)
            set funit px
        }        
        set styleArr(font-size) "[expr abs($fsize)]$funit"
        if {[font config $font -slant] == "italic"} {
            set styleArr(font-style) italic
        }
        if {[font config $font -weight] == "bold"} {
            set styleArr(font-weight) bold
        }
        if {[font config $font -underline]} {
            set styleArr(text-decoration) underline
        }
        if {[font config $font -overstrike]} {
            set styleArr(text-decoration) overline
        }
    } else {
        set styleArr(font-family) [lindex $fontDesc 0]
        if {[llength $fontDesc] > 1} {
            # Mick: added pt at end
            set fsize [lindex $fontDesc 1]
            if {$fsize > 0} {
                # points
                set funit pt
            } else {
                # pixels (actually user units)
                set funit px
            }
            set styleArr(font-size) "[expr abs($fsize)]$funit"
        }
        if {[llength $fontDesc] > 2} {
            set tkstyle [lindex $fontDesc 2]
            switch -- $tkstyle {
                bold {
                    set styleArr(font-weight) $tkstyle
                }
                italic {
                    set styleArr(font-style) $tkstyle
                }
                underline {
                    set styleArr(text-decoration) underline
                }
                overstrike {
                    set styleArr(text-decoration) overline
                }
            }
        }                
    }
    return [array get styleArr]
}

# can2svg::SplitWrappedLines --
# 
# MICK O'DONNELL: added code to split wrapped lines
# This is actally only needed for text items with -width != 0.
# If -width = 0 then just return it.

proc can2svg::SplitWrappedLines {line font wgtWidth} {

     # If the text is shorter than the widget width, no need to wrap
     # If the wgtWidth comes out as 0, don't wrap
     if {$wgtWidth == 0 || [font measure $font $line] <= $wgtWidth} {
        return [list $line]
     }

     # Wrap the line
     set width 0
     set endchar 0
     while {$width < $wgtWidth} {
        set substr [string range $line 0 [incr endchar]]
        set width [font measure $font $substr]
     }

     # Go back till we find a nonwhite char
     set char [string index $line $endchar]
     set default [expr $endchar -1]
     while {[BreakChar $char] == 0} {
        if {$endchar == 0} {
            # we got to the front without breaking, so break midword
            set endchar $default
            break
        }
        set char [string index $line [incr endchar -1]]
     }
     set first [string range $line 0 $endchar]
     set rest [string range $line [expr $endchar+1] end]
     return [concat [list $first] [SplitWrappedLines $rest $font $wgtWidth]]
}

proc can2svg::BreakChar {char} {
     if [string is space $char] {return 1}
     if {$char == "-"} {return 1}
     if {$char == ","} {return 1}
     return 0
}

# can2svg::MakeImageAttr --
#
#       Special code is needed to make the attributes for an image item.
#       
# Arguments:
#       elem 
#       
# Results:
#   

proc can2svg::MakeImageAttr {coo opts args} {
    variable confopts
    
    array set optA {-anchor nw}
    array set optA $opts
    array set argsA $args
    
    set attrList [ImageCoordsToAttr $coo $opts]

    # We should make this an URI.
    set image $optA(-image)
    set fileName [$image cget -file]
    if {$fileName ne ""} {
        if {[string equal $argsA(-uritype) "file"]} {
            set uri [FileUriFromLocalFile $fileName]
        } elseif {[string equal $argsA(-uritype) "http"]} {
            set uri [HttpFromLocalFile $fileName]
        }
        lappend attrList "xlink:href" $uri
    } else {
        # Unclear if we can use base64 data in svg.
    }    
    return $attrList
}

# Function � �: can2svg::ImageCoordsToAttr 
# ------------------------------ ------------------------------ --------- 
# Returns � � : list of x y width and height including description 
# Parameters �: coo �- coordinates of the image 
# � � � � � � � opts - argument list -anchor nw ... 
#
# Description : 
# fixme (Roger) 01/25/2008 :Why not using the bounding box? 
#
# Written � � : 2002-2007, Mats 
# Rewritten � : 01/25/2008, Roger 
# ------------------------------ ------------------------------ --------- 

proc can2svg::ImageCoordsToAttr {coo opts} { 
    
    array set optArr {-anchor nw}
    array set optArr $opts
    
    if {![info exists optArr(-image)]} { 
        return -code error "Missing -image option; can't parse that" 
    } 
    set theImage $optArr(-image) 

    lassign $coo x0 y0
    set w [image width $theImage]
    set h [image height $theImage]
    
    set x [expr {$x0 - $w/2.0}] 
    set y [expr {$y0 - $h/2.0}] 

    if { "center" ne $optArr(-anchor) } { 
        foreach orientation [split $optArr(-anchor) {}] { 
            switch $orientation { 
                n { set y $y0 } 
                s { set y [expr {$y0 - $w}] } 
                e { set x [expr {$x0 - $h}] } 
                w { set x $x0 } 
                default {} 
            } 
        } 
    } 
    return [list "x" $x "y" $y "width" $w "height" $h] 
}

proc can2svg::ImageCoordsToAttrBU {coo opts} {
    array set optA {-anchor nw}
    array set optA $opts
    if {[info exists optA(-image)]} {
        set theImage $optA(-image)
        set w [image width $theImage]
        set h [image height $theImage]
    } else {
        return -code error "Missing -image option; can't parse that"
    }
    foreach {x0 y0} $coo break
    
    switch -- $optA(-anchor) {
        nw {
            set x $x0
            set y $y0
        }
        n {
            set x [expr $x0 - $w/2.0]
            set y $y0
        }
        ne {
            set x [expr $x0 - $w]
            set y $y0
        }
        e {
            set x $x0
            set y [expr $y0 - $h/2.0]
        }
        se {
            set x [expr $x0 - $w]
            set y [expr $y0 - $h]
        }
        s {
            set x [expr $x0 - $w/2.0]
            set y [expr $y0 - $h]
        }
        sw {
            set x $x0
            set y [expr $y0 - $h]
        } 
        w {
            set x $x0
            set y [expr $y0 - $h/2.0]
        }
        center {
            set x [expr $x0 - $w/2.0]
            set y [expr $y0 - $h/2.0]
        }
    }
    set attrList [list "x" $x "y" $y "width" $w "height" $h]
    return $attrList
}

# can2svg::GetTextSVGCoords --
# 
#       Figure out the baseline coords of the svg text element from
#       the canvas text item.
#
# Arguments:
#       coo         {x y}
#       anchor
#       chdata      character data, newlines included.
#       
# Results:
#       raw xml data of the marker def element.

proc can2svg::GetTextSVGCoords {coo anchor chdata theFont nlines} {
    
    foreach {x y} $coo break
    set ascent [font metrics $theFont -ascent]
    set lineSpace [font metrics $theFont -linespace]

    # If not anchored to the west it gets more complicated.
    if {![string match $anchor "*w*"]} {
        
        # Need to figure out the extent of the text.
        if {$nlines <= 1} {
            set textWidth [font measure $theFont $chdata]
        } else {
            set textWidth 0
            foreach line [split $chdata "\n"] {
                set lineWidth [font measure $theFont $line]
                if {$lineWidth > $textWidth} {
                    set textWidth $lineWidth
                }
            }
        }
    }
    
    switch -- $anchor {
        nw {
            set xbase $x
            set ybase [expr $y + $ascent]
        }
        w {
            set xbase $x
            set ybase [expr $y - $nlines*$lineSpace/2.0 + $ascent]
        }
        sw {
            set xbase $x
            set ybase [expr $y - $nlines*$lineSpace + $ascent]
        }
        s {
            set xbase [expr $x - $textWidth/2.0]
            set ybase [expr $y - $nlines*$lineSpace + $ascent]
        }
        se {
            set xbase [expr $x - $textWidth]
            set ybase [expr $y - $nlines*$lineSpace + $ascent]
        }
        e {
            set xbase [expr $x - $textWidth]
            set ybase [expr $y - $nlines*$lineSpace/2.0 + $ascent]
        }
        ne {
            set xbase [expr $x - $textWidth]
            set ybase [expr $y + $ascent]
        } 
        n {
            set xbase [expr $x - $textWidth/2.0]
            set ybase [expr $y + $ascent]
        }
        center {
            set xbase [expr $x - $textWidth/2.0]
            set ybase [expr $y - $nlines*$lineSpace/2.0 + $ascent]
        }
    }
    
    return [list $xbase $ybase]
}

# can2svg::ParseSplineToPath --
# 
#       Make the path data string for a bezier.
#
# Arguments:
#       type        canvas type: line or polygon
#       coo         its coordinate list
#       
# Results:
#       path data string

proc can2svg::ParseSplineToPath {type coo} {
    
    set npts [expr [llength $coo]/2]
    
    # line is open ended while the polygon must be closed.
    # Need to construct a closed smooth polygon with path instructions.

    switch -- $npts {
        1 {
            set data "M [lrange $coo 0 1]"
        }
        2 {
            set data "M [lrange $coo 0 1] L [lrange $coo 2 3]"                                
        }
        3 {
            set data "M [lrange $coo 0 1] Q [lrange $coo 2 5]"
        }
        default {
            if {[string equal $type "polygon"]} {
                set x0s [expr ([lindex $coo 0] + [lindex $coo end-1])/2.]
                set y0s [expr ([lindex $coo 1] + [lindex $coo end])/2.]
                set data "M $x0s $y0s"
                    
                # Add Q1 and Q2 points.
                append data " Q [lrange $coo 0 1]"
                set x0 [expr ([lindex $coo 0] + [lindex $coo 2])/2.]
                set y0 [expr ([lindex $coo 1] + [lindex $coo 3])/2.]
                append data " $x0 $y0"
                set xctrlp [lindex $coo 2]
                set yctrlp [lindex $coo 3]
                set tind 4
            } else {
                set data "M [lrange $coo 0 1]"
                    
                # Add Q1 and Q2 points.
                append data " Q [lrange $coo 2 3]"
                set x0 [expr ([lindex $coo 2] + [lindex $coo 4])/2.]
                set y0 [expr ([lindex $coo 3] + [lindex $coo 5])/2.]
                append data " $x0 $y0"
                set xctrlp [lindex $coo 4]
                set yctrlp [lindex $coo 5]
                set tind 6
            }
            append data " T"                                
            foreach {x y} [lrange $coo $tind end-2] {
                #puts "x=$x, y=$y, xctrlp=$xctrlp, yctrlp=$yctrlp"
                
                # The T point is the midpoint between the
                # two control points.
                set x0 [expr ($x + $xctrlp)/2.0]
                set y0 [expr ($y + $yctrlp)/2.0]
                set xctrlp $x
                set yctrlp $y
                append data " $x0 $y0"
                #puts "data=$data"
            }
            if {[string equal $type "polygon"]} {
                set x0 [expr ([lindex $coo end-1] + $xctrlp)/2.0]
                set y0 [expr ([lindex $coo end] + $yctrlp)/2.0]
                append data " $x0 $y0"
                append data " $x0s $y0s"
            } else {
                append data " [lrange $coo end-1 end]"
            }
            #puts "data=$data"
        }
    }
    return $data
}

# can2svg::MakeArrowMarker --
# 
#       Make the xml for an arrow marker def element.
#
# Arguments:
#       a           arrows length along its symmetry line
#       b           arrows total length
#       c           arrows half width
#       col         its color
#       
# Results:
#       a list of xmllists of the marker def elements, both start and last.

proc can2svg::MakeArrowMarker {a b c col} {
    
    variable formatArrowMarker
    variable formatArrowMarkerLast
    
    unset -nocomplain formatArrowMarker
    
    if {![info exists formatArrowMarker]} {
        
        # "M 0 c, b 0, a c, b 2*c Z" for the start marker.
        # "M 0 0, b c, 0 2*c, b-a c Z" for the last marker.
        set data "M 0 %s, %s 0, %s %s, %s %s Z"
        set style "fill: %s; stroke: %s;"
        set attr [list "d" $data "style" $style]
        set arrowList [MakeXMLList "path" -attrlist $attr]
        set markerAttr [list "id" %s "markerWidth" %s "markerHeight" %s  \
          "refX" %s "refY" %s "orient" "auto"]
        set defElemList [MakeXMLList "defs" -subtags  \
          [list [MakeXMLList "marker" -attrlist $markerAttr \
          -subtags [list $arrowList] ] ] ]
        set formatArrowMarker $defElemList
        
        # ...and the last arrow marker.
        set dataLast "M 0 0, %s %s, 0 %s, %s %s Z"
        set attrLast [list "d" $dataLast "style" $style]
        set arrowLastList [MakeXMLList "path" -attrlist $attrLast]
        set defElemLastList [MakeXMLList "defs" -subtags  \
          [list [MakeXMLList "marker" -attrlist $markerAttr \
          -subtags [list $arrowLastList] ] ] ]
        set formatArrowMarkerLast $defElemLastList
    }
    set idKey "arrowMarkerDef_${col}_${a}_${b}_${c}"
    set idKeyLast "arrowMarkerLastDef_${col}_${a}_${b}_${c}"
    
    # Figure out the order of all %s substitutions.
    set markerXML [format $formatArrowMarker $idKey  \
      $b [expr 2*$c] 0 $c  \
      $c $b $a $c $b [expr 2*$c] $col $col]
    set markerLastXML [format $formatArrowMarkerLast $idKeyLast  \
      $b [expr 2*$c] $b $c \
      $b $c [expr 2*$c] [expr $b-$a] $c $col $col]
    
    return [list $markerXML $markerLastXML]
}

# can2svg::MakeGrayStippleDef --
#
#

proc can2svg::MakeGrayStippleDef {stipple} {
    
    variable stippleDataArr
    
    set pathList [MakeXMLList "path" -attrlist  \
      [list "d" $stippleDataArr($stipple) "style" "stroke: black; fill: none;"]]
    set patterAttr [list "id" "tile$stipple" "x" 0 "y" 0 "width" 4 "height" 4 \
      "patternUnits" "userSpaceOnUse"]
    set defElemList [MakeXMLList "defs" -subtags  \
      [list [MakeXMLList "pattern" -attrlist $patterAttr \
      -subtags [list $pathList] ] ] ]
    
    return $defElemList
}

# can2svg::MapEmptyToNone --
#
#
# Arguments:
#       elem 
#       
# Results:
#   

proc can2svg::MapEmptyToNone {val} {

    if {[string length $val] == 0} {
        return "none"
    } else {
        return $val
    }
}

# can2svg::NormalizeRectCoords --
#
#
# Arguments:
#       elem 
#       
# Results:
#   

proc can2svg::NormalizeRectCoords {coo} {
    
    foreach {x1 y1 x2 y2} $coo {}
    return [list [expr $x2 > $x1 ? $x1 : $x2]  \
      [expr $y2 > $y1 ? $y1 : $y2]  \
      [expr abs($x1-$x2)]  \
      [expr abs($y1-$y2)]]
}

# can2svg::makedocument --
#
#  Adds the prefix and suffix elements to make a complete XML/SVG
#  document.
#
#  Extended by Claude Rubinson (cjr@grundrisse.org) in 2026 to support
#  specification of the viewBox and preserveAspectRatio attributes.
#  Since the latter is dependent upon the former, they are
#  conditionally inserted together or not at all.  By default, both
#  are omitted.
#
#  Specs at 
#  https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/viewBox

proc can2svg::makedocument {
			    width
			    height
			    xml
			    {viewBox {}}
			    {preserveAspectRatio {xMidYMid meet}}
			} {

    set pre "<?xml version='1.0'?>\n\
      <!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\
      \"Graphics/SVG/1.1/DTD/svg11.dtd\">"
    set svgStart "<svg width='$width' height='$height'"
    if {[llength $viewBox]} {
	append svgStart " viewBox='$viewBox' preserveAspectRatio='$preserveAspectRatio'"
    }
    append svgStart " xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>"
    set svgEnd "</svg>"
    return "${pre}\n${svgStart}\n${xml}${svgEnd}"
}

# can2svg::canvas2file --
#
#       Takes everything on a canvas widget, translates it to XML/SVG,
#       and puts it on a file.
#       
# Arguments:
#       wcan        the canvas widget path
#       path        the file path
#       args:   -height
#               -width 
#               -viewBox
#               -preserveAspectRatio
#       
# Results:
#   

proc can2svg::canvas2file {wcan path args} {
    variable confopts
    variable defsArrowMarkerArr
    variable defsStipplePatternArr
    
    array set argsA [array get confopts]
    lassign [$wcan cget -scrollregion] x y width height
    array set argsA [list -width $width -height $height -viewBox {} -preserveAspectRatio {xMidYMid meet}]
    array set argsA $args
    
    # Need to make a fresh start for marker def's.
    unset -nocomplain defsArrowMarkerArr defsStipplePatternArr
  
    set fd [open $path w]

    # This could have been done line by line.
    set xml ""
    foreach id [$wcan find all] {
        set type [$wcan type $id]
        set opts [$wcan itemconfigure $id]
        set opcmd {}
        foreach opt $opts {
            set op [lindex $opt 0]
            set val [lindex $opt 4]
            
            # Empty val's except -fill can be stripped off.
            if {![string equal $op "-fill"] && ([string length $val] == 0)} {
                continue
            }
            lappend opcmd $op $val
        }
        set co [$wcan coords $id]
        set cmd [concat "create" $type $co $opcmd]
        append xml "\t[eval {can2svg $cmd} [array get argsA]]\n"        
    }
    puts $fd [makedocument $argsA(-width) $argsA(-height) $xml $argsA(-viewBox) $argsA(-preserveAspectRatio)]
    close $fd
}

# can2svg::MakeXML --
#
#       Creates raw xml data from a hierarchical list of xml code.
#       This proc gets called recursively for each child.
#       It makes also internal entity replacements on character data.
#       Mixed elements aren't treated correctly generally.
#       
# Arguments:
#       xmlList     a list of xml code in the format described in the header.
#       
# Results:
#       raw xml data.

proc can2svg::MakeXML {xmlList} {
        
    # Extract the XML data items.
    foreach {tag attrlist isempty chdata childlist} $xmlList {}
    set rawxml "<$tag"
    foreach {attr value} $attrlist {
        append rawxml " ${attr}='${value}'"
    }
    if {$isempty} {
        append rawxml "/>"
        return $rawxml
    } else {
        append rawxml ">"
    }
    
    # Call ourselves recursively for each child element. 
    # There is an arbitrary choice here where childs are put before PCDATA.
    foreach child $childlist {
        append rawxml [MakeXML $child]
    }
    
    # Make standard entity replacements.
    if {[string length $chdata]} {
        append rawxml [XMLCrypt $chdata]
    }
    append rawxml "</$tag>"
    return $rawxml
}

# can2svg::MakeXMLList --
#
#       Build an element list given the tag and the args.
#
# Arguments:
#       tagname:    the name of this element.
#       args:       
#           -empty   0|1      Is this an empty tag? If $chdata 
#                             and $subtags are empty, then whether 
#                             to make the tag empty or not is decided 
#                             here. (default: 1)
#            -attrlist {attr1 value1 attr2 value2 ..}   Vars is a list 
#                             consisting of attr/value pairs, as shown.
#            -chdata $chdata   ChData of tag (default: "").
#            -subtags {$subchilds $subchilds ...} is a list containing xmldata
#                             of $tagname's subtags. (default: no sub-tags)
#       
# Results:
#       a list suitable for can2svg::MakeXML.

proc can2svg::MakeXMLList {tagname args} {
        
    # Fill in the defaults.
    array set xmlarr {-isempty 1 -attrlist {} -chdata {} -subtags {}}
    
    # Override the defults with actual values.
    if {[llength $args] > 0} {
        array set xmlarr $args
    }
    if {!(($xmlarr(-chdata) eq "") && ($xmlarr(-subtags) eq ""))} {
        set xmlarr(-isempty) 0
    }
    
    # Build sub elements list.
    set sublist [list]
    foreach child $xmlarr(-subtags) {
        lappend sublist $child
    }
    set xmlList [list $tagname $xmlarr(-attrlist) $xmlarr(-isempty)  \
      $xmlarr(-chdata) $sublist]
    return $xmlList
}

# can2svg::XMLCrypt --
#
#       Makes standard XML entity replacements.
#
# Arguments:
#       chdata:     character data.
#       
# Results:
#       chdata with XML standard entities replaced.

proc can2svg::XMLCrypt {chdata} {

    foreach from {\& < > {"} {'}}   \
      to {{\&amp;} {\&lt;} {\&gt;} {\&quot;} {\&apos;}} {
        regsub -all $from $chdata $to chdata
    }        
    return $chdata
}

# can2svg::FileUriFromLocalFile --
#
#       Not foolproof!

proc can2svg::FileUriFromLocalFile {path} {
        
    # Quote the disallowed characters according to the RFC for URN scheme.
    # ref: RFC2141 sec2.2
    return file://[uriencode::quotepath $path]
}

# can2svg::HttpFromLocalFile --
#
#       Translates an absolute file path to an uri encoded http address.

proc can2svg::HttpFromLocalFile {path} {
    variable confopts
    
    set relPath [::tfileutils::relative $confopts(-httpbasedir) $path]
    set relPath [uriencode::quotepath $relPath]
    return "http://$confopts(-httpaddr)/$relPath"
}

# -------------------------------------------------------------------------------
