├── GUIDE.txt ├── History.txt ├── LICENSE.txt ├── Manifest.txt ├── README.txt ├── Rakefile ├── bin └── xlsopcodes ├── lib ├── parseexcel.rb ├── parseexcel │ ├── parseexcel.rb │ └── parser.rb ├── spreadsheet.rb └── spreadsheet │ ├── column.rb │ ├── compatibility.rb │ ├── datatypes.rb │ ├── encodings.rb │ ├── excel.rb │ ├── excel │ ├── error.rb │ ├── internals.rb │ ├── internals │ │ ├── biff5.rb │ │ └── biff8.rb │ ├── offset.rb │ ├── reader.rb │ ├── reader │ │ ├── biff5.rb │ │ └── biff8.rb │ ├── row.rb │ ├── sst_entry.rb │ ├── workbook.rb │ ├── worksheet.rb │ ├── writer.rb │ └── writer │ │ ├── biff8.rb │ │ ├── format.rb │ │ ├── workbook.rb │ │ └── worksheet.rb │ ├── font.rb │ ├── format.rb │ ├── formula.rb │ ├── helpers.rb │ ├── link.rb │ ├── row.rb │ ├── workbook.rb │ ├── worksheet.rb │ └── writer.rb ├── spreadsheet-0.6.5.gem ├── spreadsheet.gemspec └── test ├── data ├── test_changes.xls ├── test_copy.xls ├── test_datetime.xls ├── test_empty.xls ├── test_formula.xls ├── test_long_sst_record.xls ├── test_missing_row.xls ├── test_version_excel5.xls ├── test_version_excel95.xls └── test_version_excel97.xls ├── excel ├── row.rb └── writer │ ├── workbook.rb │ └── worksheet.rb ├── font.rb ├── integration.rb ├── row.rb ├── suite.rb ├── workbook.rb └── worksheet.rb /GUIDE.txt: -------------------------------------------------------------------------------- 1 | = Getting Started with Spreadsheet 2 | This guide is meant to get you started using Spreadsheet. By the end of it, 3 | you should be able to read and write Spreadsheets. 4 | 5 | 6 | == Reading is easy! 7 | First, make sure all that code is loaded: 8 | 9 | require 'spreadsheet' 10 | 11 | Worksheets come in various Encodings. You need to tell Spreadsheet which 12 | Encoding you want to deal with. The Default is UTF-8 13 | 14 | Spreadsheet.client_encoding = 'UTF-8' 15 | 16 | Let's open a workbook: 17 | 18 | book = Spreadsheet.open '/path/to/an/excel-file.xls' 19 | 20 | We can either access all the Worksheets in a Workbook... 21 | 22 | book.worksheets 23 | 24 | ...or access them by index or name (encoded in your client_encoding) 25 | 26 | sheet1 = book.worksheet 0 27 | sheet2 = Book.worksheet 'Sheet1' 28 | 29 | Now you can either iterate over all rows that contain some data. A call to 30 | Worksheet.each without argument will omit empty rows at the beginning of the 31 | Worksheet: 32 | 33 | sheet1.each do |row| 34 | # do something interesting with a row 35 | end 36 | 37 | Or you can tell Worksheet how many rows should be omitted at the beginning. 38 | The following starts at the 3rd row, regardless of whether or not it or the 39 | preceding rows contain any data: 40 | 41 | sheet2.each 2 do |row| 42 | # do something interesting with a row 43 | end 44 | 45 | Or you can access rows directly, by their index (0-based): 46 | 47 | row = sheet1.row(3) 48 | 49 | To access the values stored in a Row, treat the Row like an Array. 50 | 51 | row[0] 52 | 53 | -> this will return a String, a Float, an Integer, a Formula, a Link or a Date 54 | or DateTime object - or nil if the cell is empty. 55 | 56 | More information about the formatting of a cell can be found in the Format 57 | with the equivalent index 58 | 59 | row.format 2 60 | 61 | 62 | == Writing is easy 63 | As before, make sure you have Spreadsheet required and the client_encoding 64 | set. Then make a new Workbook: 65 | 66 | book = Spreadsheet::Workbook.new 67 | 68 | Add a Worksheet and you're good to go: 69 | 70 | sheet1 = book.create_worksheet 71 | 72 | This will create a Worksheet with the Name "Worksheet1". If you prefer another 73 | name, you may do either of the following: 74 | 75 | sheet2 = book.create_worksheet :name => 'My Second Worksheet' 76 | sheet1.name = 'My First Worksheet' 77 | 78 | Now, add data to the Worksheet, using either Worksheet#[]=, 79 | Worksheet#update_row, or work directly on Row using any of the Array-Methods 80 | that modify an Array in place: 81 | 82 | sheet1.row(0).concat %w{Name Country Acknowlegement} 83 | sheet1[1,0] = 'Japan' 84 | row = sheet1.row(1) 85 | row.push 'Creator of Ruby' 86 | row.unshift 'Yukihiro Matsumoto' 87 | sheet1.row(2).replace [ 'Daniel J. Berger', 'U.S.A.', 88 | 'Author of original code for Spreadsheet::Excel' ] 89 | sheet1.row(3).push 'Charles Lowe', 'Author of the ruby-ole Library' 90 | sheet1.row(3).insert 1, 'Unknown' 91 | sheet1.update_row 4, 'Hannes Wyss', 'Switzerland', 'Author' 92 | 93 | Add some Formatting for flavour: 94 | 95 | sheet1.row(0).height = 18 96 | 97 | format = Spreadsheet::Format.new :color => :blue, 98 | :weight => :bold, 99 | :size => 18 100 | sheet1.row(0).default_format = format 101 | 102 | bold = Spreadsheet::Format.new :weight => :bold 103 | 4.times do |x| sheet1.row(x + 1).set_format(0, bold) end 104 | 105 | And finally, write the Excel File: 106 | 107 | book.write '/path/to/output/excel-file.xls' 108 | 109 | 110 | == Modifying an existing Document 111 | 112 | Spreadsheet has some limited support for modifying an existing Document. This 113 | is done by copying verbatim those parts of an Excel-document which Spreadsheet 114 | can't modify (yet), recalculating relevant offsets, and writing the data that 115 | can be changed. 116 | Here's what should work: 117 | * Adding, changing and deleting cells. 118 | * You should be able to fill in Data to be evaluated by predefined Formulas 119 | Limitations: 120 | * Spreadsheet can only write BIFF8 (Excel97 and higher). The results of 121 | modifying an earlier version of Excel are undefined. 122 | * Spreadsheet does not modify Formatting at present. That means in particular 123 | that if you set the Value of a Cell to a Date, it can only be read as a 124 | Date if its Format was set correctly prior to the change. 125 | * Although it is theoretically possible, it is not recommended to write the 126 | resulting Document back to the same File/IO that it was read from. 127 | 128 | And here's how it works: 129 | 130 | book = Spreadsheet.open '/path/to/an/excel-file.xls' 131 | sheet = book.worksheet 0 132 | sheet.each do |row| 133 | row[0] *= 2 134 | end 135 | book.write '/path/to/output/excel-file.xls' 136 | 137 | 138 | == Date and DateTime 139 | Excel does not know a separate Datatype for Dates. Instead it encodes Dates 140 | into standard floating-point numbers and recognizes a Date-Cell by its 141 | formatting-string: 142 | 143 | row.format(3).number_format 144 | 145 | Whenever a Cell's Format describes a Date or Time, Spreadsheet will give you 146 | the decoded Date or DateTime value. Should you need to access the underlying 147 | Float, you may do the following: 148 | 149 | row.at(3) 150 | 151 | If for some reason the Date-recognition fails, you may force Date-decoding: 152 | 153 | row.date(3) 154 | row.datetime(3) 155 | 156 | When you set the value of a Cell to a Date, Time or DateTime, Spreadsheet will 157 | try to set the cell's number-format to a corresponding value (one of Excel's 158 | builtin formats). If you have already defined a Date- or DateTime-format, 159 | Spreadsheet will use that instead. If a format has already been applied to 160 | a particular Cell, Spreadsheet will leave it untouched: 161 | 162 | row[4] = Date.new 1975, 8, 21 163 | # -> assigns the builtin Date-Format: 'M/D/YY' 164 | book.add_format Format.new(:number_format => 'DD.MM.YYYY hh:mm:ss') 165 | row[5] = DateTime.new 2008, 10, 12, 11, 59 166 | # -> assigns the added DateTime-Format: 'DD.MM.YYYY hh:mm:ss' 167 | row.set_format 6, Format.new(:number_format => 'D-MMM-YYYY') 168 | row[6] = Time.new 2008, 10, 12 169 | # -> the Format of cell 6 is left unchanged. 170 | 171 | == Outline (Grouping) and Hiding 172 | Spreadsheet supports outline (grouping) and hiding functions from version 173 | 0.6.5. In order to hide rows or columns, you can use 'hidden' property. 174 | As for outline, 'outline_level' property is also available. You can use 175 | both 'hidden' and 'outline_level' at the same time. 176 | 177 | You can create a new file with outline and hiding rows and columns as 178 | follows: 179 | 180 | require 'spreadsheet' 181 | 182 | # create a new book and sheet 183 | book = Spreadsheet::Workbook.new 184 | sheet = book.create_worksheet 185 | 5.times {|j| 5.times {|i| sheet[j,i] = (i+1)*10**j}} 186 | 187 | # column 188 | sheet.column(2).hidden = true 189 | sheet.column(3).hidden = true 190 | sheet.column(2).outline_level = 1 191 | sheet.column(3).outline_level = 1 192 | 193 | # row 194 | sheet.row(2).hidden = true 195 | sheet.row(3).hidden = true 196 | sheet.row(2).outline_level = 1 197 | sheet.row(3).outline_level = 1 198 | 199 | # save file 200 | book.write 'out.xls' 201 | 202 | Also you can read an existing file and change the hidden and outline 203 | properties. Here is the example below: 204 | 205 | require 'spreadsheet' 206 | 207 | # read an existing file 208 | file = ARGV[0] 209 | book = Spreadsheet.open(file, 'rb') 210 | sheet= book.worksheet(0) 211 | 212 | # column 213 | sheet.column(2).hidden = true 214 | sheet.column(3).hidden = true 215 | sheet.column(2).outline_level = 1 216 | sheet.column(3).outline_level = 1 217 | 218 | # row 219 | sheet.row(2).hidden = true 220 | sheet.row(3).hidden = true 221 | sheet.row(2).outline_level = 1 222 | sheet.row(3).outline_level = 1 223 | 224 | # save file 225 | book.write "out.xls" 226 | 227 | Notes 228 | * The outline_level should be under 8, which is due to the Excel data format. 229 | 230 | == More about Encodings 231 | Spreadsheet assumes it's running on Ruby 1.8 with Iconv-support. It is your 232 | responsibility to handle Conversion Errors, or to prevent them e.g. by using 233 | the Iconv Transliteration and Ignore flags: 234 | Spreadsheet.client_encoding = 'LATIN1//TRANSLIT//IGNORE' 235 | 236 | 237 | == Backward Compatibility 238 | Spreadsheet is designed to be a drop-in replacement for both ParseExcel and 239 | Spreadsheet::Excel. It provides a number of require-paths for backward 240 | compatibility with its predecessors. If you have been working with ParseExcel, 241 | you have probably used one or more of the following: 242 | 243 | require 'parseexcel' 244 | require 'parseexcel/parseexcel' 245 | require 'parseexcel/parser' 246 | 247 | Either of the above will define the ParseExcel.parse method as a facade to 248 | Spreadsheet.open. Additionally, this will alter Spreadsheets behavior to define 249 | the ParseExcel::Worksheet::Cell class and fill each parsed Row with instances 250 | thereof, which in turn provide ParseExcel's Cell#to_s(encoding) and Cell#date 251 | methods. 252 | You will have to manually uninstall the parseexcel library. 253 | 254 | If you are upgrading from Spreadsheet::Excel, you were probably using 255 | Workbook#add_worksheet and Worksheet#write, write_row or write_column. 256 | Use the following to load the code which provides them: 257 | 258 | require 'spreadsheet/excel' 259 | 260 | Again, you will have to manually uninstall the spreadsheet-excel library. 261 | 262 | If you perform fancy formatting, you may run into trouble as the 263 | Format implementation has changed considerably. If that is the case, please 264 | drop me a line at hannes.wyss@gmail.com and I will try to help you. Don't 265 | forget to include the offending code-snippet! 266 | 267 | All compatibility code is deprecated and will be removed in version 1.0.0 268 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | === 0.6.5.2 / 14.2.2011 2 | 3 | * Updated test/integration.rb to work with Ruby ruby 1.9.2p136 (2010-12-25 revision 30365) [i686-linux] 4 | 5 | * Thanks for the hint tomiacannondale@gmail.com 6 | 7 | === 0.6.5.1 / 17.1.2011 8 | 9 | * One enhancement thanks to Qiong Peng, Moo Yu, and Thierry Thelliez 10 | 11 | * http://dev.ywesee.com/wiki.php/Gem/Spreadsheet 12 | 13 | === 0.6.5 / 07.12.2010 14 | 15 | * 2 Enhancements courtesy to ISS AG. 16 | 17 | * Outlining (Grouping) of lines and columns is now possible. The outlining 18 | maximum is 8. This means you can do 8 subgroups in a group. 19 | 20 | * Hiding and Unhiding of lines and columns is now possible. 21 | 22 | * Both of above two points is now possible by creating a new Excel File from 23 | scratch or editing an existing XLS and adding groups or hiding lines to it. 24 | 25 | === 0.6.4.1 / 2009-09-17 26 | 27 | * 3 Bugfixes 28 | 29 | * Fixes the issue reported by Thomas Preymesser and tracked down most of the 30 | way by Hugh McGowan in 31 | http://rubyforge.org/tracker/index.php?func=detail&aid=26647&group_id=678&atid=2677 32 | where reading the value of the first occurrence of a shared formula 33 | failed. 34 | 35 | * Fixes the issue reported by Anonymous in 36 | http://rubyforge.org/tracker/index.php?func=detail&aid=26546&group_id=678&atid=2677 37 | where InvalidDate was raised for some Dates. 38 | 39 | * Fixes the issue reported by John Lee in 40 | http://rubyforge.org/tracker/index.php?func=detail&aid=27110&group_id=678&atid=2677 41 | which is probably a duplicate of the Bug previously reported by Kadvin XJ in 42 | http://rubyforge.org/tracker/index.php?func=detail&aid=26182&group_id=678&atid=2677 43 | where unchanged rows were marked as changed in the Excel-Writer while the 44 | File was being written, triggering an Error. 45 | 46 | * 1 minor enhancement 47 | 48 | * Detect row offsets from Cell data if Row-Ops are missing 49 | This fixes a bug reported by Alexander Logvinov in 50 | http://rubyforge.org/tracker/index.php?func=detail&aid=26513&group_id=678&atid=2677 51 | 52 | 53 | === 0.6.4 / 2009-07-03 54 | 55 | * 5 Bugfixes 56 | 57 | * Fixes the issue reported by Harley Mackenzie in 58 | http://rubyforge.org/tracker/index.php?func=detail&aid=24119&group_id=678&atid=2677 59 | where in some edge-cases numbers were stored incorrectly 60 | 61 | * Fixes the issue reported and fixed by someone23 in 62 | http://rubyforge.org/tracker/index.php?func=detail&aid=25732&group_id=678&atid=2677 63 | where using Row-updater methods with blocks caused LocalJumpErrors 64 | 65 | * Fixes the issue reported and fixed by Corey Burrows in 66 | http://rubyforge.org/tracker/index.php?func=detail&aid=25784&group_id=678&atid=2677 67 | where "Setting the height of a row, either in Excel directly, or via the 68 | Spreadsheet::Row#height= method results in a row that Excel displays with 69 | the maximum row height (409)." 70 | 71 | * Fixes the issue reported by Don Park in 72 | http://rubyforge.org/tracker/index.php?func=detail&aid=25968&group_id=678&atid=2677 73 | where some Workbooks could not be parsed due to the OLE-entry being all 74 | uppercase 75 | 76 | * Fixes the issue reported by Iwan Buetti in 77 | http://rubyforge.org/tracker/index.php?func=detail&aid=24414&group_id=678&atid=2677 78 | where parsing some Workbooks failed with an Invalid date error. 79 | 80 | 81 | * 1 major enhancement 82 | 83 | * Spreadsheet now runs on Ruby 1.9 84 | 85 | === 0.6.3.1 / 2009-02-13 86 | 87 | * 3 Bugfixes 88 | 89 | * Only selects the First Worksheet by default 90 | This deals with an issue reported by Biörn Andersson in 91 | http://rubyforge.org/tracker/?func=detail&atid=2677&aid=23736&group_id=678 92 | where data-edits in OpenOffice were propagated through all selected 93 | sheets. 94 | 95 | * Honors Row, Column, Worksheet and Workbook-formats 96 | and thus fixes a Bug introduced in 97 | http://scm.ywesee.com/?p=spreadsheet;a=commit;h=52755ad76fdda151564b689107ca2fbb80af3b78 98 | and reported in 99 | http://rubyforge.org/tracker/index.php?func=detail&aid=23875&group_id=678&atid=2678 100 | and by Joachim Schneider in 101 | http://rubyforge.org/forum/forum.php?thread_id=31056&forum_id=2920 102 | 103 | * Fixes a bug reported by Alexander Skwar in 104 | http://rubyforge.org/forum/forum.php?thread_id=31403&forum_id=2920 105 | where the user-defined formatting of Dates and Times was overwritten with 106 | a default format, and other issues connected with writing Dates and Times 107 | into Spreadsheets. 108 | 109 | * 1 minor enhancements 110 | 111 | * Spreadsheet shold now be completely warning-free, 112 | as requested by Eric Peterson in 113 | http://rubyforge.org/forum/forum.php?thread_id=31346&forum_id=2920 114 | 115 | === 0.6.3 / 2009-01-14 116 | 117 | * 1 Bugfix 118 | 119 | * Fixes the issue reported by Corey Martella in 120 | http://rubyforge.org/forum/message.php?msg_id=63651 121 | as well as other issues engendered by the decision to always shorten 122 | Rows to the last non-nil value. 123 | 124 | * 2 minor enhancements 125 | 126 | * Added bin/xlsopcodes, a tool for examining Excel files 127 | 128 | * Documents created by Spreadsheet can now be Printed in Excel and 129 | Excel-Viewer. 130 | This issue was reported by Spencer Turner in 131 | http://rubyforge.org/tracker/index.php?func=detail&aid=23287&group_id=678&atid=2677 132 | 133 | === 0.6.2.1 / 2008-12-18 134 | 135 | * 1 Bugfix 136 | 137 | * Using Spreadsheet together with 'jcode' could lead to broken Excel-Files 138 | Thanks to Eugene Mikhailov for tracking this one down in: 139 | http://rubyforge.org/tracker/index.php?func=detail&aid=23085&group_id=678&atid=2677 140 | 141 | === 0.6.2 / 2008-12-11 142 | 143 | * 14 Bugfixes 144 | 145 | * Fixed a bug where #! methods did not trigger a call to 146 | #row_updated 147 | 148 | * Corrected the Row-Format in both Reader and Writer (was Biff5 for some 149 | reason) 150 | 151 | * Populates Row-instances with @default_format, @height, @outline_level 152 | and @hidden attributes 153 | 154 | * Fixed a Bug where Workbooks deriving from a Template-Workbook without 155 | SST could not be saved 156 | Reported in 157 | http://rubyforge.org/tracker/index.php?func=detail&aid=22863&group_id=678&atid=2678 158 | 159 | * Improved handling of Numeric Values (writes a RK-Entry for a Float 160 | only if it can be encoded with 4 leading zeroes, and a Number-Entry for an 161 | Integer only if it cannot be encoded as an RK) 162 | 163 | * Fixes a bug where changes to a Row were ignored if they were 164 | outside of an existing Row-Block. 165 | 166 | * Fixes a bug where MULRK-Entries sometimes only contained a single RK 167 | 168 | * Fixes a bug where formatting was ignored if it was applied to empty Rows 169 | Reported by Zomba Lumix in 170 | http://rubyforge.org/forum/message.php?msg_id=61985 171 | 172 | * Fixes a bug where modifying a Row in a loaded Workbook could lead to Rows 173 | with smaller indices being set to nil. 174 | Reported by Ivan Samsonov in 175 | http://rubyforge.org/forum/message.php?msg_id=62816 176 | 177 | * Deals with rounding-problems when calculating Time 178 | Reported by Bughunter extraordinaire Bjørn Hjelle 179 | 180 | * Correct splitting of wide characters in SST 181 | Reported by Michel Ziegler and by Eugene Mikhailov in 182 | http://rubyforge.org/tracker/index.php?func=detail&aid=23085&group_id=678&atid=2677 183 | 184 | * Fix an off-by-one error in write_mulrk that caused Excel to complain that 185 | 'Data may be lost', reported by Emma in 186 | http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/321979 187 | and by Chris Lowis in 188 | http://rubyforge.org/tracker/index.php?func=detail&aid=22892&group_id=678&atid=2677 189 | 190 | 191 | * Read formats correctly in read_mulrk 192 | Reported by Ivan Samsonov 193 | Fixes that part of http://rubyforge.org/forum/message.php?msg_id=62821 194 | which is a bug. Does nothing for the disappearance of Rich-Text 195 | formatting, which will not be addressed until 0.7.0 196 | 197 | * Fixes a (benign?) bug, where adding text to a template-file resulted in 198 | a duplicate extsst-record. 199 | 200 | * 2 minor enhancements 201 | 202 | * Improved recognition of Time-Formats 203 | 204 | * Improvement to Robustness: allow Spreadsheet::Workbook.new 205 | Takes care of http://rubyforge.org/forum/message.php?msg_id=62941 206 | Reported by David Chamberlain 207 | 208 | === 0.6.1.9 / 2008-11-07 209 | 210 | * 1 Bugfix 211 | 212 | * Fixes a precision-issue in Excel::Row#datetime: Excel records Time-Values 213 | with more significant bits (but not necessarily more precise) than 214 | DateTime can handle. 215 | (Thanks to Bjørn Hjelle for the Bugreport) 216 | 217 | * 1 minor enhancement 218 | 219 | * Added support for appending Worksheets to a Workbook 220 | (Thanks to Mohammed Rabbani for the report) 221 | 222 | === 0.6.1.8 / 2008-10-31 223 | 224 | * 1 Bugfix 225 | 226 | * Fixes a bug where out-of-sequence reading of multiple Worksheets could 227 | lead to data from the wrong Sheet being returned. 228 | (Thanks to Bugreporter extraordinaire Bjørn Hjelle) 229 | 230 | === 0.6.1.7 / 2008-10-30 231 | 232 | * 1 Bugfix 233 | 234 | * Fixes a bug where all Formulas were ignored. 235 | (Thanks to Bjørn Hjelle for the report) 236 | 237 | * 1 minor enhancement 238 | 239 | * Allow the substitution of an IO object with a StringIO. 240 | (Thanks to luxor for the report) 241 | 242 | === 0.6.1.6 / 2008-10-28 243 | 244 | * 2 Bugfixes 245 | 246 | * Fixed encoding and decoding of BigNums, negative and other large Numbers 247 | http://rubyforge.org/tracker/index.php?func=detail&aid=22581&group_id=678&atid=2677 248 | * Fix a bug where modifications to default columns weren't stored 249 | http://rubyforge.org/forum/message.php?msg_id=61567 250 | 251 | * 1 minor enhancement 252 | 253 | * Row#enriched_data won't return a Bogus-Date if the data isn't a Numeric 254 | value 255 | (Thanks to Bjørn Hjelle for the report) 256 | 257 | === 0.6.1.5 / 2008-10-24 258 | 259 | * 2 Bugfixes 260 | 261 | * Removed obsolete code which triggered Iconv::InvalidEncoding 262 | on Systems with non-gnu Iconv: 263 | http://rubyforge.org/tracker/index.php?func=detail&aid=22541&group_id=678&atid=2677 264 | * Handle empty Worksheets 265 | (Thanks to Charles Lowe for the Patches) 266 | 267 | === 0.6.1.4 / 2008-10-23 268 | 269 | * 1 Bugfix 270 | 271 | * Biff8#wide now works properly even if $KCODE=='UTF-8' 272 | (Thanks to Bjørn Hjelle for the Bugreport) 273 | 274 | * 1 minor enhancement 275 | 276 | * Read/Write functionality for Links (only URLs can be written as of now) 277 | 278 | === 0.6.1.3 / 2008-10-21 279 | 280 | * 2 Bugfixes 281 | 282 | * Renamed UTF8 to UTF-8 to support freebsd 283 | (Thanks to Jacob Atzen for the Patch) 284 | * Fixes a Bug where only the first Rowblock was read correctly if there were 285 | no DBCELL records terminating the Rowblocks. 286 | (Thanks to Bjørn Hjelle for the Bugreport) 287 | 288 | === 0.6.1.2 / 2008-10-20 289 | 290 | * 2 Bugfixes 291 | 292 | * Corrected the Font-Encoding values in Excel::Internals 293 | (Thanks to Bjørn Hjelle for the Bugreport) 294 | * Spreadsheet now skips Richtext-Formatting runs and Asian Phonetic 295 | Settings when reading the SST, fixing a problem where the presence of 296 | Richtext could lead to an incomplete SST. 297 | 298 | === 0.6.1.1 / 2008-10-20 299 | 300 | * 1 Bugfix 301 | 302 | * Corrected the Manifest - included column.rb 303 | 304 | === 0.6.1 / 2008-10-17 305 | 306 | * 3 minor enhancements 307 | 308 | * Adds Column formatting and Worksheet#format_column 309 | * Reads and writes correct Fonts (Font-indices > 3 appear to be 1-based) 310 | * Reads xf data 311 | 312 | === 0.6.0 / 2008-10-13 313 | 314 | * 1 major enhancement 315 | 316 | * Initial upload of the shiny new Spreadsheet Gem after three weeks of 317 | grueling labor in the dark binary mines of Little-Endian Biff and long 318 | hours spent polishing the surfaces of documentation. 319 | 320 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | GUIDE.txt 2 | History.txt 3 | LICENSE.txt 4 | Manifest.txt 5 | README.txt 6 | Rakefile 7 | bin/xlsopcodes 8 | lib/parseexcel.rb 9 | lib/parseexcel/parseexcel.rb 10 | lib/parseexcel/parser.rb 11 | lib/spreadsheet.rb 12 | lib/spreadsheet/column.rb 13 | lib/spreadsheet/compatibility.rb 14 | lib/spreadsheet/datatypes.rb 15 | lib/spreadsheet/encodings.rb 16 | lib/spreadsheet/excel.rb 17 | lib/spreadsheet/excel/error.rb 18 | lib/spreadsheet/excel/internals.rb 19 | lib/spreadsheet/excel/internals/biff5.rb 20 | lib/spreadsheet/excel/internals/biff8.rb 21 | lib/spreadsheet/excel/offset.rb 22 | lib/spreadsheet/excel/reader.rb 23 | lib/spreadsheet/excel/reader/biff5.rb 24 | lib/spreadsheet/excel/reader/biff8.rb 25 | lib/spreadsheet/excel/row.rb 26 | lib/spreadsheet/excel/sst_entry.rb 27 | lib/spreadsheet/excel/workbook.rb 28 | lib/spreadsheet/excel/worksheet.rb 29 | lib/spreadsheet/excel/writer.rb 30 | lib/spreadsheet/excel/writer/biff8.rb 31 | lib/spreadsheet/excel/writer/format.rb 32 | lib/spreadsheet/excel/writer/workbook.rb 33 | lib/spreadsheet/excel/writer/worksheet.rb 34 | lib/spreadsheet/font.rb 35 | lib/spreadsheet/format.rb 36 | lib/spreadsheet/formula.rb 37 | lib/spreadsheet/helpers.rb 38 | lib/spreadsheet/link.rb 39 | lib/spreadsheet/row.rb 40 | lib/spreadsheet/workbook.rb 41 | lib/spreadsheet/worksheet.rb 42 | lib/spreadsheet/writer.rb 43 | spreadsheet-0.6.5.gem 44 | spreadsheet.gemspec 45 | test/data/test_changes.xls 46 | test/data/test_copy.xls 47 | test/data/test_datetime.xls 48 | test/data/test_empty.xls 49 | test/data/test_formula.xls 50 | test/data/test_long_sst_record.xls 51 | test/data/test_missing_row.xls 52 | test/data/test_version_excel5.xls 53 | test/data/test_version_excel95.xls 54 | test/data/test_version_excel97.xls 55 | test/excel/row.rb 56 | test/excel/writer/worksheet.rb 57 | test/font.rb 58 | test/integration.rb 59 | test/row.rb 60 | test/suite.rb 61 | test/workbook.rb 62 | test/worksheet.rb 63 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Last Update: 14.02.2011 - Zeno R.R. Davatz 2 | 3 | = Spreadsheet 4 | 5 | The Mailinglist can be found here: 6 | 7 | http://groups.google.com/group/rubyspreadsheet 8 | 9 | The code can be found here: 10 | 11 | http://spreadsheet.rubyforge.org 12 | 13 | For a viewable directory of all recent changes, please see: 14 | 15 | http://scm.ywesee.com/?p=spreadsheet/.git;a=summary 16 | 17 | To get a graphical overview of the Library please see 18 | 19 | http://spreadsheet.rubyforge.org/spreadsheet.jpeg 20 | 21 | For Non-GPLv3 commercial licencing, please see: 22 | 23 | http://www.spreadsheet.ch 24 | 25 | 26 | == Description 27 | 28 | The Spreadsheet Library is designed to read and write Spreadsheet Documents. 29 | As of version 0.6.0, only Microsoft Excel compatible spreadsheets are 30 | supported. Spreadsheet is a combination/complete rewrite of the 31 | Spreadsheet::Excel Library by Daniel J. Berger and the ParseExcel Library by 32 | Hannes Wyss. Spreadsheet can read, write and modify Spreadsheet Documents. 33 | 34 | 35 | == What's new? 36 | 37 | * Supported outline (grouping) functions 38 | * Significantly improved memory-efficiency when reading large Excel Files 39 | * Limited Spreadsheet modification support 40 | * Improved handling of String Encodings 41 | 42 | 43 | == Roadmap 44 | 45 | 0.7.0:: Improved Format support/Styles 46 | 0.7.1:: Document Modification: Formats/Styles 47 | 0.8.0:: Formula Support 48 | 0.8.1:: Document Modification: Formulas 49 | 0.9.0:: Write-Support: BIFF5 50 | 1.0.0:: Ruby 1.9 Support; 51 | Remove backward compatibility code 52 | 53 | 54 | == Dependencies 55 | 56 | * ruby 1.8 57 | * ruby-ole[http://code.google.com/p/ruby-ole/] 58 | 59 | 60 | == Examples 61 | 62 | Have a look at the GUIDE[link://files/GUIDE_txt.html]. 63 | 64 | 65 | == Installation 66 | 67 | Using RubyGems[http://www.rubygems.org]: 68 | 69 | * sudo gem install spreadsheet 70 | 71 | If you don't like RubyGems[http://www.rubygems.org], let me know which 72 | installation solution you prefer and I'll include it in the future. 73 | 74 | If you can use 'rake' and 'hoe' library is also installed, you can 75 | build a gem package as follows: 76 | 77 | * rake gem 78 | 79 | The gem package is built in pkg directory. 80 | 81 | 82 | == Authors 83 | 84 | Original Code: 85 | 86 | Spreadsheet::Excel: 87 | Copyright (c) 2005 by Daniel J. Berger (djberg96@gmail.com) 88 | 89 | ParseExcel: 90 | Copyright (c) 2003 by Hannes Wyss (hannes.wyss@gmail.com) 91 | 92 | New Code: 93 | Copyright (c) 2010 ywesee GmbH (mhatakeyama@ywesee.com, zdavatz@ywesee.com) 94 | 95 | 96 | == License 97 | 98 | This library is distributed under the GPLv3. 99 | Please see the LICENSE[link://files/LICENSE_txt.html] file. 100 | 101 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | $: << File.expand_path("./lib", File.dirname(__FILE__)) 4 | 5 | require 'rubygems' 6 | require 'hoe' 7 | require './lib/spreadsheet.rb' 8 | 9 | ENV['RDOCOPT'] = '-c utf8' 10 | 11 | Hoe.plugin :git 12 | 13 | Hoe.spec('spreadsheet') do |p| 14 | p.developer('Masaomi Hatakeyama, Zeno R.R. Davatz','mhatakeyama@ywesee.com, zdavatz@ywesee.com') 15 | p.remote_rdoc_dir = 'spreadsheet' 16 | p.extra_deps << ['ruby-ole', '>=1.0'] 17 | end 18 | 19 | # vim: syntax=Ruby 20 | -------------------------------------------------------------------------------- /bin/xlsopcodes: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'spreadsheet' 4 | 5 | source, target = ARGV 6 | 7 | if source.nil? 8 | puts "Usage: #{$0} []" 9 | exit -1 10 | end 11 | 12 | target = target ? File.open(target, 'w') : STDOUT 13 | 14 | reader = Spreadsheet::Excel::Reader.new :print_opcodes => target 15 | reader.setup File.open(source) 16 | 17 | while tuple = reader.get_next_chunk 18 | end 19 | -------------------------------------------------------------------------------- /lib/parseexcel.rb: -------------------------------------------------------------------------------- 1 | ### Spreadsheet - A Library for reading and writing Spreadsheet Documents. 2 | # 3 | # Copyright (C) 2008 Hannes Wyss 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # Contact Information: 19 | # 20 | # E-Mail: hannes.wyss@gmail.com 21 | # P-Mail: ywesee GmbH 22 | # Hannes Wyss 23 | # Winterthurerstrasse 52 24 | # 8006 Zürich 25 | ### Switzerland 26 | 27 | require 'parseexcel/parseexcel' 28 | -------------------------------------------------------------------------------- /lib/parseexcel/parseexcel.rb: -------------------------------------------------------------------------------- 1 | require 'spreadsheet' 2 | 3 | warn <<-EOS 4 | [DEPRECATED] By requiring 'parseexcel', 'parseexcel/parseexcel' and/or 5 | 'parseexcel/parser' you are loading a Compatibility layer which 6 | provides a drop-in replacement for the ParseExcel library. This 7 | code makes the reading of Spreadsheet documents less efficient and 8 | will be removed in Spreadsheet version 1.0.0 9 | EOS 10 | 11 | module Spreadsheet 12 | ## 13 | # The ParseExcel module is provided as a drop-in replacement for the 14 | # ParseExcel library. This code is deprecated and will be removed in 15 | # Spreadsheet version 1.0.0 16 | module ParseExcel 17 | def ParseExcel.parse path 18 | Spreadsheet.open path 19 | end 20 | class Worksheet 21 | class Cell 22 | attr_accessor :value, :kind, :numeric, :code, :book, 23 | :format, :rich, :encoding, :annotation 24 | def initialize value, format, row, idx 25 | @format = format 26 | @idx = idx 27 | @row = row 28 | @value = value 29 | @encoding = Spreadsheet.client_encoding 30 | end 31 | def date 32 | @row.date @idx 33 | end 34 | def datetime 35 | @row.datetime @idx 36 | end 37 | def to_i 38 | @value.to_i 39 | end 40 | def to_f 41 | @value.to_f 42 | end 43 | def to_s(target_encoding=nil) 44 | if(target_encoding) 45 | begin 46 | Iconv.new(target_encoding, @encoding).iconv(@value) 47 | rescue 48 | Iconv.new(target_encoding, 'ascii').iconv(@value.to_s) 49 | end 50 | else 51 | @value.to_s 52 | end 53 | end 54 | def type 55 | if @format && (@format.date? || @format.time?) 56 | :date 57 | elsif @value.is_a?(Numeric) 58 | :numeric 59 | else 60 | :text 61 | end 62 | end 63 | end 64 | end 65 | end 66 | module Excel 67 | class Reader # :nodoc: all 68 | def set_cell worksheet, row, column, xf, value=nil 69 | cells = @current_row_block[row] ||= Row.new(nil, row) 70 | cells.formats[column] = xf = @workbook.format(xf) 71 | cells[column] = ParseExcel::Worksheet::Cell.new(value, xf, cells, column) 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/parseexcel/parser.rb: -------------------------------------------------------------------------------- 1 | require 'parseexcel' 2 | 3 | module Spreadsheet 4 | module ParseExcel # :nodoc: all 5 | class Parser 6 | def parse path 7 | Spreadsheet.open path 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/spreadsheet.rb: -------------------------------------------------------------------------------- 1 | ### Spreadsheet - A Library for reading and writing Spreadsheet Documents. 2 | # 3 | # Copyright (C) 2008-2010 ywesee GmbH 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # Contact Information: 19 | # 20 | # E-Mail: mhatakeyama@ywesee.com, zdavatz@ywesee.com 21 | # P-Mail: ywesee GmbH 22 | # Zeno R.R. Davatz 23 | # Winterthurerstrasse 52 24 | # 8006 Zürich 25 | ### Switzerland 26 | 27 | require 'spreadsheet/excel/workbook' 28 | require 'spreadsheet/excel/reader' 29 | 30 | # = Synopsis 31 | # The Spreadsheet Library is designed to read and write Spreadsheet Documents. 32 | # As of version 0.6.0, only Microsoft Excel compatible spreadsheets are 33 | # supported. 34 | # 35 | # == Example 36 | # require 'spreadsheet' 37 | # 38 | # book = Spreadsheet.open '/path/to/an/excel-file.xls' 39 | # sheet = book.worksheet 0 40 | # sheet.each do |row| puts row[0] end 41 | module Spreadsheet 42 | 43 | ## 44 | # The version of Spreadsheet you are using. 45 | VERSION = '0.6.5.2' 46 | 47 | ## 48 | # Default client Encoding. Change this value if your application uses a 49 | # different Encoding: 50 | # Spreadsheet.client_encoding = 'ISO-LATIN-1//TRANSLIT//IGNORE' 51 | @client_encoding = 'UTF-8' 52 | 53 | class << self 54 | 55 | attr_accessor :client_encoding 56 | 57 | ## 58 | # Parses a Spreadsheet Document and returns a Workbook object. At present, 59 | # only Excel-Documents can be read. 60 | def open io_or_path, mode="rb+", &block 61 | if io_or_path.respond_to? :seek 62 | Excel::Workbook.open(io_or_path) 63 | elsif block 64 | File.open(io_or_path, mode) do |fh| 65 | block.call open(fh) 66 | end 67 | else 68 | open File.open(io_or_path, mode) 69 | end 70 | end 71 | 72 | ## 73 | # Returns a Writer object for the specified path. At present, only the 74 | # Excel-Writer is available. 75 | def writer io_or_path, type=Excel 76 | Excel::Writer::Workbook.new io_or_path 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/spreadsheet/column.rb: -------------------------------------------------------------------------------- 1 | require 'spreadsheet/datatypes' 2 | 3 | module Spreadsheet 4 | ## 5 | # The Column class. Encapsulates column-formatting and width, and provides a 6 | # means to iterate over all cells in a column. 7 | # 8 | # Useful Attributes: 9 | # #width:: The width in characters (in respect to the '0' character 10 | # of the Worksheet's default Font). Float values are 11 | # permitted, for Excel the available Precision is at 1/256 12 | # characters. 13 | # #default_format:: The default Format for cells in this column (applied if 14 | # there is no explicit Cell Format and no default Row format 15 | # for the Cell). 16 | # #hidden:: The Column is hidden. 17 | # #collapsed:: The Column is collapsed. 18 | # #outline_level:: Outline level of the column. 19 | class Column 20 | class << self 21 | def updater *keys 22 | keys.each do |key| 23 | unless instance_methods.include? "unupdated_#{key}=" 24 | alias_method :"unupdated_#{key}=", :"#{key}=" 25 | define_method "#{key}=" do |value| 26 | send "unupdated_#{key}=", value 27 | @worksheet.column_updated @idx, self if @worksheet 28 | value 29 | end 30 | end 31 | end 32 | end 33 | end 34 | include Datatypes 35 | include Enumerable 36 | attr_accessor :width, :worksheet 37 | attr_reader :default_format, :idx 38 | boolean :hidden, :collapsed 39 | enum :outline_level, 0, Integer 40 | updater :collapsed, :hidden, :outline_level, :width 41 | def initialize idx, format, opts={} 42 | @worksheet = nil 43 | @idx = idx 44 | opts[:width] ||= 10 45 | opts.each do |key, value| 46 | self.send "#{key}=", value 47 | end 48 | self.default_format = format 49 | end 50 | ## 51 | # Set the default Format for Cells in this Column. 52 | def default_format= format 53 | @worksheet.add_format format if @worksheet 54 | @default_format = format 55 | @worksheet.column_updated @idx, self if @worksheet 56 | format 57 | end 58 | ## 59 | # Iterate over all cells in this column. 60 | def each 61 | @worksheet.each do |row| 62 | yield row[idx] 63 | end 64 | end 65 | def == other # :nodoc: 66 | other.is_a?(Column) && default_format == other.default_format \ 67 | && width == other.width && hidden == other.hidden \ 68 | && collapsed == other.collapsed && outline_level == other.outline_level 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/spreadsheet/compatibility.rb: -------------------------------------------------------------------------------- 1 | module Spreadsheet 2 | module Compatibility 3 | ## 4 | # One of the most incisive changes in terms of meta-programming in Ruby 1.9 5 | # is the switch from representing instance-variable names as Strings to 6 | # presenting them as Symbols. ivar_name provides compatibility. 7 | if RUBY_VERSION >= '1.9' 8 | def ivar_name symbol 9 | :"@#{symbol}" 10 | end 11 | def method_name symbol 12 | symbol.to_sym 13 | end 14 | else 15 | def ivar_name symbol 16 | "@#{symbol}" 17 | end 18 | def method_name symbol 19 | symbol.to_s 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/spreadsheet/datatypes.rb: -------------------------------------------------------------------------------- 1 | require 'spreadsheet/compatibility' 2 | 3 | module Spreadsheet 4 | ## 5 | # This module defines convenience-methods for the definition of Spreadsheet 6 | # attributes (boolean, colors and enumerations) 7 | module Datatypes 8 | include Compatibility 9 | def Datatypes.append_features mod 10 | super 11 | mod.module_eval do 12 | class << self 13 | ## 14 | # Valid colors for color attributes. 15 | COLORS = [ :builtin_black, :builtin_white, :builtin_red, :builtin_green, 16 | :builtin_blue, :builtin_yellow, :builtin_magenta, :builtin_cyan, 17 | :text, :border, :pattern_bg, :dialog_bg, :chart_text, :chart_bg, 18 | :chart_border, :tooltip_bg, :tooltip_text, :aqua, 19 | :black, :blue, :cyan, :brown, :fuchsia, :gray, :grey, :green, 20 | :lime, :magenta, :navy, :orange, :purple, :red, :silver, :white, 21 | :yellow ] 22 | ## 23 | # Define instance methods to read and write boolean attributes. 24 | def boolean *args 25 | args.each do |key| 26 | define_method key do 27 | name = ivar_name key 28 | !!(instance_variable_get(name) if instance_variables.include?(name)) 29 | end 30 | define_method "#{key}?" do 31 | send key 32 | end 33 | define_method "#{key}=" do |arg| 34 | arg = false if arg == 0 35 | instance_variable_set(ivar_name(key), !!arg) 36 | end 37 | define_method "#{key}!" do 38 | send "#{key}=", true 39 | end 40 | end 41 | end 42 | ## 43 | # Define instance methods to read and write color attributes. 44 | # For valid colors see COLORS 45 | def colors *args 46 | args.each do |key| 47 | attr_reader key 48 | define_method "#{key}=" do |name| 49 | name = name.to_s.downcase.to_sym 50 | if COLORS.include?(name) 51 | instance_variable_set ivar_name(key), name 52 | else 53 | raise ArgumentError, "unknown color '#{name}'" 54 | end 55 | end 56 | end 57 | end 58 | ## 59 | # Define instance methods to read and write enumeration attributes. 60 | # * The first argument designates the attribute name. 61 | # * The second argument designates the default value. 62 | # * All subsequent attributes are possible values. 63 | # * If the last attribute is a Hash, each value in the Hash designates 64 | # aliases for the corresponding key. 65 | def enum key, *values 66 | aliases = {} 67 | if values.last.is_a? Hash 68 | values.pop.each do |value, synonyms| 69 | if synonyms.is_a? Array 70 | synonyms.each do |synonym| aliases.store synonym, value end 71 | else 72 | aliases.store synonyms, value 73 | end 74 | end 75 | end 76 | values.each do |value| 77 | aliases.store value, value 78 | end 79 | define_method key do 80 | name = ivar_name key 81 | value = instance_variable_get(name) if instance_variables.include? name 82 | value || values.first 83 | end 84 | define_method "#{key}=" do |arg| 85 | if arg 86 | arg = aliases.fetch arg do 87 | aliases.fetch arg.to_s.downcase.gsub(/[ \-]/, '_').to_sym, arg 88 | end 89 | if values.any? do |val| val === arg end 90 | instance_variable_set(ivar_name(key), arg) 91 | else 92 | valid = values.collect do |val| val.inspect end.join ', ' 93 | raise ArgumentError, 94 | "Invalid value '#{arg.inspect}' for #{key}. Valid values are: #{valid}" 95 | end 96 | else 97 | instance_variable_set ivar_name(key), values.first 98 | end 99 | end 100 | end 101 | end 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/spreadsheet/encodings.rb: -------------------------------------------------------------------------------- 1 | module Spreadsheet 2 | ## 3 | # Methods for Encoding-conversions. You should not need to use any of these. 4 | module Encodings 5 | if RUBY_VERSION >= '1.9' 6 | def client string, internal='UTF-16LE' 7 | string.force_encoding internal 8 | string.encode Spreadsheet.client_encoding 9 | end 10 | def internal string, client=Spreadsheet.client_encoding 11 | string.force_encoding client 12 | string.encode('UTF-16LE').force_encoding('ASCII-8BIT') 13 | end 14 | def utf8 string, client=Spreadsheet.client_encoding 15 | string.force_encoding client 16 | string.encode('UTF-8') 17 | end 18 | else 19 | require 'iconv' 20 | @@iconvs = {} 21 | def client string, internal='UTF-16LE' 22 | key = [Spreadsheet.client_encoding, internal] 23 | iconv = @@iconvs[key] ||= Iconv.new(Spreadsheet.client_encoding, internal) 24 | iconv.iconv string 25 | end 26 | def internal string, client=Spreadsheet.client_encoding 27 | key = ['UTF-16LE', client] 28 | iconv = @@iconvs[key] ||= Iconv.new('UTF-16LE', client) 29 | iconv.iconv string 30 | end 31 | def utf8 string, client=Spreadsheet.client_encoding 32 | key = ['UTF-8', client] 33 | iconv = @@iconvs[key] ||= Iconv.new('UTF-8', client) 34 | iconv.iconv string 35 | end 36 | end 37 | rescue LoadError 38 | warn "You don't have Iconv support compiled in your Ruby. Spreadsheet may not work as expected" 39 | def client string, internal='UTF-16LE' 40 | string.delete "\0" 41 | end 42 | def internal string, internal='UTF-16LE' 43 | string.split('').zip(Array.new(string.size, 0.chr)).join 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/spreadsheet/excel.rb: -------------------------------------------------------------------------------- 1 | require 'spreadsheet' 2 | 3 | warn <<-EOS 4 | [DEPRECATED] By requiring 'spreadsheet/excel' you are loading a Compatibility 5 | layer which provides a drop-in replacement for Spreadsheet::Excel 6 | versions <= 0.3.5.1. This code will be removed in Spreadsheet 7 | version 1.0.0 8 | EOS 9 | ## 10 | # Spreadsheet::Excel Compatibility Layer. 11 | # Drop-in replacement for Spreadsheet::Excel version <= 0.3.5.1 12 | module Spreadsheet 13 | module Excel 14 | class ExcelCompatibleWorkbook < Workbook 15 | def initialize file_path, *args 16 | super *args 17 | @file_path = file_path 18 | end 19 | def close 20 | write @file_path 21 | end 22 | end 23 | def Excel.new file_path 24 | ExcelCompatibleWorkbook.new file_path 25 | end 26 | class Workbook 27 | def add_worksheet name 28 | if name.is_a? String 29 | create_worksheet :name => name 30 | else 31 | super 32 | end 33 | end 34 | end 35 | end 36 | class Worksheet 37 | unless instance_methods.include? "new_format_column" 38 | alias :new_format_column :format_column 39 | def format_column column, width=nil, format=nil 40 | if width.is_a? Format 41 | new_format_column column, width, format 42 | else 43 | new_format_column column, format, :width => width 44 | end 45 | end 46 | end 47 | def write row, col, data=nil, format=nil 48 | if data.is_a? Array 49 | write_row row, col, data, format 50 | else 51 | row = row(row) 52 | row[col] = data 53 | row.set_format col, format 54 | end 55 | end 56 | def write_column row, col, data=nil, format=nil 57 | if data.is_a? Array 58 | data.each do |token| 59 | if token.is_a? Array 60 | write_row row, col, token, format 61 | else 62 | write row, col, token, format 63 | end 64 | row += 1 65 | end 66 | else 67 | write row, col, data, format 68 | end 69 | end 70 | def write_row row, col, data=nil, format=nil 71 | if data.is_a? Array 72 | data.each do |token| 73 | if token.is_a? Array 74 | write_column row, col, token, format 75 | else 76 | write row, col, token, format 77 | end 78 | col += 1 79 | end 80 | else 81 | write row, col, data, format 82 | end 83 | end 84 | def write_url row, col, url, string=url, format=nil 85 | row(row)[col] = Link.new url, string 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/spreadsheet/excel/error.rb: -------------------------------------------------------------------------------- 1 | module Spreadsheet 2 | module Excel 3 | ## 4 | # This class encapsulates Excel Error-Codes 5 | class Error 6 | attr_reader :code 7 | ERROR_VALUES = { 8 | 0x00 => '#NULL!', # Intersection of two cell ranges is empty 9 | 0x07 => '#DIV/0!', # Division by zero 10 | 0x0F => '#VALUE!', # Wrong type of operand 11 | 0x17 => '#REF!', # Illegal or deleted cell reference 12 | 0x1D => '#NAME?', # Wrong function or range name 13 | 0x24 => '#NUM!', # Value range overflow 14 | 0x2A => '#N/A!', # Argument or function not available 15 | } 16 | def initialize code 17 | @code = code 18 | end 19 | ## 20 | # The String value Excel associates with an Error code 21 | def value 22 | ERROR_VALUES.fetch @code, '#UNKNOWN' 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/spreadsheet/excel/internals.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | 3 | module Spreadsheet 4 | module Excel 5 | ## 6 | # Binary Formats and other configurations internal to Excel. This Module is 7 | # likely to shrink as Support for older Versions of Excel grows and more Binary 8 | # formats are moved away from here for disambiguation. 9 | # If you need to work with constants defined in this module and are confused by 10 | # names like SEDOC_ROLOC, try reading them backwards. (The reason for this weird 11 | # naming convention is that according to my ri, Ruby 1.9 renames Hash#index to 12 | # Hash#key without backward compatibility. Since I did not want to pepper my code 13 | # with RUBY_VERSION-checks, I settled on this strategy to make the transition to 14 | # Ruby 1.9 as simple as possible. 15 | module Internals 16 | EIGHT_BYTE_DOUBLE = [0.1].pack('E').size == 8 ? 'E' : 'e' 17 | CODEPAGES = { 18 | 367 => "ASCII", 19 | 437 => "IBM437", #(US) 20 | 720 => "IBM720", #(OEM Arabic) 21 | 737 => "IBM737", #(Greek) 22 | 775 => "IBM775", #(Baltic) 23 | 850 => "IBM850", #(Latin I) 24 | 852 => "IBM852", #(Latin II (Central European)) 25 | 855 => "IBM855", #(Cyrillic) 26 | 857 => "IBM857", #(Turkish) 27 | 858 => "IBM858", #(Multilingual Latin I with Euro) 28 | 860 => "IBM860", #(Portuguese) 29 | 861 => "IBM861", #(Icelandic) 30 | 862 => "IBM862", #(Hebrew) 31 | 863 => "IBM863", #(Canadian (French)) 32 | 864 => "IBM864", #(Arabic) 33 | 865 => "IBM865", #(Nordic) 34 | 866 => "IBM866", #(Cyrillic (Russian)) 35 | 869 => "IBM869", #(Greek (Modern)) 36 | 874 => "WINDOWS-874", #(Thai) 37 | 932 => "WINDOWS-932", #(Japanese Shift-JIS) 38 | 936 => "WINDOWS-936", #(Chinese Simplified GBK) 39 | 949 => "WINDOWS-949", #(Korean (Wansung)) 40 | 950 => "WINDOWS-950", #(Chinese Traditional BIG5) 41 | 1200 => "UTF-16LE", #(BIFF8) 42 | 1250 => "WINDOWS-1250", #(Latin II) (Central European) 43 | 1251 => "WINDOWS-1251", #(Cyrillic) 44 | 1252 => "WINDOWS-1252", #(Latin I) (BIFF4-BIFF7) 45 | 1253 => "WINDOWS-1253", #(Greek) 46 | 1254 => "WINDOWS-1254", #(Turkish) 47 | 1255 => "WINDOWS-1255", #(Hebrew) 48 | 1256 => "WINDOWS-1256", #(Arabic) 49 | 1257 => "WINDOWS-1257", #(Baltic) 50 | 1258 => "WINDOWS-1258", #(Vietnamese) 51 | 1361 => "WINDOWS-1361", #(Korean (Johab)) 52 | 10000 => "MACINTOSH", 53 | 32768 => "MACINTOSH", 54 | 32769 => "WINDOWS-1252", #(Latin I) (BIFF2-BIFF3) 55 | } 56 | SEGAPEDOC = CODEPAGES.invert 57 | COLOR_CODES = { 58 | 0x0000 => :builtin_black, 59 | 0x0001 => :builtin_white, 60 | 0x0002 => :builtin_red, 61 | 0x0003 => :builtin_green, 62 | 0x0004 => :builtin_blue, 63 | 0x0005 => :builtin_yellow, 64 | 0x0006 => :builtin_magenta, 65 | 0x0007 => :builtin_cyan, 66 | 0x0008 => :black, 67 | 0x0009 => :white, 68 | 0x000a => :red, 69 | 0x000b => :lime, 70 | 0x000c => :blue, 71 | 0x000d => :yellow, 72 | 0x000e => :magenta, 73 | 0x000f => :cyan, 74 | 0x0010 => :brown, 75 | 0x0011 => :green, 76 | 0x0012 => :navy, 77 | 0x0016 => :silver, 78 | 0x0017 => :gray, 79 | 0x001d => :orange, 80 | 0x0024 => :purple, 81 | 0x0040 => :border, 82 | 0x0041 => :pattern_bg, 83 | 0x0043 => :dialog_bg, 84 | 0x004d => :chart_text, 85 | 0x004e => :chart_bg, 86 | 0x004f => :chart_border, 87 | 0x0050 => :tooltip_bg, 88 | 0x0051 => :tooltip_text, 89 | 0x7fff => :text, 90 | } 91 | SEDOC_ROLOC = COLOR_CODES.invert.update( :aqua => 0x000f, 92 | :fuchsia => 0x000e, 93 | :grey => 0x0017 ) 94 | BINARY_FORMATS = { 95 | :blank => 'v3', 96 | :boolerr => 'v3C2', 97 | :colinfo => 'v5x2', 98 | :font => 'v5C3x', 99 | :labelsst => 'v3V', 100 | :number => "v3#{EIGHT_BYTE_DOUBLE}", 101 | :pagesetup => "v8#{EIGHT_BYTE_DOUBLE}2v", 102 | :rk => 'v3V', 103 | :row => 'v4x4V', 104 | :window2 => 'v4x2v2x4', 105 | :xf => 'v3C4V2v', 106 | } 107 | # From BIFF5 on, the built-in number formats will be omitted. The built-in 108 | # formats are dependent on the current regional settings of the operating 109 | # system. The following table shows which number formats are used by 110 | # default in a US-English environment. All indexes from 0 to 163 are 111 | # reserved for built-in formats. 112 | BUILTIN_FORMATS = { # TODO: locale support 113 | 0 => 'GENERAL', 114 | 1 => '0', 115 | 2 => '0.00', 116 | 3 => '#,##0', 117 | 4 => '#,##0.00', 118 | 5 => '"$"#,##0_);("$"#,##0)', 119 | 6 => '"$"#,##0_);[Red]("$"#,##0)', 120 | 7 => '"$"#,##0.00_);("$"#,##0.00)', 121 | 8 => '"$"#,##0.00_);[Red]("$"#,##0.00)', 122 | 9 => '0%', 123 | 10 => '0.00%', 124 | 11 => '0.00E+00', 125 | 12 => '# ?/?', 126 | 13 => '# ??/??', 127 | 14 => 'M/D/YY', 128 | 15 => 'D-MMM-YY', 129 | 16 => 'D-MMM', 130 | 17 => 'MMM-YY', 131 | 18 => 'h:mm AM/PM', 132 | 19 => 'h:mm:ss AM/PM', 133 | 20 => 'h:mm', 134 | 21 => 'h:mm:ss', 135 | 22 => 'M/D/YY h:mm', 136 | 37 => '_(#,##0_);(#,##0)', 137 | 38 => '_(#,##0_);[Red](#,##0)', 138 | 39 => '_(#,##0.00_);(#,##0.00)', 139 | 40 => '_(#,##0.00_);[Red](#,##0.00)', 140 | 41 => '_("$"* #,##0_);_("$"* (#,##0);_("$"* "-"_);_(@_)', 141 | 42 => '_(* #,##0_);_(* (#,##0);_(* "-"_);_(@_)', 142 | 43 => '_("$"* #,##0.00_);_("$"* (#,##0.00);_("$"* "-"??_);_(@_)', 143 | 44 => '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)', 144 | 45 => 'mm:ss', 145 | 46 => '[h]:mm:ss', 146 | 47 => 'mm:ss.0', 147 | 48 => '##0.0E+0', 148 | 49 => '@', 149 | } 150 | BUILTIN_STYLES = { 151 | 0x00 => 'Normal', 152 | 0x01 => 'RowLevel_lv', 153 | 0x02 => 'ColLevel_lv', 154 | 0x03 => 'Comma', 155 | 0x04 => 'Currency', 156 | 0x05 => 'Percent', 157 | 0x06 => 'Comma', 158 | 0x07 => 'Currency', 159 | 0x08 => 'Hyperlink', 160 | 0x09 => 'Followed Hyperlink', 161 | } 162 | ESCAPEMENT_TYPES = { 163 | 0x0001 => :superscript, 164 | 0x0002 => :subscript, 165 | } 166 | SEPYT_TNEMEPACSE = ESCAPEMENT_TYPES.invert 167 | FONT_ENCODINGS = { 168 | 0x00 => :iso_latin1, 169 | 0x01 => :default, 170 | 0x02 => :symbol, 171 | 0x4d => :apple_roman, 172 | 0x80 => :shift_jis, 173 | 0x81 => :korean_hangul, 174 | 0x82 => :korean_johab, 175 | 0x86 => :chinese_simplified, 176 | 0x88 => :chinese_traditional, 177 | 0xa1 => :greek, 178 | 0xa2 => :turkish, 179 | 0xa3 => :vietnamese, 180 | 0xb1 => :hebrew, 181 | 0xb2 => :arabic, 182 | 0xba => :baltic, 183 | 0xcc => :cyrillic, 184 | 0xde => :thai, 185 | 0xee => :iso_latin2, 186 | 0xff => :oem_latin1, 187 | } 188 | SGNIDOCNE_TNOF = FONT_ENCODINGS.invert 189 | FONT_FAMILIES = { 190 | 0x01 => :roman, 191 | 0x02 => :swiss, 192 | 0x03 => :modern, 193 | 0x04 => :script, 194 | 0x05 => :decorative, 195 | } 196 | SEILIMAF_TNOF = FONT_FAMILIES.invert 197 | FONT_WEIGHTS = { 198 | :bold => 700, 199 | :normal => 400, 200 | } 201 | LEAP_ERROR = Date.new 1900, 2, 28 202 | OPCODES = { 203 | :blank => 0x0201, # BLANK ➜ 6.7 204 | :boolerr => 0x0205, # BOOLERR ➜ 6.10 205 | :boundsheet => 0x0085, # ●● BOUNDSHEET ➜ 6.12 206 | :codepage => 0x0042, # ○ CODEPAGE ➜ 6.17 207 | :colinfo => 0x007d, # ○○ COLINFO ➜ 6.18 208 | :continue => 0x003c, # ○ CONTINUE ➜ 6.22 209 | :datemode => 0x0022, # ○ DATEMODE ➜ 6.25 210 | :dbcell => 0x0a0b, # ○ DBCELL 211 | :dimensions => 0x0200, # ● DIMENSIONS ➜ 6.31 212 | :eof => 0x000a, # ● EOF ➜ 6.36 213 | :font => 0x0031, # ●● FONT ➜ 6.43 214 | :format => 0x041e, # ○○ FORMAT (Number Format) ➜ 6.45 215 | :formula => 0x0006, # FORMULA ➜ 6.46 216 | :hlink => 0x01b8, # HLINK ➜ 6.52 (BIFF8 only) 217 | :label => 0x0204, # LABEL ➜ 6.59 (BIFF2-BIFF7) 218 | :labelsst => 0x00fd, # LABELSST ➜ 6.61 (BIFF8 only) 219 | :mergedcells => 0x00e5, # ○○ MERGEDCELLS ➜ 5.67 (BIFF8 only) 220 | :mulblank => 0x00be, # MULBLANK ➜ 6.64 (BIFF5-BIFF8) 221 | :mulrk => 0x00bd, # MULRK ➜ 6.65 (BIFF5-BIFF8) 222 | :number => 0x0203, # NUMBER ➜ 6.68 223 | :rk => 0x027e, # RK ➜ 6.82 (BIFF3-BIFF8) 224 | :row => 0x0208, # ● ROW ➜ 6.83 225 | :rstring => 0x00d6, # RSTRING ➜ 6.84 (BIFF5/BIFF7) 226 | :sst => 0x00fc, # ● SST ➜ 6.96 227 | :string => 0x0207, # STRING ➜ 6.98 228 | :style => 0x0293, # ●● STYLE ➜ 6.99 229 | :xf => 0x00e0, # ●● XF ➜ 6.115 230 | :sharedfmla => 0x04bc, # SHAREDFMLA ➜ 5.94 231 | ########################## Unhandled Opcodes ################################ 232 | :extsst => 0x00ff, # ● EXTSST ➜ 6.40 233 | :index => 0x020b, # ○ INDEX ➜ 5.7 (Row Blocks), ➜ 6.55 234 | :uncalced => 0x005e, # ○ UNCALCED ➜ 6.104 235 | ########################## ○ Calculation Settings Block ➜ 5.3 236 | :calccount => 0x000c, # ○ CALCCOUNT ➜ 6.14 237 | :calcmode => 0x000d, # ○ CALCMODE ➜ 6.15 238 | :precision => 0x000e, # ○ PRECISION ➜ 6.74 (moved to Workbook Globals 239 | # Substream in BIFF5-BIFF8) 240 | :refmode => 0x000f, # ○ REFMODE ➜ 6.80 241 | :delta => 0x0010, # ○ DELTA ➜ 6.30 242 | :iteration => 0x0011, # ○ ITERATION ➜ 6.57 243 | :saverecalc => 0x005f, # ○ SAVERECALC ➜ 6.85 (BIFF3-BIFF8 only) 244 | ########################## ○ Workbook Protection Block ➜ 5.18 245 | :protect => 0x0012, # ○ PROTECT 246 | # Worksheet contents: 1 = protected (➜ 6.77) 247 | :windowprot => 0x0019, # ○ WINDOWPROTECT Window settings: 1 = protected 248 | # (BIFF4W only, ➜ 6.110) 249 | :objectprot => 0x0063, # ○ OBJECTPROTECT 250 | # Embedded objects: 1 = protected (➜ 6.69) 251 | :scenprotect => 0x00dd, # ○ SCENPROTECT 252 | # Scenarios: 1 = protected (BIFF5-BIFF8, ➜ 6.86) 253 | :password => 0x0013, # ○ PASSWORD Hash value of the password; 254 | # 0 = no password (➜ 6.72) 255 | ########################## ○ File Protection Block ➜ 5.19 256 | :writeprot => 0x0086, # ○ WRITEPROT File is write protected 257 | # (BIFF3-BIFF8, ➜ 6.112), password in FILESHARING 258 | :filepass => 0x002f, # ○ FILEPASS File is read/write-protected, 259 | # encryption information (➜ 6.41) 260 | :writeaccess => 0x005c, # ○ WRITEACCESS User name (BIFF3-BIFF8, ➜ 6.111) 261 | :filesharing => 0x005b, # ○ FILESHARING File sharing options 262 | # (BIFF3-BIFF8, ➜ 6.42) 263 | ########################## ○ Link Table ➜ 5.10.3 264 | # ●● SUPBOOK Block(s) 265 | # Settings for a referenced document 266 | :supbook => 0x01ae, # ● SUPBOOK ➜ 6.100 267 | :externname => 0x0223, # ○○ EXTERNNAME ➜ 6.38 268 | :xct => 0x0059, # ○○ ● XCT ➜ 6.114 269 | :crn => 0x005a, # ●● CRN ➜ 6.24 270 | :externsheet => 0x0017, # ● EXTERNSHEET ➜ 6.39 271 | :name => 0x0218, # ○○ NAME ➜ 6.66 272 | ########################## 273 | :window1 => 0x003d, # ● WINDOW1 ➜ 6.108 (has information on 274 | # which Spreadsheet is 'active') 275 | :backup => 0x0040, # ○ BACKUP ➜ 6.5 276 | :country => 0x008c, # ○ COUNTRY (Make writeable?) ➜ 6.23 277 | :hideobj => 0x008d, # ○ HIDEOBJ ➜ 6.52 278 | :palette => 0x0092, # ○ PALETTE ➜ 6.70 279 | :fngroupcnt => 0x009c, # ○ FNGROUPCOUNT 280 | :bookbool => 0x00da, # ○ BOOKBOOL ➜ 6.9 281 | :tabid => 0x013d, # ○ TABID 282 | :useselfs => 0x0160, # ○ USESELFS (Natural Language Formulas) ➜ 6.105 283 | :dsf => 0x0161, # ○ DSF (Double Stream File) ➜ 6.32 284 | :refreshall => 0x01b7, # ○ REFRESHALL 285 | ########################## ● Worksheet View Settings Block ➜ 5.5 286 | :window2 => 0x023e, # ● WINDOW2 ➜ 5.110 287 | :scl => 0x00a0, # ○ SCL ➜ 5.92 (BIFF4-BIFF8 only) 288 | :pane => 0x0041, # ○ PANE ➜ 5.75 289 | :selection => 0x001d, # ○○ SELECTION ➜ 5.93 290 | ########################## ○ Page Settings Block ➜ 5.4 291 | :hpagebreaks => 0x001b, # ○ HORIZONTALPAGEBREAKS ➜ 6.54 292 | :vpagebreaks => 0x001a, # ○ VERTICALPAGEBREAKS ➜ 6.107 293 | :header => 0x0014, # ○ HEADER ➜ 6.51 294 | :footer => 0x0015, # ○ FOOTER ➜ 6.44 295 | :hcenter => 0x0083, # ○ HCENTER ➜ 6.50 (BIFF3-BIFF8 only) 296 | :vcenter => 0x0084, # ○ VCENTER ➜ 6.106 (BIFF3-BIFF8 only) 297 | :leftmargin => 0x0026, # ○ LEFTMARGIN ➜ 6.62 298 | :rightmargin => 0x0027, # ○ RIGHTMARGIN ➜ 6.81 299 | :topmargin => 0x0028, # ○ TOPMARGIN ➜ 6.103 300 | :bottommargin => 0x0029, # ○ BOTTOMMARGIN ➜ 6.11 301 | # ○ PLS (opcode unknown) 302 | :pagesetup => 0x00a1, # ○ PAGESETUP ➜ 6.89 (BIFF4-BIFF8 only) 303 | :bitmap => 0x00e9, # ○ BITMAP ➜ 6.6 (Background-Bitmap, BIFF8 only) 304 | ########################## 305 | :printheaders => 0x002a, # ○ PRINTHEADERS ➜ 6.76 306 | :printgridlns => 0x002b, # ○ PRINTGRIDLINES ➜ 6.75 307 | :gridset => 0x0082, # ○ GRIDSET ➜ 6.48 308 | :guts => 0x0080, # ○ GUTS ➜ 6.49 309 | :defrowheight => 0x0225, # ○ DEFAULTROWHEIGHT ➜ 6.28 310 | :wsbool => 0x0081, # ○ WSBOOL ➜ 6.113 311 | :defcolwidth => 0x0055, # ○ DEFCOLWIDTH ➜ 6.29 312 | :sort => 0x0090, # ○ SORT ➜ 6.95 313 | } 314 | =begin ## unknown opcodes 315 | 0x00bf, 0x00c0, 0x00c1, 0x00e1, 0x00e2, 0x00eb, 0x01af, 0x01bc 316 | =end 317 | SEDOCPO = OPCODES.invert 318 | TWIPS = 20 319 | UNDERLINE_TYPES = { 320 | 0x0001 => :single, 321 | 0x0002 => :double, 322 | 0x0021 => :single_accounting, 323 | 0x0022 => :double_accounting, 324 | } 325 | SEPYT_ENILREDNU = UNDERLINE_TYPES.invert 326 | XF_H_ALIGN = { 327 | :default => 0, 328 | :left => 1, 329 | :center => 2, 330 | :right => 3, 331 | :fill => 4, 332 | :justify => 5, 333 | :merge => 6, 334 | :distributed => 7, 335 | } 336 | NGILA_H_FX = XF_H_ALIGN.invert 337 | XF_TEXT_DIRECTION = { 338 | :context => 0, 339 | :left_to_right => 1, 340 | :right_to_left => 2, 341 | } 342 | NOITCERID_TXET_FX = XF_TEXT_DIRECTION.invert 343 | XF_V_ALIGN = { 344 | :top => 0, 345 | :middle => 1, 346 | :bottom => 2, 347 | :justify => 3, 348 | :distributed => 4, 349 | } 350 | NGILA_V_FX = XF_V_ALIGN.invert 351 | OPCODE_SIZE = 4 352 | ROW_HEIGHT = 12.1 353 | SST_CHUNKSIZE = 20 354 | def binfmt key 355 | BINARY_FORMATS[key] 356 | end 357 | def opcode key 358 | OPCODES[key] 359 | end 360 | end 361 | end 362 | end 363 | -------------------------------------------------------------------------------- /lib/spreadsheet/excel/internals/biff5.rb: -------------------------------------------------------------------------------- 1 | module Spreadsheet 2 | module Excel 3 | module Internals 4 | ## 5 | # Binary Formats and other configurations internal to Biff5. This Module is 6 | # likely to be expanded as Support for older Versions of Excel grows. 7 | module Biff5 8 | BINARY_FORMATS = { 9 | :dimensions => 'v5', 10 | } 11 | def binfmt key # :nodoc: 12 | BINARY_FORMATS.fetch key do super end 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/spreadsheet/excel/internals/biff8.rb: -------------------------------------------------------------------------------- 1 | module Spreadsheet 2 | module Excel 3 | module Internals 4 | ## 5 | # Binary Formats and other configurations internal to Biff8. This Module is 6 | # likely to be expanded as Support for older Versions of Excel grows and more 7 | # Binary formats are moved here for disambiguation. 8 | module Biff8 9 | BINARY_FORMATS = { 10 | :bof => 'v4V2', 11 | :dimensions => 'V2v2x2', 12 | } 13 | def binfmt key # :nodoc: 14 | BINARY_FORMATS.fetch key do super end 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/spreadsheet/excel/offset.rb: -------------------------------------------------------------------------------- 1 | require 'spreadsheet/compatibility' 2 | 3 | module Spreadsheet 4 | module Excel 5 | ## 6 | # This module is used to keep track of offsets in modified Excel documents. 7 | # Considered internal and subject to change without notice. 8 | module Offset 9 | include Compatibility 10 | attr_reader :changes, :offsets 11 | def initialize *args 12 | super 13 | @changes = {} 14 | @offsets = {} 15 | end 16 | def Offset.append_features mod 17 | super 18 | mod.module_eval do 19 | class << self 20 | include Compatibility 21 | def offset *keys 22 | keys.each do |key| 23 | attr_reader key unless instance_methods.include? method_name(key) 24 | define_method "#{key}=" do |value| 25 | @changes.store key, true 26 | instance_variable_set ivar_name(key), value 27 | end 28 | define_method "set_#{key}" do |value, pos, len| 29 | instance_variable_set ivar_name(key), value 30 | @offsets.store key, [pos, len] 31 | havename = "have_set_#{key}" 32 | send(havename, value, pos, len) if respond_to? havename 33 | end 34 | end 35 | end 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/spreadsheet/excel/reader/biff5.rb: -------------------------------------------------------------------------------- 1 | module Spreadsheet 2 | module Excel 3 | class Reader 4 | ## 5 | # This Module collects reader methods such as read_string that are specific to 6 | # Biff5. This Module is likely to be expanded as Support for older Versions 7 | # of Excel grows. 8 | module Biff5 9 | ## 10 | # Read a String of 8-bit Characters 11 | def read_string work, count_length=1 12 | # Offset Size Contents 13 | # 0 1 or 2 Length of the string (character count, ln) 14 | # 1 or 2 ln Character array (8-bit characters) 15 | fmt = count_length == 1 ? 'C' : 'v' 16 | length, = work.unpack fmt 17 | work[count_length, length] 18 | end 19 | 20 | def read_range_address_list work, len 21 | # Cell range address, BIFF2-BIFF5: 22 | # Offset Size Contents 23 | # 0 2 Index to first row 24 | # 2 2 Index to last row 25 | # 4 1 Index to first column 26 | # 5 1 Index to last column 27 | # 28 | offset = 0, results = [] 29 | return results if len < 2 30 | count = work[0..1].unpack('v').first 31 | offset = 2 32 | count.times do |i| 33 | results << work[offset...offset+6].unpack('v2c2') 34 | offset += 6 35 | end 36 | results 37 | end 38 | 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/spreadsheet/excel/reader/biff8.rb: -------------------------------------------------------------------------------- 1 | module Spreadsheet 2 | module Excel 3 | class Reader 4 | ## 5 | # This Module collects reader methods such as read_string that are specific to 6 | # Biff8. This Module is likely to be expanded as Support for older Versions 7 | # of Excel grows and methods get moved here for disambiguation. 8 | module Biff8 9 | include Spreadsheet::Excel::Internals 10 | ## 11 | # When a String is too long for one Opcode, it is continued in a Continue 12 | # Opcode. Excel may reconsider compressing the remainder of the string. 13 | # This method appends the available remainder (decompressed if necessary) to 14 | # the incomplete string. 15 | def continue_string work, incomplete_string=@incomplete_string 16 | opts, _ = work.unpack 'C' 17 | wide = opts & 1 18 | head, chars = incomplete_string 19 | owing = chars - head.size / 2 20 | size = owing * (wide + 1) 21 | string = work[1, size] 22 | if wide == 0 23 | string = wide string 24 | end 25 | head << string 26 | if head.size >= chars * 2 27 | @incomplete_string = nil 28 | end 29 | size + 1 30 | end 31 | ## 32 | # When a String is too long for one Opcode, it is continued in a Continue 33 | # Opcode. Excel may reconsider compressing the remainder of the string. 34 | # This method only evaluates the header and registers the address of the 35 | # continuation with the previous SstEntry. 36 | def continue_string_header work, oppos 37 | opts, _ = work.unpack 'C' 38 | wide = opts & 1 39 | owing = @incomplete_sst.continued_chars 40 | size = [work.size, owing * (1 + wide) + 1].min 41 | chars = (size - 1) / (1 + wide) 42 | skip = size 43 | @incomplete_sst.continue oppos + OPCODE_SIZE, size, chars 44 | unless @incomplete_sst.continued? 45 | @workbook.add_shared_string @incomplete_sst 46 | skip += @incomplete_skip 47 | @incomplete_sst = nil 48 | @incomplete_skip = nil 49 | end 50 | skip 51 | end 52 | ## 53 | # Read more data into the Shared String Table. (see also: #read_sst) 54 | # This method only evaluates the header, the actual work is done in #_read_sst 55 | def continue_sst work, oppos, len 56 | pos = 0 57 | if @incomplete_sst 58 | pos = continue_string_header work, oppos 59 | elsif !@incomplete_skip.nil? 60 | pos = @incomplete_skip 61 | @incomplete_skip = nil 62 | end 63 | @sst_offset[1] += len 64 | _read_sst work, oppos, pos 65 | end 66 | def postread_workbook # :nodoc: 67 | super 68 | @incomplete_string, @sst_size, @sst_offset, @incomplete_sst = nil, @incomplete_skip = nil 69 | end 70 | ## 71 | # Store the offset of extsst, so we can write a new extsst when the 72 | # sst has changed 73 | def read_extsst work, pos, len 74 | @workbook.offsets.store :extsst, [pos, len] 75 | end 76 | ## 77 | # Read the Shared String Table present in all Biff8 Files. 78 | # This method only evaluates the header, the actual work is done in #_read_sst 79 | def read_sst work, pos, len 80 | # Offset Size Contents 81 | # 0 4 Total number of strings in the workbook (see below) 82 | # 4 4 Number of following strings (nm) 83 | # 8 var. List of nm Unicode strings, 16-bit string length (➜ 3.4) 84 | total, @sst_size = work.unpack 'V2' 85 | @sst_offset = [pos, len] 86 | @workbook.offsets.store :sst, @sst_offset 87 | _read_sst work, pos, 8 88 | end 89 | ## 90 | # Read a string from the Spreadsheet, such as a Worksheet- or Font-Name, or a 91 | # Number-Format. See also #read_string_header and #read_string_body 92 | def read_string work, count_length=1 93 | # Offset Size Contents 94 | # 0 1 or 2 Length of the string (character count, ln) 95 | # 1 or 2 1 Option flags: 96 | # Bit Mask Contents 97 | # 0 0x01 Character compression (ccompr): 98 | # 0 = Compressed (8-bit characters) 99 | # 1 = Uncompressed (16-bit characters) 100 | # 2 0x04 Asian phonetic settings (phonetic): 101 | # 0 = Does not contain Asian phonetic settings 102 | # 1 = Contains Asian phonetic settings 103 | # 3 0x08 Rich-Text settings (richtext): 104 | # 0 = Does not contain Rich-Text settings 105 | # 1 = Contains Rich-Text settings 106 | # [2 or 3] 2 (optional, only if richtext=1) 107 | # Number of Rich-Text formatting runs (rt) 108 | # [var.] 4 (optional, only if phonetic=1) 109 | # Size of Asian phonetic settings block (in bytes, sz) 110 | # var. ln Character array (8-bit characters 111 | # or 2∙ln or 16-bit characters, dependent on ccompr) 112 | # [var.] 4∙rt (optional, only if richtext=1) 113 | # List of rt formatting runs (➜ 3.2) 114 | # [var.] sz (optional, only if phonetic=1) 115 | # Asian Phonetic Settings Block (➜ 3.4.2) 116 | chars, offset, wide, phonetic, richtext, available, owing, skip \ 117 | = read_string_header work, count_length 118 | string, data = read_string_body work, offset, available, wide > 0 119 | if owing > 0 120 | @incomplete_string = [string, chars] 121 | end 122 | string 123 | end 124 | ## 125 | # Read the body of a string. Returns the String (decompressed if necessary) and 126 | # the available data (unchanged). 127 | def read_string_body work, offset, available, wide 128 | data = work[offset, available] 129 | string = wide ? data : wide(data) 130 | [string, data] 131 | end 132 | ## 133 | # Read the header of a string. Returns the following information in an Array: 134 | # * The total number of characters in the string 135 | # * The offset of the actual string data (= the length of this header in bytes) 136 | # * Whether or not the string was compressed (0/1) 137 | # * Whether or not the string contains asian phonetic settings (0/1) 138 | # * Whether or not the string contains richtext formatting (0/1) 139 | # * The number of bytes containing characters in this chunk of data 140 | # * The number of characters missing from this chunk of data and expected to 141 | # follow in a Continue Opcode 142 | def read_string_header work, count_length=1, offset=0 143 | fmt = count_length == 1 ? 'C2' : 'vC' 144 | chars, opts = work[offset, 1 + count_length].unpack fmt 145 | wide = opts & 1 146 | phonetic = (opts >> 2) & 1 147 | richtext = (opts >> 3) & 1 148 | size = chars * (wide + 1) 149 | skip = 0 150 | if richtext > 0 151 | runs, = work[offset + 1 + count_length, 2].unpack 'v' 152 | skip = 4 * runs 153 | end 154 | if phonetic > 0 155 | psize, = work[offset + 1 + count_length + richtext * 2, 4].unpack 'V' 156 | skip += psize 157 | end 158 | flagsize = 1 + count_length + richtext * 2 + phonetic * 4 159 | avbl = [work.size - offset, flagsize + size].min 160 | have_chrs = (avbl - flagsize) / (1 + wide) 161 | owing = chars - have_chrs 162 | [chars, flagsize, wide, phonetic, richtext, avbl, owing, skip] 163 | end 164 | 165 | def read_range_address_list work, len 166 | # Cell range address, BIFF8: 167 | # Offset Size Contents 168 | # 0 2 Index to first row 169 | # 2 2 Index to last row 170 | # 4 2 Index to first column 171 | # 6 2 Index to last column 172 | # ! In several cases, BIFF8 still writes the BIFF2-BIFF5 format of a cell range address 173 | # (using 8-bit values for the column indexes). This will be mentioned at the respective place. 174 | # 175 | offset = 0, results = [] 176 | return results if len < 2 177 | count = work[0..1].unpack('v').first 178 | offset = 2 179 | count.times do |i| 180 | results << work[offset...offset+8].unpack('v4') 181 | offset += 8 182 | end 183 | results 184 | end 185 | ## 186 | # Insert null-characters into a compressed UTF-16 string 187 | def wide string 188 | data = '' 189 | string.each_byte do |byte| data << byte.chr << 0.chr end 190 | data 191 | end 192 | private 193 | ## 194 | # Read the Shared String Table present in all Biff8 Files. 195 | def _read_sst work, oppos, pos 196 | worksize = work.size 197 | while @workbook.sst_size < @sst_size && pos < worksize do 198 | sst = SstEntry.new :offset => oppos + OPCODE_SIZE + pos, 199 | :ole => @data, 200 | :reader => self 201 | sst.chars, sst.flags, wide, sst.phonetic, sst.richtext, sst.available, 202 | sst.continued_chars, skip = read_string_header work, 2, pos 203 | sst.wide = wide > 0 204 | if sst.continued? 205 | @incomplete_sst = sst 206 | @incomplete_skip = skip 207 | pos += sst.available 208 | else 209 | @workbook.add_shared_string sst 210 | pos += sst.available + skip 211 | if pos > worksize 212 | @incomplete_skip = pos - worksize 213 | end 214 | end 215 | end 216 | end 217 | end 218 | end 219 | end 220 | end 221 | -------------------------------------------------------------------------------- /lib/spreadsheet/excel/row.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | require 'spreadsheet/row' 3 | 4 | module Spreadsheet 5 | module Excel 6 | ## 7 | # Excel-specific Row methods 8 | class Row < Spreadsheet::Row 9 | ## 10 | # The Excel date calculation erroneously assumes that 1900 is a leap-year. All 11 | # Dates after 28.2.1900 are off by one. 12 | LEAP_ERROR = Date.new 1900, 2, 28 13 | ## 14 | # Force convert the cell at _idx_ to a Date 15 | def date idx 16 | _date at(idx) 17 | end 18 | ## 19 | # Force convert the cell at _idx_ to a DateTime 20 | def datetime idx 21 | _datetime at(idx) 22 | end 23 | def each &block 24 | size.times do |idx| 25 | block.call self[idx] 26 | end 27 | end 28 | ## 29 | # Access data in this Row like you would in an Array. If a cell is formatted 30 | # as a Date or DateTime, the decoded Date or DateTime value is returned. 31 | def [] idx, len=nil 32 | if len 33 | idx = idx...(idx+len) 34 | end 35 | if idx.is_a? Range 36 | data = [] 37 | idx.each do |i| 38 | data.push enriched_data(i, at(i)) 39 | end 40 | data 41 | else 42 | enriched_data idx, at(idx) 43 | end 44 | end 45 | private 46 | def _date data # :nodoc: 47 | return data if data.is_a?(Date) 48 | datetime = _datetime data 49 | Date.new datetime.year, datetime.month, datetime.day 50 | end 51 | def _datetime data # :nodoc: 52 | return data if data.is_a?(DateTime) 53 | base = @worksheet.date_base 54 | date = base + data.to_f 55 | hour = (data % 1) * 24 56 | min = (hour % 1) * 60 57 | sec = ((min % 1) * 60).round 58 | min = min.floor 59 | hour = hour.floor 60 | if sec > 59 61 | sec = 0 62 | min += 1 63 | end 64 | if min > 59 65 | min = 0 66 | hour += 1 67 | end 68 | if hour > 23 69 | hour = 0 70 | date += 1 71 | end 72 | if LEAP_ERROR > base 73 | date -= 1 74 | end 75 | DateTime.new(date.year, date.month, date.day, hour, min, sec) 76 | end 77 | def enriched_data idx, data # :nodoc: 78 | res = nil 79 | if link = @worksheet.links[[@idx, idx]] 80 | res = link 81 | elsif data.is_a?(Numeric) && fmt = format(idx) 82 | res = if fmt.datetime? || fmt.time? 83 | _datetime data 84 | elsif fmt.date? 85 | _date data 86 | end 87 | end 88 | res || data 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/spreadsheet/excel/sst_entry.rb: -------------------------------------------------------------------------------- 1 | require 'spreadsheet/encodings' 2 | 3 | module Spreadsheet 4 | module Excel 5 | ## 6 | # Shared String Table Entry 7 | class SstEntry 8 | include Spreadsheet::Encodings 9 | attr_accessor :chars, :phonetic, :richtext, :flags, :available, 10 | :continued_chars, :wide 11 | def initialize opts = {} 12 | @content = nil 13 | @offset = opts[:offset] 14 | @ole = opts[:ole] 15 | @reader = opts[:reader] 16 | @continuations = [] 17 | end 18 | ## 19 | # Access the contents of this Shared String 20 | def content 21 | @content or begin 22 | data = nil 23 | data = @ole[@offset, @available] 24 | content, _ = @reader.read_string_body data, @flags, @available, @wide 25 | @continuations.each do |offset, len| 26 | @reader.continue_string(@ole[offset,len], [content, @chars]) 27 | end 28 | content = client content, 'UTF-16LE' 29 | if @reader.memoize? 30 | @content = content 31 | end 32 | content 33 | end 34 | end 35 | ## 36 | # Register the offset of a String continuation 37 | def continue offset, size, chars 38 | @continued_chars -= chars 39 | @continuations.push [offset, size] 40 | end 41 | def continued? # :nodoc: 42 | @continued_chars > 0 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/spreadsheet/excel/workbook.rb: -------------------------------------------------------------------------------- 1 | require 'spreadsheet/workbook' 2 | require 'spreadsheet/excel/offset' 3 | require 'spreadsheet/excel/writer' 4 | require 'ole/storage' 5 | 6 | module Spreadsheet 7 | module Excel 8 | ## 9 | # Excel-specific Workbook methods. These are mostly pertinent to the Excel 10 | # reader. You should have no reason to use any of these. 11 | class Workbook < Spreadsheet::Workbook 12 | include Spreadsheet::Encodings 13 | include Spreadsheet::Excel::Offset 14 | BIFF_VERSIONS = { 15 | 0x000 => 2, 16 | 0x007 => 2, 17 | 0x200 => 2, 18 | 0x300 => 3, 19 | 0x400 => 4, 20 | 0x500 => 5, 21 | 0x600 => 8, 22 | } 23 | VERSION_STRINGS = { 24 | 0x600 => 'Microsoft Excel 97/2000/XP', 25 | 0x500 => 'Microsoft Excel 95', 26 | } 27 | offset :encoding, :boundsheets, :sst 28 | attr_accessor :bof, :ole 29 | attr_writer :date_base 30 | def Workbook.open io, opts = {} 31 | @reader = Reader.new opts 32 | @reader.read io 33 | end 34 | def initialize *args 35 | super 36 | enc = 'UTF-16LE' 37 | if RUBY_VERSION >= '1.9' 38 | enc = Encoding.find enc 39 | end 40 | @encoding = enc 41 | @version = 0x600 42 | @sst = [] 43 | end 44 | def add_shared_string str 45 | @sst.push str 46 | end 47 | def add_worksheet worksheet 48 | @changes.store :boundsheets, true 49 | super 50 | end 51 | def biff_version 52 | case @bof 53 | when 0x009 54 | 2 55 | when 0x209 56 | 3 57 | when 0x409 58 | 4 59 | else 60 | BIFF_VERSIONS.fetch(@version) { raise "Unkown BIFF_VERSION '#@version'" } 61 | end 62 | end 63 | def date_base 64 | @date_base ||= DateTime.new 1899, 12, 31 65 | end 66 | def shared_string idx 67 | @sst[idx.to_i].content 68 | end 69 | def sst_size 70 | @sst.size 71 | end 72 | def uninspect_variables 73 | super.push '@sst', '@offsets', '@changes' 74 | end 75 | def version_string 76 | client VERSION_STRINGS.fetch(@version, "Unknown"), 'UTF-8' 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/spreadsheet/excel/worksheet.rb: -------------------------------------------------------------------------------- 1 | require 'spreadsheet/excel/offset' 2 | require 'spreadsheet/excel/row' 3 | require 'spreadsheet/worksheet' 4 | 5 | module Spreadsheet 6 | module Excel 7 | ## 8 | # Excel-specific Worksheet methods. These are mostly pertinent to the Excel 9 | # reader, and to recording changes to the Worksheet. You should have no reason 10 | # to use any of these. 11 | class Worksheet < Spreadsheet::Worksheet 12 | include Spreadsheet::Excel::Offset 13 | offset :dimensions 14 | attr_reader :offset, :ole, :links, :guts 15 | def initialize opts = {} 16 | @row_addresses = nil 17 | super 18 | @offset, @ole, @reader = opts[:offset], opts[:ole], opts[:reader] 19 | @dimensions = nil 20 | @links = {} 21 | @guts = {} 22 | end 23 | def add_link row, column, link 24 | @links.store [row, column], link 25 | end 26 | def column idx 27 | ensure_rows_read 28 | super 29 | end 30 | def date_base 31 | @workbook.date_base 32 | end 33 | def each *args 34 | ensure_rows_read 35 | super 36 | end 37 | def ensure_rows_read 38 | return if @row_addresses 39 | @dimensions = nil 40 | @row_addresses = [] 41 | @reader.read_worksheet self, @offset if @reader 42 | end 43 | def row idx 44 | @rows[idx] or begin 45 | ensure_rows_read 46 | if addr = @row_addresses[idx] 47 | row = @reader.read_row self, addr 48 | [:default_format, :height, :outline_level, :hidden, ].each do |key| 49 | row.send "unupdated_#{key}=", addr[key] 50 | end 51 | row.worksheet = self 52 | row 53 | else 54 | Row.new self, idx 55 | end 56 | end 57 | end 58 | def row_updated idx, row 59 | res = super 60 | @workbook.changes.store self, true 61 | @workbook.changes.store :boundsheets, true 62 | @changes.store idx, true 63 | @changes.store :dimensions, true 64 | res 65 | end 66 | def set_row_address idx, opts 67 | @offsets.store idx, opts[:row_block] 68 | @row_addresses[idx] = opts 69 | end 70 | def shared_string idx 71 | @workbook.shared_string idx 72 | end 73 | private 74 | ## premature optimization? 75 | def have_set_dimensions value, pos, len 76 | if @row_addresses.size < row_count 77 | @row_addresses.concat Array.new(row_count - @row_addresses.size) 78 | end 79 | end 80 | def recalculate_dimensions 81 | ensure_rows_read 82 | shorten @rows 83 | @dimensions = [] 84 | @dimensions[0] = [ index_of_first(@rows), 85 | index_of_first(@row_addresses) ].compact.min || 0 86 | @dimensions[1] = [ @rows.size, @row_addresses.size ].compact.max || 0 87 | compact = @rows.compact 88 | first_rows = compact.collect do |row| row.first_used end.compact.min 89 | first_addrs = @row_addresses.compact.collect do |addr| 90 | addr[:first_used] end.min 91 | @dimensions[2] = [ first_rows, first_addrs ].compact.min || 0 92 | last_rows = compact.collect do |row| row.first_unused end.max 93 | last_addrs = @row_addresses.compact.collect do |addr| 94 | addr[:first_unused] end.max 95 | @dimensions[3] = [last_rows, last_addrs].compact.max || 0 96 | @dimensions 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/spreadsheet/excel/writer.rb: -------------------------------------------------------------------------------- 1 | require 'spreadsheet/excel/writer/workbook' 2 | -------------------------------------------------------------------------------- /lib/spreadsheet/excel/writer/biff8.rb: -------------------------------------------------------------------------------- 1 | require 'spreadsheet/encodings' 2 | 3 | module Spreadsheet 4 | module Excel 5 | module Writer 6 | ## 7 | # This Module collects writer methods such as unicode_string that are specific 8 | # to Biff8. This Module is likely to be expanded as Support for older Versions 9 | # of Excel grows and methods get moved here for disambiguation. 10 | module Biff8 11 | include Spreadsheet::Encodings 12 | ## 13 | # Check whether the string _data_ can be compressed (i.e. every second byte 14 | # is a Null-byte) and perform compression. 15 | # Returns the data and compression_status (0/1) 16 | def compress_unicode_string data 17 | compressed = internal('') 18 | expect_null = false 19 | data.each_byte do |byte| 20 | if expect_null 21 | if byte != 0 22 | return [data, 1] # 1 => Data consists of wide Chars 23 | end 24 | expect_null = false 25 | else 26 | compressed << byte 27 | expect_null = true 28 | end 29 | end 30 | [compressed, 0] # 0 => Data consists of compressed Chars 31 | end 32 | ## 33 | # Encode _string_ into a Biff8 Unicode String. Header and body are encoded 34 | # separately by #_unicode_string. This method simply combines the two. 35 | def unicode_string string, count_length=1 36 | header, data, _ = _unicode_string string, count_length 37 | header << data 38 | end 39 | @@bytesize = RUBY_VERSION >= '1.9' ? :bytesize : :size 40 | ## 41 | # Encode _string_ into a Biff8 Unicode String Header and Body. 42 | def _unicode_string string, count_length=1 43 | data = internal string 44 | size = data.send(@@bytesize) / 2 45 | fmt = count_length == 1 ? 'C2' : 'vC' 46 | data, wide = compress_unicode_string data 47 | opts = wide 48 | header = [ 49 | size, # Length of the string (character count, ln) 50 | opts, # Option flags: 51 | # Bit Mask Contents 52 | # 0 0x01 Character compression (ccompr): 53 | # 0 = Compressed (8-bit characters) 54 | # 1 = Uncompressed (16-bit characters) 55 | # 2 0x04 Asian phonetic settings (phonetic): 56 | # 0 = Does not contain Asian phonetic settings 57 | # 1 = Contains Asian phonetic settings 58 | # 3 0x08 Rich-Text settings (richtext): 59 | # 0 = Does not contain Rich-Text settings 60 | # 1 = Contains Rich-Text settings 61 | #0x00,# (optional, only if richtext=1) Number of Rich-Text 62 | # formatting runs (rt) 63 | #0x00,# (optional, only if phonetic=1) Size of Asian phonetic 64 | # settings block (in bytes, sz) 65 | ].pack fmt 66 | data << '' # (optional, only if richtext=1) 67 | # List of rt formatting runs (➜ 3.2) 68 | data << '' # (optional, only if phonetic=1) 69 | # Asian Phonetic Settings Block (➜ 3.4.2) 70 | [header, data, wide] 71 | end 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/spreadsheet/excel/writer/format.rb: -------------------------------------------------------------------------------- 1 | require 'delegate' 2 | require 'spreadsheet/format' 3 | require 'spreadsheet/excel/internals' 4 | 5 | module Spreadsheet 6 | module Excel 7 | module Writer 8 | ## 9 | # This class encapsulates everything that is needed to write an XF record. 10 | class Format < DelegateClass Spreadsheet::Format 11 | include Spreadsheet::Excel::Internals 12 | def Format.boolean *args 13 | args.each do |key| 14 | define_method key do 15 | @format.send("#{key}?") ? 1 : 0 16 | end 17 | end 18 | end 19 | def Format.color key, default 20 | define_method key do 21 | color_code(@format.send(key) || default) 22 | end 23 | end 24 | boolean :hidden, :locked, :merge_range, :shrink, :text_justlast, :text_wrap, 25 | :cross_down, :cross_up, :left, :right, :top, :bottom 26 | color :left_color, :border 27 | color :right_color, :border 28 | color :top_color, :border 29 | color :bottom_color, :border 30 | color :diagonal_color, :border 31 | color :pattern_fg_color, :pattern_bg 32 | color :pattern_bg_color, :pattern_bg 33 | attr_accessor :xf_index 34 | attr_reader :format 35 | def initialize writer, workbook, format=workbook.default_format, opts={} 36 | @opts = { :type => :format }.merge opts 37 | @format = format 38 | @writer = writer 39 | @workbook = workbook 40 | super format 41 | end 42 | def color_code color 43 | SEDOC_ROLOC[color] 44 | end 45 | def font_index 46 | @writer.font_index @workbook, font.key 47 | end 48 | def horizontal_align 49 | XF_H_ALIGN.fetch @format.horizontal_align, 0 50 | end 51 | def num_format 52 | @writer.number_format_index @workbook, @format.number_format 53 | end 54 | def text_direction 55 | XF_TEXT_DIRECTION.fetch @format.text_direction, 0 56 | end 57 | def vertical_align 58 | XF_V_ALIGN.fetch @format.vertical_align, 2 59 | end 60 | def write_op writer, op, *args 61 | data = args.join 62 | writer.write [op,data.size].pack("v2") 63 | writer.write data 64 | end 65 | def write_xf writer, type=@opts[:type] 66 | xf_type = xf_type_prot type 67 | data = [ 68 | font_index, # Index to FONT record (➜ 6.43) 69 | num_format, # Index to FORMAT record (➜ 6.45) 70 | xf_type, # Bit Mask Contents 71 | # 2-0 0x0007 XF_TYPE_PROT – XF type, cell protection 72 | # Bit Mask Contents 73 | # 0 0x01 1 = Cell is locked 74 | # 1 0x02 1 = Formula is hidden 75 | # 2 0x04 0 = Cell XF; 1 = Style XF 76 | # 15-4 0xfff0 Index to parent style XF 77 | # (always 0xfff in style XFs) 78 | xf_align, # Bit Mask Contents 79 | # 2-0 0x07 XF_HOR_ALIGN – Horizontal alignment 80 | # Value Horizontal alignment 81 | # 0x00 General 82 | # 0x01 Left 83 | # 0x02 Centred 84 | # 0x03 Right 85 | # 0x04 Filled 86 | # 0x05 Justified (BIFF4-BIFF8X) 87 | # 0x06 Centred across selection 88 | # (BIFF4-BIFF8X) 89 | # 0x07 Distributed (BIFF8X) 90 | # 3 0x08 1 = Text is wrapped at right border 91 | # 6-4 0x70 XF_VERT_ALIGN – Vertical alignment 92 | # Value Vertical alignment 93 | # 0x00 Top 94 | # 0x01 Centred 95 | # 0x02 Bottom 96 | # 0x03 Justified (BIFF5-BIFF8X) 97 | # 0x04 Distributed (BIFF8X) 98 | xf_rotation, # XF_ROTATION: Text rotation angle 99 | # Value Text rotation 100 | # 0 Not rotated 101 | # 1-90 1 to 90 degrees counterclockwise 102 | # 91-180 1 to 90 degrees clockwise 103 | # 255 Letters are stacked top-to-bottom, 104 | # but not rotated 105 | xf_indent, # Bit Mask Contents 106 | # 3-0 0x0f Indent level 107 | # 4 0x10 1 = Shrink content to fit into cell 108 | # 5 0x40 1 = Merge Range (djberger) 109 | # 7-6 0xc0 Text direction (BIFF8X only) 110 | # 0 = According to context 111 | # 1 = Left-to-right 112 | # 2 = Right-to-left 113 | xf_used_attr, # Bit Mask Contents 114 | # 7-2 0xfc XF_USED_ATTRIB – Used attributes 115 | # Each bit describes the validity of a 116 | # specific group of attributes. In cell XFs 117 | # a cleared bit means the attributes of the 118 | # parent style XF are used (but only if the 119 | # attributes are valid there), a set bit 120 | # means the attributes of this XF are used. 121 | # In style XFs a cleared bit means the 122 | # attribute setting is valid, a set bit 123 | # means the attribute should be ignored. 124 | # Bit Mask Contents 125 | # 0 0x01 Flag for number format 126 | # 1 0x02 Flag for font 127 | # 2 0x04 Flag for horizontal and 128 | # vertical alignment, text wrap, 129 | # indentation, orientation, 130 | # rotation, and text direction 131 | # 3 0x08 Flag for border lines 132 | # 4 0x10 Flag for background area style 133 | # 5 0x20 Flag for cell protection (cell 134 | # locked and formula hidden) 135 | xf_borders, # Cell border lines and background area: 136 | # Bit Mask Contents 137 | # 3- 0 0x0000000f Left line style (➜ 3.10) 138 | # 7- 4 0x000000f0 Right line style (➜ 3.10) 139 | # 11- 8 0x00000f00 Top line style (➜ 3.10) 140 | # 15-12 0x0000f000 Bottom line style (➜ 3.10) 141 | # 22-16 0x007f0000 Colour index (➜ 6.70) 142 | # for left line colour 143 | # 29-23 0x3f800000 Colour index (➜ 6.70) 144 | # for right line colour 145 | # 30 0x40000000 1 = Diagonal line 146 | # from top left to right bottom 147 | # 31 0x80000000 1 = Diagonal line 148 | # from bottom left to right top 149 | xf_brdcolors, # Bit Mask Contents 150 | # 6- 0 0x0000007f Colour index (➜ 6.70) 151 | # for top line colour 152 | # 13- 7 0x00003f80 Colour index (➜ 6.70) 153 | # for bottom line colour 154 | # 20-14 0x001fc000 Colour index (➜ 6.70) 155 | # for diagonal line colour 156 | # 24-21 0x01e00000 Diagonal line style (➜ 3.10) 157 | # 31-26 0xfc000000 Fill pattern (➜ 3.11) 158 | xf_pattern # Bit Mask Contents 159 | # 6-0 0x007f Colour index (➜ 6.70) 160 | # for pattern colour 161 | # 13-7 0x3f80 Colour index (➜ 6.70) 162 | # for pattern background 163 | ] 164 | write_op writer, 0x00e0, data.pack(binfmt(:xf)) 165 | end 166 | def xf_align 167 | align = horizontal_align 168 | align |= text_wrap << 3 169 | align |= vertical_align << 4 170 | align |= text_justlast << 7 171 | align 172 | end 173 | def xf_borders 174 | border = left 175 | border |= right << 4 176 | border |= top << 8 177 | border |= bottom << 12 178 | border |= left_color << 16 179 | border |= right_color << 23 180 | border |= cross_down << 30 181 | border |= cross_up << 31 182 | border 183 | end 184 | def xf_brdcolors 185 | border = top_color 186 | border |= bottom_color << 7 187 | border |= diagonal_color << 14 188 | border |= pattern << 26 189 | border 190 | end 191 | def xf_indent 192 | indent = indent_level & 0x0f 193 | indent |= shrink << 4 194 | indent |= merge_range << 5 195 | indent |= text_direction << 6 196 | indent 197 | end 198 | def xf_pattern 199 | ptrn = pattern_fg_color 200 | ptrn |= pattern_bg_color << 7 201 | ptrn 202 | end 203 | def xf_rotation 204 | rot = @format.rotation 205 | if @format.rotation_stacked? 206 | rot = 255 207 | elsif rot >= -90 or rotation <= 90 208 | rot = -rot + 90 if rot < 0 209 | else 210 | warn "rotation outside -90..90; rotation set to 0" 211 | rot = 0 212 | end 213 | rot 214 | end 215 | def xf_type_prot type 216 | type = type.to_s.downcase == 'style' ? 0xfff5 : 0x0000 217 | type |= locked 218 | type |= hidden << 1 219 | type 220 | end 221 | def xf_used_attr 222 | atr_num = num_format & 1 223 | atr_fnt = font_index & 1 224 | atr_fnt = 1 unless @format.font.color == :text 225 | atr_alc = 0 226 | if horizontal_align != 0 \ 227 | || vertical_align != 2 \ 228 | || indent_level > 0 \ 229 | || shrink? || merge_range? || text_wrap? 230 | then 231 | atr_alc = 1 232 | end 233 | atr_bdr = [top, bottom, left, right, cross_up, cross_down].max 234 | atr_pat = 0 235 | if @format.pattern_fg_color != :border \ 236 | || @format.pattern_bg_color != :pattern_bg \ 237 | || pattern != 0x00 238 | then 239 | atr_pat = 1 240 | end 241 | atr_prot = hidden? || locked? ? 1 : 0 242 | attrs = atr_num 243 | attrs |= atr_fnt << 1 244 | attrs |= atr_alc << 2 245 | attrs |= atr_bdr << 3 246 | attrs |= atr_pat << 4 247 | attrs |= atr_prot << 5 248 | attrs << 2 249 | end 250 | end 251 | end 252 | end 253 | end 254 | -------------------------------------------------------------------------------- /lib/spreadsheet/excel/writer/workbook.rb: -------------------------------------------------------------------------------- 1 | require 'spreadsheet/excel/internals' 2 | require 'spreadsheet/writer' 3 | require 'spreadsheet/excel/writer/biff8' 4 | require 'spreadsheet/excel/writer/format' 5 | require 'spreadsheet/excel/writer/worksheet' 6 | require 'ole/storage' 7 | 8 | module Spreadsheet 9 | module Excel 10 | module Writer 11 | ## 12 | # Writer class for Excel Workbooks. Most write_* method correspond to an 13 | # Excel-Record/Opcode. Designed to be able to write several Workbooks in 14 | # parallel (just because I can't imagine why you would want to do that 15 | # doesn't mean it shouldn't be possible ;). You should not need to call any of 16 | # its methods directly. If you think you do, look at #write_workbook 17 | class Workbook < Spreadsheet::Writer 18 | include Spreadsheet::Excel::Writer::Biff8 19 | include Spreadsheet::Excel::Internals 20 | attr_reader :fonts, :date_base 21 | def initialize *args 22 | super 23 | @biff_version = 0x0600 24 | @bof = 0x0809 25 | @build_id = 3515 26 | @build_year = 1996 27 | @bof_types = { 28 | :globals => 0x0005, 29 | :visual_basic => 0x0006, 30 | :worksheet => 0x0010, 31 | :chart => 0x0020, 32 | :macro_sheet => 0x0040, 33 | :workspace => 0x0100, 34 | } 35 | @worksheets = {} 36 | @sst = {} 37 | @recordsize_limit = 8224 38 | @fonts = {} 39 | @formats = {} 40 | @number_formats = {} 41 | end 42 | def cleanup workbook 43 | worksheets(workbook).each do |worksheet| 44 | @sst.delete worksheet 45 | end 46 | @fonts.delete workbook 47 | @formats.delete workbook 48 | @number_formats.delete workbook 49 | @worksheets.delete workbook 50 | end 51 | def collect_formats workbook, opts={} 52 | # The default cell format is always present in an Excel file, described by 53 | # the XF record with the fixed index 15 (0-based). By default, it uses the 54 | # worksheet/workbook default cell style, described by the very first XF 55 | # record (index 0). 56 | formats = [] 57 | unless opts[:existing_document] 58 | 15.times do 59 | formats.push Format.new(self, workbook, workbook.default_format, 60 | :type => :style) 61 | end 62 | formats.push Format.new(self, workbook) 63 | end 64 | workbook.formats.each do |fmt| 65 | formats.push Format.new(self, workbook, fmt) 66 | end 67 | formats.each_with_index do |fmt, idx| 68 | fmt.xf_index = idx 69 | end 70 | @formats[workbook] = formats 71 | end 72 | def complete_sst_update? workbook 73 | stored = workbook.sst.collect do |entry| entry.content end 74 | current = worksheets(workbook).inject [] do |memo, worksheet| 75 | memo.concat worksheet.strings 76 | end 77 | total = current.size 78 | current.uniq! 79 | current.delete '' 80 | if (stored - current).empty? && !stored.empty? 81 | ## if all previously stored strings are still needed, we don't have to 82 | # rewrite all cells because the sst-index of such string does not change. 83 | additions = current - stored 84 | [:partial_update, total, stored + additions] 85 | else 86 | [:complete_update, total, current] 87 | end 88 | end 89 | def font_index workbook, font_key 90 | idx = @fonts[workbook][font_key] || 0 91 | ## this appears to be undocumented: the first 4 fonts seem to be accessed 92 | # with a 0-based index, but all subsequent font indices are 1-based. 93 | idx > 3 ? idx.next : idx 94 | end 95 | def number_format_index workbook, format 96 | @number_formats[workbook][format] || 0 97 | end 98 | def sanitize_worksheets sheets 99 | return sheets if sheets.empty? 100 | found_selected = false 101 | sheets.each do |sheet| 102 | found_selected ||= sheet.selected 103 | sheet.format_dates! 104 | end 105 | unless found_selected 106 | sheets.first.selected = true 107 | end 108 | sheets 109 | end 110 | def worksheets workbook 111 | @worksheets[workbook] ||= workbook.worksheets.collect do |worksheet| 112 | Excel::Writer::Worksheet.new self, worksheet 113 | end 114 | end 115 | def write_bof workbook, writer, type 116 | data = [ 117 | @biff_version, # BIFF version (always 0x0600 for BIFF8) 118 | @bof_types[type], # Type of the following data: 119 | # 0x0005 = Workbook globals 120 | # 0x0006 = Visual Basic module 121 | # 0x0010 = Worksheet 122 | # 0x0020 = Chart 123 | # 0x0040 = Macro sheet 124 | # 0x0100 = Workspace file 125 | @build_id, # Build identifier 126 | @build_year, # Build year 127 | 0x000, # File history flags 128 | 0x006, # Lowest Excel version that can read 129 | # all records in this file 130 | ] 131 | write_op writer, @bof, data.pack("v4V2") 132 | end 133 | def write_bookbool workbook, writer 134 | write_placeholder writer, 0x00da 135 | end 136 | def write_boundsheets workbook, writer, offset 137 | worksheets = worksheets(workbook) 138 | worksheets.each do |worksheet| 139 | # account for boundsheet-entry 140 | offset += worksheet.boundsheet_size 141 | end 142 | worksheets.each do |worksheet| 143 | data = [ 144 | offset, # Absolute stream position of the BOF record of the sheet 145 | # represented by this record. This field is never encrypted 146 | # in protected files. 147 | 0x00, # Visibility: 0x00 = Visible 148 | # 0x01 = Hidden 149 | # 0x02 = Strong hidden (see below) 150 | 0x00, # Sheet type: 0x00 = Worksheet 151 | # 0x02 = Chart 152 | # 0x06 = Visual Basic module 153 | ] 154 | write_op writer, 0x0085, data.pack("VC2"), worksheet.name 155 | offset += worksheet.size 156 | end 157 | end 158 | ## 159 | # Copy unchanged data verbatim, adjust offsets and write new records for 160 | # changed data. 161 | def write_changes workbook, io 162 | sanitize_worksheets workbook.worksheets 163 | collect_formats workbook, :existing_document => true 164 | reader = workbook.ole 165 | sheet_data = {} 166 | sst_status, sst_total, sst_strings = complete_sst_update? workbook 167 | sst = {} 168 | sst_strings.each_with_index do |str, idx| sst.store str, idx end 169 | sheets = worksheets(workbook) 170 | positions = [] 171 | newsheets = [] 172 | sheets.each do |sheet| 173 | @sst[sheet] = sst 174 | pos, len = workbook.offsets[sheet.worksheet] 175 | if pos 176 | positions.push pos 177 | sheet.write_changes reader, pos + len, sst_status 178 | else 179 | newsheets.push sheet 180 | sheet.write_from_scratch 181 | end 182 | sheet_data[sheet.worksheet] = sheet.data 183 | end 184 | Ole::Storage.open io do |ole| 185 | ole.file.open 'Workbook', 'w' do |writer| 186 | reader.seek lastpos = 0 187 | workbook.offsets.select do |key, pair| 188 | workbook.changes.include? key 189 | end.sort_by do |key, (pos, len)| 190 | pos 191 | end.each do |key, (pos, len)| 192 | data = reader.read(pos - lastpos) 193 | writer.write data 194 | case key 195 | when Spreadsheet::Worksheet 196 | writer.write sheet_data[key] 197 | when :boundsheets 198 | ## boundsheets are hard to calculate. The offset below is only 199 | # correct if there are no more changes in the workbook globals 200 | # string after this. 201 | oldoffset = positions.min - len 202 | lastpos = pos + len 203 | bytechange = 0 204 | buffer = StringIO.new '' 205 | if tuple = workbook.offsets[:sst] 206 | write_sst_changes workbook, buffer, writer.pos, 207 | sst_total, sst_strings 208 | pos, len = tuple 209 | if offset = workbook.offsets[:extsst] 210 | len += offset[1].to_i 211 | end 212 | bytechange = buffer.size - len 213 | write_boundsheets workbook, writer, oldoffset + bytechange 214 | reader.seek lastpos 215 | writer.write reader.read(pos - lastpos) 216 | buffer.rewind 217 | writer.write buffer.read 218 | elsif sst.empty? || workbook.biff_version < 8 219 | write_boundsheets workbook, writer, oldoffset + bytechange 220 | else 221 | write_sst workbook, buffer, writer.pos 222 | write_boundsheets workbook, writer, oldoffset + buffer.size 223 | pos = lastpos 224 | len = positions.min - lastpos 225 | if len > OPCODE_SIZE 226 | reader.seek pos 227 | writer.write reader.read(len - OPCODE_SIZE) 228 | end 229 | buffer.rewind 230 | writer.write buffer.read 231 | write_eof workbook, writer 232 | end 233 | else 234 | send "write_#{key}", workbook, writer 235 | end 236 | lastpos = [pos + len, reader.size - 1].min 237 | reader.seek lastpos 238 | end 239 | writer.write reader.read 240 | newsheets.each do |sheet| 241 | writer.write sheet.data 242 | end 243 | end 244 | end 245 | end 246 | def write_datemode workbook, writer 247 | mode = @date_base.year == 1899 ? 0x00 : 0x01 248 | data = [ 249 | mode, # 0 = Base date is 1899-Dec-31 250 | # (the cell value 1 represents 1900-Jan-01) 251 | # 1 = Base date is 1904-Jan-01 252 | # (the cell value 1 represents 1904-Jan-02) 253 | ] 254 | write_op writer, 0x0022, data.pack('v') 255 | end 256 | def write_dsf workbook, writer 257 | data = [ 258 | 0x00, # 0 = Only the BIFF8 “Workbook” stream is present 259 | # 1 = Additional BIFF5/BIFF7 “Book” stream is in the file 260 | ] 261 | write_op writer, 0x0161, data.pack('v') 262 | end 263 | def write_encoding workbook, writer 264 | enc = workbook.encoding || 'UTF-16LE' 265 | if RUBY_VERSION >= '1.9' && enc.is_a?(Encoding) 266 | enc = enc.name.upcase 267 | end 268 | cp = SEGAPEDOC[enc] or raise "Invalid or Unknown Codepage '#{enc}'" 269 | write_op writer, 0x0042, [cp].pack('v') 270 | end 271 | def write_eof workbook, writer 272 | write_op writer, 0x000a 273 | end 274 | def write_extsst workbook, offsets, writer 275 | header = [SST_CHUNKSIZE].pack('v') 276 | data = offsets.collect do |pair| pair.push(0).pack('Vv2') end 277 | write_op writer, 0x00ff, header, data 278 | end 279 | def write_font workbook, writer, font 280 | # TODO: Colors/Palette index 281 | size = font.size * TWIPS 282 | color = SEDOC_ROLOC[font.color] || SEDOC_ROLOC[:text] 283 | weight = FONT_WEIGHTS.fetch(font.weight, font.weight) 284 | weight = [[weight, 1000].min, 100].max 285 | esc = SEPYT_TNEMEPACSE.fetch(font.escapement, 0) 286 | underline = SEPYT_ENILREDNU.fetch(font.underline, 0) 287 | family = SEILIMAF_TNOF.fetch(font.family, 0) 288 | encoding = SGNIDOCNE_TNOF.fetch(font.encoding, 0) 289 | options = 0 290 | options |= 0x0001 if weight > 600 291 | options |= 0x0002 if font.italic? 292 | options |= 0x0004 if underline > 0 293 | options |= 0x0008 if font.strikeout? 294 | options |= 0x0010 if font.outline? 295 | options |= 0x0020 if font.shadow? 296 | data = [ 297 | size, # Height of the font (in twips = 1/20 of a point) 298 | options, # Option flags: 299 | # Bit Mask Contents 300 | # 0 0x0001 1 = Characters are bold (redundant, see below) 301 | # 1 0x0002 1 = Characters are italic 302 | # 2 0x0004 1 = Characters are underlined (redundant) 303 | # 3 0x0008 1 = Characters are struck out 304 | # 4 0x0010 1 = Characters are outlined (djberger) 305 | # 5 0x0020 1 = Characters are shadowed (djberger) 306 | color, # Palette index (➜ 6.70) 307 | weight, # Font weight (100-1000). Standard values are 308 | # 0x0190 (400) for normal text and 309 | # 0x02bc (700) for bold text. 310 | esc, # Escapement type: 0x0000 = None 311 | # 0x0001 = Superscript 312 | # 0x0002 = Subscript 313 | underline,# Underline type: 0x00 = None 314 | # 0x01 = Single 315 | # 0x02 = Double 316 | # 0x21 = Single accounting 317 | # 0x22 = Double accounting 318 | family, # Font family: 0x00 = None (unknown or don't care) 319 | # 0x01 = Roman (variable width, serifed) 320 | # 0x02 = Swiss (variable width, sans-serifed) 321 | # 0x03 = Modern (fixed width, 322 | # serifed or sans-serifed) 323 | # 0x04 = Script (cursive) 324 | # 0x05 = Decorative (specialised, 325 | # e.g. Old English, Fraktur) 326 | encoding, # Character set: 0x00 = 0 = ANSI Latin 327 | # 0x01 = 1 = System default 328 | # 0x02 = 2 = Symbol 329 | # 0x4d = 77 = Apple Roman 330 | # 0x80 = 128 = ANSI Japanese Shift-JIS 331 | # 0x81 = 129 = ANSI Korean (Hangul) 332 | # 0x82 = 130 = ANSI Korean (Johab) 333 | # 0x86 = 134 = ANSI Chinese Simplified GBK 334 | # 0x88 = 136 = ANSI Chinese Traditional BIG5 335 | # 0xa1 = 161 = ANSI Greek 336 | # 0xa2 = 162 = ANSI Turkish 337 | # 0xa3 = 163 = ANSI Vietnamese 338 | # 0xb1 = 177 = ANSI Hebrew 339 | # 0xb2 = 178 = ANSI Arabic 340 | # 0xba = 186 = ANSI Baltic 341 | # 0xcc = 204 = ANSI Cyrillic 342 | # 0xde = 222 = ANSI Thai 343 | # 0xee = 238 = ANSI Latin II (Central European) 344 | # 0xff = 255 = OEM Latin I 345 | ] 346 | name = unicode_string font.name # Font name: Unicode string, 347 | # 8-bit string length (➜ 3.4) 348 | write_op writer, opcode(:font), data.pack(binfmt(:font)), name 349 | end 350 | def write_fonts workbook, writer 351 | fonts = @fonts[workbook] = {} 352 | @formats[workbook].each do |format| 353 | if(font = format.font) && !fonts.include?(font.key) 354 | fonts.store font.key, fonts.size 355 | write_font workbook, writer, font 356 | end 357 | end 358 | end 359 | def write_formats workbook, writer 360 | # From BIFF5 on, the built-in number formats will be omitted. The built-in 361 | # formats are dependent on the current regional settings of the operating 362 | # system. BUILTIN_FORMATS shows which number formats are used by 363 | # default in a US-English environment. All indexes from 0 to 163 are 364 | # reserved for built-in formats. 365 | # The first user-defined format starts at 164 (0xa4). 366 | formats = @number_formats[workbook] = {} 367 | BUILTIN_FORMATS.each do |idx, str| 368 | formats.store client(str, 'UTF-8'), idx 369 | end 370 | ## Ensure at least a 'GENERAL' format is written 371 | formats.delete client('GENERAL', 'UTF-8') 372 | idx = 0xa4 373 | workbook.formats.each do |fmt| 374 | str = fmt.number_format 375 | unless formats[str] 376 | formats.store str, idx 377 | # Number format string (Unicode string, 16-bit string length, ➜ 3.4) 378 | write_op writer, opcode(:format), [idx].pack('v'), unicode_string(str, 2) 379 | idx += 1 380 | end 381 | end 382 | end 383 | ## 384 | # Write a new Excel file. 385 | def write_from_scratch workbook, io 386 | sanitize_worksheets workbook.worksheets 387 | collect_formats workbook 388 | sheets = worksheets workbook 389 | buffer1 = StringIO.new '' 390 | # ● BOF Type = workbook globals (➜ 6.8) 391 | write_bof workbook, buffer1, :globals 392 | # ○ File Protection Block ➜ 4.19 393 | # ○ WRITEACCESS User name (BIFF3-BIFF8, ➜ 5.112) 394 | # ○ FILESHARING File sharing options (BIFF3-BIFF8, ➜ 5.44) 395 | # ○ CODEPAGE ➜ 6.17 396 | write_encoding workbook, buffer1 397 | # ○ DSF ➜ 6.32 398 | write_dsf workbook, buffer1 399 | # ○ TABID 400 | write_tabid workbook, buffer1 401 | # ○ FNGROUPCOUNT 402 | # ○ Workbook Protection Block ➜ 4.18 403 | # ○ WINDOWPROTECT Window settings: 1 = protected (➜ 5.111) 404 | # ○ PROTECT Cell contents: 1 = protected (➜ 5.82) 405 | write_protect workbook, buffer1 406 | # ○ OBJECTPROTECT Embedded objects: 1 = protected (➜ 5.72) 407 | # ○ PASSWORD Hash value of the password; 0 = No password (➜ 5.76) 408 | write_password workbook, buffer1 409 | # ○ BACKUP ➜ 5.5 410 | # ○ HIDEOBJ ➜ 5.56 411 | # ● WINDOW1 ➜ 5.109 412 | write_window1 workbook, buffer1 413 | # ○ DATEMODE ➜ 5.28 414 | write_datemode workbook, buffer1 415 | # ○ PRECISION ➜ 5.79 416 | write_precision workbook, buffer1 417 | # ○ REFRESHALL 418 | write_refreshall workbook, buffer1 419 | # ○ BOOKBOOL ➜ 5.9 420 | write_bookbool workbook, buffer1 421 | # ●● FONT ➜ 5.45 422 | write_fonts workbook, buffer1 423 | # ○○ FORMAT ➜ 5.49 424 | write_formats workbook, buffer1 425 | # ●● XF ➜ 5.115 426 | write_xfs workbook, buffer1 427 | # ●● STYLE ➜ 5.103 428 | write_styles workbook, buffer1 429 | # ○ PALETTE ➜ 5.74 430 | # ○ USESELFS ➜ 5.106 431 | buffer1.rewind 432 | # ●● BOUNDSHEET ➜ 5.95 433 | buffer2 = StringIO.new '' 434 | # ○ COUNTRY ➜ 5.22 435 | # ○ Link Table ➜ 4.10.3 436 | # ○○ NAME ➜ 6.66 437 | # ○ Shared String Table ➜ 4.11 438 | # ● SST ➜ 5.100 439 | # ● EXTSST ➜ 5.42 440 | write_sst workbook, buffer2, buffer1.size 441 | # ● EOF ➜ 5.37 442 | write_eof workbook, buffer2 443 | buffer2.rewind 444 | # worksheet data can only be assembled after write_sst 445 | sheets.each do |worksheet| worksheet.write_from_scratch end 446 | Ole::Storage.open io do |ole| 447 | ole.file.open 'Workbook', 'w' do |writer| 448 | writer.write buffer1.read 449 | write_boundsheets workbook, writer, buffer1.size + buffer2.size 450 | writer.write buffer2.read 451 | sheets.each do |worksheet| 452 | writer.write worksheet.data 453 | end 454 | end 455 | end 456 | end 457 | def write_op writer, op, *args 458 | data = args.join 459 | limited = data.slice!(0...@recordsize_limit) 460 | writer.write [op,limited.size].pack("v2") 461 | writer.write limited 462 | data 463 | end 464 | def write_password workbook, writer 465 | write_placeholder writer, 0x0013 466 | end 467 | def write_placeholder writer, op, value=0x0000, fmt='v' 468 | write_op writer, op, [value].pack(fmt) 469 | end 470 | def write_precision workbook, writer 471 | # 0 = Use displayed values; 1 = Use real cell values 472 | write_placeholder writer, 0x000e, 0x0001 473 | end 474 | def write_protect workbook, writer 475 | write_placeholder writer, 0x0012 476 | end 477 | def write_refreshall workbook, writer 478 | write_placeholder writer, 0x01b7 479 | end 480 | def write_sst workbook, writer, offset 481 | # Offset Size Contents 482 | # 0 4 Total number of strings in the workbook (see below) 483 | # 4 4 Number of following strings (nm) 484 | # 8 var. List of nm Unicode strings, 16-bit string length (➜ 3.4) 485 | strings = worksheets(workbook).inject [] do |memo, worksheet| 486 | memo.concat worksheet.strings 487 | end 488 | total = strings.size 489 | strings.uniq! 490 | _write_sst workbook, writer, offset, total, strings 491 | end 492 | def _write_sst workbook, writer, offset, total, strings 493 | sst = {} 494 | worksheets(workbook).each do |worksheet| 495 | offset += worksheet.boundsheet_size 496 | @sst[worksheet] = sst 497 | end 498 | sst_size = strings.size 499 | data = [total, sst_size].pack 'V2' 500 | op = 0x00fc 501 | wide = 0 502 | offsets = [] 503 | strings.each_with_index do |string, idx| 504 | sst.store string, idx 505 | op_offset = data.size + 4 506 | if idx % SST_CHUNKSIZE == 0 507 | offsets.push [offset + writer.pos + op_offset, op_offset] 508 | end 509 | header, packed, next_wide = _unicode_string string, 2 510 | # the first few bytes (header + first character) must not be split 511 | must_fit = header.size + wide + 1 512 | while data.size + must_fit > @recordsize_limit 513 | op, data, wide = write_string_part writer, op, data, wide 514 | end 515 | wide = next_wide 516 | data << header << packed 517 | end 518 | until data.empty? 519 | op, data, wide = write_string_part writer, op, data, wide 520 | end 521 | write_extsst workbook, offsets, writer 522 | end 523 | def write_sst_changes workbook, writer, offset, total, strings 524 | _write_sst workbook, writer, offset, total, strings 525 | end 526 | def write_string_part writer, op, data, wide 527 | bef = data.size 528 | ## if we're writing wide characters, we need to make sure we don't cut 529 | # characters in half 530 | if wide > 0 && data.size > @recordsize_limit 531 | remove = @recordsize_limit - data.size 532 | remove -= remove % 2 533 | rest = data.slice!(remove..-1) 534 | write_op writer, op, data 535 | data = rest 536 | else 537 | data = write_op writer, op, data 538 | end 539 | op = 0x003c 540 | # Unicode strings are split in a special way. At the beginning of each 541 | # CONTINUE record the option flags byte is repeated. Only the 542 | # character size flag will be set in this flags byte, the Rich-Text 543 | # flag and the Far-East flag are set to zero. 544 | unless data.empty? 545 | if wide == 1 546 | # check if we can compress the rest of the string 547 | data, wide = compress_unicode_string data 548 | end 549 | data = [wide].pack('C') << data 550 | end 551 | [op, data, wide] 552 | end 553 | def write_styles workbook, writer 554 | # TODO: Style implementation. The following is simply a standard builtin 555 | # style. 556 | # TODO: User defined styles 557 | data = [ 558 | 0x8000, # Bit Mask Contents 559 | # 11- 0 0x0fff Index to style XF record (➜ 6.115) 560 | # 15 0x8000 Always 1 for built-in styles 561 | 0x00, # Identifier of the built-in cell style: 562 | # 0x00 = Normal 563 | # 0x01 = RowLevel_lv (see next field) 564 | # 0x02 = ColLevel_lv (see next field) 565 | # 0x03 = Comma 566 | # 0x04 = Currency 567 | # 0x05 = Percent 568 | # 0x06 = Comma [0] (BIFF4-BIFF8) 569 | # 0x07 = Currency [0] (BIFF4-BIFF8) 570 | # 0x08 = Hyperlink (BIFF8) 571 | # 0x09 = Followed Hyperlink (BIFF8) 572 | 0xff, # Level for RowLevel or ColLevel style (zero-based, lv), 573 | # 0xff otherwise 574 | # The RowLevel and ColLevel styles specify the formatting of 575 | # subtotal cells in a specific outline level. The level is 576 | # specified by the last field in the STYLE record. Valid values 577 | # are 0…6 for the outline levels 1…7. 578 | ] 579 | write_op writer, 0x0293, data.pack('vC2') 580 | end 581 | def write_tabid workbook, writer 582 | write_op writer, 0x013d, [1].pack('v') 583 | end 584 | def write_window1 workbook, writer 585 | selected = workbook.worksheets.find do |sheet| sheet.selected end 586 | actidx = workbook.worksheets.index selected 587 | data = [ 588 | 0x0000, # Horizontal position of the document window 589 | # (in twips = 1/20 of a point) 590 | 0x0000, # Vertical position of the document window 591 | # (in twips = 1/20 of a point) 592 | 0x4000, # Width of the document window (in twips = 1/20 of a point) 593 | 0x2000, # Height of the document window (in twips = 1/20 of a point) 594 | 0x0038, # Option flags: 595 | # Bit Mask Contents 596 | # 0 0x0001 0 = Window is visible 597 | # 1 = Window is hidden 598 | # 1 0x0002 0 = Window is open 599 | # 1 = Window is minimised 600 | # 3 0x0008 0 = Horizontal scroll bar hidden 601 | # 1 = Horizontal scroll bar visible 602 | # 4 0x0010 0 = Vertical scroll bar hidden 603 | # 1 = Vertical scroll bar visible 604 | # 5 0x0020 0 = Worksheet tab bar hidden 605 | # 1 = Worksheet tab bar visible 606 | actidx, # Index to active (displayed) worksheet 607 | 0x0000, # Index of first visible tab in the worksheet tab bar 608 | 0x0001, # Number of selected worksheets 609 | # (highlighted in the worksheet tab bar) 610 | 0x00e5, # Width of worksheet tab bar (in 1/1000 of window width). 611 | # The remaining space is used by the horizontal scrollbar. 612 | ] 613 | write_op writer, 0x003d, data.pack('v*') 614 | end 615 | ## 616 | # The main writer method. Calls #write_from_scratch or #write_changes 617 | # depending on the class and state of _workbook_. 618 | def write_workbook workbook, io 619 | unless workbook.is_a?(Excel::Workbook) && workbook.io 620 | @date_base = Date.new 1899, 12, 31 621 | write_from_scratch workbook, io 622 | else 623 | @date_base = workbook.date_base 624 | if workbook.changes.empty? 625 | super 626 | else 627 | write_changes workbook, io 628 | end 629 | end 630 | ensure 631 | cleanup workbook 632 | end 633 | def write_xfs workbook, writer 634 | # The default cell format is always present in an Excel file, described by 635 | # the XF record with the fixed index 15 (0-based). By default, it uses the 636 | # worksheet/workbook default cell style, described by the very first XF 637 | # record (index 0). 638 | @formats[workbook].each do |fmt| fmt.write_xf writer end 639 | end 640 | def sst_index worksheet, str 641 | @sst[worksheet][str] 642 | end 643 | def xf_index workbook, format 644 | if fmt = @formats[workbook].find do |fm| fm.format == format end 645 | fmt.xf_index 646 | else 647 | 0 648 | end 649 | end 650 | end 651 | end 652 | end 653 | end 654 | -------------------------------------------------------------------------------- /lib/spreadsheet/font.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spreadsheet/datatypes' 3 | require 'spreadsheet/encodings' 4 | 5 | module Spreadsheet 6 | ## 7 | # Font formatting data 8 | class Font 9 | include Spreadsheet::Datatypes 10 | include Spreadsheet::Encodings 11 | attr_accessor :name 12 | ## 13 | # You can set the following boolean Font attributes 14 | # * #italic 15 | # * #strikeout 16 | # * #outline 17 | # * #shadow 18 | boolean :italic, :strikeout, :outline, :shadow 19 | ## 20 | # Font color 21 | colors :color 22 | ## 23 | # Font weight 24 | # Valid values: :normal, :bold or any positive Integer. 25 | # In Excel: 26 | # 100 <= weight <= 1000 27 | # :bold => 700 28 | # :normal => 400 29 | # Default: :normal 30 | enum :weight, :normal, :bold, Integer, :bold => :b 31 | ## 32 | # Escapement 33 | # Valid values: :normal, :superscript or :subscript. 34 | # Default: :normal 35 | enum :escapement, :normal, :superscript, :subscript, 36 | :subscript => :sub, 37 | :superscript => :super 38 | # Font size 39 | # Valid values: Any positive Integer. 40 | # Default: 10 41 | enum :size, 10, Numeric 42 | # Underline type 43 | # Valid values: :none, :single, :double, :single_accounting and 44 | # :double_accounting. 45 | # Default: :none 46 | enum :underline, :none, :single, :double, 47 | :single_accounting, :double_accounting, 48 | :single => true 49 | # Font Family 50 | # Valid values: :none, :roman, :swiss, :modern, :script, :decorative 51 | # Default: :none 52 | enum :family, :none, :roman, :swiss, :modern, :script, :decorative 53 | # Font Family 54 | # Valid values: :default, :iso_latin1, :symbol, :apple_roman, :shift_jis, 55 | # :korean_hangul, :korean_johab, :chinese_simplified, 56 | # :chinese_traditional, :greek, :turkish, :vietnamese, 57 | # :hebrew, :arabic, :cyrillic, :thai, :iso_latin2, :oem_latin1 58 | # Default: :default 59 | enum :encoding, :default, :iso_latin1, :symbol, :apple_roman, :shift_jis, 60 | :korean_hangul, :korean_johab, :chinese_simplified, 61 | :chinese_traditional, :greek, :turkish, :vietnamese, 62 | :hebrew, :arabic, :baltic, :cyrillic, :thai, :iso_latin2, 63 | :oem_latin1 64 | def initialize name, opts={} 65 | self.name = name 66 | @color = :text 67 | opts.each do |key, val| 68 | self.send "#{key}=", val 69 | end 70 | end 71 | ## 72 | # Sets #weight to :bold if(_bool_), :normal otherwise. 73 | def bold= bool 74 | self.weight = bool ? :bold : nil 75 | end 76 | def key # :nodoc: 77 | key = @name.dup 78 | underscore = client('_', 'UTF-8') 79 | key << underscore << client(size.to_s, 'US-ASCII') 80 | key << client('_', 'UTF-8') << client(weight.to_s, 'US-ASCII') 81 | key << client('_italic', 'UTF-8') if italic? 82 | key << client('_strikeout', 'UTF-8') if strikeout? 83 | key << client('_outline', 'UTF-8') if outline? 84 | key << client('_shadow', 'UTF-8') if shadow? 85 | key << underscore << client(escapement.to_s, 'US-ASCII') 86 | key << underscore << client(underline.to_s, 'US-ASCII') 87 | key << underscore << client(color.to_s, 'US-ASCII') 88 | key << underscore << client(family.to_s, 'US-ASCII') 89 | key << underscore << client(encoding.to_s, 'US-ASCII') 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/spreadsheet/format.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spreadsheet/datatypes' 3 | require 'spreadsheet/encodings' 4 | require 'spreadsheet/font' 5 | 6 | module Spreadsheet 7 | ## 8 | # Formatting data 9 | class Format 10 | include Spreadsheet::Datatypes 11 | include Spreadsheet::Encodings 12 | ## 13 | # You can set the following boolean attributes: 14 | # #cross_down:: Draws a Line from the top-left to the bottom-right 15 | # corner of a cell. 16 | # #cross_up:: Draws a Line from the bottom-left to the top-right 17 | # corner of a cell. 18 | # #hidden:: The cell is hidden. 19 | # #locked:: The cell is locked. 20 | # #merge_range:: The cell is in a merged range. 21 | # #shrink:: Shrink the contents to fit the cell. 22 | # #text_justlast:: Force the last line of a cell to be justified. This 23 | # probably makes sense if horizontal_align = :justify 24 | # #left:: Draw a border to the left of the cell. 25 | # #right:: Draw a border to the right of the cell. 26 | # #top:: Draw a border at the top of the cell. 27 | # #bottom:: Draw a border at the bottom of the cell. 28 | # #rotation_stacked:: Characters in the cell are stacked on top of each 29 | # other. Excel will ignore other rotation values if 30 | # this is set. 31 | boolean :cross_down, :cross_up, :hidden, :locked, 32 | :merge_range, :shrink, :text_justlast, :text_wrap, :left, :right, 33 | :top, :bottom, :rotation_stacked 34 | ## 35 | # Color attributes 36 | colors :bottom_color, :top_color, :left_color, :right_color, 37 | :pattern_fg_color, :pattern_bg_color, 38 | :diagonal_color 39 | ## 40 | # Text direction 41 | # Valid values: :context, :left_to_right, :right_to_left 42 | # Default: :context 43 | enum :text_direction, :context, :left_to_right, :right_to_left, 44 | :left_to_right => [:ltr, :l2r], 45 | :right_to_left => [:rtl, :r2l] 46 | alias :reading_order :text_direction 47 | alias :reading_order= :text_direction= 48 | ## 49 | # Indentation level 50 | enum :indent_level, 0, Integer 51 | alias :indent :indent_level 52 | alias :indent= :indent_level= 53 | ## 54 | # Horizontal alignment 55 | # Valid values: :default, :left, :center, :right, :fill, :justify, :merge, 56 | # :distributed 57 | # Default: :default 58 | enum :horizontal_align, :default, :left, :center, :right, :fill, :justify, 59 | :merge, :distributed, 60 | :center => :centre, 61 | :merge => [ :center_across, :centre_across ], 62 | :distributed => :equal_space 63 | ## 64 | # Vertical alignment 65 | # Valid values: :bottom, :top, :middle, :justify, :distributed 66 | # Default: :bottom 67 | enum :vertical_align, :bottom, :top, :middle, :justify, :distributed, 68 | :distributed => [:vdistributed, :vequal_space, :equal_space], 69 | :justify => :vjustify, 70 | :middle => [:vcenter, :vcentre, :center, :centre] 71 | attr_accessor :font, :number_format, :name, :pattern, :used_merge 72 | ## 73 | # Text rotation 74 | attr_reader :rotation 75 | def initialize opts={} 76 | @font = Font.new client("Arial", 'UTF-8'), :family => :swiss 77 | @number_format = client 'GENERAL', 'UTF-8' 78 | @rotation = 0 79 | @pattern = 0 80 | @bottom_color = :builtin_black 81 | @top_color = :builtin_black 82 | @left_color = :builtin_black 83 | @right_color = :builtin_black 84 | @diagonal_color = :builtin_black 85 | @pattern_fg_color = :border 86 | @pattern_bg_color = :pattern_bg 87 | # Temp code to prevent merged formats in non-merged cells. 88 | @used_merge = 0 89 | opts.each do |key, val| 90 | writer = "#{key}=" 91 | if @font.respond_to? writer 92 | @font.send writer, val 93 | else 94 | self.send writer, val 95 | end 96 | end 97 | yield self if block_given? 98 | end 99 | ## 100 | # Combined method for both horizontal and vertical alignment. Sets the 101 | # first valid value (e.g. Format#align = :justify only sets the horizontal 102 | # alignment. Use one of the aliases prefixed with :v if you need to 103 | # disambiguate.) 104 | # 105 | # This is essentially a backward-compatibility method and may be removed at 106 | # some point in the future. 107 | def align= location 108 | self.horizontal_align = location 109 | rescue ArgumentError 110 | self.vertical_align = location rescue ArgumentError 111 | end 112 | ## 113 | # Returns an Array containing the status of the four borders: 114 | # bottom, top, right, left 115 | def border 116 | [bottom,top,right,left] 117 | end 118 | ## 119 | # Activate or deactivate all four borders (left, right, top, bottom) 120 | def border=(boolean) 121 | [:bottom=, :top=, :right=, :left=].each do |writer| send writer, boolean end 122 | end 123 | ## 124 | # Returns an Array containing the colors of the four borders: 125 | # bottom, top, right, left 126 | def border_color 127 | [@bottom_color,@top_color,@left_color,@right_color] 128 | end 129 | ## 130 | # Set all four border colors to _color_ (left, right, top, bottom) 131 | def border_color=(color) 132 | [:bottom_color=, :top_color=, :right_color=, :left_color=].each do |writer| 133 | send writer, color end 134 | end 135 | ## 136 | # Set the Text rotation 137 | # Valid values: Integers from -90 to 90, 138 | # or :stacked (sets #rotation_stacked to true) 139 | def rotation=(rot) 140 | if rot.to_s.downcase == 'stacked' 141 | @rotation_stacked = true 142 | @rotation = 0 143 | elsif rot.kind_of?(Integer) 144 | @rotation_stacked = false 145 | @rotation = rot % 360 146 | else 147 | raise TypeError, "rotation value must be an Integer or the String 'stacked'" 148 | end 149 | end 150 | ## 151 | # Backward compatibility method. May disappear at some point in the future. 152 | def center_across! 153 | self.horizontal_align = :merge 154 | end 155 | alias :merge! :center_across! 156 | ## 157 | # Is the cell formatted as a Date? 158 | def date? 159 | !!Regexp.new(client("[YMD]", 'UTF-8')).match(@number_format.to_s) 160 | end 161 | ## 162 | # Is the cell formatted as a Date or Time? 163 | def date_or_time? 164 | !!Regexp.new(client("[hmsYMD]", 'UTF-8')).match(@number_format.to_s) 165 | end 166 | ## 167 | # Is the cell formatted as a DateTime? 168 | def datetime? 169 | !!Regexp.new(client("([YMD].*[HS])|([HS].*[YMD])", 'UTF-8')).match(@number_format.to_s) 170 | end 171 | ## 172 | # Is the cell formatted as a Time? 173 | def time? 174 | !!Regexp.new(client("[hms]", 'UTF-8')).match(@number_format.to_s) 175 | end 176 | end 177 | end 178 | -------------------------------------------------------------------------------- /lib/spreadsheet/formula.rb: -------------------------------------------------------------------------------- 1 | module Spreadsheet 2 | ## 3 | # Formula implementation. At the moment this is just a placeholder. 4 | # You may access the last calculated #value, other attributes are needed for 5 | # writing the Formula back into modified Excel Files. 6 | class Formula 7 | attr_accessor :data, :value, :shared 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/spreadsheet/helpers.rb: -------------------------------------------------------------------------------- 1 | class Array 2 | def rcompact 3 | dup.rcompact! 4 | end 5 | def rcompact! 6 | while !empty? && last.nil? 7 | pop 8 | end 9 | self 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/spreadsheet/link.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'spreadsheet/encodings' 3 | 4 | module Spreadsheet 5 | ## 6 | # The Link class. Is a Subclass of String, which lets you treat a Cell that 7 | # contains a Link just as if it was a String (containing the link's description 8 | # if there is one or the url with fragment otherwise), but gives you access 9 | # to the url, fragment and target_frame if you need it. 10 | # 11 | # 12 | # Interesting Attributes 13 | # #url :: The Uniform Resource Location this Link points to. 14 | # #fragment :: Also called text mark: http://example.com/page.html#fragment 15 | # #target_frame :: Which frame a Link should be opened in, should also support 16 | # the special frames _blank, _parent, _self and _top. 17 | # #dos :: Excel may store a DOS-Filename together with the long 18 | # Filename introduced in VFAT. You probably will not need this, 19 | # but if you do, here is where you can find it. 20 | class Link < String 21 | include Encodings 22 | attr_accessor :target_frame, :url, :dos, :fragment 23 | def initialize url='', description=url, fragment=nil 24 | super description 25 | @url = url 26 | @fragment = fragment 27 | end 28 | ## 29 | # The Url with the fragment appended if present. 30 | def href 31 | href = (@url || @dos).to_s.dup 32 | if @fragment 33 | href << client('#', 'UTF-8') << @fragment 34 | end 35 | href 36 | end 37 | ## 38 | # Attempts to parse the output of href. May raise a URI::InvalidURIError 39 | def to_uri 40 | URI.parse href 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/spreadsheet/row.rb: -------------------------------------------------------------------------------- 1 | require 'spreadsheet/helpers' 2 | 3 | module Spreadsheet 4 | ## 5 | # The Row class. Encapsulates Cell data and formatting. 6 | # Since Row is a subclass of Array, you may use all the standard Array methods 7 | # to manipulate a Row. 8 | # By convention, Row#at will give you raw values, while Row#[] may be 9 | # overridden to return enriched data if necessary (see also the Date- and 10 | # DateTime-handling in Excel::Row#[] 11 | # 12 | # Useful Attributes are: 13 | # #idx:: The 0-based index of this Row in its Worksheet. 14 | # #formats:: A parallel array containing Formatting information for 15 | # all cells stored in a Row. 16 | # #default_format:: The default Format used when writing a Cell if no explicit 17 | # Format is stored in #formats for the cell. 18 | # #height:: The height of this Row in points (defaults to 12). 19 | class Row < Array 20 | include Datatypes 21 | class << self 22 | def format_updater *keys 23 | keys.each do |key| 24 | unless instance_methods.include? "unupdated_#{key}=" 25 | alias_method :"unupdated_#{key}=", :"#{key}=" 26 | define_method "#{key}=" do |value| 27 | send "unupdated_#{key}=", value 28 | @worksheet.row_updated @idx, self if @worksheet 29 | value 30 | end 31 | end 32 | end 33 | end 34 | def updater *keys 35 | keys.each do |key| 36 | ## Passing blocks to methods defined with define_method is not possible 37 | # in Ruby 1.8: 38 | # http://groups.google.com/group/ruby-talk-google/msg/778184912b769e5f 39 | # use class_eval as suggested by someone else in 40 | # http://rubyforge.org/tracker/index.php?func=detail&aid=25732&group_id=678&atid=2677 41 | class_eval <<-SRC, __FILE__, __LINE__ 42 | def #{key}(*args) 43 | res = super(*args) 44 | @worksheet.row_updated @idx, self if @worksheet 45 | res 46 | end 47 | SRC 48 | end 49 | end 50 | end 51 | attr_reader :formats, :default_format 52 | attr_accessor :idx, :height, :worksheet 53 | boolean :hidden, :collapsed 54 | enum :outline_level, 0, Integer 55 | updater :[]=, :clear, :concat, :delete, :delete_if, :fill, :insert, :map!, 56 | :pop, :push, :reject!, :replace, :reverse!, :shift, :slice!, 57 | :sort!, :uniq!, :unshift 58 | format_updater :collapsed, :height, :hidden, :outline_level 59 | def initialize worksheet, idx, cells=[] 60 | @default_format = nil 61 | @worksheet = worksheet 62 | @idx = idx 63 | super cells 64 | @formats = [] 65 | @height = 12.1 66 | end 67 | ## 68 | # Set the default Format used when writing a Cell if no explicit Format is 69 | # stored for the cell. 70 | def default_format= format 71 | @worksheet.add_format format if @worksheet 72 | @default_format = format 73 | end 74 | format_updater :default_format 75 | ## 76 | # #first_used the 0-based index of the first non-blank Cell. 77 | def first_used 78 | [ index_of_first(self), index_of_first(@formats) ].compact.min 79 | end 80 | ## 81 | # The Format for the Cell at _idx_ (0-based), or the first valid Format in 82 | # Row#default_format, Column#default_format and Worksheet#default_format. 83 | def format idx 84 | @formats[idx] || @default_format \ 85 | || @worksheet.column(idx).default_format if @worksheet 86 | end 87 | ## 88 | # Returns a copy of self with nil-values appended for empty cells that have 89 | # an associated Format. 90 | # This is primarily a helper-function for the writer classes. 91 | def formatted 92 | copy = dup 93 | @formats.rcompact! 94 | if copy.length < @formats.size 95 | copy.concat Array.new(@formats.size - copy.length) 96 | end 97 | copy 98 | end 99 | ## 100 | # Same as Row#size, but takes into account formatted empty cells 101 | def formatted_size 102 | @formats.rcompact! 103 | sz = size 104 | fs = @formats.size 105 | fs > sz ? fs : sz 106 | end 107 | ## 108 | # #first_unused (really last used + 1) - the 0-based index of the first of 109 | # all remaining contiguous blank Cells. 110 | alias :first_unused :formatted_size 111 | def inspect 112 | variables = instance_variables.collect do |name| 113 | "%s=%s" % [name, instance_variable_get(name)] 114 | end.join(' ') 115 | sprintf "#<%s:0x%014x %s %s>", self.class, object_id, variables, super 116 | end 117 | ## 118 | # Set the Format for the Cell at _idx_ (0-based). 119 | def set_format idx, fmt 120 | @formats[idx] = fmt 121 | @worksheet.add_format fmt 122 | @worksheet.row_updated @idx, self if @worksheet 123 | fmt 124 | end 125 | private 126 | def index_of_first ary # :nodoc: 127 | if first = ary.find do |elm| !elm.nil? end 128 | ary.index first 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/spreadsheet/workbook.rb: -------------------------------------------------------------------------------- 1 | require 'spreadsheet/format' 2 | require 'spreadsheet/encodings' 3 | 4 | module Spreadsheet 5 | ## 6 | # The Workbook class represents a Spreadsheet-Document and is the entry point 7 | # for all Spreadsheet manipulation. 8 | # 9 | # Interesting Attributes: 10 | # #default_format:: The default format used for all cells in this Workbook. 11 | # that have no format set explicitly or in 12 | # Row#default_format or Worksheet#default_format. 13 | class Workbook 14 | include Spreadsheet::Encodings 15 | attr_reader :io, :worksheets, :formats, :fonts 16 | attr_accessor :active_worksheet, :encoding, :default_format, :version 17 | def initialize io = nil, opts={:default_format => Format.new} 18 | @worksheets = [] 19 | @io = io 20 | @fonts = [] 21 | @formats = [] 22 | if @default_format = opts[:default_format] 23 | @formats.push @default_format 24 | end 25 | end 26 | ## 27 | # Add a Font to the Workbook. Used by the parser. You should not need to 28 | # use this Method. 29 | def add_font font 30 | @fonts.push(font).uniq! if font 31 | font 32 | end 33 | ## 34 | # Add a Format to the Workbook. If you use Row#set_format, you should not 35 | # need to use this Method. 36 | def add_format format 37 | @formats.push(format) if format && !@formats.include?(format) 38 | format 39 | end 40 | ## 41 | # Add a Worksheet to the Workbook. 42 | def add_worksheet worksheet 43 | worksheet.workbook = self 44 | @worksheets.push worksheet 45 | worksheet 46 | end 47 | ## 48 | # Create a new Worksheet in this Workbook. 49 | # Used without options this creates a Worksheet with the name 'WorksheetN' 50 | # where the new Worksheet is the Nth Worksheet in this Workbook. 51 | # 52 | # Use the option :name => 'My pretty Name' to override this 53 | # behavior. 54 | def create_worksheet opts = {} 55 | opts[:name] ||= client("Worksheet#{@worksheets.size.next}", 'UTF-8') 56 | add_worksheet Worksheet.new(opts) 57 | end 58 | ## 59 | # The Font at _idx_ 60 | def font idx 61 | @fonts[idx] 62 | end 63 | ## 64 | # The Format at _idx_, or - if _idx_ is a String - 65 | # the Format with name == _idx_ 66 | def format idx 67 | case idx 68 | when Integer 69 | @formats[idx] || @default_format 70 | when String 71 | @formats.find do |fmt| fmt.name == idx end 72 | end 73 | end 74 | def inspect 75 | variables = (instance_variables - uninspect_variables).collect do |name| 76 | "%s=%s" % [name, instance_variable_get(name)] 77 | end.join(' ') 78 | uninspect = uninspect_variables.collect do |name| 79 | var = instance_variable_get name 80 | "%s=%s[%i]" % [name, var.class, var.size] 81 | end.join(' ') 82 | sprintf "#<%s:0x%014x %s %s>", self.class, object_id, 83 | variables, uninspect 84 | end 85 | def uninspect_variables # :nodoc: 86 | %w{@formats @fonts @worksheets} 87 | end 88 | ## 89 | # The Worksheet at _idx_, or - if _idx_ is a String - 90 | # the Worksheet with name == _idx_ 91 | def worksheet idx 92 | case idx 93 | when Integer 94 | @worksheets[idx] 95 | when String 96 | @worksheets.find do |sheet| sheet.name == idx end 97 | end 98 | end 99 | ## 100 | # Write this Workbook to a File, IO Stream or Writer Object. The latter will 101 | # make more sense once there are more than just an Excel-Writer available. 102 | def write io_path_or_writer 103 | if io_path_or_writer.is_a? Writer 104 | io_path_or_writer.write self 105 | else 106 | writer(io_path_or_writer).write(self) 107 | end 108 | end 109 | ## 110 | # Returns a new instance of the default Writer class for this Workbook (can 111 | # only be an Excel::Writer::Workbook at this time) 112 | def writer io_or_path, type=Excel, version=self.version 113 | if type == Excel 114 | Excel::Writer::Workbook.new io_or_path 115 | else 116 | raise NotImplementedError, "No Writer defined for #{type}" 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/spreadsheet/worksheet.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | require 'spreadsheet/column' 3 | require 'spreadsheet/encodings' 4 | require 'spreadsheet/row' 5 | 6 | module Spreadsheet 7 | ## 8 | # The Worksheet class. Contains most of the Spreadsheet data in Rows. 9 | # 10 | # Interesting Attributes 11 | # #name :: The Name of this Worksheet. 12 | # #default_format:: The default format used for all cells in this Workhseet 13 | # that have no format set explicitly or in 14 | # Row#default_format. 15 | # #rows :: The Rows in this Worksheet. It is not recommended to 16 | # Manipulate this Array directly. If you do, call 17 | # #updated_from with the smallest modified index. 18 | # #columns :: The Column formatting in this Worksheet. Column 19 | # instances may appear at more than one position in #columns. 20 | # If you modify a Column directly, your changes will be 21 | # reflected in all those positions. 22 | # #selected :: When a user chooses to print a Workbook, Excel will include 23 | # all selected Worksheets. If no Worksheet is selected at 24 | # Workbook#write, then the first Worksheet is selected by 25 | # default. 26 | class Worksheet 27 | include Spreadsheet::Encodings 28 | include Enumerable 29 | attr_accessor :name, :selected, :workbook 30 | attr_reader :rows, :columns, :merged_cells 31 | def initialize opts={} 32 | @default_format = nil 33 | @selected = opts[:selected] 34 | @dimensions = [0,0,0,0] 35 | @name = opts[:name] || 'Worksheet' 36 | @workbook = opts[:workbook] 37 | @rows = [] 38 | @columns = [] 39 | @links = {} 40 | @merged_cells = [] 41 | end 42 | def active # :nodoc: 43 | warn "Worksheet#active is deprecated. Please use Worksheet#selected instead." 44 | selected 45 | end 46 | def active= selected # :nodoc: 47 | warn "Worksheet#active= is deprecated. Please use Worksheet#selected= instead." 48 | self.selected = selected 49 | end 50 | ## 51 | # Add a Format to the Workbook. If you use Row#set_format, you should not 52 | # need to use this Method. 53 | def add_format fmt 54 | @workbook.add_format fmt if fmt 55 | end 56 | ## 57 | # Get the enriched value of the Cell at _row_, _column_. 58 | # See also Worksheet#[], Row#[]. 59 | def cell row, column 60 | row(row)[column] 61 | end 62 | ## 63 | # Returns the Column at _idx_. 64 | def column idx 65 | @columns[idx] || Column.new(idx, default_format, :worksheet => self) 66 | end 67 | ## 68 | # The number of columns in this Worksheet which contain data. 69 | def column_count 70 | dimensions[3] - dimensions[2] 71 | end 72 | def column_updated idx, column 73 | @columns[idx] = column 74 | end 75 | ## 76 | # Delete the Row at _idx_ (0-based) from this Worksheet. 77 | def delete_row idx 78 | res = @rows.delete_at idx 79 | updated_from idx 80 | res 81 | end 82 | ## 83 | # The default Format of this Worksheet, if you have set one. 84 | # Returns the Workbook's default Format otherwise. 85 | def default_format 86 | @default_format || @workbook.default_format 87 | end 88 | ## 89 | # Set the default Format of this Worksheet. 90 | def default_format= format 91 | @default_format = format 92 | add_format format 93 | format 94 | end 95 | ## 96 | # Dimensions:: [ first used row, first unused row, 97 | # first used column, first unused column ] 98 | # ( First used means that all rows or columns before that are 99 | # empty. First unused means that this and all following rows 100 | # or columns are empty. ) 101 | def dimensions 102 | @dimensions || recalculate_dimensions 103 | end 104 | ## 105 | # If no argument is given, #each iterates over all used Rows (from the first 106 | # used Row until but omitting the first unused Row, see also #dimensions). 107 | # 108 | # If the argument skip is given, #each iterates from that row until but 109 | # omitting the first unused Row, effectively skipping the first _skip_ Rows 110 | # from the top of the Worksheet. 111 | def each skip=dimensions[0], &block 112 | skip.upto(dimensions[1] - 1) do |idx| 113 | block.call row(idx) 114 | end 115 | end 116 | def encoding # :nodoc: 117 | @workbook.encoding 118 | end 119 | ## 120 | # Sets the default Format of the column at _idx_. 121 | # 122 | # _idx_ may be an Integer, or an Enumerable that iterates over a number of 123 | # Integers. 124 | # 125 | # _format_ is a Format, or nil if you want to remove the Formatting at _idx_ 126 | # 127 | # Returns an instance of Column if _idx_ is an Integer, an Array of Columns 128 | # otherwise. 129 | def format_column idx, format=nil, opts={} 130 | opts[:worksheet] = self 131 | res = case idx 132 | when Integer 133 | column = nil 134 | if format 135 | column = Column.new(idx, format, opts) 136 | end 137 | @columns[idx] = column 138 | else 139 | idx.collect do |col| format_column col, format, opts end 140 | end 141 | shorten @columns 142 | res 143 | end 144 | ## 145 | # Formats all Date, DateTime and Time cells with _format_ or the default 146 | # formats: 147 | # - 'DD.MM.YYYY' for Date 148 | # - 'DD.MM.YYYY hh:mm:ss' for DateTime and Time 149 | def format_dates! format=nil 150 | each do |row| 151 | row.each_with_index do |value, idx| 152 | unless row.formats[idx] || row.format(idx).date_or_time? 153 | numfmt = case value 154 | when DateTime, Time 155 | format || client('DD.MM.YYYY hh:mm:ss', 'UTF-8') 156 | when Date 157 | format || client('DD.MM.YYYY', 'UTF-8') 158 | end 159 | case numfmt 160 | when Format 161 | row.set_format idx, numfmt 162 | when String 163 | fmt = row.format(idx).dup 164 | fmt.number_format = numfmt 165 | row.set_format idx, fmt 166 | end 167 | end 168 | end 169 | end 170 | end 171 | ## 172 | # Insert a Row at _idx_ (0-based) containing _cells_ 173 | def insert_row idx, cells=[] 174 | res = @rows.insert idx, Row.new(self, idx, cells) 175 | updated_from idx 176 | res 177 | end 178 | def inspect 179 | names = instance_variables 180 | names.delete '@rows' 181 | variables = names.collect do |name| 182 | "%s=%s" % [name, instance_variable_get(name)] 183 | end.join(' ') 184 | sprintf "#<%s:0x%014x %s @rows[%i]>", self.class, object_id, 185 | variables, row_count 186 | end 187 | ## The last Row containing any data 188 | def last_row 189 | row(last_row_index) 190 | end 191 | ## The index of the last Row containing any data 192 | def last_row_index 193 | [dimensions[1] - 1, 0].max 194 | end 195 | ## 196 | # Replace the Row at _idx_ with the following arguments. Like #update_row, 197 | # but truncates the Row if there are fewer arguments than Cells in the Row. 198 | def replace_row idx, *cells 199 | if(row = @rows[idx]) && cells.size < row.size 200 | cells.concat Array.new(row.size - cells.size) 201 | end 202 | update_row idx, *cells 203 | end 204 | ## 205 | # The Row at _idx_ or a new Row. 206 | def row idx 207 | @rows[idx] || Row.new(self, idx) 208 | end 209 | ## 210 | # The number of Rows in this Worksheet which contain data. 211 | def row_count 212 | dimensions[1] - dimensions[0] 213 | end 214 | ## 215 | # Tell Worksheet that the Row at _idx_ has been updated and the #dimensions 216 | # need to be recalculated. You should not need to call this directly. 217 | def row_updated idx, row 218 | @dimensions = nil 219 | @rows[idx] = row 220 | end 221 | ## 222 | # Updates the Row at _idx_ with the following arguments. 223 | def update_row idx, *cells 224 | res = if row = @rows[idx] 225 | row[0, cells.size] = cells 226 | row 227 | else 228 | Row.new self, idx, cells 229 | end 230 | row_updated idx, res 231 | res 232 | end 233 | ## 234 | # Renumbers all Rows starting at _idx_ and calls #row_updated for each of 235 | # them. 236 | def updated_from index 237 | index.upto(@rows.size - 1) do |idx| 238 | row = row(idx) 239 | row.idx = idx 240 | row_updated idx, row 241 | end 242 | end 243 | ## 244 | # Get the enriched value of the Cell at _row_, _column_. 245 | # See also Worksheet#cell, Row#[]. 246 | def [] row, column 247 | row(row)[column] 248 | end 249 | ## 250 | # Set the value of the Cell at _row_, _column_ to _value_. 251 | # See also Row#[]=. 252 | def []= row, column, value 253 | row(row)[column] = value 254 | end 255 | ## 256 | # Merges multiple cells into one. 257 | def merge_cells start_row, start_col, end_row, end_col 258 | # FIXME enlarge or dup check 259 | @merged_cells.push [start_row, end_row, start_col, end_col] 260 | end 261 | private 262 | def index_of_first ary # :nodoc: 263 | return unless ary 264 | ary.index(ary.find do |elm| elm end) 265 | end 266 | def recalculate_dimensions # :nodoc: 267 | shorten @rows 268 | @dimensions = [] 269 | @dimensions[0] = index_of_first(@rows) || 0 270 | @dimensions[1] = @rows.size 271 | compact = @rows.compact 272 | @dimensions[2] = compact.collect do |row| row.first_used end.compact.min || 0 273 | @dimensions[3] = compact.collect do |row| row.first_unused end.max || 0 274 | @dimensions 275 | end 276 | def shorten ary # :nodoc: 277 | return unless ary 278 | while !ary.empty? && !ary.last 279 | ary.pop 280 | end 281 | ary unless ary.empty? 282 | end 283 | end 284 | end 285 | -------------------------------------------------------------------------------- /lib/spreadsheet/writer.rb: -------------------------------------------------------------------------------- 1 | module Spreadsheet 2 | ## 3 | # Parent Class for all Writers. Implements the copying of unmodified 4 | # Spreadsheet documents. 5 | class Writer 6 | def initialize io_or_path 7 | @io_or_path = io_or_path 8 | end 9 | def write workbook 10 | if @io_or_path.respond_to? :seek 11 | @io_or_path.binmode 12 | write_workbook workbook, @io_or_path 13 | else 14 | File.open(@io_or_path, "wb+") do |fh| 15 | write_workbook workbook, fh 16 | end 17 | end 18 | end 19 | private 20 | def write_workbook workbook, io 21 | reader = workbook.io 22 | unless io == reader 23 | reader.rewind 24 | data = reader.read 25 | io.rewind 26 | io.write data 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spreadsheet-0.6.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voraz/spreadsheet/1a4035579f6e3b003a3eb7e1cced37a9ee4959ff/spreadsheet-0.6.5.gem -------------------------------------------------------------------------------- /spreadsheet.gemspec: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "rake" 3 | 4 | spec = Gem::Specification.new do |s| 5 | s.name = "spreadsheet" 6 | s.version = "0.6.5" 7 | s.summary = "The Spreadsheet Library is designed to read and write Spreadsheet Documents" 8 | s.description = "As of version 0.6.0, only Microsoft Excel compatible spreadsheets are supported" 9 | s.author = "Masaomi Hatakeyama, Zeno R.R. Davatz" 10 | s.email = "mhatakeyama@ywesee.com, zdavatz@ywesee.com" 11 | s.platform = Gem::Platform::RUBY 12 | s.files = Dir.glob("{bin,lib,test}/**/*") + Dir.glob("*.txt") 13 | s.test_file = "test/suite.rb" 14 | s.executables << 'xlsopcodes' 15 | s.add_dependency('ruby-ole') 16 | s.homepage = "http://scm.ywesee.com/?p=spreadsheet/.git;a=summary" 17 | end 18 | 19 | if $0 == __FILE__ 20 | Gem.manage_gems 21 | Gem::Builder.new(spec).build 22 | end 23 | -------------------------------------------------------------------------------- /test/data/test_changes.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voraz/spreadsheet/1a4035579f6e3b003a3eb7e1cced37a9ee4959ff/test/data/test_changes.xls -------------------------------------------------------------------------------- /test/data/test_copy.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voraz/spreadsheet/1a4035579f6e3b003a3eb7e1cced37a9ee4959ff/test/data/test_copy.xls -------------------------------------------------------------------------------- /test/data/test_datetime.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voraz/spreadsheet/1a4035579f6e3b003a3eb7e1cced37a9ee4959ff/test/data/test_datetime.xls -------------------------------------------------------------------------------- /test/data/test_empty.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voraz/spreadsheet/1a4035579f6e3b003a3eb7e1cced37a9ee4959ff/test/data/test_empty.xls -------------------------------------------------------------------------------- /test/data/test_formula.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voraz/spreadsheet/1a4035579f6e3b003a3eb7e1cced37a9ee4959ff/test/data/test_formula.xls -------------------------------------------------------------------------------- /test/data/test_long_sst_record.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voraz/spreadsheet/1a4035579f6e3b003a3eb7e1cced37a9ee4959ff/test/data/test_long_sst_record.xls -------------------------------------------------------------------------------- /test/data/test_missing_row.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voraz/spreadsheet/1a4035579f6e3b003a3eb7e1cced37a9ee4959ff/test/data/test_missing_row.xls -------------------------------------------------------------------------------- /test/data/test_version_excel5.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voraz/spreadsheet/1a4035579f6e3b003a3eb7e1cced37a9ee4959ff/test/data/test_version_excel5.xls -------------------------------------------------------------------------------- /test/data/test_version_excel95.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voraz/spreadsheet/1a4035579f6e3b003a3eb7e1cced37a9ee4959ff/test/data/test_version_excel95.xls -------------------------------------------------------------------------------- /test/data/test_version_excel97.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voraz/spreadsheet/1a4035579f6e3b003a3eb7e1cced37a9ee4959ff/test/data/test_version_excel97.xls -------------------------------------------------------------------------------- /test/excel/row.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Excel::TestRow -- Spreadsheet -- 12.10.2008 -- hwyss@ywesee.com 3 | 4 | $: << File.expand_path('../../lib', File.dirname(__FILE__)) 5 | 6 | require 'test/unit' 7 | require 'spreadsheet' 8 | 9 | module Spreadsheet 10 | module Excel 11 | class TestRow < Test::Unit::TestCase 12 | def setup 13 | @workbook = Excel::Workbook.new 14 | @worksheet = Excel::Worksheet.new 15 | @workbook.add_worksheet @worksheet 16 | end 17 | def test_date 18 | row = Row.new @worksheet, 0, [nil, 27627.6789] 19 | assert_equal Date.new(1975,8,21), row.date(1) 20 | end 21 | def test_datetime 22 | row = Row.new @worksheet, 0, [nil, 27627.765] 23 | d1 = DateTime.new(1975,8,21) + 0.765 24 | d2 = row.datetime 1 25 | assert_equal d1, d2 26 | end 27 | def test_datetime_overflow 28 | row = Row.new @worksheet, 0, [nil, 40010.6666666666] 29 | d1 = DateTime.new(2009,07,16,16) 30 | d2 = row.datetime 1 31 | assert_equal d1, d2 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/excel/writer/workbook.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Excel::Writer::TestWorkbook -- Spreadsheet -- 20.07.2011 3 | 4 | $: << File.expand_path('../../../lib', File.dirname(__FILE__)) 5 | 6 | require 'test/unit' 7 | require 'spreadsheet' 8 | 9 | module Spreadsheet 10 | module Excel 11 | module Writer 12 | class TestWorkbook < Test::Unit::TestCase 13 | def test_sanitize_worksheets 14 | book = Spreadsheet::Excel::Workbook.new 15 | assert_instance_of Excel::Workbook, book 16 | assert_equal book.worksheets.size, 0 17 | workbook_writer = Excel::Writer::Workbook.new book 18 | assert_nothing_raised { workbook_writer.sanitize_worksheets book.worksheets } 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/excel/writer/worksheet.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Excel::Writer::TestWorksheet -- Spreadsheet -- 21.11.2007 -- hwyss@ywesee.com 3 | 4 | require 'test/unit' 5 | require 'spreadsheet/excel/writer/worksheet' 6 | 7 | module Spreadsheet 8 | module Excel 9 | module Writer 10 | class TestWorksheet < Test::Unit::TestCase 11 | def test_need_number 12 | sheet = Worksheet.new nil, nil 13 | assert_equal false, sheet.need_number?(10) 14 | assert_equal false, sheet.need_number?(114.55) 15 | assert_equal false, sheet.need_number?(0.1) 16 | assert_equal false, sheet.need_number?(0.01) 17 | assert_equal true, sheet.need_number?(0.001) 18 | assert_equal true, sheet.need_number?(10000000.0) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/font.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # TestFont -- Spreadsheet -- 09.10.2008 -- hwyss@ywesee.com 3 | 4 | $: << File.expand_path('../lib', File.dirname(__FILE__)) 5 | 6 | require 'test/unit' 7 | require 'spreadsheet' 8 | 9 | module Spreadsheet 10 | class TestFont < Test::Unit::TestCase 11 | def setup 12 | @font = Font.new 'Arial' 13 | end 14 | def test_italic 15 | assert_equal false, @font.italic 16 | @font.italic! 17 | assert_equal true, @font.italic 18 | @font.italic = nil 19 | assert_equal false, @font.italic 20 | @font.italic = 1 21 | assert_equal true, @font.italic 22 | end 23 | def test_encoding 24 | assert_equal :default, @font.encoding 25 | @font.encoding = :apple_roman 26 | assert_equal :apple_roman, @font.encoding 27 | @font.encoding = 'Chinese Simplified' 28 | assert_equal :chinese_simplified, @font.encoding 29 | assert_raises ArgumentError do @font.size = 'ascii' end 30 | assert_equal :chinese_simplified, @font.encoding 31 | @font.encoding = nil 32 | assert_equal :default, @font.encoding 33 | end 34 | def test_family 35 | assert_equal :none, @font.family 36 | @font.family = :roman 37 | assert_equal :roman, @font.family 38 | @font.family = 'Swiss' 39 | assert_equal :swiss, @font.family 40 | assert_raises ArgumentError do @font.size = :greek end 41 | assert_equal :swiss, @font.family 42 | @font.family = nil 43 | assert_equal :none, @font.family 44 | end 45 | def test_name 46 | assert_equal 'Arial', @font.name 47 | @font.name = 'Helvetica' 48 | assert_equal 'Helvetica', @font.name 49 | end 50 | def test_outline 51 | assert_equal false, @font.outline 52 | @font.outline! 53 | assert_equal true, @font.outline 54 | @font.outline = nil 55 | assert_equal false, @font.outline 56 | @font.outline = 1 57 | assert_equal true, @font.outline 58 | end 59 | def test_escapement 60 | assert_equal :normal, @font.escapement 61 | @font.escapement = :superscript 62 | assert_equal :superscript, @font.escapement 63 | @font.escapement = 'sub' 64 | assert_equal :subscript, @font.escapement 65 | assert_raises ArgumentError do @font.size = "upwards" end 66 | assert_equal :subscript, @font.escapement 67 | @font.escapement = nil 68 | assert_equal :normal, @font.escapement 69 | end 70 | def test_shadow 71 | assert_equal false, @font.shadow 72 | @font.shadow! 73 | assert_equal true, @font.shadow 74 | @font.shadow = nil 75 | assert_equal false, @font.shadow 76 | @font.shadow = 1 77 | assert_equal true, @font.shadow 78 | end 79 | def test_size 80 | assert_equal 10, @font.size 81 | @font.size = 12 82 | assert_equal 12, @font.size 83 | @font.size = 11.2 84 | assert_equal 11.2, @font.size 85 | assert_raises ArgumentError do @font.size = "123" end 86 | end 87 | def test_strikeout 88 | assert_equal false, @font.strikeout 89 | @font.strikeout! 90 | assert_equal true, @font.strikeout 91 | @font.strikeout = nil 92 | assert_equal false, @font.strikeout 93 | @font.strikeout = 1 94 | assert_equal true, @font.strikeout 95 | end 96 | def test_underline 97 | assert_equal :none, @font.underline 98 | @font.underline = :single 99 | assert_equal :single, @font.underline 100 | @font.underline = 'double accounting' 101 | assert_equal :double_accounting, @font.underline 102 | assert_raises ArgumentError do @font.size = :triple end 103 | assert_equal :double_accounting, @font.underline 104 | @font.underline = nil 105 | assert_equal :none, @font.underline 106 | @font.underline = true 107 | assert_equal :single, @font.underline 108 | end 109 | def test_weight 110 | assert_equal :normal, @font.weight 111 | @font.weight = :bold 112 | assert_equal :bold, @font.weight 113 | @font.weight = 100 114 | assert_equal 100, @font.weight 115 | assert_raises ArgumentError do @font.weight = Object.new end 116 | assert_equal 100, @font.weight 117 | @font.weight = 'bold' 118 | assert_equal :bold, @font.weight 119 | @font.weight = nil 120 | assert_equal :normal, @font.weight 121 | end 122 | def test_key 123 | expected = 'Arial_10_normal_normal_none_text_none_default' 124 | assert_equal expected, @font.key 125 | @font.name = 'Helvetica' 126 | expected = 'Helvetica_10_normal_normal_none_text_none_default' 127 | assert_equal expected, @font.key 128 | @font.size = 12 129 | expected = 'Helvetica_12_normal_normal_none_text_none_default' 130 | assert_equal expected, @font.key 131 | @font.weight = :bold 132 | expected = 'Helvetica_12_bold_normal_none_text_none_default' 133 | assert_equal expected, @font.key 134 | @font.italic! 135 | expected = 'Helvetica_12_bold_italic_normal_none_text_none_default' 136 | assert_equal expected, @font.key 137 | @font.strikeout! 138 | expected = 'Helvetica_12_bold_italic_strikeout_normal_none_text_none_default' 139 | assert_equal expected, @font.key 140 | @font.outline! 141 | expected = 'Helvetica_12_bold_italic_strikeout_outline_normal_none_text_none_default' 142 | assert_equal expected, @font.key 143 | @font.shadow! 144 | expected = 'Helvetica_12_bold_italic_strikeout_outline_shadow_normal_none_text_none_default' 145 | assert_equal expected, @font.key 146 | @font.escapement = :super 147 | expected = 'Helvetica_12_bold_italic_strikeout_outline_shadow_superscript_none_text_none_default' 148 | assert_equal expected, @font.key 149 | @font.underline = :double 150 | expected = 'Helvetica_12_bold_italic_strikeout_outline_shadow_superscript_double_text_none_default' 151 | assert_equal expected, @font.key 152 | @font.color = :blue 153 | expected = 'Helvetica_12_bold_italic_strikeout_outline_shadow_superscript_double_blue_none_default' 154 | assert_equal expected, @font.key 155 | @font.family = :swiss 156 | expected = 'Helvetica_12_bold_italic_strikeout_outline_shadow_superscript_double_blue_swiss_default' 157 | assert_equal expected, @font.key 158 | @font.encoding = :iso_latin1 159 | expected = 'Helvetica_12_bold_italic_strikeout_outline_shadow_superscript_double_blue_swiss_iso_latin1' 160 | assert_equal expected, @font.key 161 | end 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /test/row.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # TestRow -- Spreadsheet -- 08.01.2009 -- hwyss@ywesee.com 3 | 4 | $: << File.expand_path('../../lib', File.dirname(__FILE__)) 5 | 6 | require 'test/unit' 7 | require 'spreadsheet' 8 | 9 | module Spreadsheet 10 | class TestRow < Test::Unit::TestCase 11 | def setup 12 | @workbook = Excel::Workbook.new 13 | @worksheet = Excel::Worksheet.new 14 | @workbook.add_worksheet @worksheet 15 | end 16 | def test_formatted 17 | row = Row.new @worksheet, 0, [nil, 1] 18 | assert_equal 2, row.formatted.size 19 | row.set_format 3, Format.new 20 | assert_equal 4, row.formatted.size 21 | end 22 | def test_concat 23 | row = Row.new @worksheet, 0, [nil, 1, nil] 24 | assert_equal [nil, 1, nil], row 25 | row.concat [2, nil] 26 | assert_equal [nil, 1, nil, 2, nil], row 27 | row.concat [3] 28 | assert_equal [nil, 1, nil, 2, nil, 3], row 29 | row.concat [nil, 4] 30 | assert_equal [nil, 1, nil, 2, nil, 3, nil, 4], row 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/suite.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # suite.rb -- oddb -- 08.01.2009 -- hwyss@ywesee.com 3 | 4 | require 'find' 5 | require 'rubygems' 6 | 7 | here = File.dirname(__FILE__) 8 | 9 | $: << here 10 | 11 | Find.find(here) do |file| 12 | if /[^suite\W]\.rb$/o.match(file) 13 | require file 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/workbook.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # TestWorkbook -- Spreadsheet -- 24.09.2008 -- hwyss@ywesee.com 3 | 4 | $: << File.expand_path('../lib', File.dirname(__FILE__)) 5 | 6 | require 'test/unit' 7 | require 'spreadsheet' 8 | require 'fileutils' 9 | require 'stringio' 10 | 11 | module Spreadsheet 12 | class TestWorkbook < Test::Unit::TestCase 13 | def setup 14 | @io = StringIO.new '' 15 | @book = Workbook.new 16 | end 17 | def test_writer__default_excel 18 | assert_instance_of Excel::Writer::Workbook, @book.writer(@io) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/worksheet.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # TestWorksheet -- Spreadsheet -- 30.09.2008 -- hwyss@ywesee.com 3 | 4 | $: << File.expand_path('../lib', File.dirname(__FILE__)) 5 | 6 | require 'test/unit' 7 | require 'spreadsheet' 8 | 9 | module Spreadsheet 10 | class TestWorksheet < Test::Unit::TestCase 11 | def setup 12 | @book = Workbook.new 13 | @sheet = @book.create_worksheet 14 | end 15 | def test_cell_writer 16 | assert_nil @sheet[0,0] 17 | assert_equal 0, @sheet.column_count 18 | assert_equal 0, @sheet.row_count 19 | @sheet[0,0] = 'foo' 20 | assert_equal 'foo', @sheet[0,0] 21 | assert_equal 1, @sheet.column_count 22 | assert_equal 1, @sheet.row_count 23 | @sheet[1,0] = 'bar' 24 | assert_equal 1, @sheet.column_count 25 | assert_equal 2, @sheet.row_count 26 | @sheet[0,1] = 'bar' 27 | assert_equal 2, @sheet.column_count 28 | assert_equal 2, @sheet.row_count 29 | @sheet[1,0] = nil 30 | assert_equal 2, @sheet.column_count 31 | assert_equal 2, @sheet.row_count 32 | @sheet[0,1] = nil 33 | assert_equal 2, @sheet.column_count 34 | assert_equal 2, @sheet.row_count 35 | end 36 | def test_column_count 37 | assert_equal 0, @sheet.column_count 38 | @sheet.replace_row 3, nil, nil, 1, 2, 'foo, bar' 39 | assert_equal 3, @sheet.column_count 40 | @sheet.replace_row 8, nil, 'something', 4, 7, nil 41 | assert_equal 4, @sheet.column_count 42 | @sheet.replace_row 5, 4, 'something', 4, 7, nil 43 | assert_equal 5, @sheet.column_count 44 | @sheet.replace_row 5, nil, 'something', 4, 7, nil 45 | assert_equal 4, @sheet.column_count 46 | @sheet.replace_row 3 47 | assert_equal 4, @sheet.column_count 48 | end 49 | def test_row_count 50 | assert_equal 0, @sheet.row_count 51 | @sheet.replace_row 3, nil, nil, 1, 2, 'foo, bar' 52 | assert_equal 1, @sheet.row_count 53 | @sheet.replace_row 8, nil, 'something', 4, 7, nil 54 | assert_equal 6, @sheet.row_count 55 | @sheet.replace_row 5, 4, 'something', 4, 7, nil 56 | assert_equal 6, @sheet.row_count 57 | @sheet.replace_row 5, nil, 'something', 4, 7, nil 58 | assert_equal 6, @sheet.row_count 59 | @sheet.replace_row 3 60 | assert_equal 6, @sheet.row_count 61 | @sheet.delete_row 3 62 | assert_equal 5, @sheet.row_count 63 | @sheet.delete_row 3 64 | assert_equal 4, @sheet.row_count 65 | @sheet.delete_row 2 66 | assert_equal 4, @sheet.row_count 67 | @sheet.delete_row 2 68 | assert_equal 3, @sheet.row_count 69 | end 70 | def test_modify_column 71 | assert_equal 10, @sheet.column(0).width 72 | @sheet.column(1).width = 20 73 | assert_equal 10, @sheet.column(0).width 74 | assert_equal 20, @sheet.column(1).width 75 | @sheet.column(0).width = 30 76 | assert_equal 30, @sheet.column(0).width 77 | assert_equal 20, @sheet.column(1).width 78 | end 79 | end 80 | end 81 | --------------------------------------------------------------------------------