rspreadsheet 0.2.15 → 0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -117,7 +117,8 @@ module Tools
117
117
  'loext'=>"urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
118
118
  'field'=>"urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0",
119
119
  'formx'=>"urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0",
120
- 'css3t'=>"http://www.w3.org/TR/css3-text/"
120
+ 'css3t'=>"http://www.w3.org/TR/css3-text/",
121
+ 'manifest'=>"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
121
122
  }
122
123
  if @pomnode.nil?
123
124
  @pomnode = LibXML::XML::Node.new('xxx')
@@ -148,6 +149,7 @@ module Tools
148
149
  end
149
150
  def self.get_ns_attribute(node,ns_prefix,key,default=:undefined_default)
150
151
  if default==:undefined_default
152
+ raise 'Nil does not have any attributes' if node.nil?
151
153
  node.attributes.get_attribute_ns(Tools.get_namespace(ns_prefix).href,key)
152
154
  else
153
155
  node.nil? ? default : node.attributes.get_attribute_ns(Tools.get_namespace(ns_prefix).href,key) || default
@@ -167,6 +169,23 @@ module Tools
167
169
  def self.prepare_ns_node(ns_prefix,nodename,value=nil)
168
170
  LibXML::XML::Node.new(nodename,value, Tools.get_namespace(ns_prefix))
169
171
  end
172
+ def self.insert_as_first_node_child(node,subnode)
173
+ if node.first?
174
+ node.first.prev = subnode
175
+ else
176
+ node << subnode
177
+ end
178
+ end
179
+
180
+
181
+ def self.get_unused_filename(zip,prefix, extension)
182
+ (1000..9999).each do |ndx|
183
+ filename = prefix + ndx.to_s + ((Time.now.to_r*1000000000).to_i.to_s(16)) + extension
184
+ return filename if zip.find_entry(filename).nil?
185
+ end
186
+ raise 'Could not get unused filename within sane times of iterations'
187
+ end
188
+
170
189
  end
171
190
 
172
191
  end
@@ -1,3 +1,3 @@
1
1
  module Rspreadsheet
2
- VERSION = "0.2.15"
2
+ VERSION = "0.3"
3
3
  end
@@ -9,15 +9,16 @@ class Workbook
9
9
 
10
10
  #@!group Worskheets methods
11
11
  def create_worksheet_from_node(source_node)
12
- sheet = Worksheet.new(source_node)
12
+ sheet = Worksheet.new(source_node,self)
13
13
  register_worksheet(sheet)
14
14
  return sheet
15
15
  end
16
16
  def create_worksheet(name = "Sheet#{worksheets_count+1}")
17
- sheet = Worksheet.new(name)
17
+ sheet = Worksheet.new(name,self)
18
18
  register_worksheet(sheet)
19
19
  return sheet
20
20
  end
21
+ alias :add_worksheet :create_worksheet
21
22
  # @return [Integer] number of sheets in the workbook
22
23
  def worksheets_count; @worksheets.length end
23
24
  # @return [String] names of sheets in the workbook
@@ -28,10 +29,10 @@ class Workbook
28
29
  case index_or_name
29
30
  when Integer then begin
30
31
  case index_or_name
31
- when 0 then nil
32
+ when 0 then nil
32
33
  when 1..Float::INFINITY then @worksheets[index_or_name-1]
33
34
  when -Float::INFINITY..-1 then @worksheets[index_or_name] # zaporne indexy znamenaji pocitani zezadu
34
- end
35
+ end
35
36
  end
36
37
  when String then @worksheets.select{|ws| ws.name == index_or_name}.first
37
38
  when NilClass then nil
@@ -42,47 +43,107 @@ class Workbook
42
43
  alias :sheet :worksheets
43
44
  alias :sheets :worksheets
44
45
  def [](index_or_name); self.worksheets(index_or_name) end
