Surpass

bitmap.rb

class ObjBmpRecord < BiffRecord
  RECORD_ID = 0x005D    # Record identifier
  
  def initialize(row, col, sheet, im_data_bmp, x, y, scale_x, scale_y)
    width = im_data_bmp.width * scale_x
    height = im_data_bmp.height * scale_y
    
    col_start, x1, row_start, y1, col_end, x2, row_end, y2 = position_image(sheet, row, col, x, y, width, height)
    
    # Store the OBJ record that precedes an IMDATA record. This could be generalise
    # to support other Excel objects.
    cobj = 0x0001      # count of objects in file (set to 1)
    ot = 0x0008        # object type. 8 = picture
    id = 0x0001        # object id
    grbit = 0x0614     # option flags
    coll = col_start    # col containing upper left corner of object
    dxl = x1            # distance from left side of cell
    rwt = row_start     # row containing top left corner of object
    dyt = y1            # distance from top of cell
    colr = col_end      # col containing lower right corner of object
    dxr = x2            # distance from right of cell
    rwb = row_end       # row containing bottom right corner of object
    dyb = y2            # distance from bottom of cell
    cbmacro = 0x0000    # length of fmla structure
    reserved1 = 0x0000  # reserved
    reserved2 = 0x0000  # reserved
    icvback = 0x09      # background colour
    icvfore = 0x09      # foreground colour
    fls = 0x00          # fill pattern
    fauto = 0x00        # automatic fill
    icv = 0x08          # line colour
    lns = 0xff          # line style
    lnw = 0x01          # line weight
    fautob = 0x00       # automatic border
    frs = 0x0000        # frame style
    cf = 0x0009         # image format, 9 = bitmap
    reserved3 = 0x0000  # reserved
    cbpictfmla = 0x0000 # length of fmla structure
    reserved4 = 0x0000  # reserved
    grbit2 = 0x0001     # option flags
    reserved5 = 0x0000  # reserved
    
    args = [cobj, ot, id, grbit, coll, dxl, rwt, dyt, colr, dxr, rwb, dyb, cbmacro, reserved1, reserved2, icvback, icvfore, fls, fauto, icv, lns, lnw, fautob, frs, cf, reserved3, cbpictfmla, reserved4, grbit2, reserved5]
    @record_data = args.pack('L v12 L v C8 v L v4 L')
  end
  
  # Calculate the vertices that define the position of the image as required by
  # the OBJ record.
  # 
  #          +------------+------------+
  #          |     A      |      B     |
  #    +-----+------------+------------+
  #    |     |(x1,y1)     |            |
  #    |  1  |(A1)._______|______      |
  #    |     |    |              |     |
  #    |     |    |              |     |
  #    +-----+----|    BITMAP    |-----+
  #    |     |    |              |     |
  #    |  2  |    |______________.     |
  #    |     |            |        (B2)|
  #    |     |            |     (x2,y2)|
  #    +---- +------------+------------+
  # 
  # Example of a bitmap that covers some of the area from cell A1 to cell B2.
  # 
  # Based on the width and height of the bitmap we need to calculate 8 vars:
  #     col_start, row_start, col_end, row_end, x1, y1, x2, y2.
  # The width and height of the cells are also variable and have to be taken into
  # account.
  # The values of col_start and row_start are passed in from the calling
  # function. The values of col_end and row_end are calculated by subtracting
  # the width and height of the bitmap from the width and height of the
  # underlying cells.
  # The vertices are expressed as a percentage of the underlying cell width as
  # follows (rhs values are in pixels):
  # 
  #        x1 = X / W *1024
  #        y1 = Y / H *256
  #        x2 = (X-1) / W *1024
  #        y2 = (Y-1) / H *256
  # 
  #        Where:  X is distance from the left side of the underlying cell
  #                Y is distance from the top of the underlying cell
  #                W is the width of the cell
  #                H is the height of the cell
  # 
  # Note: the SDK incorrectly states that the height should be expressed as a
  # percentage of 1024.
  # 
  # col_start  - Col containing upper left corner of object
  # row_start  - Row containing top left corner of object
  # x1  - Distance to left side of object
  # y1  - Distance to top of object
  # width  - Width of image frame
  # height  - Height of image frame
  def position_image(sheet, row_start, col_start, x1, y1, width, height)
    while x1 >= size_col(sheet, col_start) do
      x1 -= size_col(sheet, col_start)
      col_start += 1
    end
    
    # Adjust start row for offsets that are greater than the row height
    while y1 >= size_row(sheet, row_start) do
      y1 -= size_row(sheet, row_start)
      row_start += 1
    end
    
    # Initialise end cell to the same as the start cell
    row_end = row_start   # Row containing bottom right corner of object
    col_end = col_start   # Col containing lower right corner of object
    width = width + x1 - 1
    height = height + y1 - 1
    
    # Subtract the underlying cell widths to find the end cell of the image
    while (width >= size_col(sheet, col_end)) do
      width -= size_col(sheet, col_end)
      col_end += 1
    end
    
    # Subtract the underlying cell heights to find the end cell of the image
    while (height >= size_row(sheet, row_end)) do
      height -= size_row(sheet, row_end)
      row_end += 1
    end
    
    # Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell
    # with zero height or width.
    starts_or_ends_in_hidden_cell = ((size_col(sheet, col_start) == 0) or (size_col(sheet, col_end) == 0) or (size_row(sheet, row_start) == 0) or (size_row(sheet, row_end) == 0))
    return if starts_or_ends_in_hidden_cell

    # Convert the pixel values to the percentage value expected by Excel
    x1 = (x1.to_f / size_col(sheet, col_start) * 1024).to_i
    y1 = (y1.to_f / size_row(sheet, row_start) * 256).to_i
    # Distance to right side of object
    x2 = (width.to_f / size_col(sheet, col_end) * 1024).to_i
    # Distance to bottom of object
    y2 = (height.to_f / size_row(sheet, row_end) * 256).to_i
    
    [col_start, x1, row_start, y1, col_end, x2, row_end, y2]
  end
  
  def size_col(sheet, col)
    sheet.col_width(col)
  end

  def size_row(sheet, row)
    sheet.row_height(row)
  end
