#  can2svg.tcl ---
#  
#      This file provides translation from canvas commands to XML/SVG format.
#      
#  Copyright (c) 2002-2004  Mats Bengtsson
#  
#  This particular package is BSD licensed. 
#
# $Id: can2svg.tcl,v 1.18 2004/12/02 15:22:07 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
#                       -ovalasellipse 0|1
#                       -reusedefs     0|1
#                       -uritype    file|http
#                       -usestyleattribute 0|1
#                       -usetags    0|all|first|last
#                       -windowitemhandler
#                       
#      can2svg::config ?options?
#           options:	-allownewlines      0
#	                -filtertags         ""
#	                -httpaddr           localhost
#	                -ovalasellipse      0
#	                -reusedefs          1
#	                -uritype            file
#	                -usetags            all
#	                -usestyleattribute  1
#                       -windowitemhandler
#
# ########################### 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 1 0 h2 M 0 2 h2 m 1 0 h1 M 0 3 h3}
    set stippleDataArr(gray50)  \
      {M 0 0 h1 m 1 0 h1 M 1 1 h1 m 1 0 h1 \
      M 0 2 h1 m 1 0 h1 M 1 3 h1 m 1 0 h1}
    set stippleDataArr(gray25)  \
      {M 0 0 h1 M 2 1 h1 M 1 2 h1 M 3 3 h1}
    set stippleDataArr(gray12) {M 0 0 h1 M 2 2 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
#               -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
#               -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 xmlListList {}
    
    array set argsArr [array get confopts]
    array set argsArr $args
    set args [array get argsArr]
    
    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 {}
    } 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 optArr $opts
    
    # Figure out if we've got a spline.
    set haveSpline 0
    if {[info exists optArr(-smooth)] && ($optArr(-smooth) != "0") &&  \
      [info exists optArr(-splinesteps)] && ($optArr(-splinesteps) > 2)} {
	set haveSpline 1
    }
    if {[info exists optArr(-fill)]} {
	set fillValue $optArr(-fill)
	if {![regexp {#[0-9]+} $fillValue]} {
	    set fillValue [FormatColorName $fillValue]
	}
    } else {
	set fillValue black
    }
    if {[string length $argsArr(-filtertags)] && [info exists optArr(-tags)]} {
	set tag [uplevel #0 $argsArr(-filtertags) [list $optArr(-tags)]]
	set idAttr [list id $tag]
    } elseif {($argsArr(-usetags) != "0") && [info exists optArr(-tags)]} {
	
	# Remove any 'current' tag.
	set optArr(-tags) \
	  [lsearch -all -not -inline $optArr(-tags) current]
	
	switch -- $argsArr(-usetags) {
	    all {			
		set idAttr [list id $optArr(-tags)]
	    }
	    first {
		set idAttr [list id [lindex $optArr(-tags) 0]]
	    }
	    last {
		set idAttr [list id [lindex $optArr(-tags) end]]
	    }
	}
    } else {
	set idAttr ""
    }
    
    # If we need a marker (arrow head) need to make that first.
    if {[info exists optArr(-arrow)] && ![string equal $optArr(-arrow) "none"]} {
	if {[info exists optArr(-arrowshape)]} {
	    
	    # Make a key of the arrowshape list into the array.
	    regsub -all -- $wsp_ $optArr(-arrowshape) _ shapeKey
	    set arrowKey ${fillValue}_${shapeKey}
	    set arrowShape $optArr(-arrowshape)
	} else {
	    set arrowKey ${fillValue}
	    set arrowShape {8 10 3}
	}
	if {!$argsArr(-reusedefs) || \
	  ![info exists defsArrowMarkerArr($arrowKey)]} {
	    set defsArrowMarkerArr($arrowKey)  \
	      [eval {MakeArrowMarker} $arrowShape {$fillValue}]
	    set xmlListList \
	      [concat $xmlListList $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 optArr($key)] &&  \
	  ([lsearch $grayStipples $optArr($key)] >= 0)} {
	    set stipple $optArr($key)
	    if {![info exists defsStipplePatternArr($stipple)]} {
		set defsStipplePatternArr($stipple)  \
		  [MakeGrayStippleDef $stipple]
	    }
	    lappend xmlListList $defsStipplePatternArr($stipple)
	}
    }
    
    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 $argsArr(-usestyleattribute)]]
	    lappend xmlListList [MakeXMLList $elem -attrlist $attr]
	}
	bitmap - image {
	    set elem "image"
	    set attr [eval {MakeImageAttr $coo $opts} $args]
	    if {[string length $idAttr] > 0} {
		set attr [concat $attr $idAttr]
	    }
	    lappend xmlListList [MakeXMLList $elem -attrlist $attr]
	}
	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 $argsArr(-usestyleattribute)]]
	    lappend xmlListList [MakeXMLList $elem -attrlist $attr]
	}
	oval {
	    set attr [CoordsToAttr $type $coo $opts elem]	    
	    foreach {x y w h} [NormalizeRectCoords $coo] break
	    if {[expr $w == $h] && !$argsArr(-ovalasellipse)} {
		set elem "circle"
	    } else {
		set elem "ellipse"
	    }
	    if {[string length $idAttr] > 0} {
		set attr [concat $attr $idAttr]
	    }
	    set attr [concat $attr [MakeAttrList \
	      $type $opts $argsArr(-usestyleattribute)]]
	    lappend xmlListList [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 $argsArr(-usestyleattribute)]]
	    lappend xmlListList [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 $argsArr(-usestyleattribute)]]
	    lappend xmlListList [MakeXMLList $elem -attrlist $attr]
	}
	text {
	    set elem "text"
	    set nlines 1
	    if {[info exists optArr(-font)]} {
		set theFont $optArr(-font)
	    } else {
		set theFont $defaultFont
	    }
	    set ascent [font metrics $theFont -ascent]
	    set lineSpace [font metrics $theFont -linespace]
	    if {[info exists optArr(-text)]} {
		set chdata $optArr(-text)
		
		# 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 $optArr(-width)]
		    set newlines [concat $newlines $lines2]
		}
		set chdata [join $newlines \n]
		if {!$argsArr(-allownewlines) || \
		  ([llength $newlines] > [llength $lines])} {
		    set nlines [expr [regexp -all "\n" $chdata] + 1]
		}
	    } else {
		set chdata ""
	    }
	    
	    # Figure out the coords of the first baseline.
	    set anchor center
	    if {[info exists optArr(-anchor)]} {
		set anchor $optArr(-anchor)
	    }		    		    
	    
	    foreach {xbase ybase}  \
	      [GetTextSVGCoords $coo $anchor $chdata $theFont $nlines] {}
	    
	    set attr [list "x" $xbase "y" $ybase]
	    if {[string length $idAttr] > 0} {
		set attr [concat $attr $idAttr]
	    }
	    set attr [concat $attr [MakeAttrList \
	      $type $opts $argsArr(-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 xmlListList [MakeXMLList $elem -attrlist $attr \
		  -subtags $subList]
	    } else {
		lappend xmlListList [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 $argsArr(-windowitemhandler)]} {
		set xmllist \
		  [uplevel #0 $argsArr(-windowitemhandler) [list $cmd] $args]
		if {[llength $xmllist]} {
		    lappend xmlListList $xmllist
		}
	    }
	}
    }
    return $xmlListList
}

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

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

    array set optArr $opts
    
    # Figure out if we've got a spline.
    set haveSpline 0
    if {[info exists optArr(-smooth)] && ($optArr(-smooth) != "0") &&  \
      [info exists optArr(-splinesteps)] && ($optArr(-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 {
	    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 optArr {
	-extent 90
	-start  0
	-style  pieslice
    }
    array set optArr $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 * $optArr(-start)]
    set extent [expr $anglesToRadians * $optArr(-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 -- $optArr(-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 optArr $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 * $optArr(-extent)]
    set start    [expr $anglesToRadians * $optArr(-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 optArr(-style)]} {
	
	switch -- $optArr(-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 argsArr {
	-setdefaults 1 
    }
    array set argsArr $args
    
    # Defaults for everything except text.
    if {$argsArr(-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$stippleValue)"
    }
    if {[info exists outlineStippleValue]} {
	
	# Overwrite any existing.
	set styleArr(stroke) "url(#tile$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 styleArr(stroke-dasharray) $value
	}
    }
    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 optArr {-anchor nw}
    array set optArr $opts
    array set argsArr $args
    
    set attrList [ImageCoordsToAttr $coo $opts]

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

proc can2svg::ImageCoordsToAttr {coo opts} {
    
    array set optArr {-anchor nw}
    array set optArr $opts
    if {[info exists optArr(-image)]} {
	set theImage $optArr(-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 -- $optArr(-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.
#
# Arguments:
#       elem 
#       
# Results:
#   

proc can2svg::makedocument {width height xml} {
    
    set pre "<?xml version='1.0'?>\n\
      <!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\"\
      \"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">"
    
    set svgStart "<svg width='$width' height='$height'>"
    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 
#       
# Results:
#   

proc can2svg::canvas2file {wcan path args} {
    variable confopts
    variable defsArrowMarkerArr
    variable defsStipplePatternArr
    
    array set argsArr [array get confopts]
    foreach {x y width height} [$wcan cget -scrollregion] break
    array set argsArr [list -width $width -height $height]
    array set argsArr $args
    set args [array get argsArr]
    
    # 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} $args]\n"	
    }
    puts $fd [makedocument $argsArr(-width) $argsArr(-height) $xml]
    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) == "") && ($xmlarr(-subtags) == ""))} {
	set xmlarr(-isempty) 0
    }
    
    # Build sub elements list.
    set sublist {}
    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"
}

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