ooxml

Artifact [7eb87025b4]
Login

Artifact [7eb87025b4]

Artifact 7eb87025b4a25929c785cfeb0f38d8ff98e101cec310b737dacb7adaf0b80427:


#
#  ooxml ECMA-376 Office Open XML File Formats
#  https://www.ecma-international.org/publications/standards/Ecma-376.htm
#
#  $Id: ooxml.tcl,v 1.28 2018/06/20 15:01:38 alex Exp $
#
#  Copyright (C) 2018 Alexander Schoepe, Bochum, DE, <schoepe@users.sourceforge.net>
#  All rights reserved.
#
#  Redistribution and use in source and binary forms, with or without modification,
#  are permitted provided that the following conditions are met:
#
#  1. Redistributions of source code must retain the above copyright notice, this
#     list of conditions and the following disclaimer.
#  2. Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions and the following disclaimer in the documentation
#     and/or other materials provided with the distribution.
#  3. Neither the name of the project nor the names of its contributors may be used
#     to endorse or promote products derived from this software without specific
#     prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
#  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
#  OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT
#  SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
#  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
#  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
#  BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
#  ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
#  SUCH DAMAGE.
#


# 
# INDEX and ID are zero based
# 
# 
# BORDERLINESTYLE
#   dashDot | dashDotDot | dashed | dotted | double | hair | medium |
#   mediumDashDot | mediumDashDotDot | mediumDashDotDot | none | slantDashDot | thick | thin
# 
# COLOR
#   0-65
#   Aqua | Black | Blue | BlueRomance | Canary | CarnationPink | Citrus | Cream | DarkSlateBlue | DeepSkyBlue |
#   Eucalyptus | Fuchsia | Gray | Green | Karaka | LavenderBlue | LightCoral | LightCyan | LightSkyBlue | Lime |
#   Lipstick | Maroon | Mauve | MediumTurquoise | Myrtle | Navy | NavyBlue | NightRider | Nobel | Olive |
#   OrangePeel | PeachOrange | Portage | PrussianBlue | Purple | Red | RoyalBlue | SaddleBrown | SafetyOrange |
#   Scampi | Silver | TangerineYellow | Teal | White | Yellow |
#   SystemBackground | SystemForeground
#   RGB
#   ARGB
# 
# DEGREE
#   0-360
# 
# DIAGONALDIRECTION
#   up | down
# 
# HORIZONTAL
#   left | center | right
# 
# PATTERNTYPE
#   darkDown | darkGray | darkGrid | darkHorizontal | darkTrellis | darkUp | darkVertical |
#   gray0625 | gray125 | lightDown | lightGray |
#   lightGrid | lightHorizontal | lightTrellis | lightUp | lightVertical | mediumGray | none | solid
# 
# VERTICAL
#   top | center | bottom
# 
# 
# ::ooxml::Default name value
#   name = path
#
# 
# ::ooxml::RowColumnToString rowcol
#   return name
# 
# 
# ::ooxml::StringToRowColumn name
#   return rowcol
# 
# 
# ::ooxml::CalcColumnWidth numberOfCharacters {maximumDigitWidth 7} {pixelPadding 5}
#   return width
# 
# 
# ::ooxml::xl_sheets file
#   return sheetInformation
# 
# 
# ::ooxml::xl_read file
#   -valuesonly -keylist -sheets PATTERN -sheetnames PATTERN -datefmt FORMAT
#   return workbookData
# 
# 
# ::ooxml::xl_write
# 
#   constructor args
#     -creator CREATOR
#     return class
# 
#   method numberformat args
#     -format FORMAT -general -date -time -datetime -iso8601 -number -decimal -red -separator -fraction -scientific -percent -text -string
#     return NUMFMTID
#
#   method defaultdatestyle STYLEID
# 
#   method font args
#     -list -name NAME -family FAMILY -size SIZE -color COLOR -scheme SCHEME -bold -italic -underline -color COLOR
#     return FONTID
# 
#   method fill args
#     -list -patterntype PATTERNTYPE -fgcolor COLOR -bgcolor COLOR
#     return FILLID
# 
#   method border args
#     -list -leftstyle BORDERLINESTYLE -leftcolor COLOR -rightstyle BORDERLINESTYLE -rightcolor COLOR -topstyle BORDERLINESTYLE -topcolor COLOR
#     -bottomstyle BORDERLINESTYLE -bottomcolor COLOR -diagonalstyle BORDERLINESTYLE -diagonalcolor COLOR -diagonaldirection DIAGONALDIRECTION
#     return BORDERID
# 
#   method style args
#     -list -numfmt NUMFMTID -font FONTID -fill FILLID -border BORDERID -xf XFID -horizontal HORIZONTAL -vertical VERTICAL -rotate DEGREE
#     return STYLEID
# 
#   method worksheet name
#     return SHEETID
# 
#   method column sheet args
#     -index INDEX -to INDEX -width WIDTH -style STYLEID -bestfit -customwidth -string -nozero -calcfit
#     autoincrement of column if INDEX not applied
#     return column
# 
#   method row sheet args
#     -index INDEX -height HEIGHT
#     autoincrement of row if INDEX not applied
#     return row
# 
#   method cell sheet {data {}} args
#     -index INDEX -style STYLEID -formula FORMULA -string -nozero -globalstyle -height HEIGHT
#     autoincrement of column if INDEX not applied
#     return row,column
# 
#   method autofilter sheet indexFrom indexTo
# 
#   method freeze sheet index
# 
#   method presetstyles
#
#   method presetsheets
#
#   method write filename
# 
#
# ::ooxml::tablelist_to_xl lb args
#   -callback CALLBACK -path PATH -file FILENAME -creator CREATOR -name NAME -rootonly -addtimestamp
#   Callback arguments
#     spreadsheet sheet maxcol column title width align sortmode hide
#


package require Tcl 8.6
package require vfs::zip
package require tdom 0.9.0-
package require msgcat


namespace eval ::ooxml {
  namespace export xl_sheets xl_read xl_write

  variable defaults
  variable predefNumFmts
  variable predefColors
  variable predefColorsName
  variable predefColorsARBG
  variable predefBorderLineStyles
  variable predefPatternType

  set defaults(path) {.}

  set defaults(numFmts,start) 166
  set defaults(cols,width) 10.83203125