45
- #@!group Loading and saving related methods
46
+ #@!group Loading and saving related methods
47
+
48
+ # @return Mime of the file
49
+ def mime; 'application/vnd.oasis.opendocument.spreadsheet'.freeze end
50
+ # @return [String] Prefered file extension
51
+ def mime_preferred_extension; 'ods'.freeze end
52
+ alias :mime_default_extension :mime_preferred_extension
53
+
46
54
  def initialize(afilename=nil)
47
55
  @worksheets=[]
48
56
  @filename = afilename
49
- @content_xml = Zip::File.open(@filename || File.dirname(__FILE__)+'/empty_file_template.ods') do |zip|
50
- LibXML::XML::Document.io zip.get_input_stream('content.xml')
57
+ @content_xml = Zip::File.open(@filename || TEMPLATE_FILE) do |zip|
58
+ LibXML::XML::Document.io zip.get_input_stream(CONTENT_FILE_NAME)
51
59
  end
52
60
  @xmlnode = @content_xml.find_first('//office:spreadsheet')
53
61
  @xmlnode.find('./table:table').each do |node|
54
62
  create_worksheet_from_node(node)
55
63
  end
56
64
  end
65
+
57
66
  # @param [String] Optional new filename
58
67
  # Saves the worksheet. Optionally you can provide new filename.
68
+
59
69
  def save(new_filename_or_io_object=nil)
60
- if @filename.nil? and new_filename_or_io_object.nil? then raise 'New file should be named on first save.' end
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
61
72
 
62
- if new_filename_or_io_object.kind_of? StringIO
63
- new_filename_or_io_object.write(@content_xml.to_s(indent: false))
64
- elsif new_filename_or_io_object.nil? or new_filename_or_io_object.kind_of? String
73
+ if @par.kind_of? StringIO
74
+ @par.write(@content_xml.to_s(indent: false))
75
+ elsif @par.nil? or @par.kind_of? String
65
76
 
66
- if new_filename_or_io_object.kind_of? String # the filename has changed
77
+ if @par.kind_of? String # the filename has changed
67
78
  # first copy the original file to new location (or template if it is a new file)
68
- FileUtils.cp(@filename || File.dirname(__FILE__)+'/empty_file_template.ods', new_filename_or_io_object)
69
- @filename = new_filename_or_io_object
79
+ FileUtils.cp(@filename || File.dirname(__FILE__)+'/empty_file_template.ods', @par)
80
+ @filename = @par
70
81
  end
82
+
83
+
71
84
  Zip::File.open(@filename) do |zip|
72
- # it is easy, because @xmlnode in in sync with contents all the time
85
+ # open manifest
86
+ @manifest_xml = LibXML::XML::Document.io zip.get_input_stream('META-INF/manifest.xml')
87
+
88
+ # save all pictures - iterate through sheets and pictures and check if they are saved and if not, save them
89
+ @worksheets.each do |sheet|
90
+ sheet.images.each do |image|
91
+ # check if it is saved
92
+ @ifname = image.internal_filename
93
+ if @ifname.nil? or zip.find_entry(@ifname).nil?
94
+ # if it does not have name -> make up unused name
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
111
+ end
112
+ end
113
+
73
114
  zip.get_output_stream('content.xml') do |f|
74
115
  f.write @content_xml.to_s(:indent => false)
75
116
  end
117
+
118
+ zip.get_output_stream('META-INF/manifest.xml') do |f|
119
+ f.write @manifest_xml.to_s
120
+ end
76
121
  end
77
122
  end
78
123
  end
79
- # @return Mime of the file
80
- def mime; 'application/vnd.oasis.opendocument.spreadsheet' end
81
- # @return [String] Prefered file extension
82
- def mime_preferred_extension; 'ods' end
83
- alias :mime_default_extension :mime_preferred_extension
124
+
125
+ # Saves the worksheet to IO stream.
126
+ def save_to_io(io = ::StringIO.new)
127
+ ::Zip::OutputStream.write_buffer(io) do |output|
128
+ ::Zip::File.open(TEMPLATE_FILE) do |input|
129
+ input.
130
+ select { |entry| entry.file? }.
131
+ select { |entry| entry.name != CONTENT_FILE_NAME }.
132
+ each do |entry|
133
+ output.put_next_entry(entry.name)
134
+ output.write(entry.get_input_stream.read)
135
+ end
136
+ end
137
+
138
+ output.put_next_entry(CONTENT_FILE_NAME)
139
+ output.write(@content_xml.to_s(indent: false))
140
+ end
141
+ end
142
+ alias :to_io :save_to_io
84
143
 
