/*********************************************************************
*
* Copyright (C) 2002 Andrew Khan
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
***************************************************************************/
package jxl.biff.drawing;
import java.io.FileInputStream;
import java.io.IOException;
import jxl.common.Assert;
import jxl.common.Logger;
import jxl.common.LengthUnit;
import jxl.common.LengthConverter;
import jxl.Image;
import jxl.Sheet;
import jxl.CellView;
import jxl.write.biff.File;
/**
* Contains the various biff records used to insert a drawing into a
* worksheet
*/
public class Drawing implements DrawingGroupObject, Image
{
/**
* The logger
*/
private static Logger logger = Logger.getLogger(Drawing.class);
/**
* The spContainer that was read in
*/
private EscherContainer readSpContainer;
/**
* The MsoDrawingRecord associated with the drawing
*/
private MsoDrawingRecord msoDrawingRecord;
/**
* The ObjRecord associated with the drawing
*/
private ObjRecord objRecord;
/**
* Initialized flag
*/
private boolean initialized = false;
/**
* The file containing the image
*/
private java.io.File imageFile;
/**
* The raw image data, used instead of an image file
*/
private byte[] imageData;
/**
* The object id, assigned by the drawing group
*/
private int objectId;
/**
* The blip id
*/
private int blipId;
/**
* The column position of the image
*/
private double x;
/**
* The row position of the image
*/
private double y;
/**
* The width of the image in cells
*/
private double width;
/**
* The height of the image in cells
*/
private double height;
/**
* The number of places this drawing is referenced
*/
private int referenceCount;
/**
* The top level escher container
*/
private EscherContainer escherData;
/**
* Where this image came from (read, written or a copy)
*/
private Origin origin;
/**
* The drawing group for all the images
*/
private DrawingGroup drawingGroup;
/**
* The drawing data
*/
private DrawingData drawingData;
/**
* The type of this drawing object
*/
private ShapeType type;
/**
* The shape id
*/
private int shapeId;
/**
* The drawing position on the sheet
*/
private int drawingNumber;
/**
* A reference to the sheet containing this drawing. Used to calculate
* the drawing dimensions in pixels
*/
private Sheet sheet;
/**
* Reader for the raw image data
*/
private PNGReader pngReader;
/**
* The client anchor properties
*/
private ImageAnchorProperties imageAnchorProperties;
// Enumeration type for the image anchor properties
protected static class ImageAnchorProperties
{
private int value;
private static ImageAnchorProperties[] o = new ImageAnchorProperties[0];
ImageAnchorProperties(int v)
{
value = v;
ImageAnchorProperties[] oldArray = o;
o = new ImageAnchorProperties[oldArray.length + 1];
System.arraycopy(oldArray, 0, o, 0, oldArray.length);
o[oldArray.length] = this;
}
int getValue()
{
return value;
}
static ImageAnchorProperties getImageAnchorProperties(int val)
{
ImageAnchorProperties iap = MOVE_AND_SIZE_WITH_CELLS;
int pos = 0;
while (pos < o.length)
{
if (o[pos].getValue()== val)
{
iap = o[pos];
break;
}
else
{
pos++;
}
}
return iap;
}
}
// The image anchor properties
public static ImageAnchorProperties MOVE_AND_SIZE_WITH_CELLS =
new ImageAnchorProperties(1);
public static ImageAnchorProperties MOVE_WITH_CELLS =
new ImageAnchorProperties(2);
public static ImageAnchorProperties NO_MOVE_OR_SIZE_WITH_CELLS =
new ImageAnchorProperties(3);
/**
* The default font size for columns
*/
private static final double DEFAULT_FONT_SIZE = 10;
/**
* Constructor used when reading images
*
* @param mso the drawing record
* @param obj the object record
* @param dd the drawing data for all drawings on this sheet
* @param dg the drawing group
*/
public Drawing(MsoDrawingRecord mso,
ObjRecord obj,
DrawingData dd,
DrawingGroup dg,
Sheet s)
{
drawingGroup = dg;
msoDrawingRecord = mso;
drawingData = dd;
objRecord = obj;
sheet = s;
initialized = false;
origin = Origin.READ;
drawingData.addData(msoDrawingRecord.getData());
drawingNumber = drawingData.getNumDrawings() - 1;
drawingGroup.addDrawing(this);
Assert.verify(mso != null && obj != null);
initialize();
}
/**
* Copy constructor used to copy drawings from read to write
*
* @param dgo the drawing group object
* @param dg the drawing group
*/
protected Drawing(DrawingGroupObject dgo, DrawingGroup dg)
{
Drawing d = (Drawing) dgo;
Assert.verify(d.origin == Origin.READ);
msoDrawingRecord = d.msoDrawingRecord;
objRecord = d.objRecord;
initialized = false;
origin = Origin.READ;
drawingData = d.drawingData;
drawingGroup = dg;
drawingNumber = d.drawingNumber;
drawingGroup.addDrawing(this);
}
/**
* Constructor invoked when writing the images
*
* @param x the column
* @param y the row
* @param w the width in cells
* @param h the height in cells
* @param image the image file
*/
public Drawing(double x,
double y,
double w,
double h,
java.io.File image)
{
imageFile = image;
initialized = true;
origin = Origin.WRITE;
this.x = x;
this.y = y;
this.width = w;
this.height = h;
referenceCount = 1;
imageAnchorProperties = MOVE_WITH_CELLS;
type = ShapeType.PICTURE_FRAME;
}
/**
* Constructor invoked when writing the images
*
* @param x the column
* @param y the row
* @param w the width in cells
* @param h the height in cells
* @param image the image data
*/
public Drawing(double x,
double y,
double w,
double h,
byte[] image)
{
imageData = image;
initialized = true;
origin = Origin.WRITE;
this.x = x;
this.y = y;
this.width = w;
this.height = h;
referenceCount = 1;
imageAnchorProperties = MOVE_WITH_CELLS;
type = ShapeType.PICTURE_FRAME;
}
/**
* Initializes the member variables from the Escher stream data
*/
private void initialize()
{
readSpContainer = drawingData.getSpContainer(drawingNumber);
Assert.verify(readSpContainer != null);
EscherRecord[] children = readSpContainer.getChildren();
Sp sp = (Sp) readSpContainer.getChildren()[0];
shapeId = sp.getShapeId();
objectId = objRecord.getObjectId();
type = ShapeType.getType(sp.getShapeType());
if (type == ShapeType.UNKNOWN)
{
logger.warn("Unknown shape type");
}
Opt opt = (Opt) readSpContainer.getChildren()[1];
if (opt.getProperty(260) != null)
{
blipId = opt.getProperty(260).value;
}
if (opt.getProperty(261) != null)
{
imageFile = new java.io.File(opt.getProperty(261).stringValue);
}
else
{
if (type == ShapeType.PICTURE_FRAME)
{
logger.warn("no filename property for drawing");
imageFile = new java.io.File(Integer.toString(blipId));
}
}
ClientAnchor clientAnchor = null;
for (int i = 0; i < children.length && clientAnchor == null; i++)
{
if (children[i].getType() == EscherRecordType.CLIENT_ANCHOR)
{
clientAnchor = (ClientAnchor) children[i];
}
}
if (clientAnchor == null)
{
logger.warn("client anchor not found");
}
else
{
x = clientAnchor.getX1();
y = clientAnchor.getY1();
width = clientAnchor.getX2() - x;
height = clientAnchor.getY2() - y;
imageAnchorProperties = ImageAnchorProperties.getImageAnchorProperties
(clientAnchor.getProperties());
}
if (blipId == 0)
{
logger.warn("linked drawings are not supported");
}
initialized = true;
}
/**
* Accessor for the image file
*
* @return the image file
*/
public java.io.File getImageFile()
{
return imageFile;
}
/**
* Accessor for the image file path. Normally this is the absolute path
* of a file on the directory system, but if this drawing was constructed
* using an byte[] then the blip id is returned
*
* @return the image file path, or the blip id
*/
public String getImageFilePath()
{
if (imageFile == null)
{
// return the blip id, if it exists
return blipId != 0 ? Integer.toString(blipId) : "__new__image__";
}
return imageFile.getPath();
}
/**
* Sets the object id. Invoked by the drawing group when the object is
* added to id
*
* @param objid the object id
* @param bip the blip id
* @param sid the shape id
*/
public final void setObjectId(int objid, int bip, int sid)
{
objectId = objid;
blipId = bip;
shapeId = sid;
if (origin == Origin.READ)
{
origin = Origin.READ_WRITE;
}
}
/**
* Accessor for the object id
*
* @return the object id
*/
public final int getObjectId()
{
if (!initialized)
{
initialize();
}
return objectId;
}
/**
* Accessor for the shape id
*
* @return the shape id
*/
public int getShapeId()
{
if (!initialized)
{
initialize();
}
return shapeId;
}
/**
* Accessor for the blip id
*
* @return the blip id
*/
public final int getBlipId()
{
if (!initialized)
{
initialize();
}
return blipId;
}
/**
* Gets the drawing record which was read in
*
* @return the drawing record
*/
public MsoDrawingRecord getMsoDrawingRecord()
{
return msoDrawingRecord;
}
/**
* Creates the main Sp container for the drawing
*
* @return the SP container
*/
public EscherContainer getSpContainer()
{
if (!initialized)
{
initialize();
}
if (origin == Origin.READ)
{
return getReadSpContainer();
}
SpContainer spContainer = new SpContainer();
Sp sp = new Sp(type, shapeId, 2560);
spContainer.add(sp);
Opt opt = new Opt();
opt.addProperty(260, true, false, blipId);
if (type == ShapeType.PICTURE_FRAME)
{
String filePath = imageFile != null ? imageFile.getPath() : "";
opt.addProperty(261, true, true, filePath.length() * 2, filePath);
opt.addProperty(447, false, false, 65536);
opt.addProperty(959, false, false, 524288);
spContainer.add(opt);
}
ClientAnchor clientAnchor = new ClientAnchor
(x, y, x + width, y + height,
imageAnchorProperties.getValue());
spContainer.add(clientAnchor);
ClientData clientData = new ClientData();
spContainer.add(clientData);
return spContainer;
}
/**
* Sets the drawing group for this drawing. Called by the drawing group
* when this drawing is added to it
*
* @param dg the drawing group
*/
public void setDrawingGroup(DrawingGroup dg)
{
drawingGroup = dg;
}
/**
* Accessor for the drawing group
*
* @return the drawing group
*/
public DrawingGroup getDrawingGroup()
{
return drawingGroup;
}
/**
* Gets the origin of this drawing
*
* @return where this drawing came from
*/
public Origin getOrigin()
{
return origin;
}
/**
* Accessor for the reference count on this drawing
*
* @return the reference count
*/
public int getReferenceCount()
{
return referenceCount;
}
/**
* Sets the new reference count on the drawing
*
* @param r the new reference count
*/
public void setReferenceCount(int r)
{
referenceCount = r;
}
/**
* Accessor for the column of this drawing
*
* @return the column
*/
public double getX()
{
if (!initialized)
{
initialize();
}
return x;
}
/**
* Sets the column position of this drawing
*
* @param x the column
*/
public void setX(double x)
{
if (origin == Origin.READ)
{
if (!initialized)
{
initialize();
}
origin = Origin.READ_WRITE;
}
this.x = x;
}
/**
* Accessor for the row of this drawing
*
* @return the row
*/
public double getY()
{
if (!initialized)
{
initialize();
}
return y;
}
/**
* Accessor for the row of the drawing
*
* @param y the row
*/
public void setY(double y)
{
if (origin == Origin.READ)
{
if (!initialized)
{
initialize();
}
origin = Origin.READ_WRITE;
}
this.y = y;
}
/**
* Accessor for the width of this drawing
*
* @return the number of columns spanned by this image
*/
public double getWidth()
{
if (!initialized)
{
initialize();
}
return width;
}
/**
* Accessor for the width
*
* @param w the number of columns to span
*/
public void setWidth(double w)
{
if (origin == Origin.READ)
{
if (!initialized)
{
initialize();
}
origin = Origin.READ_WRITE;
}
width = w;
}
/**
* Accessor for the height of this drawing
*
* @return the number of rows spanned by this image
*/
public double getHeight()
{
if (!initialized)
{
initialize();
}
return height;
}
/**
* Accessor for the height of this drawing
*
* @param h the number of rows spanned by this image
*/
public void setHeight(double h)
{
if (origin == Origin.READ)
{
if (!initialized)
{
initialize();
}
origin = Origin.READ_WRITE;
}
height = h;
}
/**
* Gets the SpContainer that was read in
*
* @return the read sp container
*/
private EscherContainer getReadSpContainer()
{
if (!initialized)
{
initialize();
}
return readSpContainer;
}
/**
* Accessor for the image data
*
* @return the image data
*/
public byte[] getImageData()
{
Assert.verify(origin == Origin.READ || origin == Origin.READ_WRITE);
if (!initialized)
{
initialize();
}
return drawingGroup.getImageData(blipId);
}
/**
* Accessor for the image data
*
* @return the image data
*/
public byte[] getImageBytes() throws IOException
{
if (origin == Origin.READ || origin == Origin.READ_WRITE)
{
return getImageData();
}
Assert.verify(origin == Origin.WRITE);
if (imageFile == null)
{
Assert.verify(imageData != null);
return imageData;
}
byte[] data = new byte[(int) imageFile.length()];
FileInputStream fis = new FileInputStream(imageFile);
fis.read(data, 0, data.length);
fis.close();
return data;
}
/**
* Accessor for the type
*
* @return the type
*/
public ShapeType getType()
{
return type;
}
/**
* Writes any other records associated with this drawing group object
*
* @param outputFile the output file
* @exception IOException
*/
public void writeAdditionalRecords(File outputFile) throws IOException
{
if (origin == Origin.READ)
{
outputFile.write(objRecord);
return;
}
// Create the obj record
ObjRecord objrec = new ObjRecord(objectId,
ObjRecord.PICTURE);
outputFile.write(objrec);
}
/**
* Writes any records that need to be written after all the drawing group
* objects have been written
* Does nothing here
*
* @param outputFile the output file
*/
public void writeTailRecords(File outputFile) throws IOException
{
// does nothing
}
/**
* Interface method
*
* @return the column number at which the image is positioned
*/
public double getColumn()
{
return getX();
}
/**
* Interface method
*
* @return the row number at which the image is positions
*/
public double getRow()
{
return getY();
}
/**
* Accessor for the first drawing on the sheet. This is used when
* copying unmodified sheets to indicate that this drawing contains
* the first time Escher gubbins
*
* @return TRUE if this MSORecord is the first drawing on the sheet
*/
public boolean isFirst()
{
return msoDrawingRecord.isFirst();
}
/**
* Queries whether this object is a form object. Form objects have their
* drawings records spread over TXO and CONTINUE records and
* require special handling
*
* @return TRUE if this is a form object, FALSE otherwise
*/
public boolean isFormObject()
{
return false;
}
/**
* Removes a row
*
* @param r the row to be removed
*/
public void removeRow(int r)
{
if (y > r)
{
setY(r);
}
}
/**
* Accessor for the image dimensions. See technotes for Bill's explanation
* of the calculation logic
*
* @return approximate drawing size in pixels
*/
private double getWidthInPoints()
{
if (sheet == null)
{
logger.warn("calculating image width: sheet is null");
return 0;
}
// The start and end row numbers
int firstCol = (int) x;
int lastCol = (int) Math.ceil(x + width) - 1;
// **** MAGIC NUMBER ALERT ***
// multiply the point size of the font by 0.59 to give the point size
// I know of no explanation for this yet, other than that it seems to
// give the right answer
// Get the width of the image within the first col, allowing for
// fractional offsets
CellView cellView = sheet.getColumnView(firstCol);
int firstColWidth = cellView.getSize();
double firstColImageWidth = (1 - (x - firstCol)) * firstColWidth;
double pointSize = (cellView.getFormat() != null) ?
cellView.getFormat().getFont().getPointSize() : DEFAULT_FONT_SIZE;
double firstColWidthInPoints = firstColImageWidth * 0.59 * pointSize / 256;
// Get the height of the image within the last row, allowing for
// fractional offsets
int lastColWidth = 0;
double lastColImageWidth = 0;
double lastColWidthInPoints = 0;
if (lastCol != firstCol)
{
cellView = sheet.getColumnView(lastCol);
lastColWidth = cellView.getSize();
lastColImageWidth = (x + width - lastCol) * lastColWidth;
pointSize = (cellView.getFormat() != null) ?
cellView.getFormat().getFont().getPointSize() : DEFAULT_FONT_SIZE;
lastColWidthInPoints = lastColImageWidth * 0.59 * pointSize / 256;
}
// Now get all the columns in between
double width = 0;
for (int i = 0 ; i < lastCol - firstCol - 1 ; i++)
{
cellView = sheet.getColumnView(firstCol + 1 +i);
pointSize = (cellView.getFormat() != null) ?
cellView.getFormat().getFont().getPointSize() : DEFAULT_FONT_SIZE;
width += cellView.getSize() * 0.59 * pointSize / 256;
}
// Add on the first and last row contributions to get the height in twips
double widthInPoints = width +
firstColWidthInPoints + lastColWidthInPoints;
return widthInPoints;
}
/**
* Accessor for the image dimensions. See technotes for Bill's explanation
* of the calculation logic
*
* @return approximate drawing size in pixels
*/
private double getHeightInPoints()
{
if (sheet == null)
{
logger.warn("calculating image height: sheet is null");
return 0;
}
// The start and end row numbers
int firstRow = (int) y;
int lastRow = (int) Math.ceil(y + height) - 1;
// Get the height of the image within the first row, allowing for
// fractional offsets
int firstRowHeight = sheet.getRowView(firstRow).getSize();
double firstRowImageHeight = (1 - (y - firstRow)) * firstRowHeight;
// Get the height of the image within the last row, allowing for
// fractional offsets
int lastRowHeight = 0;
double lastRowImageHeight = 0;
if (lastRow != firstRow)
{
lastRowHeight = sheet.getRowView(lastRow).getSize();
lastRowImageHeight = (y + height - lastRow) * lastRowHeight;
}
// Now get all the rows in between
double height = 0;
for (int i = 0 ; i < lastRow - firstRow - 1 ; i++)
{
height += sheet.getRowView(firstRow + 1 + i).getSize();
}
// Add on the first and last row contributions to get the height in twips
double heightInTwips = height + firstRowHeight + lastRowHeight;
// Now divide by the magic number to converts twips into pixels and
// return the value
double heightInPoints = heightInTwips / 20.0;
return heightInPoints;
}
/**
* Get the width of this image as rendered within Excel
*
* @param unit the unit of measurement
* @return the width of the image within Excel
*/
public double getWidth(LengthUnit unit)
{
double widthInPoints = getWidthInPoints();
return widthInPoints * LengthConverter.getConversionFactor
(LengthUnit.POINTS, unit);
}
/**
* Get the height of this image as rendered within Excel
*
* @param unit the unit of measurement
* @return the height of the image within Excel
*/
public double getHeight(LengthUnit unit)
{
double heightInPoints = getHeightInPoints();
return heightInPoints * LengthConverter.getConversionFactor
(LengthUnit.POINTS, unit);
}
/**
* Gets the width of the image. Note that this is the width of the
* underlying image, and does not take into account any size manipulations
* that may have occurred when the image was added into Excel
*
* @return the image width in pixels
*/
public int getImageWidth()
{
return getPngReader().getWidth();
}
/**
* Gets the height of the image. Note that this is the height of the
* underlying image, and does not take into account any size manipulations
* that may have occurred when the image was added into Excel
*
* @return the image width in pixels
*/
public int getImageHeight()
{
return getPngReader().getHeight();
}
/**
* Gets the horizontal resolution of the image, if that information
* is available.
*
* @return the number of dots per unit specified, if available, 0 otherwise
*/
public double getHorizontalResolution(LengthUnit unit)
{
int res = getPngReader().getHorizontalResolution();
return res / LengthConverter.getConversionFactor(LengthUnit.METRES, unit);
}
/**
* Gets the vertical resolution of the image, if that information
* is available.
*
* @return the number of dots per unit specified, if available, 0 otherwise
*/
public double getVerticalResolution(LengthUnit unit)
{
int res = getPngReader().getVerticalResolution();
return res / LengthConverter.getConversionFactor(LengthUnit.METRES, unit);
}
private PNGReader getPngReader()
{
if (pngReader != null)
{
return pngReader;
}
byte[] imdata = null;
if (origin == Origin.READ || origin == Origin.READ_WRITE)
{
imdata = getImageData();
}
else
{
try
{
imdata = getImageBytes();
}
catch (IOException e)
{
logger.warn("Could not read image file");
imdata = new byte[0];
}
}
pngReader = new PNGReader(imdata);
pngReader.read();
return pngReader;
}
/**
* Accessor for the anchor properties
*/
protected void setImageAnchor(ImageAnchorProperties iap)
{
imageAnchorProperties = iap;
if (origin == Origin.READ)
{
origin = Origin.READ_WRITE;
}
}
/**
* Accessor for the anchor properties
*/
protected ImageAnchorProperties getImageAnchor()
{
if (!initialized)
{
initialize();
}
return imageAnchorProperties;
}
}
|