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