  # predefined formats
  array set predefNumFmts {
    0 {dt 0 fmt {General}}
    1 {dt 0 fmt {0}}
    2 {dt 0 fmt {0.00}}
    3 {dt 0 fmt {#,##0}}
    4 {dt 0 fmt {#,##0.00}}
    9 {dt 0 fmt {0%}}
    10 {dt 0 fmt {0.00%}}
    11 {dt 0 fmt {0.00E+00}}
    12 {dt 0 fmt {#\ ?/?}}
    13 {dt 0 fmt {#\ ??/??}}
    14 {dt 1 fmt {mm-dd-yy}}
    15 {dt 1 fmt {d-mmm-yy}}
    16 {dt 1 fmt {d-mmm}}
    17 {dt 1 fmt {mmm-yy}}
    18 {dt 1 fmt {h:mm\ AM/PM}}
    19 {dt 1 fmt {h:mm:ss\ AM/PM}}
    20 {dt 1 fmt {h:mm}}
    21 {dt 1 fmt {h:mm:ss}}
    22 {dt 1 fmt {m/d/yy h:mm}}
    37 {dt 0 fmt {#,##0\ ;(#,##0)}}
    38 {dt 0 fmt {#,##0\ ;[Red](#,##0)}}
    39 {dt 0 fmt {#,##0.00;(#,##0.00)}}
    40 {dt 0 fmt {#,##0.00;[Red](#,##0.00)}}
    45 {dt 1 fmt {mm:ss}}
    46 {dt 1 fmt {[h]:mm:ss}}
    47 {dt 1 fmt {mmss.0}}
    48 {dt 0 fmt {##0.0E+0}}
    49 {dt 0 fmt {@}}
  }

  array set predefColors {
    0 {argb 00000000 name Black}
    1 {argb 00FFFFFF name White}
    2 {argb 00FF0000 name Red}
    3 {argb 0000FF00 name Lime}
    4 {argb 000000FF name Blue}
    5 {argb 00FFFF00 name Yellow}
    6 {argb 00FF00FF name Fuchsia}
    7 {argb 0000FFFF name Aqua}
    8 {argb 00000000 name Black}
    9 {argb 00FFFFFF name White}
    10 {argb 00FF0000 name Red}
    11 {argb 0000FF00 name Lime}
    12 {argb 000000FF name Blue}
    13 {argb 00FFFF00 name Yellow}
    14 {argb 00FF00FF name Fuchsia}
    15 {argb 0000FFFF name Aqua}
    16 {argb 00800000 name Maroon}
    17 {argb 00008000 name Green}
    18 {argb 00000080 name Navy}
    19 {argb 00808000 name Olive}
    20 {argb 00800080 name Purple}
    21 {argb 00008080 name Teal}
    22 {argb 00C0C0C0 name Silver}
    23 {argb 00808080 name Gray}
    24 {argb 009999FF name Portage}
    25 {argb 00993366 name Lipstick}
    26 {argb 00FFFFCC name Cream}
    27 {argb 00CCFFFF name LightCyan}
    28 {argb 00660066 name Purple}
    29 {argb 00FF8080 name LightCoral}
    30 {argb 000066CC name NavyBlue}
    31 {argb 00CCCCFF name LavenderBlue}
    32 {argb 00000080 name Navy}
    33 {argb 00FF00FF name Fuchsia}
    34 {argb 00FFFF00 name Yellow}
    35 {argb 0000FFFF name Aqua}
    36 {argb 00800080 name Purple}
    37 {argb 00800000 name Maroon}
    38 {argb 00008080 name Teal}
    39 {argb 000000FF name Blue}
    40 {argb 0000CCFF name DeepSkyBlue}
    41 {argb 00CCFFFF name LightCyan}
    42 {argb 00CCFFCC name BlueRomance}
    43 {argb 00FFFF99 name Canary}
    44 {argb 0099CCFF name LightSkyBlue}
    45 {argb 00FF99CC name CarnationPink}
    46 {argb 00CC99FF name Mauve}
    47 {argb 00FFCC99 name PeachOrange}
    48 {argb 003366FF name RoyalBlue}
    49 {argb 0033CCCC name MediumTurquoise}
    50 {argb 0099CC00 name Citrus}
    51 {argb 00FFCC00 name TangerineYellow}
    52 {argb 00FF9900 name OrangePeel}
    53 {argb 00FF6600 name SafetyOrange}
    54 {argb 00666699 name Scampi}
    55 {argb 00969696 name Nobel}
    56 {argb 00003366 name PrussianBlue}
    57 {argb 00339966 name Eucalyptus}
    58 {argb 00003300 name Myrtle}
    59 {argb 00333300 name Karaka}
    60 {argb 00993300 name SaddleBrown}
    61 {argb 00993366 name Lipstick}
    62 {argb 00333399 name DarkSlateBlue}
    63 {argb 00333333 name NightRider}
    64 {argb {} name SystemForeground}
    65 {argb {} name SystemBackground}
  }
  set predefColorsName {}
  set predefColorsARBG {}
  foreach idx [lsort -integer [array names predefColors]] {
    lappend predefColorsName [dict get $predefColors($idx) name]
    lappend predefColorsARBG [dict get $predefColors($idx) argb]
  }

  set predefPatternType {
    darkDown
    darkGray
    darkGrid
    darkHorizontal
    darkTrellis
    darkUp
    darkVertical
    gray0625
    gray125
    lightDown
    lightGray
    lightGrid
    lightHorizontal
    lightTrellis
    lightUp
    lightVertical
    mediumGray
    none
    solid
  }

  set predefBorderLineStyles {
    dashDot
    dashDotDot
    dashed
    dotted
    double
    hair
    medium
    mediumDashDot
    mediumDashDotDot
    mediumDashDotDot
    none
    slantDashDot
    thick
    thin
  }

  # ar - Arabic - العربية 
  msgcat::mcset ar LANGUAGE \u0627\u0644\u0639\u0631\u0628\u064a\u0629
  msgcat::mcset ar Book \u0627\u0644\u0643\u062a\u0627\u0628
  msgcat::mcset ar Worksheets "\u0623\u0648\u0631\u0627\u0642 \u0627\u0644\u0639\u0645\u0644"
  msgcat::mcset ar Sheet \u0627\u0644\u0648\u0631\u0642\u0629
  # cs - Czech - čeština, český jazyk
  msgcat::mcset cs LANGUAGE \u010de\u0161tina
  msgcat::mcset cs Book Ses\u030cit
  msgcat::mcset cs Worksheets Listy
  msgcat::mcset cs Sheet List
  # da - Danish - dansk
  msgcat::mcset da LANGUAGE dansk
  msgcat::mcset da Book Mappe
  msgcat::mcset da Worksheets Regneark
  msgcat::mcset da Sheet Ark
  # de - German - Deutsch
  msgcat::mcset de LANGUAGE Deutsch
  msgcat::mcset de Book Mappe
  msgcat::mcset de Worksheets Arbeitsbl\u00e4tter
  msgcat::mcset de Sheet Blatt
  msgcat::mcset de {Tablelist does not exists!} {Die Tablelist existiert nicht!}
  msgcat::mcset de {No file selected!} "Keine Datei ausgew\u00e4hlt!"
  # el - Greek - ελληνικά
  msgcat::mcset el LANGUAGE \u03b5\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac
  msgcat::mcset el Book \u0392\u03b9\u03b2\u03bb\u03b9\u0301\u03bf
  msgcat::mcset el Worksheets "\u03a6\u03cd\u03bb\u03bb\u03b1 \u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1\u03c2"
  msgcat::mcset el Sheet \u03a6\u03cd\u03bb\u03bb\u03bf
  # en - English - English
  msgcat::mcset en LANGUAGE English
  msgcat::mcset en Book Book
  msgcat::mcset en Worksheets Worksheets
  msgcat::mcset en Sheet Sheet
  msgcat::mcset en {Tablelist does not exists!} {Tablelist does not exists!}
  msgcat::mcset en {No file selected!} {No file selected!}
  # es - Spanish - Español
  msgcat::mcset es LANGUAGE Espa\u00f1ol
  msgcat::mcset es Book Libro
  msgcat::mcset es Worksheets "Hojas de c\u00e1lculo"
  msgcat::mcset es Sheet Hoja
  msgcat::mcset es {Tablelist does not exists!} "\u00a1Tablelist no existe!"
  msgcat::mcset es {No file selected!} "\u00a1Ning\u00fan archivo seleccionado!"
  # fi - Finnish - suomi, suomen kieli
  msgcat::mcset fi LANGUAGE suomi
  msgcat::mcset fi Book Tyo\u0308kirja
  msgcat::mcset fi Worksheets Laskentataulukot
  msgcat::mcset fi Sheet Taulukko
  # fr - French - français, langue française
  msgcat::mcset fr LANGUAGE fran\u00e7ais
  msgcat::mcset fr Book Classeur
  msgcat::mcset fr Worksheets "Feuilles de calcul"
  msgcat::mcset fr Sheet Feuil
  msgcat::mcset fr {Tablelist does not exists!} {Tablelist n'existe pas!}
  msgcat::mcset fr {No file selected!} "Aucun fichier s\u00e9lectionn\u00e9!"
  # he - Hebrew - עברית
  msgcat::mcset he LANGUAGE \u05e2\u05d1\u05e8\u05d9\u05ea
  msgcat::mcset he Book \u05d7\u05d5\u05d1\u05e8\u05ea
  msgcat::mcset he Worksheets "\u05d2\u05dc\u05d9\u05d5\u05e0\u05d5\u05ea \u05e2\u05d1\u05d5\u05d3\u05d4"
  msgcat::mcset he Sheet \u05d2\u05d9\u05dc\u05d9\u05d5\u05df
  # hu - Hungarian - magyar
  msgcat::mcset hu LANGUAGE magyar
  msgcat::mcset hu Book Munkafu\u0308zet
  msgcat::mcset hu Worksheets Munkalapok
  msgcat::mcset hu Sheet Munkalap
  # it - italian - Italiano
  msgcat::mcset it LANGUAGE Italiano
  msgcat::mcset it Book Cartel
  msgcat::mcset it Worksheets "Fogli di lavoro"
  msgcat::mcset it Sheet Foglio
  msgcat::mcset it {Tablelist does not exists!} {Tablelist non esiste!}
  msgcat::mcset it {No file selected!} {Nessun file selezionato!}
  # ja - Japanese - 日本語 (にほんご)
  msgcat::mcset ja LANGUAGE "\u65e5\u672c\u8a9e (\u306b\u307b\u3093\u3054)"
  msgcat::mcset ja Book Book
  msgcat::mcset ja Worksheets \u30ef\u30fc\u30af\u30b7\u30fc\u30c8
  msgcat::mcset ja Sheet Sheet
  # ko - Korean - 한국어
  msgcat::mcset ko LANGUAGE "\ud55c\uad6d\uc5b4"
  msgcat::mcset ko Book "\u1110\u1169\u11bc\u1112\u1161\u11b8 \u1106\u116e\u11ab\u1109\u1165"
  msgcat::mcset ko Worksheets \uc6cc\ud06c\uc2dc\ud2b8
  msgcat::mcset ko Sheet \uc2dc\ud2b8
  # nl - Dutch, Flemish - Nederlands, Vlaams
  msgcat::mcset nl LANGUAGE Nederlands
  msgcat::mcset nl Book Map
  msgcat::mcset nl Worksheets Werkbladen
  msgcat::mcset nl Sheet Blad
  msgcat::mcset nl {Tablelist does not exists!} {Tablelist bestaat niet!}
  msgcat::mcset nl {No file selected!} {Geen bestand geselecteerd!}
  # no - Norwegian - Norsk
  msgcat::mcset no LANGUAGE Norsk
  msgcat::mcset no Book Bok
  msgcat::mcset no Worksheets Regneark
  msgcat::mcset no Sheet Ark
  # pl - Polish - język polski, polszczyzna
  msgcat::mcset pl LANGUAGE polszczyzna
  msgcat::mcset pl Book Skoroszyt
  msgcat::mcset pl Worksheets Arkusze
  msgcat::mcset pl Sheet Arkusz
  # pt - Portuguese - Português
  msgcat::mcset pt LANGUAGE Portugu\u00eas
  msgcat::mcset pt Book Livro
  msgcat::mcset pt Worksheets "Folhas de C\u00e1lculo"
  msgcat::mcset pt Sheet Folha
  # ru - Russian - русский
  msgcat::mcset ru LANGUAGE \u0440\u0443\u0441\u0441\u043a\u0438\u0439
  msgcat::mcset ru Book \u041a\u043d\u0438\u0433\u0430
  msgcat::mcset ru Worksheets \u041b\u0438\u0441\u0442\u044b
  msgcat::mcset ru Sheet \u041b\u0438\u0441\u0442
  # sl - Slovenian - Slovenski Jezik, Slovenščina
  msgcat::mcset sl LANGUAGE Sloven\u0161\u010dina
  msgcat::mcset sl Book Zos\u030cit
  msgcat::mcset sl Worksheets H\u00e1rky
  msgcat::mcset sl Sheet H\u00e1rok
  # sv - Swedish - Svenska
  msgcat::mcset sv LANGUAGE Svenska
  msgcat::mcset sv Book Bok
  msgcat::mcset sv Worksheets Kalkylblad
  msgcat::mcset sv Sheet Blad
  # th - Thai - ไทย
  msgcat::mcset th LANGUAGE \u0e44\u0e17\u0e22
  msgcat::mcset th Book \u0e2a\u0e21\u0e38\u0e14\u0e07\u0e32\u0e19
  msgcat::mcset th Worksheets \u0e40\u0e27\u0e34\u0e23\u0e4c\u0e01\u0e0a\u0e35\u0e15
  msgcat::mcset th Sheet \u0e41\u0e1c\u0e48\u0e19\u0e07\u0e32\u0e19
  # tr - Turkish - Türkçe
  msgcat::mcset tr LANGUAGE T\u00fcrk\u00e7e
  msgcat::mcset tr Book Kitap
  msgcat::mcset tr Worksheets "\u00c7al\u0131\u015fma Sayfalar\u0131"
  msgcat::mcset tr Sheet Sayfa
  # zh - Chinese - 中文 (Zhōngwén), 汉语, 漢語
  msgcat::mcset zh LANGUAGE \u4e2d\u6587
  msgcat::mcset zh Book \u5de5\u4f5c\u7c3f
  msgcat::mcset zh Worksheets \u5de5\u4f5c\u8868
  msgcat::mcset zh Sheet \u5de5\u4f5c\u8868
}


proc ::ooxml::Default { name value } {
  variable defaults

  switch -- $name {
    path {
      set defaults($name) [string trim $value]
      if {$value eq {}} {
	set defaults($name) .
      }
    }
    default {
    } 
  }
}


proc ::ooxml::Getopt { *result declaration to_parse } {
  if {${*result} ne {}} {
    upvar 1 ${*result} result
  }

  set error 0
  set errmsg {} 
  array set result {}

  set optopts {}
  set argcnt [llength $declaration]
  for {set argc 0} {$argc < $argcnt} {incr argc} {
    set opt [lindex $declaration $argc]
    if {[lsearch -exact {. -} [string index $opt 0]] > -1} {
      set error 1
      lappend errmsg "option name can not start with '.' or '-'"
      set result(-error) $error
      set result(-errmsg) $errmsg
      return $error
    }
    if {[regsub -- {\..*$} $opt {} name]} {
      regsub -- (${name}) $opt {} opt
    }
    if {![regsub -- {\.arg\M} $opt {} opt]} {
      dict set optopts $name arg 0
      set result($name) 0
    } else {
      dict set optopts $name arg 1
      incr argc
      if {$argc < $argcnt} {
	set result($name) [lindex $declaration $argc]
      } else {
	set result($name) {}
	set error 1
	lappend errmsg "declaration of '$name' missing default value"
	set result(-error) $error
	set result(-errmsg) $errmsg
	return $error
      }
    }
  }

  set argcnt [llength $to_parse]
  for {set argc 0} {$argc < $argcnt} {incr argc} {
    set opt [lindex $to_parse $argc]
    if {$opt eq {--}} {
      set result(--) [lrange $to_parse ${argc}+1 end]
      break
    } elseif {[string index $opt 0] eq {-}} {
      set opt [string range $opt 1 end]
      if {[dict keys $optopts $opt] eq $opt || [llength [set opt [dict keys $optopts ${opt}*]]] == 1} {
	if {[dict get $optopts $opt arg]} {
	  incr argc
	  if {$argc < $argcnt} {
	    set result($opt) [lindex $to_parse $argc]
	  } else {
	    set error 1
	    lappend errmsg "${opt}: missing argument"
	  }
	} else {
	  set result($opt) 1
	}
      } else {
	set error 1
	lappend errmsg "[lindex $to_parse $argc]: unknown or ambiguous option"
      }
    } else {
      set error 1
      lappend errmsg "${opt}: syntax error"
    }
  }

  if {![info exists result(--)]} {
    set result(--) {}
  }

  set result(-error) $error
  set result(-errmsg) $errmsg

  return $error
}


proc ::ooxml::ScanDateTime { scan {iso8601 0} } {
  set d  1
  set m  1
  set ml {}
  set y  1970
  set H  0
  set M  0
  set S  0
  set F  0

  if {[regexp {^(\d+)\.(\d+)\.(\d+)T?\s*(\d+)?:?(\d+)?:?(\d+)?\.?(\d+)?\s*([+-])?(\d+)?:?(\d+)?$} $scan all d m y H M S F x a b] ||
      [regexp {^(\d+)-(\d+)-(\d+)T?\s*(\d+)?:?(\d+)?:?(\d+)?\.?(\d+)?\s*([+-])?(\d+)?:?(\d+)?$} $scan all y m d H M S F x a b] ||
      [regexp {^(\d+)-(\w+)-(\d+)T?\s*(\d+)?:?(\d+)?:?(\d+)?\.?(\d+)?\s*([+-])?(\d+)?:?(\d+)?$} $scan all d ml y H M S F x a b] ||
      [regexp {^(\d+)/(\d+)/(\d+)T?\s*(\d+)?:?(\d+)?:?(\d+)?\.?(\d+)?\s*([+-])?(\d+)?:?(\d+)?$} $scan all m d y H M S F x a b]} {
    scan $y %u y

    if {[string is integer -strict $y] && $y >= 0 && $y <= 2038} {
      switch -- [string tolower $ml] {
	jan -
	ene -
	gen -
	tam {set m 1}
	feb -
	fev -
	fév -
	hel {set m 2}
	mrz -
	mar -
	mär -
	maa {set m 3}
	apr -
	avr -
	abr -
	huh {set m 4}
	mai -
	may -
	mei -
	mag -
	maj -
	tou {set m 5}
	jun -
	jui -
	giu -
	kes {set m 6}
	jul -
	jui -
	lug -
	hei {set m 7}
	aug -
	aou -
	aoû -
	ago -
	elo {set m 8}
	sep -
	set -
	syy {set m 9}
	okt -
	oct -
	out -
	ott -
	lok {set m 10}
	nov -
	mar {set m 11}
	dez -
	dec -
	déc -
	dic -
	des -
	jou {set m 12}
	default { set m [string trimleft $m 0] }
      }

      foreach name {y m d H M S F a b} {
	upvar 0 $name var
	set var [string trimleft $var 0]
	if {![string is integer -strict $var]} {
	  set var 0
	}
      }

      if {$y < 100} {
	if {$y < 50} {
	  incr y 2000
	} else {
	  incr y 1900
	}
      }
      if {$y < 1900} {
	return {}
      }

      set Y [format %04u $y]
      set y [format %02u [expr {$y - int($y / 100) * 100}]]
      set m [format %02u $m]
      set d [format %02u $d]
      set H [format %02u $H]
      set M [format %02u $M]
      set S [format %02u $S]

      if {$iso8601} {
	return [list ${Y}-${m}-${d}T${H}:${M}:${S}]
      }
      return [set ole [expr {[clock scan ${Y}${m}${d}T${H}${M}${S} -gmt 1] / 86400.0 + 25569}]]
    }
  }
  return {}
}


proc ::ooxml::Zip { zipfile directory files } {
  array set v { fd {} base {} toc {} }

  # this code is a rewrite and extension of the zipper code found
  # at http://equi4.com/critlib/ and http://wiki.tcl.tk/36689
  # by Tom Krehbiel 2012 krehbiel.tom at gmail dot com

  proc Initialize { *v file } {
    upvar ${*v} v

    set fd [open $file w]
    set v(fd) $fd
    set v(base) [tell $fd]
    set v(toc) {}
    fconfigure $fd -translation binary -encoding binary
  }

  proc Emit { *v s } {
    upvar ${*v} v

    puts -nonewline $v(fd) $s
  }

  proc DosTime { sec } {
    set f [clock format $sec -format {%Y %m %d %H %M %S} -gmt 1]
    regsub -all { 0(\d)} $f { \1} f
    foreach {Y M D h m s} $f break
    set date [expr {(($Y-1980)<<9) | ($M<<5) | $D}]
    set time [expr {($h<<11) | ($m<<5) | ($s>>1)}]
    return [list $date $time]
  }

  proc AddEntry { *v name contents {date {}} {force 0} } {
    upvar ${*v} v

    if {$date eq {}} {
      set date [clock seconds]
    }
    lassign [DosTime $date] date time
    set flag 0
    set type 0 ;# stored
    set fsize [string length $contents]
    set csize $fsize
    set fnlen [string length $name]
    
    if {$force > 0 && $force != [string length $contents]} {
      set csize $fsize
      set fsize $force
      set type 8 ;# if we're passing in compressed data, it's deflated
    }
    
    if {[catch {zlib crc32 $contents} crc]} {
      set crc 0
    } elseif {$type == 0} {
      set cdata [zlib deflate $contents 9]
      if {[string length $cdata] < [string length $contents]} {
	set contents $cdata
	set csize [string length $cdata]
	set type 8 ;# deflate
      }
    }
    
    lappend v(toc) "[binary format a2c6ssssiiiss4ii PK {1 2 20 0 20 0} $flag $type $time $date $crc $csize $fsize $fnlen {0 0 0 0} 128 [tell $v(fd)]]$name"
    
    Emit v [binary format a2c4ssssiiiss PK {3 4 20 0} $flag $type $time $date $crc $csize $fsize $fnlen 0]
    Emit v $name
    Emit v $contents
  }

  proc AddDirectory { *v name {date {}} {force 0} } {
    upvar ${*v} v

    set name "${name}/"
    if {$date eq {}} {
      set date [clock seconds]
    }
    lassign [DosTime $date] date time
    set flag 0
    set type 0 ;# stored
    set fsize 0
    set csize 0
    set fnlen [string length $name]
    set crc 0
    
    lappend v(toc) "[binary format a2c6ssssiiiss4ii PK {1 2 20 0 20 0} $flag $type $time $date $crc $csize $fsize $fnlen {0 0 0 0} 128 [tell $v(fd)]]$name"
    
    Emit v [binary format a2c4ssssiiiss PK {3 4 20 0} $flag $type $time $date $crc $csize $fsize $fnlen 0]
    Emit v $name
  }

  proc Finalize { *v } {
    upvar ${*v} v

    set pos [tell $v(fd)]
    set ntoc [llength $v(toc)]
    foreach x $v(toc) {
      Emit v $x
    }
    set v(toc) {}
    
    set len [expr {[tell $v(fd)] - $pos}]
    incr pos -$v(base)
    
    Emit v [binary format a2c2ssssiis PK {5 6} 0 0 $ntoc $ntoc $len $pos 0]
    
    close $v(fd)
  }

  Initialize v $zipfile
  foreach file $files {
    regsub {^\./} $file {} to
    set from [file join [file normalize $directory] $to]
    if {[file isfile $from]} {
      set fd [open $from r]
      fconfigure $fd -translation binary -encoding binary
      AddEntry v $to [read $fd] [file mtime $from]
      close $fd
    } elseif {[file isdir $from]} {
      AddDirectory v $to [file mtime $from]
      lappend dirs $file
    }
  }
  Finalize v
}


proc ::ooxml::RowColumnToString { rowcol } {
  proc Column { col } {
    set name {}
    while {$col >= 0} {
      set char [binary format c [expr {($col % 26) + 65}]]
      set name $char$name
      set col [expr {$col / 26 -1}]
    }
    return $name
  }
  lassign [split $rowcol ,] row col
  return [Column $col][incr row 1]
}


proc ::ooxml::StringToRowColumn { name } {
  set row 0
  set col 0
  binary scan [string toupper $name] c* vals
  foreach val $vals {
    if {$val < 58} {
      # 0-9, "0" = 48
      set row [expr {$row * 10 + ($val-48)}]
    } else {
      # A-Z, "A" = 65 (-1 zero based shift)
      set col [expr {$col * 26 + ($val-64)}]
    }
  }
  return [incr row -1],[incr col -1]
}


proc ::ooxml::IndexToString { index } {
  lassign [split $index ,] row col
  if {[string is integer -strict $row] && [string is integer -strict $col] && $row > -1 && $col > -1} {
    return [RowColumnToString $index]
  } else {
    lassign [split [StringToRowColumn $index] ,] row col
    if {[string is integer -strict $row] && [string is integer -strict $col] && $row > -1 && $col > -1} {
      return $index
    }
  }
  return {}
}


proc ::ooxml::CalcColumnWidth { numberOfCharacters {maximumDigitWidth 7} {pixelPadding 5} } {
  return [expr {int(($numberOfCharacters * $maximumDigitWidth + $pixelPadding + 0.0) / $maximumDigitWidth * 256.0) / 256.0}]
}


# Seite 3947
# <xsd:complexType name="CT_Color">
#   <xsd:attribute name="auto" type="xsd:boolean" use="optional"/>
#   <xsd:attribute name="indexed" type="xsd:unsignedInt" use="optional"/>
#   <xsd:attribute name="rgb" type="ST_UnsignedIntHex" use="optional"/>
#   <xsd:attribute name="theme" type="xsd:unsignedInt" use="optional"/>
#   <xsd:attribute name="tint" type="xsd:double" use="optional" default="0.0"/>
# </xsd:complexType>

proc ::ooxml::Color { color } {
  variable predefColors
  variable predefColorsName
  variable predefColorsARBG

  if {[string trim $color] eq {}} {
    return {}
  } elseif {$color in {auto none}} {
    return [list $color 1]
  } elseif {[string is integer -strict $color] && $color >= 0 && $color <= 65} {
    return [list indexed $color]
  } elseif {[set idx [lsearch -exact -nocase [array names predefColors] $color]] && $idx > -1} {
    return [list indexed $idx]
  } elseif {[set idx [lsearch -exact -nocase $predefColorsName $color]] && $idx > -1} {
    return [list indexed $idx]
  }
  if {[string is xdigit -strict $color]} {
    if {[string length $color] == 6} {
      set color 00$color
    }
    if {[set idx [lsearch -exact -nocase $predefColorsARBG $color]] && $idx > -1} {
      return [list indexed $idx]
    } else {
      return [list rgb $color]
    }
  }
  return {}
}


#
# ooxml::xl_sheets
#

proc ::ooxml::xl_sheets { file } {
  set sheets {}

  set mnt [vfs::zip::Mount $file xlsx]

  set rels 0
  if {![catch {open xlsx/xl/_rels/workbook.xml.rels r} fd]} {
    fconfigure $fd -encoding utf-8
    if {![catch {dom parse [read $fd]} rdoc]} {
      set rels 1
      set relsroot [$rdoc documentElement]
    }
    close $fd
  }

  if {![catch {open xlsx/xl/workbook.xml r} fd]} {
    fconfigure $fd -encoding utf-8
    if {![catch {dom parse [read $fd]} doc]} {
      set root [$doc documentElement]
      set idx -1
      foreach node [$root selectNodes -namespaces [list X [$root namespaceURI]] {/X:workbook/X:sheets/X:sheet}] {
	if {[$node hasAttribute sheetId] && [$node hasAttribute name]} {
	  set sheetId [$node getAttribute sheetId]
	  set name [$node getAttribute name]
	  set rid [$node getAttribute r:id]
	  foreach node [$relsroot selectNodes -namespaces [list X [$relsroot namespaceURI]] [subst -nobackslashes -nocommands {/X:Relationships/X:Relationship[@Id="$rid"]}]] {
	    if {[$node hasAttribute Target]} {
	      lappend sheets [incr idx] [list sheetId $sheetId name $name rId $rid]
	    }
	  }
	}
      }
      $doc delete
    }
    close $fd
  }

  if {$rels} {
    $rdoc delete
  }

  vfs::zip::Unmount $mnt xlsx

  return $sheets
}


#
# ooxml::xl_read
#

proc ::ooxml::xl_read { file args } {
  variable predefNumFmts

  array set cellXfs {}
  array set numFmts [array get predefNumFmts]
  array set sharedStrings {}
  set sheets {}

  if {[::ooxml::Getopt opts {valuesonly keylist sheets.arg {} sheetnames.arg {} datefmt.arg {%Y-%m-%d %H:%M:%S} as.arg array} $args]} {
    error $opts(-errmsg)
  }
  if {[string trim $opts(sheets)] eq {} && [string trim $opts(sheetnames)] eq {}} {
    set opts(sheetnames) *
  }


  set mnt [vfs::zip::Mount $file xlsx]

  set rels 0
  if {![catch {open xlsx/xl/_rels/workbook.xml.rels r} fd]} {
    fconfigure $fd -encoding utf-8
    if {![catch {dom parse [read $fd]} rdoc]} {
      set rels 1
      set relsroot [$rdoc documentElement]
    }
    close $fd
  }

  if {![catch {open xlsx/xl/workbook.xml r} fd]} {
    fconfigure $fd -encoding utf-8
    if {![catch {dom parse [read $fd]} doc]} {
      set root [$doc documentElement]
      set idx -1
      foreach node [$root selectNodes -namespaces [list X [$root namespaceURI]] {/X:workbook/X:sheets/X:sheet}] {
	if {[$node hasAttribute sheetId] && [$node hasAttribute name]} {
	  set sheetId [$node getAttribute sheetId]
	  set name [$node getAttribute name]
	  set rid [$node getAttribute r:id]
	  foreach node [$relsroot selectNodes -namespaces [list X [$relsroot namespaceURI]] [subst -nobackslashes -nocommands {/X:Relationships/X:Relationship[@Id="$rid"]}]] {
	    if {[$node hasAttribute Target]} {
	      lappend sheets [incr idx] $sheetId $name $rid [$node getAttribute Target]
	    }
	  }
	}
      }
      $doc delete
    }
    close $fd
  }

  if {$rels} {
    $rdoc delete
  }

  if {![catch {open xlsx/xl/sharedStrings.xml r} fd]} {
    fconfigure $fd -encoding utf-8
    if {![catch {dom parse [read $fd]} doc]} {
      set root [$doc documentElement]
      set idx -1
      foreach shared [$root selectNodes -namespaces [list X [$root namespaceURI]] {/X:sst/X:si}] {
	incr idx
	foreach node [$shared selectNodes -namespaces [list X [$shared namespaceURI]] {X:t/text()}] {
	  append sharedStrings($idx) [$node nodeValue]
	}
	foreach node [$shared selectNodes -namespaces [list X [$shared namespaceURI]] {*/X:t/text()}] {
	  append sharedStrings($idx) [$node nodeValue]
	}
      }
      $doc delete
    }
    close $fd
  }


  if {![catch {open xlsx/xl/styles.xml r} fd]} {
    fconfigure $fd -encoding utf-8
    if {![catch {dom parse [read $fd]} doc]} {
      set root [$doc documentElement]
      set idx -1
      foreach node [$root selectNodes -namespaces [list X [$root namespaceURI]] {/X:styleSheet/X:numFmts/X:numFmt}] {
        incr idx
	if {[$node hasAttribute numFmtId] && [$node hasAttribute formatCode]} {
	  set numFmtId [$node getAttribute numFmtId]
	  set formatCode [$node getAttribute formatCode]
	  set datetime 0
	  foreach tag {*y* *m* *d* *h* *s*} {
	    if {[string match -nocase $tag [string map {Black {} Blue {} Cyan {} Green {} Magenta {} Red {} White {} Yellow {}} $formatCode]]} {
	      set datetime 1
	      break
	    }
	  }
	  set numFmts($numFmtId) [list dt $datetime fmt $formatCode]
	}
      }
      set idx -1
      foreach node [$root selectNodes -namespaces [list X [$root namespaceURI]] {/X:styleSheet/X:cellXfs/X:xf}] {
        incr idx
	if {[$node hasAttribute numFmtId]} {
	  set numFmtId [$node getAttribute numFmtId]
	  if {[$node hasAttribute applyNumberFormat]} {
	    set applyNumberFormat [$node getAttribute applyNumberFormat]
	  } else {
	    set applyNumberFormat 0
	  }
	  set cellXfs($idx) [list nfi $numFmtId anf $applyNumberFormat]
	}
      }

      ### READING KNOWN FORMATS AND STYLES ###

      set wb(s,@) {}

      array unset a *
      set a(max) 0
      set wb(s,numFmtsIds) {}
      foreach node [$root selectNodes -namespaces [list X [$root namespaceURI]] {/X:styleSheet/X:numFmts/X:numFmt}] {
        if {[$node hasAttribute numFmtId] && [$node hasAttribute formatCode]} {
	  set wb(s,numFmts,[set idx [$node getAttribute numFmtId]]) [$node getAttribute formatCode]
	  lappend wb(s,numFmtsIds) $idx
	  if {$idx > $a(max)} {
	    set a(max) $idx
	  }
	}
      }
      if {$a(max) < $::ooxml::defaults(numFmts,start)} {
        set a(max) $::ooxml::defaults(numFmts,start)
      }
      lappend wb(s,@) numFmtId [incr a(max)]


      set idx -1
      array unset a *
      foreach node [$root selectNodes -namespaces [list X [$root namespaceURI]] {/X:styleSheet/X:fonts/X:font}] {
	incr idx
	array set a {name {} family {} size {} color {} scheme {} bold 0 italic 0 underline 0 color {}}
	foreach node1 [$node childNodes] {
	  switch -- [$node1 nodeName] {
	    b {
	      set a(bold) 1
	    }
	    i {
	      set a(italic) 1
	    }
	    u {
	      set a(underline) 1
	    }
	    sz {
	      if {[$node1 hasAttribute val]} {
		set a(size) [$node1 getAttribute val]
	      }
	    }
	    color {
	      if {[$node1 hasAttribute auto]} {
		set a(color) [list auto [$node1 getAttribute auto]]
	      } elseif {[$node1 hasAttribute rgb]} {
		set a(color) [list rgb [$node1 getAttribute rgb]]
	      } elseif {[$node1 hasAttribute indexed]} {
		set a(color) [list indexed [$node1 getAttribute indexed]]
	      } elseif {[$node1 hasAttribute theme]} {
		set a(color) [list theme [$node1 getAttribute theme]]
	      }
	    }
	    name {
	      if {[$node1 hasAttribute val]} {
		set a(name) [$node1 getAttribute val]
	      }
	    }
	    family {
	      if {[$node1 hasAttribute val]} {
		set a(family) [$node1 getAttribute val]
	      }
	    }
	    scheme {
	      if {[$node1 hasAttribute val]} {
		set a(scheme) [$node1 getAttribute val]
	      }
	    }
	  }
	}
	set wb(s,fonts,$idx) [array get a]
      }
      lappend wb(s,@) fonts [incr idx]


      set idx -1
      array unset a *
      foreach node [$root selectNodes -namespaces [list X [$root namespaceURI]] {/X:styleSheet/X:fills/X:fill}] {
	incr idx
	array set a {patterntype {} fgcolor {} bgcolor {}}
	foreach node1 [$node childNodes] {
	  switch -- [$node1 nodeName] {
	    patternFill {
	      if {[$node1 hasAttribute patternType]} {
		set a(patterntype) [$node1 getAttribute patternType]
	      }
	      foreach node2 [$node1 childNodes] {
		if {[$node2 nodeName] in { fgColor bgColor}} {
		  if {[$node2 hasAttribute auto]} {
		    set a([string tolower [$node2 nodeName]]) [list auto [$node2 getAttribute auto]]
		  } elseif {[$node2 hasAttribute rgb]} {
		    set a([string tolower [$node2 nodeName]]) [list rgb [$node2 getAttribute rgb]]
		  } elseif {[$node2 hasAttribute indexed]} {
		    set a([string tolower [$node2 nodeName]]) [list indexed [$node2 getAttribute indexed]]
		  } elseif {[$node2 hasAttribute theme]} {
		    set a([string tolower [$node2 nodeName]]) [list theme [$node2 getAttribute theme]]
		  }
		}
	      }
	    }
	  }
	}
	set wb(s,fills,$idx) [array get a]
      }
      lappend wb(s,@) fills [incr idx]


      set idx -1
      unset -nocomplain d
      foreach node [$root selectNodes -namespaces [list X [$root namespaceURI]] {/X:styleSheet/X:borders/X:border}] {
	incr idx
	set d {left {style {} color {}} right {style {} color {}} top {style {} color {}} bottom {style {} color {}} diagonal {style {} color {} direction {}}}
	foreach node1 [$node childNodes] {
	  if {[$node1 hasAttribute style]} {
	    set style [$node1 getAttribute style]
	  } else {
	    set style {}
	  }
	  set color {}
	  foreach node2 [$node1 childNodes] {
	    if {[$node2 nodeName] eq {color}} {
	      if {[$node2 hasAttribute auto]} {
		set color [list auto [$node2 getAttribute auto]]
	      } elseif {[$node2 hasAttribute rgb]} {
		set color [list rgb [$node2 getAttribute rgb]]
	      } elseif {[$node2 hasAttribute indexed]} {
		set color [list indexed [$node2 getAttribute indexed]]
	      } elseif {[$node2 hasAttribute theme]} {
		set color [list theme [$node2 getAttribute theme]]
	      }
	    }
	  }
	  if {[$node1 nodeName] in {left right top bottom diagonal}} {
	    if {$style ne {}} {
	      dict set d [$node1 nodeName] style $style
	    }
	    if {$color ne {}} {
	      dict set d [$node1 nodeName] color $color
	    }
	  }
	}
	if {[$node hasAttribute diagonalUp]} {
	  dict set d diagonal direction up
	} elseif {[$node hasAttribute diagonalDown]} {
	  dict set d diagonal direction down
	}
	set wb(s,borders,$idx) $d
      }
      lappend wb(s,@) borders [incr idx]


      set idx -1
      array unset a *
      foreach node [$root selectNodes -namespaces [list X [$root namespaceURI]] {/X:styleSheet/X:cellXfs/X:xf}] {
	incr idx
	array set a {numfmt 0 font 0 fill 0 border 0 xf 0 horizontal {} vertical {} rotate {}}
        if {[$node hasAttribute numFmtId]} {
	  set a(numfmt) [$node getAttribute numFmtId]
	}
        if {[$node hasAttribute fontId]} {
	  set a(font) [$node getAttribute fontId]
	}
        if {[$node hasAttribute fillId]} {
	  set a(fill) [$node getAttribute fillId]
	}
        if {[$node hasAttribute borderId]} {
	  set a(border) [$node getAttribute borderId]
	}
        if {[$node hasAttribute xfId]} {
	  set a(xf) [$node getAttribute xfId]
	}
	foreach node1 [$node childNodes] {
	  switch -- [$node1 nodeName] {
	    alignment {
	      if {[$node1 hasAttribute horizontal]} {
		set a(horizontal) [$node1 getAttribute horizontal]
	      }
	      if {[$node1 hasAttribute vertical]} {
		set a(vertical) [$node1 getAttribute vertical]
	      }
	      if {[$node1 hasAttribute textRotation]} {
		set a(rotate) [$node1 getAttribute textRotation]
	      }
	    }
	  }
	}
	set wb(s,styles,$idx) [array get a]
      }
      lappend wb(s,@) styles [incr idx]

      $doc delete
    }
    close $fd
  }


  ### SHEET AND DATA ###

  array set wb {}

  foreach {sheet sid name rid target} $sheets {
    set read false
    if {$opts(sheets) ne {}} {
      foreach pat $opts(sheets) {
	if {[string match $pat $sheet]} {
	  set read true
	  break
	}
      }
    }
    if {!$read && $opts(sheetnames) ne {}} {
      foreach pat $opts(sheetnames) {
	if {[string match $pat $name]} {
	  set read true
	  break
	}
      }
    }
    if {!$read} continue

    lappend wb(sheets) $sheet
    set wb($sheet,n) $name
    set wb($sheet,max_row) -1
    set wb($sheet,max_column) -1

    if {![catch {open [file join xlsx/xl $target] r} fd]} {
      fconfigure $fd -encoding utf-8
      if {![catch {dom parse [read $fd]} doc]} {
	set root [$doc documentElement]
	set idx -1
	foreach col [$root selectNodes -namespaces [list X [$root namespaceURI]] {/X:worksheet/X:cols/X:col}] {
	  incr idx
	  foreach item {min max width style bestFit customWidth} {
	    if {[$col hasAttribute $item]} {
	      switch -- $item {
	        min - max {
		  lappend wb($sheet,col,$idx) [string tolower $item] [expr {[$col getAttribute $item] - 1}]
		}
		default {
		  lappend wb($sheet,col,$idx) [string tolower $item] [$col getAttribute $item]
		}
	      }
	    } else {
	      lappend wb($sheet,col,$idx) [string tolower $item] 0
	    }
	  }
	  lappend wb($sheet,col,$idx) string 0 nozero 0 calcfit 0
	}
	set wb($sheet,cols) [incr idx]
	foreach cell [$root selectNodes -namespaces [list X [$root namespaceURI]] {/X:worksheet/X:sheetData/X:row/X:c}] {
	  if {[$cell hasAttribute t]} {
	    set type [$cell getAttribute t]
	  } else {
	    set type n
	  }
	  set value {}
	  set datetime {}
	  switch -- $type {
	    n - b - d - str {
	      # number (default), boolean, iso-date, formula string
	      if {[set node [$cell selectNodes -namespaces [list X [$cell namespaceURI]] X:v/text()]] ne {}} {
		set value [$node nodeValue]
		if {$type eq {n} && [$cell hasAttribute s] && [string is double -strict $value]} {
		  set idx [$cell getAttribute s]
		  if {[dict exists $cellXfs($idx) nfi]} {
		    set numFmtId [dict get $cellXfs($idx) nfi]
		    if {[info exists numFmts($numFmtId)] && [dict exists $numFmts($numFmtId) dt] && [dict get $numFmts($numFmtId) dt]} {
		      set datetime $value
		      catch {clock format [expr {int(($value - 25569) * 86400.0)}] -format $opts(datefmt) -gmt 1} value
		    }
		  }
		} 
	      } else {
		if {![$cell hasAttribute s]} continue
	      }
	    }
	    s {
	      # shared string
	      if {[set node [$cell selectNodes -namespaces [list X [$cell namespaceURI]] X:v/text()]] ne {}} {
		set index [$node nodeValue]
		if {[info exists sharedStrings($index)]} {
		  set value $sharedStrings($index)
		}
	      } else {
		if {![$cell hasAttribute s]} continue
	      }
	    }
	    inlineStr {
	      # inline string
	      if {[set string [$cell selectNodes -namespaces [list X [$cell namespaceURI]] X:is]] ne {}} {
		foreach node [$string selectNodes -namespaces [list X [$string namespaceURI]] {X:t/text()}] {
		  append value [$node nodeValue]
		}
		foreach node [$string selectNodes -namespaces [list X [$string namespaceURI]] {*/X:t/text()}] {
		  append value [$node nodeValue]
		}
	      } else {
		if {![$cell hasAttribute s]} continue
	      }
	    }
	    e {
	      # error
	    }
	  }

	  if {[$cell hasAttribute r]} {
	    if {!$opts(valuesonly)} {
	      set wb($sheet,c,[StringToRowColumn [$cell getAttribute r]]) [$cell getAttribute r]
	    }
	    if {!$opts(valuesonly)} {
	      if {[$cell hasAttribute s]} {
		set wb($sheet,s,[StringToRowColumn [$cell getAttribute r]]) [$cell getAttribute s]
	      }
	    }
	    if {!$opts(valuesonly)} {
	      if {[$cell hasAttribute t]} {
		set wb($sheet,t,[StringToRowColumn [$cell getAttribute r]]) [$cell getAttribute t]
	      }
	    }
	    set wb($sheet,v,[StringToRowColumn [$cell getAttribute r]]) $value
	    if {!$opts(valuesonly) && $datetime ne {}} {
	      set wb($sheet,d,[StringToRowColumn [$cell getAttribute r]]) $datetime
	    }
	    if {!$opts(valuesonly) && [set node [$cell selectNodes -namespaces [list X [$cell namespaceURI]] X:f/text()]] ne {}} {
	      set wb($sheet,f,[StringToRowColumn [$cell getAttribute r]]) [$node nodeValue]
	    }
	  }
	}
	if {!$opts(valuesonly)} {
	  foreach row [$root selectNodes -namespaces [list X [$root namespaceURI]] {/X:worksheet/X:sheetData/X:row}] {
	    if {[$row hasAttribute r] && [$row hasAttribute ht] && [$row hasAttribute customHeight] && [$row getAttribute customHeight] == 1} {
	      dict set wb($sheet,rowheight) [expr {[$row getAttribute r] - 1}] [$row getAttribute ht]
	    }
	  }
	}
	if {!$opts(valuesonly)} {
	  foreach freeze [$root selectNodes -namespaces [list X [$root namespaceURI]] {/X:worksheet/X:sheetViews/X:sheetView/X:pane}] {
	    if {[$freeze hasAttribute topLeftCell] && [$freeze hasAttribute state] && [$freeze getAttribute state] eq {frozen}} {
	      set wb($sheet,freeze) [$freeze getAttribute topLeftCell]
	    }
	  }
	}
	if {!$opts(valuesonly)} {
	  foreach filter [$root selectNodes -namespaces [list X [$root namespaceURI]] {/X:worksheet/X:autoFilter}] {
	    if {[$filter hasAttribute ref]} {
	      lappend wb($sheet,filter) [$filter getAttribute ref]
	    }
	  }
	}
	if {!$opts(valuesonly)} {
	  foreach merge [$root selectNodes -namespaces [list X [$root namespaceURI]] {/X:worksheet/X:mergeCells/X:mergeCell}] {
	    if {[$merge hasAttribute ref]} {
	      lappend wb($sheet,merge) [$merge getAttribute ref]
	    }
	  }
	}
	$doc delete
      }
      close $fd
    }
  }

  vfs::zip::Unmount $mnt xlsx

  foreach cell [lsort -dictionary [array names wb *,v,*]] {
    lassign [split $cell ,] sheet tag row column
    if {$opts(keylist)} {
      dict lappend wb($sheet,k) $row $column
    }
    if {$row > $wb($sheet,max_row)} {
      set wb($sheet,max_row) $row
    }
    if {$column > $wb($sheet,max_column)} {
      set wb($sheet,max_column) $column
    }
  }
  return [array get wb]
}


#
# ooxml::xl_write
#


oo::class create ooxml::xl_write {
  constructor { args } {
    my variable obj
    my variable cells
    my variable sharedStrings
    my variable fonts
    my variable numFmts
    my variable styles
    my variable fills
    my variable borders
    my variable cols

    if {[::ooxml::Getopt opts {creator.arg {unknown}} $args]} {
      error $opts(-errmsg)
    }

    set obj(blockPreset) 0

    set obj(encoding) utf-8
    dom setResultEncoding $obj(encoding)
    set obj(indent) none

    set obj(creator) $opts(creator)
    set obj(created) [clock format [clock seconds] -format %Y-%m-%dT%H:%M:%SZ -gmt 1]

    set obj(sheets) 0
    array set sheets {}

    set obj(sharedStrings) 0
    set sharedStrings {}

    set obj(numFmts) $::ooxml::defaults(numFmts,start)
    array set numFmts {}

    set obj(borders) 1
    set borders(0) {left {style {} color {}} right {style {} color {}} top {style {} color {}} bottom {style {} color {}} diagonal {style {} color {} direction {}}}

    set obj(fills) 2
    set fills(0) {patterntype none fgcolor {} bgcolor {}}
    set fills(1) {patterntype gray125 fgcolor {} bgcolor {}}

    set obj(fonts) 1
    set fonts(0) {name Calibri family 2 size 12 color {theme 1} scheme minor bold 0 italic 0 underline 0 color {}}

    set obj(styles) 1
    set styles(0) {numfmt 0 font 0 fill 0 border 0 xf 0 horizontal {} vertical {} rotate {}}

    set obj(cols) 0
    array set cols {}

    set obj(calcChain) 0

    set obj(defaultdatestyle) 0

    array set cells {}

    return 0
  }

  destructor {
    return 0
  }

  method numberformat { args } {
    my variable obj
    my variable numFmts

    if {[::ooxml::Getopt opts {list format.arg {} general date time datetime iso8601 number decimal red separator fraction scientific percent string text} $args]} {
      error $opts(-errmsg)
    }

    if {$opts(list)} {
      array set tmp [array get ::ooxml::predefNumFmts]
      array set tmp [array get numFmts]
      return [array get tmp]
    }

    set obj(blockPreset) 1

    if {$opts(general)} {
      return 0
    }
    if {$opts(date)} {
      return 14
    }
    if {$opts(time)} {
      return 20
    }
    if {$opts(number)} {
      if {$opts(separator)} {
	if {$opts(red)} {
	  return 38
	} else {
	  return 3
	}
      } else {
	if {$opts(red)} {
	  return -1
	} else {
	  return 1
	}
      }
    }
    if {$opts(decimal)} {
      if {$opts(percent)} {
        return 10
      }
      if {$opts(separator)} {
	if {$opts(red)} {
	  return 40
	} else {
	  return 4
	}
      } else {
	if {$opts(red)} {
	  return -1
	} else {
	  return 2
	}
      }
    }
    if {$opts(fraction)} {
      return 12
    }
    if {$opts(scientific)} {
      return 11
    }
    if {$opts(percent)} {
      return 9
    }
    if {$opts(text) || $opts(string)} {
      return 49
    }

    if {$opts(datetime)} {
      set opts(format) {dd/mm/yyyy\ hh:mm;@}
    }
    if {$opts(iso8601)} {
      set opts(format) {yyyy\-mm\-dd\ hh:mm:ss;@}
    }

    foreach idx [array names ::ooxml::predefNumFmts] {
      if {[dict get $::ooxml::predefNumFmts($idx) fmt] eq $opts(format)} {
        return $idx
      }
    }
    foreach idx [array names numFmts] {
      if {$numFmts($idx) eq $opts(format)} {
        return $idx
      }
    }

    if {$opts(format) eq {}} {
      return -1
    }

    set idx $obj(numFmts)
    set numFmts($idx) $opts(format)
    incr obj(numFmts)

    return $idx
  }

  method defaultdatestyle { style } {
    my variable obj

    set obj(defaultdatestyle) $style
  }

  method font { args } {
    my variable obj
    my variable fonts

    array set a $fonts(0)
    if {[::ooxml::Getopt opts [list list name.arg $a(name) family.arg $a(family) size.arg $a(size) color $a(color) scheme $a(scheme) bold italic underline color.arg {}] $args]} {
      error $opts(-errmsg)
    }

    if {$opts(list)} {
      return [array get fonts]
    }

    set obj(blockPreset) 1

    if {$opts(name) eq {}} {
      set opts(name) $a(name)
    }
    if {![string is integer -strict $opts(family)] || $opts(family) < 0} {
      set opts(family) $a(family)
    }
    if {![string is integer -strict $opts(size)] || $opts(size) < 0} {
      set opts(size) $a(size)
    }
    if {$opts(scheme) ni {major minor none}} {
      set opts(scheme) $a(scheme)
    }
    set opts(color) [::ooxml::Color $opts(color)]

    foreach idx [lsort -integer [array names fonts]] {
      array set a $fonts($idx)
      set found 1
      foreach name [array names a] {
        if {$a($name) ne $opts($name)} {
	  set found 0
	  break
	}
      }
      if {$found} {
        return $idx
      }
    }

    set fonts($obj(fonts)) {}
    foreach item {name family size bold italic underline color scheme} {
      lappend fonts($obj(fonts)) $item $opts($item)
    }
    set idx $obj(fonts)
    incr obj(fonts)
    return $idx
  }

  method fill { args } {
    my variable obj
    my variable fills

    if {[::ooxml::Getopt opts {list patterntype.arg none fgcolor.arg {} bgcolor.arg {}} $args]} {
      error $opts(-errmsg)
    }

    if {$opts(list)} {
      return [array get fills]
    }

    set obj(blockPreset) 1

    if {$opts(patterntype) ni $::ooxml::predefPatternType} {
      set opts(patterntype) none
    }
    set opts(fgcolor) [::ooxml::Color $opts(fgcolor)]
    set opts(bgcolor) [::ooxml::Color $opts(bgcolor)]

    foreach idx [lsort -integer [array names fills]] {
      array set a $fills($idx)
      set found 1
      foreach name [array names a] {
        if {$a($name) ne $opts($name)} {
	  set found 0
	  break
	}
      }
      if {$found} {
        return $idx
      }
    }

    set fills($obj(fills)) {}
    foreach item {patterntype fgcolor bgcolor} {
      lappend fills($obj(fills)) $item $opts($item)
    }
    set idx $obj(fills)
    incr obj(fills)
    
    return $idx
  }

  method border { args } {
    my variable obj
    my variable borders

    if {[::ooxml::Getopt opts {list leftstyle.arg {} leftcolor.arg {} rightstyle.arg {} rightcolor.arg {} topstyle.arg {} topcolor.arg {} bottomstyle.arg {} bottomcolor.arg {} diagonalstyle.arg {} diagonalcolor.arg {} diagonaldirection.arg {}} $args]} {
      error $opts(-errmsg)
    }

    if {$opts(list)} {
      return [array get borders]
    }

    set obj(blockPreset) 1

    if {$opts(leftstyle) ni $::ooxml::predefBorderLineStyles || $opts(leftstyle) eq {none}} {
      set opts(leftstyle) {}
    }
    set opts(leftcolor) [::ooxml::Color $opts(leftcolor)]
    if {$opts(rightstyle) ni $::ooxml::predefBorderLineStyles || $opts(rightstyle) eq {none}} {
      set opts(rightstyle) {}
    }
    set opts(rightcolor) [::ooxml::Color $opts(rightcolor)]
    if {$opts(topstyle) ni $::ooxml::predefBorderLineStyles || $opts(topstyle) eq {none}} {
      set opts(topstyle) {}
    }
    set opts(topcolor) [::ooxml::Color $opts(topcolor)]
    if {$opts(bottomstyle) ni $::ooxml::predefBorderLineStyles || $opts(bottomstyle) eq {none}} {
      set opts(bottomstyle) {}
    }
    set opts(bottomcolor) [::ooxml::Color $opts(bottomcolor)]
    if {$opts(diagonalstyle) ni $::ooxml::predefBorderLineStyles || $opts(diagonalstyle) eq {none}} {
      set opts(diagonalstyle) {}
    }
    set opts(diagonalcolor) [::ooxml::Color $opts(diagonalcolor)]
    if {$opts(diagonaldirection) ni {up down}} {
      set opts(diagonaldirection) {}
    }
    switch -- $opts(diagonaldirection) {
      up {
	set opts(diagonaldirection) diagonalUp
      }
      down {
	set opts(diagonaldirection) diagonalDown
      }
      default {
	set opts(diagonaldirection) {}
      }
    }

    dict set tmp left style $opts(leftstyle)
    dict set tmp left color $opts(leftcolor)
    dict set tmp right style $opts(rightstyle)
    dict set tmp right color $opts(rightcolor)
    dict set tmp top style $opts(topstyle)
    dict set tmp top color $opts(topcolor)
    dict set tmp bottom style $opts(bottomstyle)
    dict set tmp bottom color $opts(bottomcolor)
    dict set tmp diagonal style $opts(diagonalstyle)
    dict set tmp diagonal color $opts(diagonalcolor)
    dict set tmp diagonal direction $opts(diagonaldirection)

    foreach idx [lsort -integer [array names borders]] {
      set found 1
      foreach key [dict keys $tmp] {
	foreach subkey [dict keys [dict get $tmp $key]] {
	  if {[dict get $borders($idx) $key $subkey] ne [dict get $tmp $key $subkey]} {
	    set found 0
	    break
	  }
	}
      }
      if {$found} {
        return $idx
      }
    }

    set borders($obj(borders)) $tmp
    set idx $obj(borders)
    incr obj(borders)
    
    return $idx
  }

  method style { args } {
    my variable obj
    my variable styles

    if {[::ooxml::Getopt opts {list numfmt.arg 0 font.arg 0 fill.arg 0 border.arg 0 xf.arg 0 horizontal.arg {} vertical.arg {} rotate.arg {}} $args]} {
      error $opts(-errmsg)
    }

    if {$opts(list)} {
      return [array get styles]
    }

    set obj(blockPreset) 1
    
    if {![string is integer -strict $opts(numfmt)] || $opts(numfmt) < 0} {
      set opts(numfmt) 0
    }
    if {![string is integer -strict $opts(font)] || $opts(font) < 0} {
      set opts(font) 0
    }
    if {![string is integer -strict $opts(fill)] || $opts(fill) < 0} {
      set opts(fill) 0
    }
    if {![string is integer -strict $opts(border)] || $opts(border) < 0} {
      set opts(border) 0
    }
    if {![string is integer -strict $opts(xf)] || $opts(xf) < 0} {
      set opts(xf) 0
    }
    if {$opts(horizontal) ni {right center left}} {
      set opts(horizontal) {}
    }
    if {$opts(vertical) ni {top center bottom}} {
      set opts(vertical) {}
    }
    if {![string is integer -strict $opts(rotate)] || $opts(rotate) < 0 || $opts(rotate) > 360} {
      set opts(rotate) {}
    }

    foreach idx [lsort -integer [array names styles]] {
      array set a $styles($idx)
      set found 1
      foreach name [array names a] {
        if {$a($name) ne $opts($name)} {
	  set found 0
	  break
	}
      }
      if {$found} {
        return $idx
      }
    }

    set styles($obj(styles)) {}
    foreach item {numfmt font fill border xf horizontal vertical rotate} {
      lappend styles($obj(styles)) $item $opts($item)
    }
    set idx $obj(styles)
    incr obj(styles)
    return $idx
  }

  method worksheet { name } {
    my variable obj

    incr obj(sheets)
    set obj(callRow,$obj(sheets)) 0
    set obj(sheet,$obj(sheets)) $name
    set obj(gCol,$obj(sheets)) -1
    set obj(row,$obj(sheets)) -1
    set obj(col,$obj(sheets)) -1
    set obj(dminrow,$obj(sheets)) 4294967295
    set obj(dmaxrow,$obj(sheets)) 0
    set obj(dmincol,$obj(sheets)) 4294967295
    set obj(dmaxcol,$obj(sheets)) 0
    set obj(autofilter,$obj(sheets)) {}
    set obj(freeze,$obj(sheets)) {}
    set obj(merge,$obj(sheets)) {}
    set obj(rowHeight,$obj(sheets)) {}

    return $obj(sheets)
  }

  method column { sheet args } {
    my variable obj
    my variable cols

    if {[::ooxml::Getopt opts {index.arg {} to.arg {} width.arg {} style.arg {} bestfit customwidth string nozero calcfit} $args]} {
      error $opts(-errmsg)
    }

    lassign [split $opts(index) ,] row col
    if {[string is integer -strict $opts(index)] && $opts(index) > -1} {
      set obj(gCol,$sheet) $opts(index)
    } elseif {[string is integer -strict $col] && $col > -1} {
      set obj(gCol,$sheet) $col
    } elseif {[string trim $opts(index)] eq {}} {
      incr obj(gCol,$sheet)
    }
    set opts(index) $obj(gCol,$sheet)

    if {$opts(to) eq {}} {
      set opts(to) $opts(index)
    } else {
      set opts(to) [::ooxml::IndexToString $opts(to)]
    }
    set opts(index) [lindex [split $opts(index) ,] end]
    set opts(to) [lindex [split $opts(to) ,] end]
    if {[string is integer -strict $opts(index)]} {
      set obj(gCol,$sheet) $opts(index)
    }
    
    if {![string is double -strict $opts(width)] || $opts(width) < 0} {
      set opts(width) {}
    }
    if {![string is integer -strict $opts(style)] || $opts(style) < 0} {
      set opts(style) {}
    }

    if {$opts(width) ne {} || ([string is integer -strict $opts(style)] && $opts(style) > 0) || $opts(bestfit) > 0} {
      if {$opts(width) eq {}} {
        set opts(width) $::ooxml::defaults(cols,width)
      }
      set cols($sheet,$opts(index)) [list min $opts(index) max $opts(to) width $opts(width) style $opts(style) bestfit $opts(bestfit) customwidth $opts(customwidth) string $opts(string) nozero $opts(nozero)]
    }
    set obj($sheet,cols) [llength [array names cols $sheet,*]]

    return $obj(gCol,$sheet)
  }

  method row { sheet args } {
    my variable obj

    if {[::ooxml::Getopt opts {index.arg {} height.arg {}} $args]} {
      error $opts(-errmsg)
    }

    if {![string is integer -strict $opts(height)] || $opts(height) < 1 || $opts(height) > 1024} {
      set opts(height) {}
    }
    if {[string is integer -strict $opts(index)] && $opts(index) > -1} {
      set obj(callRow,$obj(sheets)) 1
      set obj(col,$obj(sheets)) -1
      set obj(row,$sheet) $opts(index)
      if {$opts(height) ne {}} {
        dict set obj(rowHeight,$sheet) $obj(row,$sheet) $opts(height)
      }
      return $obj(row,$sheet)
    }
    if {[string trim $opts(index)] eq {}} {
      set obj(callRow,$obj(sheets)) 1
      set obj(col,$obj(sheets)) -1
      incr obj(row,$sheet)
      if {$opts(height) ne {}} {
        dict set obj(rowHeight,$sheet) $obj(row,$sheet) $opts(height)
      }
      return $obj(row,$sheet)
    }
    return -1
  }

  method rowheight { sheet row height } {
    my variable obj

    if {![string is integer -strict $row] || ![string is integer -strict $height] || $height < 1 || $height > 1024} {
      return -1
    }

    dict set obj(rowHeight,$sheet) $row $height
    return $row
  }

  method cell { sheet {data {}} args } {
    my variable obj
    my variable cells
    my variable cols

    if {[::ooxml::Getopt opts {index.arg {} style.arg 0 formula.arg {} string nozero globalstyle height.arg {}} $args]} {
      error $opts(-errmsg)
    }

    if {!$obj(callRow,$obj(sheets))} {
      set obj(callRow,$obj(sheets)) 1
      incr obj(row,$sheet)
    }

    lassign [split $opts(index) ,] row col
    if {[string is integer -strict $opts(index)] && $opts(index) > -1} {
      set obj(col,$sheet) $opts(index)
    } elseif {[string is integer -strict $row] && [string is integer -strict $col] && $row > -1 && $col > -1} {
      set obj(row,$sheet) $row
      set obj(col,$sheet) $col
    } elseif {[string trim $opts(index)] eq {}} {
      incr obj(col,$sheet)
    }
    if {$obj(row,$sheet) < 0 || $obj(col,$sheet) < 0} {
      return -1
    }

    if {$opts(globalstyle) && [string is integer -strict $opts(style)] && $opts(style) < 1} {
      if {[info exists cols($sheet,$obj(col,$sheet))] && [dict get $cols($sheet,$obj(col,$sheet)) style] > 0} {
        set opts(style) [dict get $cols($sheet,$obj(col,$sheet)) style]
      }
    }
    if {$opts(string) == 0 && [info exists cols($sheet,$obj(col,$sheet))] && [dict get $cols($sheet,$obj(col,$sheet)) string] == 1} {
      set opts(string) 1
    }
    if {$opts(nozero) == 0 && [info exists cols($sheet,$obj(col,$sheet))] && [dict get $cols($sheet,$obj(col,$sheet)) nozero] == 1} {
      set opts(nozero) 1
    }

    set cell ${sheet},$obj(row,$sheet),$obj(col,$sheet)
    set cells($cell) {}

    if {[string is integer -strict $opts(height)] && $opts(height) > 0 && $opts(height) < 1024} {
      dict set obj(rowHeight,$sheet) $obj(row,$sheet) $opts(height)
    }

    set data [string trimright $data]
    if {$opts(nozero) && [string is double -strict $data] && $data == 0} {
      set data {}
    }

    if {$opts(string)} {
      set type s
    } elseif {[set datetime [::ooxml::ScanDateTime $data]] ne {}} {
      set type n
      set data $datetime
      if {[string is integer -strict $opts(style)] && $opts(style) < 1} {
        set opts(style) $obj(defaultdatestyle)
      }
    } elseif {[string is double -strict $data]} {
      set type n
      set data [string trim $data]
      if {$data in {Inf infinity NaN -NaN} || $opts(string)} {
	set type s
      }
    } else {
      set type s
    }

    if {[string is integer -strict $opts(style)] && $opts(style) > 0} {
      lappend cells($cell) s $opts(style)
    }
    if {[string trim $opts(formula)] ne {}} {
      lappend cells($cell) t $type
      lappend cells($cell) f $opts(formula)
    } else {
      lappend cells($cell) v $data t $type
    }

    if {[string trim $data] eq {} && [string trim $opts(formula)] eq {} && ![string is integer -strict $opts(style)] && $opts(style) < 1} {
      unset -nocomplain cells($cell)
    } else {
      if {$obj(row,$sheet) < $obj(dminrow,$sheet)} {
	set obj(dminrow,$sheet) $obj(row,$sheet)
      }
      if {$obj(row,$sheet) > $obj(dmaxrow,$sheet)} {
	set obj(dmaxrow,$sheet) $obj(row,$sheet)
      }
      if {$obj(col,$sheet) < $obj(dmincol,$sheet)} {
	set obj(dmincol,$sheet) $obj(col,$sheet)
      }
      if {$obj(col,$sheet) > $obj(dmaxcol,$sheet)} {
	set obj(dmaxcol,$sheet) $obj(col,$sheet)
      }
    }
    
    return $obj(row,$sheet),$obj(col,$sheet)
  }

  method autofilter { sheet indexFrom indexTo } {
    my variable obj

    set indexFrom [::ooxml::IndexToString $indexFrom]
    set indexTo [::ooxml::IndexToString $indexTo]
    if {$indexFrom ne {} && $indexTo ne {}} {
      set obj(autofilter,$sheet) $indexFrom:$indexTo
      return 0
    }
    return 1
  }

  method freeze { sheet index } {
    my variable obj

    set index [::ooxml::IndexToString $index]
    if {$index ne {}} {
      set obj(freeze,$sheet) $index
      return 0
    }
    return 1
  }

  method merge { sheet indexFrom indexTo } {
    my variable obj

    set indexFrom [::ooxml::IndexToString $indexFrom]
    set indexTo [::ooxml::IndexToString $indexTo]
    if {$indexFrom ne {} && $indexTo ne {} && "$indexFrom:$indexTo" ni $obj(merge,$sheet)} {
      lappend obj(merge,$sheet) $indexFrom:$indexTo
      return 0
    }
    return 1
  }

  method presetstyles { valName args } {
    my variable obj
    my variable cols
    my variable fonts
    my variable numFmts
    my variable styles
    my variable fills
    my variable borders

    if {$obj(blockPreset)} {
      return 1
    }

    upvar $valName a

    if {[info exists a(s,@)]} {
      set obj(blockPreset) 1

      if {[dict exists $a(s,@) numFmtId]} {
	set obj(numFmts) [dict get $a(s,@) numFmtId]
	foreach idx $a(s,numFmtsIds) {
	  if {[info exists a(s,numFmts,$idx)]} {
	    set numFmts($idx) $a(s,numFmts,$idx)
	  }
	}
      }

      foreach item {fonts fills borders styles} {
        if {[dict exists $a(s,@) $item]} {
	  upvar 0 $item ad
	  for {set idx 0} {$idx < [dict get $a(s,@) $item]} {incr idx} {
	    if {[info exists a(s,$item,$idx)]} {
	      set ad($idx) $a(s,$item,$idx)
	    }
	  }
	  set obj($item) [dict get $a(s,@) $item]
	}
      }
    }

    foreach sheet $a(sheets) {
      for {set idx 0} {$idx < $a($sheet,cols)} {incr idx} {
	if {[info exists a($sheet,col,$idx)]} {
	  set cols([expr {$sheet + 1}],$idx) $a($sheet,col,$idx)
	}
      }
      set obj([expr {$sheet + 1}],cols) [llength [array names cols]]
    }

    return 0
  }

  method presetsheets { valName args } {
    upvar $valName a

    # [self object] -> my
    if {[info exists a(sheets)]} {
      foreach sheet $a(sheets) {
	if {[set currentSheet [my worksheet $a($sheet,n)]] > -1} {
	  dict set a(sheetmap) $sheet $currentSheet
	  foreach item [lsort -dictionary [array names a $sheet,v,*]] {
	    lassign [split $item ,] sheet tag row col
	    set options [list -index $row,$col]
	    if {[info exists a($sheet,f,$row,$col)]} {
	      lappend options -formula $a($sheet,f,$row,$col)
	    }
	    if {[info exists a($sheet,t,$row,$col)] && $a($sheet,t,$row,$col) eq {s}} {
	      lappend options -string
	    }
	    if {[info exists a($sheet,s,$row,$col)]} {
	      lappend options -style $a($sheet,s,$row,$col)
	    }
	    my cell $currentSheet $a($item) {*}$options
	  }
	  if {[info exists a($sheet,rowheight)]} {
	    foreach {row height} $a($sheet,rowheight) {
	      my rowheight $currentSheet $row $height
	    }
	  }
	  if {[info exists a($sheet,freeze)]} {
	    my freeze $currentSheet $a($sheet,freeze)
	  }
	  if {[info exists a($sheet,filter)]} {
	    foreach item $a($sheet,filter) {
	      my autofilter $currentSheet {*}[split $item :]
	    }
	  }
	  if {[info exists a($sheet,merge)]} {
	    foreach item $a($sheet,merge) {
	      my merge $currentSheet {*}[split $item :]
	    }
	  }
	}
      }
    }
  }

  method debug { args } {
    foreach item $args {
      catch {
	my variable $item
	parray $item
      }
    }
  }

  method write { file args } {
    my variable obj
    my variable cells
    my variable sharedStrings
    my variable fonts
    my variable numFmts
    my variable styles
    my variable fills
    my variable borders
    my variable cols

    if {[::ooxml::Getopt opts {holdcontainerdirectory} $args]} {
      error $opts(-errmsg)
    }

    foreach {n v} [array get cells] {
      if {[dict exists $v t] && [dict get $v t] eq {s} && [dict exists $v v] && [dict get $v v] ne {}} {
	if {[set pos [lsearch -exact $sharedStrings [dict get $v v]]] == -1} {
	  lappend sharedStrings [dict get $v v]
	  set pos [lsearch -exact $sharedStrings [dict get $v v]]
	}
	set obj(sharedStrings) 1
	dict set cells($n) v $pos
      }
    }
    unset -nocomplain n v

    # _rels/.rels
    set doc [set obj(doc,_rels/.rels) [dom createDocument Relationships]]
    set root [$doc documentElement]

    set rId 0

    dom createNodeCmd -tagName Relationship elementNode Tag_Relationship

    $root setAttribute xmlns http://schemas.openxmlformats.org/package/2006/relationships

    $root appendFromScript {
      Tag_Relationship Id rId1 Type http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument Target xl/workbook.xml {}
      Tag_Relationship Id rId2 Type http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties Target docProps/app.xml {}
      Tag_Relationship Id rId3 Type http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties Target docProps/core.xml {}
    }


    # [Content_Types].xml
    set doc [set obj(doc,\[Content_Types\].xml) [dom createDocument Types]]
    set root [$doc documentElement]

    dom createNodeCmd -tagName Default elementNode Tag_Default
    dom createNodeCmd -tagName Override elementNode Tag_Override

    $root setAttribute xmlns http://schemas.openxmlformats.org/package/2006/content-types

    $root appendFromScript {
      Tag_Default Extension xml ContentType application/xml {}
      Tag_Default Extension rels ContentType application/vnd.openxmlformats-package.relationships+xml {}
      Tag_Override PartName /xl/workbook.xml ContentType application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml {}
      Tag_Override PartName /xl/worksheets/sheet1.xml ContentType application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml {}
      for {set ws 1} {$ws <= $obj(sheets)} {incr ws} {
	Tag_Override PartName /xl/theme/theme${ws}.xml ContentType application/vnd.openxmlformats-officedocument.theme+xml {}
      }
      Tag_Override PartName /xl/styles.xml ContentType application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml {}
      if {$obj(sharedStrings) > 0} {
	Tag_Override PartName /xl/sharedStrings.xml ContentType application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml {}
      }
      if {$obj(calcChain)} {
	Tag_Override PartName /xl/calcChain.xml ContentType application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml {}
      }
      Tag_Override PartName /docProps/core.xml ContentType application/vnd.openxmlformats-package.core-properties+xml {}
      Tag_Override PartName /docProps/app.xml ContentType application/vnd.openxmlformats-officedocument.extended-properties+xml {}
    }


    # docProps/app.xml
    set doc [set obj(doc,docProps/app.xml) [dom createDocument Properties]]
    set root [$doc documentElement]

    dom createNodeCmd textNode Text
    dom createNodeCmd -tagName AppVersion elementNode Tag_AppVersion
    dom createNodeCmd -tagName Application elementNode Tag_Application
    dom createNodeCmd -tagName Company elementNode Tag_Company
    dom createNodeCmd -tagName DocSecurity elementNode Tag_DocSecurity
    dom createNodeCmd -tagName HeadingPairs elementNode Tag_HeadingPairs
    dom createNodeCmd -tagName HyperlinksChanged elementNode Tag_HyperlinksChanged
    dom createNodeCmd -tagName LinksUpToDate elementNode Tag_LinksUpToDate
    dom createNodeCmd -tagName ScaleCrop elementNode Tag_ScaleCrop
    dom createNodeCmd -tagName SharedDoc elementNode Tag_SharedDoc
    dom createNodeCmd -tagName TitlesOfParts elementNode Tag_TitlesOfParts
    dom createNodeCmd -tagName vt:i4 elementNode Tag_vt:i4
    dom createNodeCmd -tagName vt:lpstr elementNode Tag_vt:lpstr
    dom createNodeCmd -tagName vt:variant elementNode Tag_vt:variant
    dom createNodeCmd -tagName vt:vector elementNode Tag_vt:vector

    $root setAttribute xmlns http://schemas.openxmlformats.org/officeDocument/2006/extended-properties
    $root setAttribute xmlns:vt http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes

    $root appendFromScript {
      Tag_Application { Text {Tcl - Office Open XML - Spreadsheet} }
      Tag_DocSecurity { Text 0 }
      Tag_ScaleCrop { Text false }
      Tag_HeadingPairs {
	Tag_vt:vector size 2 baseType variant {
	  Tag_vt:variant {
	    Tag_vt:lpstr { Text [msgcat::mc Worksheets] }
	  }
	  Tag_vt:variant {
	    Tag_vt:i4 { Text 3 }
	  }
	}
      }
      Tag_TitlesOfParts {
	Tag_vt:vector size $obj(sheets) baseType lpstr {
	  Tag_vt:lpstr {
	    for {set ws 1} {$ws <= $obj(sheets)} {incr ws} {
	      Text [msgcat::mc Sheet]$ws
	    }
	  }
	}
      }
      Tag_Company {}
      Tag_LinksUpToDate { Text false }
      Tag_SharedDoc { Text false }
      Tag_HyperlinksChanged { Text false }
      Tag_AppVersion { Text 1.0 }
    }


    # docProps/core.xml
    set doc [set obj(doc,docProps/core.xml) [dom createDocument cp:coreProperties]]
    set root [$doc documentElement]

    dom createNodeCmd textNode Text
    dom createNodeCmd -tagName cp:lastModifiedBy elementNode Tag_cp:lastModifiedBy
    dom createNodeCmd -tagName dc:creator elementNode Tag_dc:creator
    dom createNodeCmd -tagName dcterms:created elementNode Tag_dcterms:created
    dom createNodeCmd -tagName dcterms:modified elementNode Tag_dcterms:modified

    $root setAttribute xmlns:cp http://schemas.openxmlformats.org/package/2006/metadata/core-properties
    $root setAttribute xmlns:dc http://purl.org/dc/elements/1.1/
    $root setAttribute xmlns:dcterms http://purl.org/dc/terms/
    $root setAttribute xmlns:dcmitype http://purl.org/dc/dcmitype/
    $root setAttribute xmlns:xsi http://www.w3.org/2001/XMLSchema-instance

    $root appendFromScript {
      Tag_dc:creator { Text $obj(creator) }
      Tag_cp:lastModifiedBy { Text $obj(creator) }
      Tag_dcterms:created xsi:type dcterms:W3CDTF { Text $obj(created) }
      Tag_dcterms:modified xsi:type dcterms:W3CDTF { Text $obj(created) }
    }


    # xl/_rels/workbook.xml.rels
    set doc [set obj(doc,xl/_rels/workbook.xml.rels) [dom createDocument Relationships]]
    set root [$doc documentElement]

    dom createNodeCmd -tagName Relationship elementNode Tag_Relationship

    $root setAttribute xmlns http://schemas.openxmlformats.org/package/2006/relationships

    $root appendFromScript {
      for {set ws 1} {$ws <= $obj(sheets)} {incr ws} {
	Tag_Relationship Id rId$ws Type http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet Target worksheets/sheet${ws}.xml {}
      }
      set rId [incr ws -1]
      Tag_Relationship Id rId[incr rId] Type http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme Target theme/theme1.xml {}
      Tag_Relationship Id rId[incr rId] Type http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles Target styles.xml {}
      if {$obj(sharedStrings) > 0} {
	Tag_Relationship Id rId[incr rId] Type http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings Target sharedStrings.xml {}
      }
      if {$obj(calcChain)} {
	Tag_Relationship Id rId[incr rId] Type http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain Target calcChain.xml {}
      }
    }


    # xl/sharedStrings.xml
    if {$obj(sharedStrings) > 0} {
      set doc [set obj(doc,xl/sharedStrings.xml) [dom createDocument sst]]
      set root [$doc documentElement]

      dom createNodeCmd textNode Text
      dom createNodeCmd -tagName si elementNode Tag_si
      dom createNodeCmd -tagName t elementNode Tag_t

      $root setAttribute xmlns http://schemas.openxmlformats.org/spreadsheetml/2006/main
      $root setAttribute count [llength $sharedStrings]
      $root setAttribute uniqueCount [llength $sharedStrings]

      $root appendFromScript {
	foreach string $sharedStrings {
	  Tag_si {
	    Tag_t { Text $string }
	  }
	}
      }
    }


    # xl/calcChain.xml
    if {$obj(calcChain)} {
      set doc [set obj(doc,xl/calcChain.xml) [dom createDocument calcChain]]
      set root [$doc documentElement]
      dom createNodeCmd -tagName c elementNode Tag_c

      $root setAttribute xmlns http://schemas.openxmlformats.org/spreadsheetml/2006/main

      $root appendFromScript {
	Tag_c r C1 i 3 l 1 {}
	Tag_c r A3 i 2 {}
      }
    }


    # xl/styles.xml
    set doc [set obj(doc,xl/styles.xml) [dom createDocument styleSheet]]
    set root [$doc documentElement]

    dom createNodeCmd -tagName alignment elementNode Tag_alignment
    dom createNodeCmd -tagName b elementNode Tag_b
    dom createNodeCmd -tagName bgColor elementNode Tag_bgColor
    dom createNodeCmd -tagName border elementNode Tag_border
    dom createNodeCmd -tagName borders elementNode Tag_borders
    dom createNodeCmd -tagName bottom elementNode Tag_bottom
    dom createNodeCmd -tagName cellStyle elementNode Tag_cellStyle
    dom createNodeCmd -tagName cellStyleXfs elementNode Tag_cellStyleXfs
    dom createNodeCmd -tagName cellStyles elementNode Tag_cellStyles
    dom createNodeCmd -tagName cellXfs elementNode Tag_cellXfs
    dom createNodeCmd -tagName color elementNode Tag_color
    dom createNodeCmd -tagName diagonal elementNode Tag_diagonal
    dom createNodeCmd -tagName dxfs elementNode Tag_dxfs
    dom createNodeCmd -tagName family elementNode Tag_family
    dom createNodeCmd -tagName fgColor elementNode Tag_fgColor
    dom createNodeCmd -tagName fill elementNode Tag_fill
    dom createNodeCmd -tagName fills elementNode Tag_fills
    dom createNodeCmd -tagName font elementNode Tag_font
    dom createNodeCmd -tagName fonts elementNode Tag_fonts
    dom createNodeCmd -tagName i elementNode Tag_i
    dom createNodeCmd -tagName left elementNode Tag_left
    dom createNodeCmd -tagName name elementNode Tag_name
    dom createNodeCmd -tagName numFmt elementNode Tag_numFmt
    dom createNodeCmd -tagName numFmts elementNode Tag_numFmts
    dom createNodeCmd -tagName patternFill elementNode Tag_patternFill
    dom createNodeCmd -tagName right elementNode Tag_right
    dom createNodeCmd -tagName scheme elementNode Tag_scheme
    dom createNodeCmd -tagName sz elementNode Tag_sz
    dom createNodeCmd -tagName tableStyles elementNode Tag_tableStyles
    dom createNodeCmd -tagName top elementNode Tag_top
    dom createNodeCmd -tagName u elementNode Tag_u
    dom createNodeCmd -tagName xf elementNode Tag_xf

    $root setAttribute xmlns http://schemas.openxmlformats.org/spreadsheetml/2006/main
    $root setAttribute xmlns:mc http://schemas.openxmlformats.org/markup-compatibility/2006
    $root setAttribute xmlns:x14ac http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac
    $root setAttribute mc:Ignorable x14ac

    $root appendFromScript {
      if {$obj(numFmts) > $::ooxml::defaults(numFmts,start)} {
	Tag_numFmts count [llength [array names numFmts]] {
	  foreach idx [lsort -integer [array names numFmts]] {
	    Tag_numFmt numFmtId $idx formatCode $numFmts($idx) {}
	  }
	}
      }
      Tag_fonts count $obj(fonts) x14ac:knownFonts 1 {
	foreach idx [lsort -integer [array names fonts]] {
	  Tag_font {
	    if {[dict get $fonts($idx) color] ne {}} {
	      Tag_color [lindex [dict get $fonts($idx) color] 0] [lindex [dict get $fonts($idx) color] 1]
	    }
	    if {[dict get $fonts($idx) bold] == 1} {
	      Tag_b {}
	    }
	    if {[dict get $fonts($idx) italic] == 1} {
	      Tag_i {}
	    }
	    if {[dict get $fonts($idx) underline] == 1} {
	      Tag_u {}
	    }
	    Tag_sz val [dict get $fonts($idx) size] {}
	    Tag_name val [dict get $fonts($idx) name] {}
	    Tag_family val [dict get $fonts($idx) family] {}
	    Tag_scheme val [dict get $fonts($idx) scheme] {}
	  }
	}
      }
      if {$obj(fills) > 0} {
	Tag_fills count $obj(fills) {
	  foreach idx [lsort -integer [array names fills]] {
	    Tag_fill {
	      Tag_patternFill patternType [dict get $fills($idx) patterntype] {
		foreach tag {fgColor bgColor} {
		  set key [string tolower $tag]
		  if {[dict get $fills($idx) $key] ne {}} {
		    Tag_$tag [lindex [dict get $fills($idx) $key] 0] [lindex [dict get $fills($idx) $key] 1] {}
		  }
		}
	      }
	    }
	  }
	}
      }
      if {$obj(borders) > 0} {
	Tag_borders count $obj(borders) {
	  foreach idx [lsort -integer [array names borders]] {
	    set attr {}
	    if {[dict exists $borders($idx) diagonal direction] && [dict get $borders($idx) diagonal direction] ne {}} {
	      lappend attr [string map {up diagonalUp down diagonalDown} [dict get $borders($idx) diagonal direction]] 1
	    }
	    Tag_border {*}$attr {
	      foreach item {left right top bottom diagonal} {
		set attr {}
		if {[dict exists $borders($idx) $item style] && [dict get $borders($idx) $item style] ne {}} {
		  lappend attr style [dict get $borders($idx) $item style]
		}
	        Tag_$item {*}$attr {
		  if {[dict exists $borders($idx) $item color] && [dict get $borders($idx) $item color] ne {}} {
		    Tag_color [lindex [dict get $borders($idx) $item color] 0] [lindex [dict get $borders($idx) $item color] 1] {}
		  }
		}
	      }
	    }
	  }
	}
      }
      Tag_cellStyleXfs count 1 {
	Tag_xf numFmtId 0 fontId 0 fillId 0 borderId 0 {}
      }
      Tag_cellXfs count $obj(styles) {
	foreach idx [lsort -integer [array names styles]] {
	  set attr {}
	  lappend attr numFmtId [dict get $styles($idx) numfmt]
	  lappend attr fontId [dict get $styles($idx) font]
	  lappend attr fillId [dict get $styles($idx) fill]
	  lappend attr borderId [dict get $styles($idx) border]
	  lappend attr xfId [dict get $styles($idx) xf]
	  if {[dict get $styles($idx) numfmt] > 0} {
	    lappend attr applyNumberFormat 1
	  }
	  if {[dict get $styles($idx) font] > 0} {
	    lappend attr applyFont 1
	  }
	  if {[dict get $styles($idx) fill] > 0} {
	    lappend attr applyFill 1
	  }
	  if {[dict get $styles($idx) border] > 0} {
	    lappend attr applyBorder 1
	  }
	  # lappend attr applyProtection 1 quotePrefix 1
	  if {[dict get $styles($idx) horizontal] ne {} || [dict get $styles($idx) vertical] ne {} || [dict get $styles($idx) rotate] ne {}} {
	    lappend attr applyAlignment 1
	    set alignment 1
	  } else {
	    set alignment 0
	  }
	  Tag_xf {*}$attr {
	    set attr {}
	    if {$alignment} {
	      if {[dict get $styles($idx) horizontal] ne {}} {
		lappend attr horizontal [dict get $styles($idx) horizontal]
	      }
	      if {[dict get $styles($idx) vertical] ne {}} {
		lappend attr vertical [dict get $styles($idx) vertical]
	      }
	      if {[dict get $styles($idx) rotate] ne {}} {
		lappend attr textRotation [dict get $styles($idx) rotate]
	      }
	      Tag_alignment {*}$attr {}
	    }
	  }
	}
      }
      Tag_cellStyles count 1 {
	Tag_cellStyle name Standard xfId 0 builtinId 0 {}
      }
      Tag_dxfs count 0 {}
      Tag_tableStyles count 0 {}
    }


    # xl/theme/theme1.xml
    set doc [set obj(doc,xl/theme/theme1.xml) [dom createDocument a:theme]]
    set root [$doc documentElement]

    dom createNodeCmd -tagName a:accent1 elementNode Tag_a:accent1
    dom createNodeCmd -tagName a:accent2 elementNode Tag_a:accent2
    dom createNodeCmd -tagName a:accent3 elementNode Tag_a:accent3
    dom createNodeCmd -tagName a:accent4 elementNode Tag_a:accent4
    dom createNodeCmd -tagName a:accent5 elementNode Tag_a:accent5
    dom createNodeCmd -tagName a:accent6 elementNode Tag_a:accent6
    dom createNodeCmd -tagName a:alpha elementNode Tag_a:alpha
    dom createNodeCmd -tagName a:bevelT elementNode Tag_a:bevelT
    dom createNodeCmd -tagName a:bgFillStyleLst elementNode Tag_a:bgFillStyleLst
    dom createNodeCmd -tagName a:bodyPr elementNode Tag_a:bodyPr
    dom createNodeCmd -tagName a:camera elementNode Tag_a:camera
    dom createNodeCmd -tagName a:clrScheme elementNode Tag_a:clrScheme
    dom createNodeCmd -tagName a:cs elementNode Tag_a:cs
    dom createNodeCmd -tagName a:dk1 elementNode Tag_a:dk1
    dom createNodeCmd -tagName a:dk2 elementNode Tag_a:dk2
    dom createNodeCmd -tagName a:ea elementNode Tag_a:ea
    dom createNodeCmd -tagName a:effectLst elementNode Tag_a:effectLst
    dom createNodeCmd -tagName a:effectRef elementNode Tag_a:effectRef
    dom createNodeCmd -tagName a:effectStyle elementNode Tag_a:effectStyle
    dom createNodeCmd -tagName a:effectStyleLst elementNode Tag_a:effectStyleLst
    dom createNodeCmd -tagName a:extraClrSchemeLst elementNode Tag_a:extraClrSchemeLst
    dom createNodeCmd -tagName a:fillRef elementNode Tag_a:fillRef
    dom createNodeCmd -tagName a:fillStyleLst elementNode Tag_a:fillStyleLst
    dom createNodeCmd -tagName a:fillToRect elementNode Tag_a:fillToRect
    dom createNodeCmd -tagName a:fmtScheme elementNode Tag_a:fmtScheme
    dom createNodeCmd -tagName a:folHlink elementNode Tag_a:folHlink
    dom createNodeCmd -tagName a:font elementNode Tag_a:font
    dom createNodeCmd -tagName a:fontRef elementNode Tag_a:fontRef
    dom createNodeCmd -tagName a:fontScheme elementNode Tag_a:fontScheme
    dom createNodeCmd -tagName a:gradFill elementNode Tag_a:gradFill
    dom createNodeCmd -tagName a:gs elementNode Tag_a:gs
    dom createNodeCmd -tagName a:gsLst elementNode Tag_a:gsLst
    dom createNodeCmd -tagName a:hlink elementNode Tag_a:hlink
    dom createNodeCmd -tagName a:latin elementNode Tag_a:latin
    dom createNodeCmd -tagName a:lightRig elementNode Tag_a:lightRig
    dom createNodeCmd -tagName a:lin elementNode Tag_a:lin
    dom createNodeCmd -tagName a:ln elementNode Tag_a:ln
    dom createNodeCmd -tagName a:lnDef elementNode Tag_a:lnDef
    dom createNodeCmd -tagName a:lnRef elementNode Tag_a:lnRef
    dom createNodeCmd -tagName a:lnStyleLst elementNode Tag_a:lnStyleLst
    dom createNodeCmd -tagName a:lstStyle elementNode Tag_a:lstStyle
    dom createNodeCmd -tagName a:lt1 elementNode Tag_a:lt1
    dom createNodeCmd -tagName a:lt2 elementNode Tag_a:lt2
    dom createNodeCmd -tagName a:majorFont elementNode Tag_a:majorFont
    dom createNodeCmd -tagName a:minorFont elementNode Tag_a:minorFont
    dom createNodeCmd -tagName a:objectDefaults elementNode Tag_a:objectDefaults
    dom createNodeCmd -tagName a:outerShdw elementNode Tag_a:outerShdw
    dom createNodeCmd -tagName a:path elementNode Tag_a:path
    dom createNodeCmd -tagName a:prstDash elementNode Tag_a:prstDash
    dom createNodeCmd -tagName a:rot elementNode Tag_a:rot
    dom createNodeCmd -tagName a:satMod elementNode Tag_a:satMod
    dom createNodeCmd -tagName a:scene3d elementNode Tag_a:scene3d
    dom createNodeCmd -tagName a:schemeClr elementNode Tag_a:schemeClr
    dom createNodeCmd -tagName a:shade elementNode Tag_a:shade
    dom createNodeCmd -tagName a:solidFill elementNode Tag_a:solidFill
    dom createNodeCmd -tagName a:sp3d elementNode Tag_a:sp3d
    dom createNodeCmd -tagName a:spDef elementNode Tag_a:spDef
    dom createNodeCmd -tagName a:spPr elementNode Tag_a:spPr
    dom createNodeCmd -tagName a:srgbClr elementNode Tag_a:srgbClr
    dom createNodeCmd -tagName a:style elementNode Tag_a:style
    dom createNodeCmd -tagName a:sysClr elementNode Tag_a:sysClr
    dom createNodeCmd -tagName a:themeElements elementNode Tag_a:themeElements
    dom createNodeCmd -tagName a:tint elementNode Tag_a:tint

    $root setAttribute xmlns:a http://schemas.openxmlformats.org/drawingml/2006/main
    $root setAttribute name Office-Design

    $root appendFromScript {
      Tag_a:themeElements {
	Tag_a:clrScheme name Office {
	  Tag_a:dk1 {
	    Tag_a:sysClr val windowText lastClr 000000 {}
	  }
	  Tag_a:lt1 {
	    Tag_a:sysClr val window lastClr FFFFFF {}
	  }
	  Tag_a:dk2 {
	    Tag_a:srgbClr val 1F497D {}
	  }
	  Tag_a:lt2 {
	    Tag_a:srgbClr val EEECE1 {}
	  }
	  Tag_a:accent1 {
	    Tag_a:srgbClr val 4F81BD {}
	  }
	  Tag_a:accent2 {
	    Tag_a:srgbClr val C0504D {}
	  }
	  Tag_a:accent3 {
	    Tag_a:srgbClr val 9BBB59 {}
	  }
	  Tag_a:accent4 {
	    Tag_a:srgbClr val 8064A2 {}
	  }
	  Tag_a:accent5 {
	    Tag_a:srgbClr val 4BACC6 {}
	  }
	  Tag_a:accent6 {
	    Tag_a:srgbClr val F79646 {}
	  }
	  Tag_a:hlink {
	    Tag_a:srgbClr val 0000FF {}
	  }
	  Tag_a:folHlink {
	    Tag_a:srgbClr val 800080 {}
	  }
	}
	Tag_a:fontScheme name Office {
	  Tag_a:majorFont {
	    Tag_a:latin typeface Cambria {}
	    Tag_a:ea typeface {} {}
	    Tag_a:cs typeface {} {}
	    Tag_a:font script Jpan typeface \uFF2D\uFF33\u0020\uFF30\u30B4\u30B7\u30C3\u30AF {}
	    Tag_a:font script Hang typeface \uB9D1\uC740\u0020\uACE0\uB515 {}
	    Tag_a:font script Hans typeface \u5B8B\u4F53 {}
	    Tag_a:font script Hant typeface \u65B0\u7D30\u660E\u9AD4 {}
	    Tag_a:font script Arab typeface {Times New Roman} {}
	    Tag_a:font script Hebr typeface {Times New Roman} {}
	    Tag_a:font script Thai typeface Tahoma {}
	    Tag_a:font script Ethi typeface Nyala {}
	    Tag_a:font script Beng typeface Vrinda {}
	    Tag_a:font script Gujr typeface Shruti {}
	    Tag_a:font script Khmr typeface MoolBoran {}
	    Tag_a:font script Knda typeface Tunga {}
	    Tag_a:font script Guru typeface Raavi {}
	    Tag_a:font script Cans typeface Euphemia {}
	    Tag_a:font script Cher typeface {Plantagenet Cherokee} {}
	    Tag_a:font script Yiii typeface {Microsoft Yi Baiti} {}
	    Tag_a:font script Tibt typeface {Microsoft Himalaya} {}
	    Tag_a:font script Thaa typeface {MV Boli} {}
	    Tag_a:font script Deva typeface Mangal {}
	    Tag_a:font script Telu typeface Gautami {}
	    Tag_a:font script Taml typeface Latha {}
	    Tag_a:font script Syrc typeface {Estrangelo Edessa} {}
	    Tag_a:font script Orya typeface Kalinga {}
	    Tag_a:font script Mlym typeface Kartika {}
	    Tag_a:font script Laoo typeface DokChampa {}
	    Tag_a:font script Sinh typeface {Iskoola Pota} {}
	    Tag_a:font script Mong typeface {Mongolian Baiti} {}
	    Tag_a:font script Viet typeface {Times New Roman} {}
	    Tag_a:font script Uigh typeface {Microsoft Uighur} {}
	    Tag_a:font script Geor typeface Sylfaen {}
	  }
	  Tag_a:minorFont {
	    Tag_a:latin typeface Calibri {}
	    Tag_a:ea typeface {} {}
	    Tag_a:cs typeface {} {}
	    Tag_a:font script Jpan typeface \uFF2D\uFF33\u0020\uFF30\u30B4\u30B7\u30C3\u30AF {}
	    Tag_a:font script Hang typeface \uB9D1\uC740\u0020\uACE0\uB515 {}
	    Tag_a:font script Hans typeface \u5B8B\u4F53 {}
	    Tag_a:font script Hant typeface \u65B0\u7D30\u660E\u9AD4 {}
	    Tag_a:font script Arab typeface Arial {}
	    Tag_a:font script Hebr typeface Arial {}
	    Tag_a:font script Thai typeface Tahoma {}
	    Tag_a:font script Ethi typeface Nyala {}
	    Tag_a:font script Beng typeface Vrinda {}
	    Tag_a:font script Gujr typeface Shruti {}
	    Tag_a:font script Khmr typeface DaunPenh {}
	    Tag_a:font script Knda typeface Tunga {}
	    Tag_a:font script Guru typeface Raavi {}
	    Tag_a:font script Cans typeface Euphemia {}
	    Tag_a:font script Cher typeface {Plantagenet Cherokee} {}
	    Tag_a:font script Yiii typeface {Microsoft Yi Baiti} {}
	    Tag_a:font script Tibt typeface {Microsoft Himalaya} {}
	    Tag_a:font script Thaa typeface {MV Boli} {}
	    Tag_a:font script Deva typeface Mangal {}
	    Tag_a:font script Telu typeface Gautami {}
	    Tag_a:font script Taml typeface Latha {}
	    Tag_a:font script Syrc typeface {Estrangelo Edessa} {}
	    Tag_a:font script Orya typeface Kalinga {}
	    Tag_a:font script Mlym typeface Kartika {}
	    Tag_a:font script Laoo typeface DokChampa {}
	    Tag_a:font script Sinh typeface {Iskoola Pota} {}
	    Tag_a:font script Mong typeface {Mongolian Baiti} {}
	    Tag_a:font script Viet typeface Arial {}
	    Tag_a:font script Uigh typeface {Microsoft Uighur} {}
	    Tag_a:font script Geor typeface Sylfaen {}
	  }
	}
	Tag_a:fmtScheme name Office {
	  Tag_a:fillStyleLst {
	    Tag_a:solidFill {
	      Tag_a:schemeClr val phClr {}
	    }
	    Tag_a:gradFill rotWithShape 1 {
	      Tag_a:gsLst {
		Tag_a:gs pos 0 {
		  Tag_a:schemeClr val phClr {
		    Tag_a:tint val 50000 {}
		    Tag_a:satMod val 300000 {}
		  }
		}
		Tag_a:gs pos 35000 {
		  Tag_a:schemeClr val phClr {
		    Tag_a:tint val 37000 {}
		    Tag_a:satMod val 300000 {}
		  }
		}
		Tag_a:gs pos 100000 {
		  Tag_a:schemeClr val phClr {
		    Tag_a:tint val 15000 {}
		    Tag_a:satMod val 350000 {}
		  }
		}
	      }
	      Tag_a:lin ang 16200000 scaled 1 {}
	    }
	    Tag_a:gradFill rotWithShape 1 {
	      Tag_a:gsLst {
		Tag_a:gs pos 0 {
		  Tag_a:schemeClr val phClr {
		    Tag_a:tint val 100000 {}
		    Tag_a:shade val 100000 {}
		    Tag_a:satMod val 130000 {}
		  }
		}
		Tag_a:gs pos 100000 {
		  Tag_a:schemeClr val phClr {
		    Tag_a:tint val 50000 {}
		    Tag_a:shade val 100000 {}
		    Tag_a:satMod val 350000 {}
		  }
		}
	      }
	      Tag_a:lin ang 16200000 scaled 0 {}
	    }
	  }
	  Tag_a:lnStyleLst {
	    Tag_a:ln w 9525 cap flat cmpd sng algn ctr {
	      Tag_a:solidFill {
		Tag_a:schemeClr val phClr {
		  Tag_a:shade val 95000 {
		  }
		  Tag_a:satMod val 105000 {
		  }
		}
	      }
	      Tag_a:prstDash val solid {}
	    }
	    Tag_a:ln w 25400 cap flat cmpd sng algn ctr {
	      Tag_a:solidFill {
		Tag_a:schemeClr val phClr {}
	      }
	      Tag_a:prstDash val solid {}
	    }
	    Tag_a:ln w 38100 cap flat cmpd sng algn ctr {
	      Tag_a:solidFill {
		Tag_a:schemeClr val phClr {}
	      }
	      Tag_a:prstDash val solid {}
	    }
	  }
	  Tag_a:effectStyleLst {
	    Tag_a:effectStyle {
	      Tag_a:effectLst {
		Tag_a:outerShdw blurRad 40000 dist 20000 dir 5400000 rotWithShape 0 {
		  Tag_a:srgbClr val 000000 {
		    Tag_a:alpha val 38000 {}
		  }
		}
	      }
	    }
	    Tag_a:effectStyle {
	      Tag_a:effectLst {
		Tag_a:outerShdw blurRad 40000 dist 23000 dir 5400000 rotWithShape 0 {
		  Tag_a:srgbClr val 000000 {
		    Tag_a:alpha val 35000 {}
		  }
		}
	      }
	    }
	    Tag_a:effectStyle {
	      Tag_a:effectLst {
		Tag_a:outerShdw blurRad 40000 dist 23000 dir 5400000 rotWithShape 0 {
		  Tag_a:srgbClr val 000000 {
		    Tag_a:alpha val 35000 {}
		  }
		}
	      }
	      Tag_a:scene3d {
		Tag_a:camera prst orthographicFront {
		  Tag_a:rot lat 0 lon 0 rev 0 {
		  }
		}
		Tag_a:lightRig rig threePt dir t {
		  Tag_a:rot lat 0 lon 0 rev 1200000 {
		  }
		}
	      }
	      Tag_a:sp3d {
		Tag_a:bevelT w 63500 h 25400 {}
	      }
	    }
	  }
	  Tag_a:bgFillStyleLst {
	    Tag_a:solidFill {
	      Tag_a:schemeClr val phClr {}
	    }
	    Tag_a:gradFill rotWithShape 1 {
	      Tag_a:gsLst {
		Tag_a:gs pos 0 {
		  Tag_a:schemeClr val phClr {
		    Tag_a:tint val 40000 {}
		    Tag_a:satMod val 350000 {}
		  }
		}
		Tag_a:gs pos 40000 {
		  Tag_a:schemeClr val phClr {
		    Tag_a:tint val 45000 {}
		    Tag_a:shade val 99000 {}
		    Tag_a:satMod val 350000 {}
		  }
		}
		Tag_a:gs pos 100000 {
		  Tag_a:schemeClr val phClr {
		    Tag_a:shade val 20000 {}
		    Tag_a:satMod val 255000 {}
		  }
		}
	      }
	      Tag_a:path path circle {
		Tag_a:fillToRect l 50000 t -80000 r 50000 b 180000 {}
	      }
	    }
	    Tag_a:gradFill rotWithShape 1 {
	      Tag_a:gsLst {
		Tag_a:gs pos 0 {
		  Tag_a:schemeClr val phClr {
		    Tag_a:tint val 80000 {}
		    Tag_a:satMod val 300000 {}
		  }
		}
		Tag_a:gs pos 100000 {
		  Tag_a:schemeClr val phClr {
		    Tag_a:shade val 30000 {}
		    Tag_a:satMod val 200000 {}
		  }
		}
	      }
	      Tag_a:path path circle {
		Tag_a:fillToRect l 50000 t 50000 r 50000 b 50000 {}
	      }
	    }
	  }
	}
      }
      Tag_a:objectDefaults {
	Tag_a:spDef {
	  Tag_a:spPr {}
	  Tag_a:bodyPr {}
	  Tag_a:lstStyle {}
	  Tag_a:style {
	    Tag_a:lnRef idx 1 {
	      Tag_a:schemeClr val accent1 {}
	    }
	    Tag_a:fillRef idx 3 {
	      Tag_a:schemeClr val accent1 {}
	    }
	    Tag_a:effectRef idx 2 {
	      Tag_a:schemeClr val accent1 {}
	    }
	    Tag_a:fontRef idx minor {
	      Tag_a:schemeClr val lt1 {}
	    }
	  }
	}
	Tag_a:lnDef {
	  Tag_a:spPr {}
	  Tag_a:bodyPr {}
	  Tag_a:lstStyle {}
	  Tag_a:style {
	    Tag_a:lnRef idx 2 {
	      Tag_a:schemeClr val accent1 {}
	    }
	    Tag_a:fillRef idx 0 {
	      Tag_a:schemeClr val accent1 {}
	    }
	    Tag_a:effectRef idx 1 {
	      Tag_a:schemeClr val accent1 {}
	    }
	    Tag_a:fontRef idx minor {
	      Tag_a:schemeClr val tx1 {}
	    }
	  }
	}
      }
      Tag_a:extraClrSchemeLst {}
    }


    # xl/workbook.xml
    set doc [set obj(doc,xl/workbook.xml) [dom createDocument workbook]]
    set root [$doc documentElement]

    dom createNodeCmd textNode Text
    dom createNodeCmd -tagName bookViews elementNode Tag_bookViews
    dom createNodeCmd -tagName calcPr elementNode Tag_calcPr
    dom createNodeCmd -tagName definedName elementNode Tag_definedName
    dom createNodeCmd -tagName definedNames elementNode Tag_definedNames
    dom createNodeCmd -tagName fileVersion elementNode Tag_fileVersion
    dom createNodeCmd -tagName sheet elementNode Tag_sheet
    dom createNodeCmd -tagName sheets elementNode Tag_sheets
    dom createNodeCmd -tagName workbookPr elementNode Tag_workbookPr
    dom createNodeCmd -tagName workbookView elementNode Tag_workbookView

    $root setAttribute xmlns http://schemas.openxmlformats.org/spreadsheetml/2006/main
    $root setAttribute xmlns:r http://schemas.openxmlformats.org/officeDocument/2006/relationships

    $root appendFromScript {
      Tag_fileVersion appName xl lastEdited 5 lowestEdited 5 rupBuild 5000 {}
      Tag_workbookPr showInkAnnotation 0 autoCompressPictures 0 {}
      Tag_bookViews {
	Tag_workbookView activeTab 1 {}
      }
      Tag_sheets {
	for {set ws 1} {$ws <= $obj(sheets)} {incr ws} {
	  Tag_sheet name $obj(sheet,$ws) sheetId $ws r:id rId$ws {}
	}
      }
      if {0} {
	Tag_definedNames {
	  Tag_definedName name _xlnm._FilterDatabase localSheetId 0 hidden 1 { Text Blatt1!$A$1:$C$1 }
	}
      }
      Tag_calcPr calcId 140000 concurrentCalc 0 {}
      # fullCalcOnLoad 1
    }


    # xl/worksheets/sheet1.xml SHEET
    dom createNodeCmd textNode Text
    dom createNodeCmd -tagName autoFilter elementNode Tag_autoFilter
    dom createNodeCmd -tagName c elementNode Tag_c
    dom createNodeCmd -tagName col elementNode Tag_col
    dom createNodeCmd -tagName cols elementNode Tag_cols
    dom createNodeCmd -tagName dimension elementNode Tag_dimension
    dom createNodeCmd -tagName mergeCell elementNode Tag_mergeCell
    dom createNodeCmd -tagName mergeCells elementNode Tag_mergeCells
    dom createNodeCmd -tagName pageMargins elementNode Tag_pageMargins
    dom createNodeCmd -tagName pane elementNode Tag_pane
    dom createNodeCmd -tagName row elementNode Tag_row
    dom createNodeCmd -tagName sheetData elementNode Tag_sheetData
    dom createNodeCmd -tagName sheetFormatPr elementNode Tag_sheetFormatPr
    dom createNodeCmd -tagName sheetView elementNode Tag_sheetView
    dom createNodeCmd -tagName sheetViews elementNode Tag_sheetViews
    dom createNodeCmd -tagName v elementNode Tag_v

    for {set ws 1} {$ws <= $obj(sheets)} {incr ws} {
      set doc [set obj(doc,xl/worksheets/sheet$ws.xml) [dom createDocument worksheet]]
      set root [$doc documentElement]
      $root setAttribute xmlns http://schemas.openxmlformats.org/spreadsheetml/2006/main
      $root setAttribute xmlns:r http://schemas.openxmlformats.org/officeDocument/2006/relationships
      $root setAttribute xmlns:mc http://schemas.openxmlformats.org/markup-compatibility/2006
      $root setAttribute xmlns:x14ac http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac
      $root setAttribute mc:Ignorable x14ac

      $root appendFromScript {
	Tag_dimension ref [::ooxml::RowColumnToString $obj(dminrow,$ws),$obj(dmincol,$ws)]:[::ooxml::RowColumnToString $obj(dmaxrow,$ws),$obj(dmaxcol,$ws)] {}
	Tag_sheetViews {
	  Tag_sheetView workbookViewId 0 {
	    if {$obj(freeze,$ws) ne {}} {
	      lassign [split [::ooxml::StringToRowColumn $obj(freeze,$ws)] ,] row col
	      Tag_pane xSplit $col ySplit $row topLeftCell $obj(freeze,$ws) state frozen {}
	    }
	  }
	}
	Tag_sheetFormatPr baseColWidth 10 defaultRowHeight 16 x14ac:dyDescent 0.2 {}
	if {[info exists obj($ws,cols)] && $obj($ws,cols) > 0} {
	  Tag_cols {}
	}
	Tag_sheetData {
	  set lastRow -1
	  set rows {}
	  foreach idx [lsort -dictionary [array names cells $ws,*,*]] {
	    lassign [split $idx ,] sheet row col
	    lappend rows $row
	  }
	  foreach row [lsort -unique -integer $rows] {
	    set maxCol $col
	    if {$row != $lastRow} {
	      set lastRow $row
	      set minCol $col
	    }
	    set attr {}
	    if {[dict exists $obj(rowHeight,$ws) $row]} {
	      lappend attr ht [dict get $obj(rowHeight,$ws) $row] customHeight 1
	    }
	    # lappend attr spans [expr {$minCol + 1}]:[expr {$maxCol + 1}]
	    Tag_row r [expr {$row + 1}] {*}$attr {
	      foreach idx [lsort -dictionary [array names cells $ws,$row,*]] {
		lassign [split $idx ,] sheet row col
		if {([dict exists $cells($idx) v] && [string trim [dict get $cells($idx) v]] ne {}) || ([dict exists $cells($idx) f] && [string trim [dict get $cells($idx) f]] ne {})} {
		  set attr {}
		  if {[dict exists $cells($idx) s] && [dict get $cells($idx) s] > 0} {
		    lappend attr s [dict get $cells($idx) s]
		  }
		  if {[dict exists $cells($idx) t] && [dict get $cells($idx) t] ne {n}} {
		    lappend attr t [dict get $cells($idx) t]
		  }
		  Tag_c r [::ooxml::RowColumnToString $row,$col] {*}$attr {
		    if {[dict exists $cells($idx) v] && [dict get $cells($idx) v] ne {}} {
		      Tag_v { Text [dict get $cells($idx) v] }
		    }
		    if {[dict exists $cells($idx) f] && [dict get $cells($idx) f] ne {}} {
		      Tag_f { Text [dict get $cells($idx) f] }
		    }
		  }
		} elseif {[dict exists $cells($idx) s] && [string is integer -strict [dict get $cells($idx) s]] && [dict get $cells($idx) s] > 0} {
		  Tag_c r [::ooxml::RowColumnToString $row,$col] s [dict get $cells($idx) s] {}
		}
	      }
	    }
	  }
	}
	if {$obj(autofilter,$ws) ne {}} {
	  Tag_autoFilter ref $obj(autofilter,$ws) {}
	}
	if {[info exists obj(merge,$ws)] && $obj(merge,$ws) ne {}} {
	  Tag_mergeCells count [llength $obj(merge,$ws)] {
	    foreach item $obj(merge,$ws) {
	      Tag_mergeCell ref $item {}
	    }
	  }
	}
	Tag_pageMargins left 0.75 right 0.75 top 1 bottom 1 header 0.5 footer 0.5 {}
      }

      if {[set colsNode [$root selectNodes {/worksheet/cols}]] ne {}} {
	if {[info exists obj($ws,cols)] && $obj($ws,cols) > 0} {
	  $colsNode appendFromScript {
	    foreach idx [lsort -dictionary [array names cols $ws,*]] {
	      set attr {}
	      lappend attr min [expr {[dict get $cols($idx) min] + 1}] max [expr {[dict get $cols($idx) max] + 1}]
	      if {[dict get $cols($idx) width] ne {}} {
		lappend attr width [dict get $cols($idx) width]
		if {[dict get $cols($idx) width] != $::ooxml::defaults(cols,width)} {
		  dict set $cols($idx) customwidth 1
		}
	      }
	      if {[dict get $cols($idx) style] ne {} && [dict get $cols($idx) style] > 0} {
		lappend attr style [dict get $cols($idx) style]
	      }
	      if {[dict get $cols($idx) bestfit] == 1} {
		lappend attr bestFit [dict get $cols($idx) bestfit]
	      }
	      if {[dict get $cols($idx) customwidth] == 1} {
		lappend attr customWidth [dict get $cols($idx) customwidth]
	      }
	      Tag_col {*}$attr {}
	    }
	  }
	}
      }
    }

    # Content-Type application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
    set file [string trim $file]
    if {$file eq {}} {
      set file {spreadsheetml.xlsx}
    }
    if {[file extension $file] ne {.xlsx}} {
      append file {.xlsx}
    }
    set path [file dirname $file]
    set uid [format xl_%X [clock microseconds]]
    set filesToZip {}
    foreach {tag doc} [array get obj doc,*] {
      lappend filesToZip [set docname [lindex [split $tag ,] 1]]
      set xmlfile [file join $path $uid $docname]
      file mkdir [file dirname $xmlfile]
      if {![catch {open $xmlfile w} fd]} {
	fconfigure $fd -encoding utf-8
	#puts $fd "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
	puts $fd [[$doc documentElement] asXML -indent $obj(indent) -xmlDeclaration 1 -encString [string toupper $obj(encoding)]]
	close $fd
	$doc delete
      }
    }
    set pwd [pwd]
    cd [file join $path $uid]
    if {$path eq {.}} {
      set file [file join .. $file]
    }
    ::ooxml::Zip $file . $filesToZip
    cd $pwd
    if {!$opts(holdcontainerdirectory)} {
      file delete -force [file join $path $uid]
    }
    return 0
  }
}


#
# ooxml::tablelist_to_xl
#

proc ::ooxml::tablelist_to_xl { lb args } {
  variable defaults

  if {![winfo exists $lb]} {
    tk_messageBox -message [msgcat::mc {Tablelist does not exists!}]
    return
  }

  if {[::ooxml::Getopt opts [list callback.arg {::ooxml::tablelist_to_xl_callback} path.arg $defaults(path) file.arg {tablelist.xlsx} creator.arg {unknown} name.arg {Tablelist1} rootonly addtimestamp] $args]} {
    error $opts(-errmsg)
  }
  if {[string trim $opts(path)] eq {}} {
    set opts(path) {.}
  }
  if {[string trim $opts(file)] eq {}} {
    set opts(file) {tablelist.xlsx}
  }
  if {[file extension $opts(file)] eq {.xlsx}} {
    set opts(file) [file tail [file rootname $opts(file)]]
  }
  if {$opts(addtimestamp)} {
    append opts(file) _[clock format [clock seconds] -format %Y%m%dT%H%M%S]
  }
  append opts(file) {.xlsx}

  set file [tk_getSaveFile -confirmoverwrite 1 -filetypes {{{Excel Office Open XML} {.xlsx}}} -initialdir $opts(path) -initialfile $opts(file) -parent . -title "Excel Office Open XML"]
  if {$file eq {}} {
    tk_messageBox -message [msgcat::mc {No file selected!}]
    return
  }

  set spreadsheet [::ooxml::xl_write new -creator $opts(creator)]
  if {[set sheet [$spreadsheet worksheet $opts(name)]] > -1} {
    set columncount [expr {[$lb columncount] - 1}]
    if {$columncount > 0} {
      $spreadsheet autofilter $sheet 0,0 0,$columncount
    }
    set titlecolumns [$lb cget -titlecolumns]
    if {$titlecolumns > 0} {
      $spreadsheet freeze $sheet 1,$titlecolumns
    }

    set col -1
    set title SETUP
    set width 0
    set align {}
    set sortmode {}
    set hide 1
    $opts(callback) $spreadsheet $sheet $columncount $col $title $width $align $sortmode $hide

    $spreadsheet row $sheet
    for {set col 0} {$col <= $columncount} {incr col} {
      set title [$lb columncget $col -title]
      set width [$lb columncget $col -width]
      set align [$lb columncget $col -align]
      set sortmode [$lb columncget $col -sortmode]
      set hide [$lb columncget $col -hide]

      if {[info commands $opts(callback)] eq $opts(callback)} {
        $opts(callback) $spreadsheet $sheet $columncount $col $title $width $align $sortmode $hide
      }

      $spreadsheet cell $sheet $title
    }

    if {$opts(rootonly)} {
      foreach row [$lb get [$lb childkeys root]] {
	$spreadsheet row $sheet
	set idx 0
	foreach col $row {
	  if {[string trim $col] ne {}} {
	    $spreadsheet cell $sheet $col -index $idx
	  }
	  incr idx
	}
      }
    } else {
      foreach row [$lb get 0 end] {
	$spreadsheet row $sheet
	set idx 0
	foreach col $row {
	  if {[string trim $col] ne {}} {
	    $spreadsheet cell $sheet $col -index $idx
	  }
	  incr idx
	}
      }
    }

    $spreadsheet write $file
  }
}

proc ::ooxml::tablelist_to_xl_callback { spreadsheet sheet maxcol column title width align sortmode hide } {
  set left 0
  set center [$spreadsheet style -horizontal center]
  set right [$spreadsheet style -horizontal right]
  set date [$spreadsheet style -numfmt [$spreadsheet numberformat -datetime]]
  set decimal [$spreadsheet style -numfmt [$spreadsheet numberformat -decimal -red]]
  set text [$spreadsheet style -numfmt [$spreadsheet numberformat -string]]

  if {$column == -1} {
    $spreadsheet defaultdatestyle $date
  } else {
    switch -- $align {
      center {
        $spreadsheet column $sheet -index $column -style $center
      }
      right {
        $spreadsheet column $sheet -index $column -style $right
      }
      default {
        $spreadsheet column $sheet -index $column -style $left
      }
    }
  }
}

package provide ooxml 1.1