85
144
  private
145
+ CONTENT_FILE_NAME = 'content.xml'
146
+ TEMPLATE_FILE = (File.dirname(__FILE__)+'/empty_file_template.ods').freeze
86
147
  def register_worksheet(worksheet)
87
148
  index = worksheets_count+1
88
149
  @worksheets[index-1]=worksheet
@@ -1,17 +1,19 @@
1
1
  require 'rspreadsheet/row'
2
2
  require 'rspreadsheet/column'
3
+ require 'rspreadsheet/image'
3
4
  require 'rspreadsheet/tools'
5
+ require 'helpers/class_extensions'
4
6
  # require 'forwardable'
5
7
 
6
8
  module Rspreadsheet
7
9
 
8
10
  class Worksheet
9
- include XMLTiedArray
11
+ include XMLTiedArray_WithRepeatableItems
10
12
  attr_accessor :xmlnode
11
13
  def subitem_xml_options; {:xml_items_node_name => 'table-row', :xml_repeated_attribute => 'number-rows-repeated'} end
12
14
 
13
- def initialize(xmlnode_or_sheet_name)
14
- @itemcache = Hash.new #TODO: move to module XMLTiedArray
15
+ def initialize(xmlnode_or_sheet_name,workbook) # workbook is here ONLY because of inserting images - to find unique name - it would be much better if it should bot be there
16
+ initialize_xml_tied_array
15
17
  # set up the @xmlnode according to parameter
16
18
  case xmlnode_or_sheet_name
17
19
  when LibXML::XML::Node
@@ -29,31 +31,50 @@ class Worksheet
29
31
  def name=(value); Tools.set_ns_attribute(@xmlnode,'table','name', value) end
30
32
 
31
33
  def rowxmlnode(rowi)
32
- find_my_subnode_respect_repeated(rowi, {:xml_items_node_name => 'table-row', :xml_repeated_attribute => 'number-rows-repeated'})
34
+ my_subnode(rowi)
33
35
  end
34
36
 
35
37
  def first_unused_row_index
36
- find_first_unused_index_respect_repeated({:xml_items_node_name => 'table-row', :xml_repeated_attribute => 'number-rows-repeated'})
38
+ first_unused_subitem_index
37
39
  end
38
40
 
39
41
  def add_row_above(arowi)
40
- add_empty_subitem_before(arowi)
42
+ insert_new_empty_subitem_before(arowi)
41
43
  end
42
44
 
43
- def insert_cell_before(arowi,acoli)
45
+ def insert_cell_before(arowi,acoli) # TODO: maybe move this to row level
44
46
  detach_row_in_xml(arowi)
45
- rows(arowi).add_empty_subitem_before(acoli)
47
+ rows(arowi).insert_new_item(acoli)
46
48
  end
47
49
 
48
50
  def detach_row_in_xml(rowi)
49
- return detach_my_subnode_respect_repeated(rowi, {:xml_items_node_name => 'table-row', :xml_repeated_attribute => 'number-rows-repeated'})
51
+ return detach_my_subnode_respect_repeated(rowi)
50
52
  end
51
53
 
52
54
  def nonemptycells
53
55
  used_rows_range.collect{ |rowi| rows(rowi).nonemptycells }.flatten
54
56
  end
55
57
 
