Skip to content

Commit

Permalink
Merge pull request #45 from ms-ati/excel-2003-xml-support-font-colors
Browse files Browse the repository at this point in the history
[Excel 2003 XML] Add support for font colors + required fixes
  • Loading branch information
Empact authored Feb 22, 2018
2 parents 00ccb4c + 5a06c76 commit 7a7e715
Show file tree
Hide file tree
Showing 5 changed files with 449 additions and 30 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ This library extends Roo to add support for handling class Excel files, includin
There is no support for formulas in Roo for .xls files - you can get the result
of a formula but not the formula itself.

## Limitations

Roo::Xls currently doesn't provide support for the following features in Roo:
* [Option `:expand_merged_ranged => true`](https://github.com/roo-rb/roo#expand_merged_ranges)

## License

While Roo and Roo::Xls are licensed under the MIT / Expat license, please note that the `spreadsheet` gem [is released under](https://github.com/zdavatz/spreadsheet/blob/master/LICENSE.txt) the GPLv3 license. Please be aware that the author of the `spreadsheet` gem [claims you need a commercial license](http://spreadsheet.ch/2014/10/24/using-ruby-spreadsheet-on-heroku-with-dynos/) to use it as part of a public-facing, closed-source service, an interpretation [at odds with the FSF's intent and interpretation of the license](http://www.gnu.org/licenses/gpl-faq.html#UnreleasedMods).
Expand Down
69 changes: 45 additions & 24 deletions lib/roo/xls/excel_2003_xml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def initialize(filename, options = {})
end
@doc = ::Roo::Utils.load_xml(@filename)
end
namespace = @doc.namespaces.select{|xmlns, urn| urn == 'urn:schemas-microsoft-com:office:spreadsheet'}.keys.last
namespace = @doc.namespaces.select { |_, urn| urn == 'urn:schemas-microsoft-com:office:spreadsheet' }.keys.last
@namespace = (namespace.nil? || namespace.empty?) ? 'ss' : namespace.split(':').last
super(filename, options)
@formula = {}
Expand Down Expand Up @@ -58,7 +58,7 @@ def formula(row, col, sheet = nil)
alias_method :formula?, :formula

class Font
attr_accessor :bold, :italic, :underline
attr_accessor :bold, :italic, :underline, :color, :name, :size

def bold?
@bold == '1'
Expand Down Expand Up @@ -191,31 +191,49 @@ def read_cells(sheet = nil)
validate_sheet!(sheet)
return if @cells_read[sheet]
sheet_found = false

@doc.xpath("/#{@namespace}:Workbook/#{@namespace}:Worksheet[@#{@namespace}:Name='#{sheet}']").each do |ws|
sheet_found = true
row = 1
column_styles = {}

# Column Styles
col = 1
column_attributes = {}
idx = 0
ws.xpath("./#{@namespace}:Table/#{@namespace}:Column").each do |c|
column_attributes[(idx += 1).to_s] = c['StyleID']
skip_to_col = c["#{@namespace}:Index"].to_i
col = skip_to_col if skip_to_col > 0
col_style_name = c["#{@namespace}:StyleID"]
column_styles[col] = col_style_name unless col_style_name.nil?
col += 1
end

# Rows
row = 1
ws.xpath("./#{@namespace}:Table/#{@namespace}:Row").each do |r|
skip_to_row = r['Index'].to_i
skip_to_row = r["#{@namespace}:Index"].to_i
row = skip_to_row if skip_to_row > 0
style_name = r['StyleID'] if r['StyleID']

# Excel uses a 'Span' attribute on a 'Row' to indicate the presence of
# empty rows to skip.
skip_next_rows = r["#{@namespace}:Span"].to_i

row_style_name = r["#{@namespace}:StyleID"]

# Cells
col = 1
r.xpath("./#{@namespace}:Cell").each do |c|
skip_to_col = c['Index'].to_i
skip_to_col = c["#{@namespace}:Index"].to_i
col = skip_to_col if skip_to_col > 0
if c['StyleID']
style_name = c['StyleID']
elsif
style_name ||= column_attributes[c['Index']]
end

skip_next_cols = c["#{@namespace}:MergeAcross"].to_i

cell_style_name = c["#{@namespace}:StyleID"]
style_name = cell_style_name || row_style_name || column_styles[col]

# Cell Data
c.xpath("./#{@namespace}:Data").each do |cell|
formula = cell['Formula']
value_type = cell["#{@namespace}:Type"].downcase.to_sym
v = cell.content
v = cell.content
str_v = v
case value_type
when :number
Expand All @@ -234,10 +252,9 @@ def read_cells(sheet = nil)
end
set_cell_values(sheet, col, row, 0, v, value_type, formula, cell, str_v, style_name)
end
col += 1
col += (skip_next_cols + 1)
end
row += 1
col = 1
row += (skip_next_rows + 1)
end
end
unless sheet_found
Expand All @@ -248,12 +265,16 @@ def read_cells(sheet = nil)

def read_styles
@doc.xpath("/#{@namespace}:Workbook/#{@namespace}:Styles/#{@namespace}:Style").each do |style|
style_id = style['ID']
@style_definitions[style_id] = Roo::Excel2003XML::Font.new
if font = style.at_xpath("./#{@namespace}:Font")
@style_definitions[style_id].bold = font['Bold']
@style_definitions[style_id].italic = font['Italic']
@style_definitions[style_id].underline = font['Underline']
style_id = style["#{@namespace}:ID"]
font = style.at_xpath("./#{@namespace}:Font")
unless font.nil?
@style_definitions[style_id] = Roo::Excel2003XML::Font.new
@style_definitions[style_id].bold = font["#{@namespace}:Bold"]
@style_definitions[style_id].italic = font["#{@namespace}:Italic"]
@style_definitions[style_id].underline = font["#{@namespace}:Underline"]
@style_definitions[style_id].color = font["#{@namespace}:Color"]
@style_definitions[style_id].name = font["#{@namespace}:FontName"]
@style_definitions[style_id].size = font["#{@namespace}:Size"]
end
end
end
Expand Down
202 changes: 196 additions & 6 deletions spec/lib/roo/xls/excel2003xml_spec.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,204 @@
require 'spec_helper'

describe Roo::Excel2003XML do
RSpec.describe Roo::Excel2003XML do
subject { instance }

let(:instance) { described_class.new(path) }
let(:path) { File.join(test_files_dir, test_file) }
let(:test_files_dir) { 'test/files' }

describe '.new' do
context 'with an xml file' do
let(:path) { 'test/files/datetime.xml' }
let(:test_file) { 'datetime.xml' }

it 'loads the file without error' do
expect { subject }.to_not raise_error
end
end
end

describe '#cell' do
subject { super().cell(cell[:row], cell[:col]) }

before(:each) { instance.default_sheet = instance.sheets.first }

context 'with merged cells' do
# See font_colors_screenshot_in_Mac_Excel_16.10.png for a screenshot of
# this how this file looks in Mac Excel 16.10.
let(:test_file) { 'font_colors.xml' }

context 'the top-left cell in a merged cell' do
let(:cell) { { :row => 1, :col => 'A' } }

it 'returns the contents of the merged cell' do
is_expected.to eq 'Roo::Xls Test of Font Colors'
end
end

context 'the cell to the right of the top-left cell in a merged cell' do
let(:cell) { { :row => 2, :col => 'A' } }
it { is_expected.to be_nil }
end

context 'the cell below the top-left cell in a merged cell' do
let(:cell) { { :row => 2, :col => 'B' } }
it { is_expected.to be_nil }
end

context 'the first cell to the right of an entire merged cell' do
let(:cell) { { :row => 1, :col => 'K' } }

it 'returns the expected contents' do
is_expected.to eq 'This entire COLUMN should be ITALIC and GREEN'
end
end

context 'the first cell below an entire merged cell' do
let(:cell) { { :row => 6, :col => 'A' } }

it 'returns the expected contents' do
is_expected.to eq '(The above should be font "Courier New", size 24)'
end
end
end
end

describe '#font' do
subject { super().font(cell[:row], cell[:col]) }

before(:each) { instance.default_sheet = instance.sheets.first }

# See font_colors_screenshot_in_Mac_Excel_16.10.png for a screenshot of
# this how this file looks in Mac Excel 16.10.
let(:test_file) { 'font_colors.xml' }

let(:default_attrs) do
{
:name => 'Arial',
:size => '12',
:color => '#000000',
:bold? => false,
:italic? => false,
:underline? => false
}
end

let(:expected_attrs) { default_attrs }

context 'with no font styling' do
let(:cell) { { :row => 6, :col => 'A' } }

it 'returns default font attributes' do
is_expected.to have_attributes(default_attrs)
end
end

context 'with styling set on an individual cell' do
context 'when set font name and size' do
let(:cell) { { :row => 1, :col => 'A' } }

it 'returns expected font attributes including name and size' do
expects = default_attrs.merge({ :name => 'Courier New', :size => '24' })
is_expected.to have_attributes(expects)
end
end

context 'when colored BLACK' do
let(:cell) { { :row => 7, :col => 'A' } }

it 'returns default font attributes (which include black)' do
is_expected.to have_attributes(default_attrs)
end
end

context 'when colored RED' do
let(:cell) { { :row => 8, :col => 'A' } }

it 'returns defaults plus red color' do
expects = default_attrs.merge({ :color => '#FF0000' })
is_expected.to have_attributes(expects)
end
end

context 'when colored BLUE' do
let(:cell) { { :row => 9, :col => 'A' } }

it 'returns defaults plus blue color' do
expects = default_attrs.merge({ :color => '#0066CC' })
is_expected.to have_attributes(expects)
end
end

context 'when BOLD' do
let(:cell) { { :row => 10, :col => 'A' } }

it 'returns defaults plus bold style' do
# somehow in Excel, this ended up "no color" rather than black...
expects = default_attrs.merge({ :bold? => true, :color => nil })
is_expected.to have_attributes(expects)
end
end

context 'when ITALIC' do
let(:cell) { { :row => 11, :col => 'A' } }

it 'returns defaults plus italic style' do
# somehow in Excel, this ended up "no color" rather than black...
expects = default_attrs.merge({ :italic? => true, :color => nil })
is_expected.to have_attributes(expects)
end
end

context 'when UNDERLINED' do
let(:cell) { { :row => 12, :col => 'A' } }

it 'returns defaults plus underlined style' do
# somehow in Excel, this ended up "no color" rather than black...
expects = default_attrs.merge({ :underline? => true, :color => nil })
is_expected.to have_attributes(expects)
end
end

context 'when BOLD, ITALIC, UNDERLINED, and colored PURPLE' do
let(:cell) { { :row => 13, :col => 'A' } }

it 'returns defaults plus bold, italic, underlined, and purple color' do
expects = default_attrs.merge({
:color => '#666699',
:bold? => true,
:italic? => true,
:underline? => true
})
is_expected.to have_attributes(expects)
end
end
end

context 'with styling set on an entire row' do
let(:row_style) do
default_attrs.merge({ :color => '#ED7D31', :bold? => true })
end

context 'when no cell styling' do
let(:cell) { { :row => 14, :col => 'L' } }

it 'returns the row style' do
is_expected.to have_attributes(row_style)
end
end
end

context 'with styling set on an entire column' do
let(:col_style) do
default_attrs.merge({ :color => '#00FF00', :italic? => true })
end

context 'when no cell styling' do
let(:cell) { { :row => 20, :col => 'K' } }

it 'loads the file' do
expect {
Roo::Excel2003XML.new(path)
}.to_not raise_error
it 'returns the column style' do
is_expected.to have_attributes(col_style)
end
end
end
end
Expand Down
Loading

0 comments on commit 7a7e715

Please sign in to comment.