end

class ImDataBmpRecord < BiffRecord
  RECORD_ID = 0x007F
  
  attr_accessor :width
  attr_accessor :height
  attr_accessor :size
  
  # Insert a 24bit bitmap image in a worksheet. The main record required is
  # IMDATA but it must be proceeded by a OBJ record to define its position.
  def initialize(filename)
    @width, @height, @size, data = process_bitmap(filename)
    
    cf = 0x09
    env = 0x01
    lcb = @size
    
    @record_data =  [cf, env, lcb].pack('v2L') + data
  end
  
  # Convert a 24 bit bitmap into the modified internal format used by Windows.
  # This is described in BITMAPCOREHEADER and BITMAPCOREINFO structures in the
  # MSDN library.
  def process_bitmap(filename)
    data = nil
    File.open(filename, "rb") do |f|
      data = f.read
    end
    
    raise "bitmap #{filename} doesn't contain enough data" if data.length <= 0x36
    raise "bitmap #{filename} is not valid" unless data[0, 2] === "BM"
    
    # Remove bitmap data: ID.
    data = data[2..-1]

    # Read and remove the bitmap size. This is more reliable than reading
    # the data size at offset 0x22.
    size = data[0,4].unpack('L')[0]
    size -=  0x36   # Subtract size of bitmap header.
    size +=  0x0C   # Add size of BIFF header.

    data = data[4..-1]
    # Remove bitmap data: reserved, offset, header length.
    data = data[12..-1]
    # Read and remove the bitmap width and height. Verify the sizes.
    width, height = data[0,8].unpack('L2')
    data = data[8..-1]
    raise "bitmap #{filename} largest image width supported is 65k." if (width > 0xFFFF)
    raise "bitmap #{filename} largest image height supported is 65k." if (height > 0xFFFF)
    
    # Read and remove the bitmap planes and bpp data. Verify them.
    planes, bitcount = data[0,4].unpack('v2')
    data = data[4..-1]
    raise "bitmap #{filename} isn't a 24bit true color bitmap." if (bitcount != 24)
    raise "bitmap #{filename} only 1 plane supported in bitmap image." if (planes != 1)

    # Read and remove the bitmap compression. Verify compression.
    compression = data[0,4].unpack('L')[0]
    data = data[4..-1]
    raise "bitmap #{filename} compression not supported in bitmap image." if (compression != 0)
        
    # Remove bitmap data: data size, hres, vres, colours, imp. colours.
    data = data[20..-1]
    # Add the BITMAPCOREHEADER data
    header = [0x000c, width, height, 0x01, 0x18].pack('Lv4')
    
    [width, height, size, header + data]
  end
end