rspreadsheet 0.2.15 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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