# -*- coding: utf-8 -*- module Writexlsx ############################################################################### # # Shape - A class for writing Excel shapes. # # Used in conjunction with WriteXLSX. # # Copyright 2000-2012, John McNamara, jmcnamara@cpan.org # Converted to ruby by Hideo NAKAMURA, nakamura.hideo@gmail.com # class Shape include Writexlsx::Utility attr_reader :edit_as, :type, :drawing attr_reader :tx_box, :fill, :line, :format attr_reader :align, :valign, :anchor attr_accessor :name, :connect, :type, :id, :start, :end, :rotation attr_accessor :flip_h, :flip_v, :adjustments, :palette, :text, :stencil attr_accessor :row_start, :row_end, :column_start, :column_end attr_accessor :x1, :x2, :y1, :y2, :x_abs, :y_abs, :start_index, :end_index attr_accessor :x_offset, :y_offset, :width, :height, :scale_x, :scale_y attr_accessor :width_emu, :height_emu, :element, :line_weight, :line_type attr_accessor :start_side, :end_side def initialize(properties = {}) @writer = Package::XMLWriterSimple.new @name = nil @type = 'rect' # Is a Connector shape. 1/0 Value is a hash lookup from type. @connect = 0 # Is a Drawing. Always 0, since a single shape never fills an entire sheet. @drawing = 0 # OneCell or Absolute: options to move and/or size with cells. @edit_as = nil # Auto-incremented, unless supplied by user. @id = 0 # Shape text (usually centered on shape geometry). @text = 0 # Shape stencil mode. A copy (child) is created when inserted. # The link to parent is broken. @stencil = 1 # Index to _shapes array when inserted. @element = -1 # Shape ID of starting connection, if any. @start = nil # Shape vertex, starts at 0, numbered clockwise from 12 o'clock. @start_index = nil @end = nil @end_index = nil # Number and size of adjustments for shapes (usually connectors). @adjustments = [] # Start and end sides. t)op, b)ottom, l)eft, or r)ight. @start_side = '' @end_side = '' # Flip shape Horizontally. eg. arrow left to arrow right. @flip_h = 0 # Flip shape Vertically. eg. up arrow to down arrow. @flip_v = 0 # shape rotation (in degrees 0-360). @rotation = 0 # An alternate way to create a text box, because Excel allows it. # It is just a rectangle with text. @tx_box = false # Shape outline colour, or 0 for noFill (default black). @line = '000000' # Line type: dash, sysDot, dashDot, lgDash, lgDashDot, lgDashDotDot. @line_type = '' # Line weight (integer). @line_weight = 1 # Shape fill colour, or 0 for noFill (default noFill). @fill = 0 # Formatting for shape text, if any. @format = {} # copy of colour palette table from Workbook.pm. @palette = [] # Vertical alignment: t, ctr, b. @valign = 'ctr' # Alignment: l, ctr, r, just @align = 'ctr' @x_offset = 0 @y_offset = 0 # Scale factors, which also may be set when the shape is inserted. @scale_x = 1 @scale_y = 1 # Default size, which can be modified and/or scaled. @width = 50 @height = 50 # Initial assignment. May be modified when prepared. @column_start = 0 @row_start = 0 @x1 = 0 @y1 = 0 @column_end = 0 @row_end = 0 @x2 = 0 @y2 = 0 @x_abs = 0 @y_abs = 0 set_properties(properties) end def set_properties(properties) # Override default properties with passed arguments properties.each do |key, value| # Strip leading "-" from Tk style properties e.g. -color => 'red'. k = key.to_s.sub(/^-/, '') self.instance_variable_set("@#{key}", value) end end # # Set the shape adjustments array (as a reference). # def adjustments=(args) @adjustments = *args end # # Calculate the vertices that define the position of a shape object within # the worksheet in EMUs. Save the vertices with the object. # # The vertices are expressed as English Metric Units (EMUs). There are 12,700 # EMUs per point. Therefore, 12,700 * 3 /4 = 9,525 EMUs per pixel. # def calc_position_emus(worksheet) c_start, r_start, xx1, yy1, c_end, r_end, xx2, yy2, x_abslt, y_abslt = worksheet.position_object_pixels( @column_start, @row_start, @x_offset, @y_offset, @width * @scale_x, @height * @scale_y ) # Now that x2/y2 have been calculated with a potentially negative # width/height we use the absolute value and convert to EMUs. @width_emu = (@width * 9_525).abs.to_i @height_emu = (@height * 9_525).abs.to_i @column_start = c_start.to_i @row_start = r_start.to_i @column_end = c_end.to_i @row_end = r_end.to_i # Convert the pixel values to EMUs. See above. @x1 = (xx1 * 9_525).to_i @y1 = (yy1 * 9_525).to_i @x2 = (xx2 * 9_525).to_i @y2 = (yy2 * 9_525).to_i @x_abs = (x_abslt * 9_525).to_i @y_abs = (y_abslt * 9_525).to_i end def set_position(row_start, column_start, x_offset, y_offset, x_scale, y_scale, anchor) @row_start = row_start @column_start = column_start @x_offset = x_offset || 0 @y_offset = y_offset || 0 @anchor = anchor || 1 # Override shape scale if supplied as an argument. Otherwise, use the # existing shape scale factors. @scale_x = x_scale if x_scale @scale_y = y_scale if y_scale end # # Re-size connector shapes if they are connected to other shapes. # def auto_locate_connectors(shapes, shape_hash) # Valid connector shapes. connector_shapes = { :straightConnector => 1, :Connector => 1, :bentConnector => 1, :curvedConnector => 1, :line => 1 } shape_base = @type.chop.to_sym # Remove the number of segments from end of type. @connect = connector_shapes[shape_base] ? 1 : 0 return if @connect == 0 # Both ends have to be connected to size it. return if @start == 0 && @end == 0 # Both ends need to provide info about where to connect. return if @start_side == 0 && @end_side == 0 sid = @start eid = @end slink_id = shape_hash[sid] || 0 sls = shapes.fetch(slink_id, Shape.new) elink_id = shape_hash[eid] || 0 els = shapes.fetch(elink_id, Shape.new) # Assume shape connections are to the middle of an object, and # not a corner (for now). connect_type = @start_side + @end_side smidx = sls.x_offset + sls.width / 2 emidx = els.x_offset + els.width / 2 smidy = sls.y_offset + sls.height / 2 emidy = els.y_offset + els.height / 2 netx = (smidx - emidx).abs nety = (smidy - emidy).abs if connect_type == 'bt' sy = sls.y_offset + sls.height ey = els.y_offset @width = (emidx - smidx).to_i.abs @x_offset = [smidx, emidx].min.to_i @height = (els.y_offset - (sls.y_offset + sls.height)).to_i.abs @y_offset = [sls.y_offset + sls.height, els.y_offset].min.to_i @flip_h = smidx < emidx ? 1 : 0 @rotation = 90 if sy > ey @flip_v = 1 # Create 3 adjustments for an end shape vertically above a # start @ Adjustments count from the upper left object. if @adjustments.empty? @adjustments = [-10, 50, 110] end @type = 'bentConnector5' end elsif connect_type == 'rl' @width = (els.x_offset - (sls.x_offset + sls.width)).to_i.abs @height = (emidy - smidy).to_i.abs @x_offset = [sls.x_offset + sls.width, els.x_offset].min @y_offset = [smidy, emidy].min @flip_h = 1 if smidx < emidx && smidy > emidy @flip_h = 1 if smidx > emidx && smidy < emidy if smidx > emidx # Create 3 adjustments for an end shape to the left of a # start @ if @adjustments.empty? @adjustments = [-10, 50, 110] end @type = 'bentConnector5' end end end # # Check shape attributes to ensure they are valid. # def validate(index) unless %w[l ctr r just].include?(@align) raise "Shape #{index} (#{@type}) alignment (#{@align}) not in ['l', 'ctr', 'r', 'just']\n" end unless %w[t ctr b].include?(@valign) raise "Shape #{index} (#{@type}) vertical alignment (#{@valign}) not in ['t', 'ctr', 'v']\n" end end def dimensions [ @column_start, @row_start, @x1, @y1, @column_end, @row_end, @x2, @y2, @x_abs, @y_abs, @width_emu, @height_emu ] end end end