56
- #@!group XMLTiedArray connected methods
58
+ #@!group images
59
+ def worksheet_images
60
+ @worksheet_images ||= WorksheetImages.new(self)
61
+ end
62
+ def images_count
63
+ worksheet_images.size
64
+ end
65
+ def images(*params)
66
+ worksheet_images.subitems(*params)
67
+ end
68
+ def insert_image(filename,mime='image/png')
69
+ worksheet_images.insert_image(filename,mime)
70
+ end
71
+ def insert_image_to(x,y,filename,mime='image/png')
72
+ img = insert_image(filename,mime)
73
+ img.move_to(x,y)
74
+ img
75
+ end
76
+
77
+ #@!group XMLTiedArray_WithRepeatableItems connected methods
57
78
  def rows(*params); subitems(*params) end
58
79
  alias :row :rows
59
80
  def prepare_subitem(rowi); Row.new(self,rowi) end
@@ -0,0 +1,179 @@
1
+ require 'helpers/class_extensions'
2
+
3
+ module Rspreadsheet
4
+
5
+ using ClassExtensions if RUBY_VERSION > '2.1'
6
+
7
+ # @private
8
+ class XMLTied
9
+ def xml
10
+ xmlnode.to_s
11
+ end
12
+ end
13
+
14
+ # Abstract class representing and array which is tied to a particular element of XML file.
15
+ # It uses cashing to make access to array more effective. Implements the following methods:
16
+ #
17
+ # * subitems(index) - returns subitem object on index
18
+ # * subitems - returns array of all subitems. Please note that first item is always nil so
19
+ # the array can be accessed using 1-based indexes.
20
+ #
21
+ # Importer must provide:
22
+ #
23
+ # * prepare_subitem(aindex) - must return newly created object representing item on aindex
24
+ # * delete - ???
25
+ # * xmlnode - must return xmlnode to which the array is tied. If speed is not a concern,
26
+ # consider not cashing it into variable, but finding it through document or parent.
27
+ # This prevents "broken" links. Sometimes when array is empty, the node does note
28
+ # necessarily exists. That is fine, XMLTiedArray behaves correctly even with nil xmlnode,
29
+ # of course util you want to insert something. If this may happens, importer must
30
+ # provide method prepare_empty_xmlnode which prepares (and returns) empty xml node.
31
+ # It is lazy called, as late as possible.
32
+ # * subitem_xml_options - returns hash of options used to locate subitems in xml (TODO: rewrite in clear way)
33
+ # * intilize must call initialize_xml_tied_array
34
+ #
35
+ # Terminology
36
+ # * item, subitem is object from @itemcache (quite often subclass of XMLTiedItem)
37
+ # * node, subnode is LibXML::XML::Node object
38
+ #
39
+ # this class is made to be included.
40
+ #
41
+ # Note for developers:
42
+ # Beware that the implementation of methods needs to be done in a way that it continues to
43
+ # work when items are "repeatable" - see XMLTiedArray_WithRepeatableItems. When impractical or impossible
44
+ # please implement the corresponding method in XMLTiedArray_WithRepeatableItems or at least override it there
45
+ # and make it raise exception.
46
+ #
47
+ # @private
48
+ module XMLTiedArray
49
+ attr_reader :itemcache
50
+
51
+ def initialize_xml_tied_array
52
+ @itemcache = Hash.new
53
+ end
54
+
55
+ # @!group accessing items
56
+
57
+ # Returns item with index aindex
58
+ def subitem(aindex)
59
+ aindex = aindex.to_i
60
+ if aindex.to_i<=0
61
+ raise 'Item index should be greater then 0' if Rspreadsheet.raise_on_negative_coordinates
62
+ nil
63
+ else
64
+ @itemcache[aindex] ||= prepare_subitem(aindex)
65
+ end
66
+ end
67
+
68
+ def last
69
+ subitem(size)
70
+ end
71
+
72
+ # Returns an array of subitems (when called without parameter) or an item on paricular index (when called with parameter).
73
+ def subitems(*params)
74
+ case params.length
75
+ when 0 then subitems_array
76
+ when 1 then subitem(params[0])
77
+ else raise Exception.new('Wrong number of arguments.')
78
+ end
79
+ end
80
+
81
+ # Returns array of subitems (repeated friendly)
82
+ def subitems_array
83
+ (1..self.size).collect do |i|
84
+ subitem(i)
85
+ end
86
+ end
87
+
88
+ # Number of subitems
89
+ def size; first_unused_subitem_index-1 end
90
+ alias :lenght :size
91
+
92
+ # Finds first unused subitem index
93
+ def first_unused_subitem_index
94
+ (1 + xmlsubnodes.sum { |node| how_many_times_node_is_repeated(node) }).to_i
95
+ end
96
+
97
+ # @!group inserting new items
98
+ # Inserts empty subitem at the index position. Item currently on this position and all items
99
+ # after are shifter by index one.
100
+ def insert_new_item(aindex)
101
+ @itemcache.keys.sort.reverse.select{|i| i>=aindex }.each do |i|
102
+ @itemcache[i+1]=@itemcache.delete(i)
103
+ @itemcache[i+1]._shift_by(1)
104
+ end
105
+ insert_new_empty_subnode_before(aindex) # nyní vlož node do xml
106
+ @itemcache[aindex] = subitem(aindex)
107
+ end
108
+ alias :insert_new_empty_subitem_before :insert_new_item
109
+
110
+ def push_new
111
+ insert_new_item(first_unused_subitem_index)
112
+ end
113
+
114
+ # @!group other subitems methods
115
+ # This is used (i.e. in first_unused_subitem_index) so it is flexible and can be reused in XMLTiedArray_WithRepeatableItems
116
+ # @private
117
+ def how_many_times_node_is_repeated(node); 1 end
118
+
119
+ # # @!supergroup XML STRUCTURE internal handling methods #######################################
120
+
121
+ # # @!group accessing subnodes
122
+ # returns xmlnode with index
123
+ # DOES not respect repeated_attribute
124
+ def my_subnode(aindex)
125
+ raise 'Using method which does not respect repeated_attribute with options that are using it. You probably donot want to do that.' unless subitem_xml_options[:xml_repeated_attribute].nil?
126
+ return xmlsubnodes[aindex-1]
127
+ end
128
+
129
+ # @!group inserting new subnodes TODO: refactor out repeatable connected code
130
+ def insert_new_empty_subnode_before(aindex)
131
+ node_after = my_subnode(aindex)
132
+
133
+ if !node_after.nil?
134
+ node_after.prev = prepare_empty_subnode
135
+ return node_after.prev
136
+ elsif aindex==size+1
137
+ # check whether xmlnode is ready for insetion
138
+ if xmlnode.nil?
139
+ prepare_empty_xmlnode
140
+ if xmlnode.nil?
141
+ raise 'Attempted call prepare_empty_xmlnode, but it did not created xmlnode correctly (it is still nil).'
142
+ end
143
+ end
144
+ # do the insertion
145
+ xmlnode << prepare_empty_subnode
146
+ return xmlnode.last
147
+ else
148
+ raise IndexError.new("Index #{aindex} out of bounds (1..#{self.size})")
149
+ end
150
+ end
151
+
152
+ def prepare_empty_subnode
153
+ Tools.prepare_ns_node(
154
+ subitem_xml_options[:xml_items_node_namespace] || 'table',
155
+ subitem_xml_options[:xml_items_node_name]
156
+ )
157
+ end
158
+
159
+ # @!supergroup internal procedures dealing solely with xml structure ==========
160
+
161
+ # importer must provide this only if it may happen that xmlnode is empty AND we will want to insert subitems
162
+ def prepare_empty_xmlnode
163
+ raise 'xmlnode is empty and I do not know how to create empty xmlnode. Please provide prepare_empty_xmlnode method in your object.'
164
+ end
165
+
166
+ # @!group finding and accessing subnodes
167
+ # array containing subnodes of xmlnode which represent subitems
168
+ def xmlsubnodes
169
+ return [] if xmlnode.nil?
170
+ so = subitem_xml_options[:xml_items_node_name]
171
+
172
+ xmlnode.elements.select do |node|
173
+ node.andand.name == so
174
+ end
175
+ end
176
+
177
+ end
178
+
179
+ end