rspreadsheet 0.3 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/DEVEL_BLOG.md +3 -2
- data/GUIDE.md +9 -1
- data/README.md +1 -1
- data/lib/rspreadsheet/version.rb +1 -1
- data/lib/rspreadsheet/workbook.rb +107 -66
- data/rspreadsheet.gemspec +7 -9
- data/spec/image_spec.rb +5 -2
- data/spec/io_spec.rb +42 -16
- data/spec/spec_helper.rb +1 -0
- data/spec/testfile1.ods +0 -0
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88cd9c36fae6899582d7fed02831bb3b07d0e1a2
|
4
|
+
data.tar.gz: 3c1101531b2fb416d20968aea2d927f303be9269
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e90c357bf816cdfc28c6e35abab33558b20c20467a45ed187e9a2d2edda9db186052d66d37a988001feaa645b0010857d3cf5c9fea067b162553b044a867b1d1
|
7
|
+
data.tar.gz: ac8a7b2def1b7ace32c901e4ef0f71a1059f13b2076ea6e7fb81736b2610892095048fd514a2a70b51e9e784271de9c028b2d55d6e622405e9e1aee71c86b892
|
data/DEVEL_BLOG.md
CHANGED
@@ -75,8 +75,9 @@ RSpreadsheet.generate('pricelist.ods') do
|
|
75
75
|
## Release notes
|
76
76
|
|
77
77
|
2017-01
|
78
|
-
|
79
|
-
|
78
|
+
- file can be saced to any IO now, making it suitable for creating files on fly.
|
79
|
+
- basic image handling implemented (issue [#24](https://github.com/gorn/rspreadsheet/issues/24))
|
80
|
+
- bug corrected: inserted row was not empty, but rather copy of the row below.
|
80
81
|
|
81
82
|
## Developing this gem
|
82
83
|
|
data/GUIDE.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
## Guide to basic functionality
|
2
2
|
### Opening the file
|
3
3
|
|
4
|
-
You can open ODS file like this
|
4
|
+
You can open ODS file (OpenDocument Spreadsheet) like this
|
5
5
|
````ruby
|
6
6
|
@workbook = Rspreadsheet.open('./test.ods')
|
7
7
|
````
|
@@ -33,6 +33,14 @@ You can mix these two at will, for example like this
|
|
33
33
|
i = @sheet.images.first
|
34
34
|
i.move_to('100mm','99.98mm')
|
35
35
|
|
36
|
+
### Saving
|
37
|
+
The file needs to be saved after doing changes.
|
38
|
+
````ruby
|
39
|
+
@workbook.save
|
40
|
+
@workbook.save('new_filename.ods') # changes filename and saves
|
41
|
+
@workbook.save(any_io_object) # file can be saved to any IO like object as well
|
42
|
+
````
|
43
|
+
|
36
44
|
## Examples
|
37
45
|
|
38
46
|
* [basic functionality](https://gist.github.com/gorn/42e33d086d9b4fda10ec)
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# rspreadsheet
|
4
4
|
|
5
|
-
Manipulating spreadsheets with Ruby. Read, **modify**, write or create new OpenDocument Spreadsheet files from ruby code.
|
5
|
+
Manipulating spreadsheets with Ruby. Read, **modify**, write or create new OpenDocument Spreadsheet files from ruby code.
|
6
6
|
|
7
7
|
The gem allows you to acces your file and modify any cell of it, **without** touching the rest of the file, which makes it compatible with all advanced features of ODS files (both existing and future ones). You do not have to worry if it supports feature XY, if it does not, it won't touch it. This itself makes it distinct from most of [similar gems](#why-another-opendocument-spreadsheet-gem). Alhought this gem is still in beta stage I use in everyday and it works fine.
|
8
8
|
|
data/lib/rspreadsheet/version.rb
CHANGED
@@ -54,7 +54,7 @@ class Workbook
|
|
54
54
|
def initialize(afilename=nil)
|
55
55
|
@worksheets=[]
|
56
56
|
@filename = afilename
|
57
|
-
@content_xml = Zip::File.open(@filename ||
|
57
|
+
@content_xml = Zip::File.open(@filename || TEMPLATE_FILE_NAME) do |zip|
|
58
58
|
LibXML::XML::Document.io zip.get_input_stream(CONTENT_FILE_NAME)
|
59
59
|
end
|
60
60
|
@xmlnode = @content_xml.find_first('//office:spreadsheet')
|
@@ -63,87 +63,128 @@ class Workbook
|
|
63
63
|
end
|
64
64
|
end
|
65
65
|
|
66
|
-
# @param [String] Optional new filename
|
67
|
-
# Saves the worksheet. Optionally you can provide new filename.
|
68
|
-
|
69
|
-
def save(new_filename_or_io_object=nil)
|
70
|
-
@par = new_filename_or_io_object
|
71
|
-
if @filename.nil? and @par.nil? then raise 'New file should be named on first save.' end
|
72
|
-
|
73
|
-
if @par.kind_of? StringIO
|
74
|
-
@par.write(@content_xml.to_s(indent: false))
|
75
|
-
elsif @par.nil? or @par.kind_of? String
|
76
|
-
|
77
|
-
if @par.kind_of? String # the filename has changed
|
78
|
-
# first copy the original file to new location (or template if it is a new file)
|
79
|
-
FileUtils.cp(@filename || File.dirname(__FILE__)+'/empty_file_template.ods', @par)
|
80
|
-
@filename = @par
|
81
|
-
end
|
82
|
-
|
83
66
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
if @ifname.nil?
|
96
|
-
@ifname = image.internal_filename = Rspreadsheet::Tools.get_unused_filename(zip,'Pictures/',File.extname(image.original_filename))
|
97
|
-
end
|
98
|
-
raise 'Could not set up internal_filename correctly.' if @ifname.nil?
|
99
|
-
|
100
|
-
# save it to zip file
|
101
|
-
zip.add(@ifname, image.original_filename)
|
102
|
-
|
103
|
-
# make sure it is in manifest
|
104
|
-
if @manifest_xml.find("//manifest:file-entry[@manifest:full-path='#{@ifname}']").empty?
|
105
|
-
node = Tools.prepare_ns_node('manifest','file-entry')
|
106
|
-
Tools.set_ns_attribute(node,'manifest','full-path',@ifname)
|
107
|
-
Tools.set_ns_attribute(node,'manifest','media-type',image.mime)
|
108
|
-
@manifest_xml.find_first("//manifest:manifest") << node
|
109
|
-
end
|
110
|
-
end
|
67
|
+
# @param [String] Optional new filename
|
68
|
+
# Saves the worksheet. Optionally you can provide new filename or IO stream to which the file should be saved.
|
69
|
+
def save(io=nil)
|
70
|
+
case
|
71
|
+
when @filename.nil? && io.nil?
|
72
|
+
raise 'New file should be named on first save.'
|
73
|
+
when @filename.nil? && (io.kind_of?(String) || io.kind_of?(File) || io.kind_of?(IO) || io.kind_of?(StringIO))
|
74
|
+
Zip::File.open(TEMPLATE_FILE_NAME) do |empty_template_zip| # open empty_template file
|
75
|
+
write_zip_to(io) do |output_zip| # open output stream of file
|
76
|
+
copy_internally_without_content_and_manifest(empty_template_zip,output_zip) # copy empty_template internals
|
77
|
+
update_manifest_and_content_xml(empty_template_zip,output_zip) # update xmls + pictures
|
111
78
|
end
|
112
79
|
end
|
113
|
-
|
114
|
-
|
115
|
-
|
80
|
+
when @filename.kind_of?(String) && io.nil?
|
81
|
+
write_zip_to(@filename) do |input_and_output_zip| # open old file
|
82
|
+
update_manifest_and_content_xml(input_and_output_zip,input_and_output_zip) # input and output are identical
|
116
83
|
end
|
117
84
|
|
118
|
-
|
119
|
-
|
85
|
+
when @filename.kind_of?(String) && (io.kind_of?(String) || io.kind_of?(File))
|
86
|
+
io = io.path if io.kind_of?(File) # convert file to its filename
|
87
|
+
FileUtils.cp(@filename , io) # copy file externally
|
88
|
+
@filename = io # remember new name
|
89
|
+
save_to_io(nil) # continue modyfying file on spot
|
90
|
+
|
91
|
+
when @filename.kind_of?(String) && (io.kind_of?(IO) || io.kind_of?(StringIO))
|
92
|
+
Zip::File.open(@filename) do | old_zip | # open old file
|
93
|
+
write_zip_to(io) do |output_zip_stream| # open output stream
|
94
|
+
copy_internally_without_content_and_manifest(old_zip,output_zip_stream) # copy the old internals
|
95
|
+
update_manifest_and_content_xml(old_zip,output_zip_stream) # update xmls + pictures
|
96
|
+
end
|
120
97
|
end
|
121
|
-
|
98
|
+
# rewind result
|
99
|
+
io.rewind
|
100
|
+
else
|
122
101
|
end
|
123
102
|
end
|
103
|
+
alias :to_io :save
|
104
|
+
alias :save_to_io :save
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def update_manifest_and_content_xml(input_zip,output_zip)
|
109
|
+
update_manifest_xml(input_zip,output_zip)
|
110
|
+
update_content_xml(output_zip)
|
111
|
+
end
|
124
112
|
|
125
|
-
|
126
|
-
def
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
113
|
+
|
114
|
+
def update_content_xml(zip)
|
115
|
+
save_entry_to_zip(zip,CONTENT_FILE_NAME,@content_xml.to_s(indent: false))
|
116
|
+
end
|
117
|
+
|
118
|
+
def update_manifest_xml(input_zip,output_zip)
|
119
|
+
# read manifest
|
120
|
+
@manifest_xml = LibXML::XML::Document.io input_zip.get_input_stream(MANIFEST_FILE_NAME)
|
121
|
+
|
122
|
+
# save all pictures - iterate through sheets and pictures and check if they are saved and if not, save them
|
123
|
+
@worksheets.each do |sheet|
|
124
|
+
sheet.images.each do |image|
|
125
|
+
# check if it is saved
|
126
|
+
@ifname = image.internal_filename
|
127
|
+
if @ifname.nil? or input_zip.find_entry(@ifname).nil?
|
128
|
+
# if it does not have name -> make up unused name
|
129
|
+
if @ifname.nil?
|
130
|
+
@ifname = image.internal_filename = Rspreadsheet::Tools.get_unused_filename(input_zip,'Pictures/',File.extname(image.original_filename))
|
135
131
|
end
|
132
|
+
raise 'Could not set up internal_filename correctly.' if @ifname.nil?
|
133
|
+
raise 'This should not happen' if image.original_filename.nil?
|
134
|
+
|
135
|
+
# save it to zip file
|
136
|
+
save_entry_to_zip(output_zip, @ifname, File.open(image.original_filename,'r').read)
|
137
|
+
|
138
|
+
# make sure it is in manifest
|
139
|
+
if @manifest_xml.find("//manifest:file-entry[@manifest:full-path='#{@ifname}']").empty?
|
140
|
+
node = Tools.prepare_ns_node('manifest','file-entry')
|
141
|
+
Tools.set_ns_attribute(node,'manifest','full-path',@ifname)
|
142
|
+
Tools.set_ns_attribute(node,'manifest','media-type',image.mime)
|
143
|
+
@manifest_xml.find_first("//manifest:manifest") << node
|
144
|
+
end
|
145
|
+
end
|
136
146
|
end
|
147
|
+
end
|
137
148
|
|
138
|
-
|
139
|
-
|
149
|
+
# write manifest
|
150
|
+
save_entry_to_zip(output_zip, MANIFEST_FILE_NAME, @manifest_xml.to_s)
|
151
|
+
end
|
152
|
+
|
153
|
+
def copy_internally_without_content_and_manifest(input_zip,output_zip)
|
154
|
+
input_zip.each do |entry|
|
155
|
+
next unless entry.file?
|
156
|
+
next if entry.name == CONTENT_FILE_NAME || entry.name == MANIFEST_FILE_NAME
|
157
|
+
save_entry_to_zip(output_zip, entry.name, entry.get_input_stream.read)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def save_entry_to_zip(zip,internal_filename,contents)
|
162
|
+
if zip.kind_of? Zip::File
|
163
|
+
# raise [internal_filename,contents].inspect unless File.exists?(internal_filename)
|
164
|
+
zip.get_output_stream(internal_filename) do |f|
|
165
|
+
f.write contents
|
166
|
+
end
|
167
|
+
else
|
168
|
+
zip.put_next_entry(internal_filename)
|
169
|
+
zip.write(contents)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def write_zip_to(io,&block)
|
174
|
+
if io.kind_of? File or io.kind_of? String
|
175
|
+
Zip::File.open(io, 'br+') do |zip|
|
176
|
+
yield zip
|
177
|
+
end
|
178
|
+
elsif io.kind_of? StringIO # or io.kind_of? IO
|
179
|
+
Zip::OutputStream.write_buffer(io) do |zip|
|
180
|
+
yield zip
|
181
|
+
end
|
140
182
|
end
|
141
183
|
end
|
142
|
-
alias :to_io :save_to_io
|
143
184
|
|
144
|
-
private
|
145
185
|
CONTENT_FILE_NAME = 'content.xml'
|
146
|
-
|
186
|
+
MANIFEST_FILE_NAME = 'META-INF/manifest.xml'
|
187
|
+
TEMPLATE_FILE_NAME = (File.dirname(__FILE__)+'/empty_file_template.ods').freeze
|
147
188
|
def register_worksheet(worksheet)
|
148
189
|
index = worksheets_count+1
|
149
190
|
@worksheets[index-1]=worksheet
|
data/rspreadsheet.gemspec
CHANGED
@@ -30,19 +30,17 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.add_runtime_dependency 'andand', '~>1.3'
|
31
31
|
|
32
32
|
# development dependencies
|
33
|
-
spec.add_development_dependency "bundler",
|
33
|
+
spec.add_development_dependency "bundler", '~> 1.5'
|
34
34
|
spec.add_development_dependency "rake", '~>0.9'
|
35
35
|
# testig - see http://bit.ly/1n5yM51
|
36
|
-
spec.add_development_dependency "rspec", '~>2' #
|
37
|
-
spec.add_development_dependency 'pry-nav', '~>0' # enables pry 'next', 'step' commands
|
36
|
+
spec.add_development_dependency "rspec", '~>2.0' # running tests
|
37
|
+
spec.add_development_dependency 'pry-nav', '~>0.0' # enables pry 'next', 'step' commands
|
38
|
+
spec.add_development_dependency "coveralls", '~>0.7' # inspecting coverage of tests
|
38
39
|
|
39
40
|
# optional and testing
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
# ruby_dep starts to require ruby 2.2.5 which raises errors with ruby 1.9.3
|
44
|
-
# spec.add_development_dependency "guard", '~>2.13'
|
45
|
-
# spec.add_development_dependency "guard-rspec", '~>4.6'
|
41
|
+
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.2.5')
|
42
|
+
spec.add_development_dependency "guard", '~>2.13'
|
43
|
+
spec.add_development_dependency "guard-rspec", '~>4.6'
|
46
44
|
end
|
47
45
|
|
48
46
|
# spec.add_development_dependency 'equivalent-xml' # implementing xml diff
|
data/spec/image_spec.rb
CHANGED
@@ -2,8 +2,8 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Rspreadsheet::Image do
|
4
4
|
before do
|
5
|
-
@testfile_filename =
|
6
|
-
@tmp_testfile_filename = '/tmp/testfile2.ods'
|
5
|
+
@testfile_filename = $test_filename_images
|
6
|
+
@tmp_testfile_filename = '/tmp/testfile2-image.ods'
|
7
7
|
File.delete(@tmp_testfile_filename) if File.exists?(@tmp_testfile_filename) # delete temp file
|
8
8
|
|
9
9
|
@testimage_filename = './spec/test-image-blue.png'
|
@@ -12,6 +12,9 @@ describe Rspreadsheet::Image do
|
|
12
12
|
@sheet = @workbook.worksheets(1)
|
13
13
|
@sheet2 = @workbook.worksheets(2)
|
14
14
|
end
|
15
|
+
after do
|
16
|
+
File.delete(@tmp_testfile_filename) if File.exists?(@tmp_testfile_filename) # delete temp file
|
17
|
+
end
|
15
18
|
it 'is accesible when included in spreadsheet', :xpending do
|
16
19
|
@sheet.images_count.should == 1
|
17
20
|
@image = @sheet.images(1)
|
data/spec/io_spec.rb
CHANGED
@@ -3,48 +3,74 @@ using ClassExtensions if RUBY_VERSION > '2.1'
|
|
3
3
|
|
4
4
|
describe Rspreadsheet do
|
5
5
|
before do
|
6
|
-
@tmp_filename = '/tmp/testfile.ods'
|
6
|
+
@tmp_filename = '/tmp/testfile.ods'
|
7
7
|
File.delete(@tmp_filename) if File.exists?(@tmp_filename) # delete temp file
|
8
8
|
end
|
9
9
|
|
10
10
|
|
11
|
-
it '
|
11
|
+
it 'when saved to file is identical' do
|
12
12
|
spreadsheet = Rspreadsheet.new($test_filename) # open a file
|
13
13
|
spreadsheet.save(@tmp_filename) # and save spreadsheet as temp file
|
14
14
|
|
15
15
|
# now compare content saved file to original
|
16
|
-
|
16
|
+
contents_of_files_should_be_identical($test_filename,@tmp_filename).should == true
|
17
17
|
end
|
18
18
|
|
19
|
-
it '
|
19
|
+
it 'when saved to file via save_to_io is identical' do
|
20
|
+
@tmp_filename = '/tmp/testfile4.ods'
|
20
21
|
spreadsheet = Rspreadsheet.new($test_filename) # open a file
|
22
|
+
File.open(@tmp_filename, 'w+') do |file|
|
23
|
+
spreadsheet.save_to_io(file) # and save spreadsheet as temp file
|
24
|
+
end
|
25
|
+
|
26
|
+
# now compare content saved file to original
|
27
|
+
contents_of_files_should_be_identical($test_filename,@tmp_filename)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'can be saved to IO object' do
|
31
|
+
@tmp_filename = '/tmp/testfile5.ods'
|
32
|
+
spreadsheet = Rspreadsheet.new($test_filename_images) # open a file
|
21
33
|
|
22
34
|
stringio = StringIO.new
|
23
35
|
spreadsheet.save_to_io(stringio)
|
24
|
-
|
36
|
+
# stringio.size.should > 300000
|
25
37
|
|
26
38
|
# save it to temp file
|
27
39
|
File.open(@tmp_filename, "w") do |f|
|
28
40
|
f.write stringio.read
|
29
41
|
end
|
30
42
|
|
31
|
-
|
43
|
+
contents_of_files_should_be_identical($test_filename_images,@tmp_filename)
|
32
44
|
end
|
45
|
+
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
def xml_from_entry(zip,entryname)
|
50
|
+
LibXML::XML::Document.io(zip.get_input_stream(entryname))
|
33
51
|
end
|
34
52
|
|
35
|
-
def
|
36
|
-
|
37
|
-
|
53
|
+
def contents_of_files_should_be_identical(filename1,filename2)
|
54
|
+
Zip::File.open(filename1) do |zip|
|
55
|
+
@content1_xml = xml_from_entry(zip,'content.xml')
|
56
|
+
@manifest1_xml = xml_from_entry(zip,'META-INF/manifest.xml')
|
57
|
+
@images1_count = zip.glob('Pictures/**').count
|
38
58
|
end
|
39
|
-
|
40
|
-
|
59
|
+
|
60
|
+
Zip::File.open(filename2) do |zip|
|
61
|
+
@content2_xml = xml_from_entry(zip,'content.xml')
|
62
|
+
@manifest2_xml = xml_from_entry(zip,'META-INF/manifest.xml')
|
63
|
+
@images2_count = zip.glob('Pictures/**').count
|
41
64
|
end
|
42
|
-
|
65
|
+
|
66
|
+
@images1_count.should == @images2_count
|
67
|
+
xmls_should_be_identical(@content1_xml,@content2_xml)
|
68
|
+
xmls_should_be_identical(@manifest1_xml,@manifest2_xml)
|
43
69
|
end
|
44
70
|
|
45
|
-
def
|
46
|
-
|
47
|
-
|
71
|
+
def xmls_should_be_identical(xml1,xml2)
|
72
|
+
xml2.root.first_diff(xml1.root).should be_nil
|
73
|
+
xml1.root.first_diff(xml2.root).should be_nil
|
48
74
|
|
49
|
-
|
75
|
+
xml1.root.should == xml2.root
|
50
76
|
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/testfile1.ods
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspreadsheet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jakub A.Těšínský
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-01-
|
11
|
+
date: 2017-01-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rubyzip
|
@@ -72,28 +72,28 @@ dependencies:
|
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: '2'
|
75
|
+
version: '2.0'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '2'
|
82
|
+
version: '2.0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: pry-nav
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
89
|
+
version: '0.0'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
96
|
+
version: '0.0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: coveralls
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|