From: Nick Burch Date: Mon, 1 Jan 2007 21:02:22 +0000 (+0000) Subject: Comment support from bug 41198, patch from Yegor X-Git-Tag: REL_3_0_RC1~47 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=c1d68a5e30a5fbc602480c7a66531c3624476d15;p=poi.git Comment support from bug 41198, patch from Yegor git-svn-id: https://svn.apache.org/repos/asf/jakarta/poi/trunk@491629 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/src/documentation/content/xdocs/hssf/quick-guide.xml b/src/documentation/content/xdocs/hssf/quick-guide.xml index 59a27dc653..8bc58d6ba3 100644 --- a/src/documentation/content/xdocs/hssf/quick-guide.xml +++ b/src/documentation/content/xdocs/hssf/quick-guide.xml @@ -47,6 +47,7 @@
  • Outlining
  • Images
  • Named Ranges and Named Cells
  • +
  • How to set cell comments
  • Features @@ -1034,6 +1035,85 @@
    - + +
    Cell Comments +

    + In Excel a comment is a kind of a text shape, + so inserting a comment is very similar to placing a text box in a worksheet: +

    + + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet("Cell comments in POI HSSF"); + + // Create the drawing patriarch. This is the top level container for all shapes including cell comments. + HSSFPatriarch patr = sheet.createDrawingPatriarch(); + + //create a cell in row 3 + HSSFCell cell1 = sheet.createRow(3).createCell((short)1); + cell1.setCellValue(new HSSFRichTextString("Hello, World")); + + //anchor defines size and position of the comment in worksheet + HSSFComment comment1 = patr.createComment(new HSSFClientAnchor(0, 0, 0, 0, (short)4, 2, (short) 6, 5)); + + // set text in the comment + comment1.setString(new HSSFRichTextString("We can set comments in POI")); + + //set comment author. + //you can see it in the status bar when moving mouse over the commented cell + comment1.setAuthor("Apache Software Foundation"); + + // The first way to assign comment to a cell is via HSSFCell.setCellComment method + cell1.setCellComment(comment1); + + //create another cell in row 6 + HSSFCell cell2 = sheet.createRow(6).createCell((short)1); + cell2.setCellValue(36.6); + + + HSSFComment comment2 = patr.createComment(new HSSFClientAnchor(0, 0, 0, 0, (short)4, 8, (short) 6, 11)); + //modify background color of the comment + comment2.setFillColor(204, 236, 255); + + HSSFRichTextString string = new HSSFRichTextString("Normal body temperature"); + + //apply custom font to the text in the comment + HSSFFont font = wb.createFont(); + font.setFontName("Arial"); + font.setFontHeightInPoints((short)10); + font.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD); + font.setColor(HSSFColor.RED.index); + string.applyFont(font); + + comment2.setString(string); + //by default comments are hidden. This one is always visible. + comment2.setVisible(true); + + comment2.setAuthor("Bill Gates"); + + /** + * The second way to assign comment to a cell is to implicitly specify its row and column. + * Note, it is possible to set row and column of a non-existing cell. + * It works, the commnet is visible. + */ + comment2.setRow(6); + comment2.setColumn((short)1); + + FileOutputStream out = new FileOutputStream("poi_comment.xls"); + wb.write(out); + out.close(); + +

    + Reading cell comments +

    + + HSSFCell cell = sheet.get(3).getColumn((short)1); + HSSFComment comment = cell.getCellComment(); + if (comment != null) { + HSSFRichTextString str = comment.getString(); + String author = comment.getAuthor(); + } + +
    + diff --git a/src/examples/src/org/apache/poi/hssf/usermodel/examples/CellComments.java b/src/examples/src/org/apache/poi/hssf/usermodel/examples/CellComments.java new file mode 100644 index 0000000000..7ec606fea3 --- /dev/null +++ b/src/examples/src/org/apache/poi/hssf/usermodel/examples/CellComments.java @@ -0,0 +1,99 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.hssf.usermodel.examples; + +import org.apache.poi.hssf.usermodel.*; +import org.apache.poi.hssf.util.HSSFColor; + +import java.io.*; + +/** + * Demonstrates how to work with excel cell comments. + * + *

    + * Excel comment is a kind of a text shape, + * so inserting a comment is very similar to placing a text box in a worksheet + *

    + * + * @author Yegor Kozlov + */ +public class CellComments { + + public static void main(String[] args) throws IOException { + + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet("Cell comments in POI HSSF"); + + // Create the drawing patriarch. This is the top level container for all shapes including cell comments. + HSSFPatriarch patr = sheet.createDrawingPatriarch(); + + //create a cell in row 3 + HSSFCell cell1 = sheet.createRow(3).createCell((short)1); + cell1.setCellValue(new HSSFRichTextString("Hello, World")); + + //anchor defines size and position of the comment in worksheet + HSSFComment comment1 = patr.createComment(new HSSFClientAnchor(0, 0, 0, 0, (short)4, 2, (short) 6, 5)); + + // set text in the comment + comment1.setString(new HSSFRichTextString("We can set comments in POI")); + + //set comment author. + //you can see it in the status bar when moving mouse over the commented cell + comment1.setAuthor("Apache Software Foundation"); + + // The first way to assign comment to a cell is via HSSFCell.setCellComment method + cell1.setCellComment(comment1); + + //create another cell in row 6 + HSSFCell cell2 = sheet.createRow(6).createCell((short)1); + cell2.setCellValue(36.6); + + + HSSFComment comment2 = patr.createComment(new HSSFClientAnchor(0, 0, 0, 0, (short)4, 8, (short) 6, 11)); + //modify background color of the comment + comment2.setFillColor(204, 236, 255); + + HSSFRichTextString string = new HSSFRichTextString("Normal body temperature"); + + //apply custom font to the text in the comment + HSSFFont font = wb.createFont(); + font.setFontName("Arial"); + font.setFontHeightInPoints((short)10); + font.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD); + font.setColor(HSSFColor.RED.index); + string.applyFont(font); + + comment2.setString(string); + comment2.setVisible(true); //by default comments are hidden. This one is always visible. + + comment2.setAuthor("Bill Gates"); + + /** + * The second way to assign comment to a cell is to implicitly specify its row and column. + * Note, it is possible to set row and column of a non-existing cell. + * It works, the commnet is visible. + */ + comment2.setRow(6); + comment2.setColumn((short)1); + + FileOutputStream out = new FileOutputStream("poi_comment.xls"); + wb.write(out); + out.close(); + + + } +} diff --git a/src/java/org/apache/poi/hssf/dev/BiffViewer.java b/src/java/org/apache/poi/hssf/dev/BiffViewer.java index 6cf4e38256..2d5f02bebc 100644 --- a/src/java/org/apache/poi/hssf/dev/BiffViewer.java +++ b/src/java/org/apache/poi/hssf/dev/BiffViewer.java @@ -509,6 +509,9 @@ public class BiffViewer { case FilePassRecord.sid: retval = new FilePassRecord(in); break; + case NoteRecord.sid: + retval = new NoteRecord( in ); + break; default: retval = new UnknownRecord( in ); } diff --git a/src/java/org/apache/poi/hssf/eventmodel/EventRecordFactory.java b/src/java/org/apache/poi/hssf/eventmodel/EventRecordFactory.java index daa0ba08a9..04660c5680 100644 --- a/src/java/org/apache/poi/hssf/eventmodel/EventRecordFactory.java +++ b/src/java/org/apache/poi/hssf/eventmodel/EventRecordFactory.java @@ -107,6 +107,7 @@ import org.apache.poi.hssf.record.WindowTwoRecord; import org.apache.poi.hssf.record.WriteAccessRecord; import org.apache.poi.hssf.record.WriteProtectRecord; import org.apache.poi.hssf.record.FilePassRecord; +import org.apache.poi.hssf.record.NoteRecord; /** @@ -158,7 +159,8 @@ public class EventRecordFactory LeftMarginRecord.class, RightMarginRecord.class, TopMarginRecord.class, BottomMarginRecord.class, PaletteRecord.class, StringRecord.class, SharedFormulaRecord.class, - WriteProtectRecord.class, FilePassRecord.class, PaneRecord.class + WriteProtectRecord.class, FilePassRecord.class, PaneRecord.class, + NoteRecord.class }; } diff --git a/src/java/org/apache/poi/hssf/model/AbstractShape.java b/src/java/org/apache/poi/hssf/model/AbstractShape.java index 609f78c964..19878db797 100644 --- a/src/java/org/apache/poi/hssf/model/AbstractShape.java +++ b/src/java/org/apache/poi/hssf/model/AbstractShape.java @@ -36,7 +36,11 @@ public abstract class AbstractShape public static AbstractShape createShape( HSSFShape hssfShape, int shapeId ) { AbstractShape shape; - if (hssfShape instanceof HSSFTextbox) + if (hssfShape instanceof HSSFComment) + { + shape = new CommentShape( (HSSFComment)hssfShape, shapeId ); + } + else if (hssfShape instanceof HSSFTextbox) { shape = new TextboxShape( (HSSFTextbox)hssfShape, shapeId ); } diff --git a/src/java/org/apache/poi/hssf/model/CommentShape.java b/src/java/org/apache/poi/hssf/model/CommentShape.java new file mode 100644 index 0000000000..9be3fa3254 --- /dev/null +++ b/src/java/org/apache/poi/hssf/model/CommentShape.java @@ -0,0 +1,133 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.hssf.model; + +import org.apache.poi.hssf.record.*; +import org.apache.poi.hssf.usermodel.HSSFComment; +import org.apache.poi.hssf.usermodel.HSSFShape; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.ddf.*; + +import java.util.List; +import java.util.Iterator; + +/** + * Represents a cell comment. + * This class converts highlevel model data from HSSFComment + * to low-level records. + * + * @author Yegor Kozlov + */ +public class CommentShape extends TextboxShape { + + private NoteRecord note; + + /** + * Creates the low-level records for a comment. + * + * @param hssfShape The highlevel shape. + * @param shapeId The shape id to use for this shape. + */ + public CommentShape( HSSFComment hssfShape, int shapeId ) + { + super(hssfShape, shapeId); + + note = createNoteRecord(hssfShape, shapeId); + + ObjRecord obj = getObjRecord(); + List records = obj.getSubRecords(); + int cmoIdx = 0; + for (int i = 0; i < records.size(); i++) { + Object r = records.get(i); + + if (r instanceof CommonObjectDataSubRecord){ + //modify autofill attribute inherited from TextObjectRecord + CommonObjectDataSubRecord cmo = (CommonObjectDataSubRecord)r; + cmo.setAutofill(false); + cmoIdx = i; + } + } + //add NoteStructure sub record + //we don't know it's format, for now the record data is empty + NoteStructureSubRecord u = new NoteStructureSubRecord(); + obj.addSubRecord(cmoIdx+1, u); + } + + /** + * Creates the low level NoteRecord + * which holds the comment attributes. + */ + private NoteRecord createNoteRecord( HSSFComment shape, int shapeId ) + { + NoteRecord note = new NoteRecord(); + note.setColumn(shape.getColumn()); + note.setRow((short)shape.getRow()); + note.setFlags(shape.isVisible() ? NoteRecord.NOTE_VISIBLE : NoteRecord.NOTE_HIDDEN); + note.setShapeId((short)shapeId); + note.setAuthor(shape.getAuthor() == null ? "" : shape.getAuthor()); + return note; + } + + /** + * Sets standard escher options for a comment. + * This method is responsible for setting default background, + * shading and other comment properties. + * + * @param shape The highlevel shape. + * @param opt The escher records holding the proerties + * @return number of escher options added + */ + protected int addStandardOptions( HSSFShape shape, EscherOptRecord opt ) + { + super.addStandardOptions(shape, opt); + + //remove unnecessary properties inherited from TextboxShape + java.util.List props = opt.getEscherProperties(); + for ( Iterator iterator = props.iterator(); iterator.hasNext(); ) { + EscherProperty prop = (EscherProperty) iterator.next(); + switch (prop.getId()){ + case EscherProperties.TEXT__TEXTLEFT: + case EscherProperties.TEXT__TEXTRIGHT: + case EscherProperties.TEXT__TEXTTOP: + case EscherProperties.TEXT__TEXTBOTTOM: + case EscherProperties.GROUPSHAPE__PRINT: + case EscherProperties.FILL__FILLBACKCOLOR: + case EscherProperties.LINESTYLE__COLOR: + iterator.remove(); + break; + } + } + + HSSFComment comment = (HSSFComment)shape; + opt.addEscherProperty( new EscherSimpleProperty( EscherProperties.GROUPSHAPE__PRINT, comment.isVisible() ? 0x000A0000 : 0x000A0002) ); + opt.addEscherProperty( new EscherSimpleProperty( EscherProperties.SHADOWSTYLE__SHADOWOBSURED, 0x00030003 ) ); + opt.addEscherProperty( new EscherSimpleProperty( EscherProperties.SHADOWSTYLE__COLOR, 0x00000000 ) ); + opt.sortProperties(); + return opt.getEscherProperties().size(); // # options added + } + + /** + * Return the NoteRecord holding the comment attributes + * + * @return NoteRecord holding the comment attributes + */ + public NoteRecord getNoteRecord() + { + return note; + } + +} diff --git a/src/java/org/apache/poi/hssf/record/EscherAggregate.java b/src/java/org/apache/poi/hssf/record/EscherAggregate.java index 3e69d61417..28a717682a 100644 --- a/src/java/org/apache/poi/hssf/record/EscherAggregate.java +++ b/src/java/org/apache/poi/hssf/record/EscherAggregate.java @@ -23,6 +23,7 @@ import org.apache.poi.hssf.model.AbstractShape; import org.apache.poi.hssf.model.TextboxShape; import org.apache.poi.hssf.model.DrawingManager2; import org.apache.poi.hssf.model.ConvertAnchor; +import org.apache.poi.hssf.model.CommentShape; import java.util.*; @@ -260,6 +261,11 @@ public class EscherAggregate extends AbstractEscherHolderRecord private DrawingManager2 drawingManager; private short drawingGroupId; + /** + * list of "tail" records that need to be serialized after all drawing group records + */ + private List tailRec = new ArrayList(); + public EscherAggregate( DrawingManager2 drawingManager ) { this.drawingManager = drawingManager; @@ -450,6 +456,13 @@ public class EscherAggregate extends AbstractEscherHolderRecord } + // write records that need to be serialized after all drawing group records + for ( int i = 0; i < tailRec.size(); i++ ) + { + Record rec = (Record)tailRec.get(i); + pos += rec.serialize( pos, data ); + } + int bytesWritten = pos - offset; if ( bytesWritten != getRecordSize() ) throw new RecordFormatException( bytesWritten + " bytes written but getRecordSize() reports " + getRecordSize() ); @@ -484,7 +497,13 @@ public class EscherAggregate extends AbstractEscherHolderRecord Record r = (Record) iterator.next(); objRecordSize += r.getRecordSize(); } - return drawingRecordSize + objRecordSize; + int tailRecordSize = 0; + for ( Iterator iterator = tailRec.iterator(); iterator.hasNext(); ) + { + Record r = (Record) iterator.next(); + tailRecordSize += r.getRecordSize(); + } + return drawingRecordSize + objRecordSize + tailRecordSize; } /** @@ -529,6 +548,7 @@ public class EscherAggregate extends AbstractEscherHolderRecord if ( patriarch != null ) { shapeToObj.clear(); + tailRec.clear(); clearEscherRecords(); if ( patriarch.getChildren().size() != 0 ) { @@ -568,6 +588,12 @@ public class EscherAggregate extends AbstractEscherHolderRecord EscherRecord escherTextbox = ( (TextboxShape) shapeModel ).getEscherTextbox(); shapeToObj.put( escherTextbox, ( (TextboxShape) shapeModel ).getTextObjectRecord() ); // escherParent.addChildRecord(escherTextbox); + + if ( shapeModel instanceof CommentShape ){ + CommentShape comment = (CommentShape)shapeModel; + tailRec.add(comment.getNoteRecord()); + } + } escherParent.addChildRecord( shapeModel.getSpContainer() ); } diff --git a/src/java/org/apache/poi/hssf/record/NoteRecord.java b/src/java/org/apache/poi/hssf/record/NoteRecord.java new file mode 100644 index 0000000000..018ae7e76d --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/NoteRecord.java @@ -0,0 +1,246 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record; + +import org.apache.poi.util.LittleEndian; + +/** + * NOTE: Comment Associated with a Cell (1Ch) + * + * @author Yegor Kozlov + */ +public class NoteRecord extends Record { + public final static short sid = 0x1C; + + /** + * Flag indicating that the comment is hidden (default) + */ + public final static short NOTE_HIDDEN = 0x0; + + /** + * Flag indicating that the comment is visible + */ + public final static short NOTE_VISIBLE = 0x2; + + private short field_1_row; + private short field_2_col; + private short field_3_flags; + private short field_4_shapeid; + private String field_5_author; + + /** + * Construct a new NoteRecord and + * fill its data with the default values + */ + public NoteRecord() + { + field_5_author = ""; + field_3_flags = 0; + } + + /** + * Constructs a NoteRecord and fills its fields + * from the supplied RecordInputStream. + * + * @param in the stream to read from + */ + public NoteRecord(RecordInputStream in) + { + super(in); + + } + + /** + * @return id of this record. + */ + public short getSid() + { + return sid; + } + + /** + * Checks the sid matches the expected side for this record + * + * @param id the expected sid. + */ + protected void validateSid(short id) + { + if (id != sid) + { + throw new RecordFormatException("Not a NoteRecord record"); + } + } + + /** + * Read the record data from the supplied RecordInputStream + */ + protected void fillFields(RecordInputStream in) + { + field_1_row = in.readShort(); + field_2_col = in.readShort(); + field_3_flags = in.readShort(); + field_4_shapeid = in.readShort(); + int length = in.readShort(); + byte[] bytes = in.readRemainder(); + field_5_author = new String(bytes, 1, length); + } + + /** + * Serialize the record data into the supplied array of bytes + * + * @param offset offset in the data + * @param data the data to serialize into + * + * @return size of the record + */ + public int serialize(int offset, byte [] data) + { + LittleEndian.putShort(data, 0 + offset, sid); + LittleEndian.putShort(data, 2 + offset, (short)(getRecordSize() - 4)); + + LittleEndian.putShort(data, 4 + offset , field_1_row); + LittleEndian.putShort(data, 6 + offset , field_2_col); + LittleEndian.putShort(data, 8 + offset , field_3_flags); + LittleEndian.putShort(data, 10 + offset , field_4_shapeid); + LittleEndian.putShort(data, 12 + offset , (short)field_5_author.length()); + + byte[] str = field_5_author.getBytes(); + System.arraycopy(str, 0, data, 15 + offset, str.length); + + return getRecordSize(); + } + + /** + * Size of record + */ + public int getRecordSize() + { + int retval = 4 + 2 + 2 + 2 + 2 + 2 + 1 + field_5_author.length() + 1; + + return retval; + } + + /** + * Convert this record to string. + * Used by BiffViewer and other utulities. + */ + public String toString() + { + StringBuffer buffer = new StringBuffer(); + + buffer.append("[NOTE]\n"); + buffer.append(" .recordid = 0x" + Integer.toHexString( getSid() ) + ", size = " + getRecordSize() + "\n"); + buffer.append(" .row = " + field_1_row + "\n"); + buffer.append(" .col = " + field_2_col + "\n"); + buffer.append(" .flags = " + field_3_flags + "\n"); + buffer.append(" .shapeid = " + field_4_shapeid + "\n"); + buffer.append(" .author = " + field_5_author + "\n"); + buffer.append("[/NOTE]\n"); + return buffer.toString(); + } + + /** + * Return the row that contains the comment + * + * @return the row that contains the comment + */ + public short getRow(){ + return field_1_row; + } + + /** + * Specify the row that contains the comment + * + * @param row the row that contains the comment + */ + public void setRow(short row){ + field_1_row = row; + } + + /** + * Return the column that contains the comment + * + * @return the column that contains the comment + */ + public short getColumn(){ + return field_2_col; + } + + /** + * Specify the column that contains the comment + * + * @param col the column that contains the comment + */ + public void setColumn(short col){ + field_2_col = col; + } + + /** + * Options flags. + * + * @return the options flag + * @see NoteRecord.NOTE_VISIBLE + * @see NoteRecord.NOTE_HIDDEN + */ + public short getFlags(){ + return field_3_flags; + } + + /** + * Options flag + * + * @param flags the options flag + * @see #NOTE_VISIBLE + * @see #NOTE_HIDDEN + */ + public void setFlags(short flags){ + field_3_flags = flags; + } + + /** + * Object id for OBJ record that contains the comment + */ + public short getShapeId(){ + return field_4_shapeid; + } + + /** + * Object id for OBJ record that contains the comment + */ + public void setShapeId(short id){ + field_4_shapeid = id; + } + + /** + * Name of the original comment author + * + * @return the name of the original author of the comment + */ + public String getAuthor(){ + return field_5_author; + } + + /** + * Name of the original comment author + * + * @param author the name of the original author of the comment + */ + public void setAuthor(String author){ + field_5_author = author; + } +} diff --git a/src/java/org/apache/poi/hssf/record/NoteStructureSubRecord.java b/src/java/org/apache/poi/hssf/record/NoteStructureSubRecord.java new file mode 100644 index 0000000000..6ad3f8eb63 --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/NoteStructureSubRecord.java @@ -0,0 +1,130 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record; + +import org.apache.poi.util.*; + +/** + * Represents a NoteStructure (0xD) sub record. + * + *

    + * The docs say nothing about it. The length of this record is always 26 bytes. + *

    + * + * @author Yegor Kozlov + */ +public class NoteStructureSubRecord + extends SubRecord +{ + public final static short sid = 0x0D; + + private byte[] reserved; + + /** + * Construct a new NoteStructureSubRecord and + * fill its data with the default values + */ + public NoteStructureSubRecord() + { + //all we know is that the the length of NoteStructureSubRecord is always 22 bytes + reserved = new byte[22]; + } + + /** + * Constructs a NoteStructureSubRecord and sets its fields appropriately. + * + */ + public NoteStructureSubRecord(RecordInputStream in) + { + super(in); + + } + + /** + * Checks the sid matches the expected side for this record + * + * @param id the expected sid. + */ + protected void validateSid(short id) + { + if (id != sid) + { + throw new RecordFormatException("Not a Note Structure record"); + } + } + + /** + * Read the record data from the supplied RecordInputStream + */ + protected void fillFields(RecordInputStream in) + { + //just grab the raw data + reserved = in.readRemainder(); + } + + /** + * Convert this record to string. + * Used by BiffViewer and other utulities. + */ + public String toString() + { + StringBuffer buffer = new StringBuffer(); + + String nl = System.getProperty("line.separator"); + buffer.append("[ftNts ]" + nl); + buffer.append(" size = ").append(getRecordSize()).append(nl); + buffer.append(" reserved = ").append(HexDump.toHex(reserved)).append(nl); + buffer.append("[/ftNts ]" + nl); + return buffer.toString(); + } + + /** + * Serialize the record data into the supplied array of bytes + * + * @param offset offset in the data + * @param data the data to serialize into + * + * @return size of the record + */ + public int serialize(int offset, byte[] data) + { + LittleEndian.putShort(data, 0 + offset, sid); + LittleEndian.putShort(data, 2 + offset, (short)(getRecordSize() - 4)); + System.arraycopy(reserved, 0, data, offset + 4, getRecordSize() - 4); + + return getRecordSize(); + } + + /** + * Size of record + */ + public int getRecordSize() + { + return 4 + reserved.length; + } + + /** + * @return id of this record. + */ + public short getSid() + { + return sid; + } +} + + diff --git a/src/java/org/apache/poi/hssf/record/RecordFactory.java b/src/java/org/apache/poi/hssf/record/RecordFactory.java index 9f777ca423..0ddfb60408 100644 --- a/src/java/org/apache/poi/hssf/record/RecordFactory.java +++ b/src/java/org/apache/poi/hssf/record/RecordFactory.java @@ -73,7 +73,8 @@ public class RecordFactory ObjRecord.class, TextObjectRecord.class, PaletteRecord.class, StringRecord.class, RecalcIdRecord.class, SharedFormulaRecord.class, HorizontalPageBreakRecord.class, VerticalPageBreakRecord.class, - WriteProtectRecord.class, FilePassRecord.class, PaneRecord.class + WriteProtectRecord.class, FilePassRecord.class, PaneRecord.class, + NoteRecord.class }; } private static Map recordsMap = recordsToMap(records); diff --git a/src/java/org/apache/poi/hssf/record/SubRecord.java b/src/java/org/apache/poi/hssf/record/SubRecord.java index deca230f11..944c671d6a 100644 --- a/src/java/org/apache/poi/hssf/record/SubRecord.java +++ b/src/java/org/apache/poi/hssf/record/SubRecord.java @@ -64,6 +64,9 @@ abstract public class SubRecord case EndSubRecord.sid: r = new EndSubRecord( in ); break; + case NoteStructureSubRecord.sid: + r = new NoteStructureSubRecord( in ); + break; default: r = new UnknownRecord( in ); } diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java b/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java index 1f706d316f..ec81c0d614 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java @@ -33,8 +33,7 @@ import org.apache.poi.hssf.record.formula.Ptg; import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; +import java.util.*; /** * High level representation of a cell in a row of a spreadsheet. @@ -51,6 +50,7 @@ import java.util.Date; * @author Andrew C. Oliver (acoliver at apache dot org) * @author Dan Sherman (dsherman at isisph.com) * @author Brian Sanders (kestrel at burdell dot org) Active Cell support + * @author Yegor Kozlov cell comments support * @version 1.0-pre */ @@ -113,6 +113,7 @@ public class HSSFCell private Workbook book; private Sheet sheet; private CellValueRecordInterface record; + private HSSFComment comment; /** * Creates new Cell - Should only be called by HSSFRow. This creates a cell @@ -959,4 +960,60 @@ public class HSSFCell return "Unknown Cell Type: " + getCellType(); } } + + /** + * Assign a comment to this cell + * + * @param comment comment associated with this cell + */ + public void setCellComment(HSSFComment comment){ + comment.setRow((short)record.getRow()); + comment.setColumn(record.getColumn()); + this.comment = comment; + } + + /** + * Returns the comment associated with this cell + * + * @return comment associated with this cell + */ + public HSSFComment getCellComment(){ + if (comment == null) { + HashMap txshapes = new HashMap(); //map shapeId and TextObjectRecord + for (Iterator it = sheet.getRecords().iterator(); it.hasNext(); ) { + Record rec = ( Record ) it.next(); + if (rec instanceof NoteRecord){ + NoteRecord note = (NoteRecord)rec; + if (note.getRow() == record.getRow() && note.getColumn() == record.getColumn()){ + TextObjectRecord txo = (TextObjectRecord)txshapes.get(new Integer(note.getShapeId())); + comment = new HSSFComment(null, null); + comment.setRow(note.getRow()); + comment.setColumn(note.getColumn()); + comment.setAuthor(note.getAuthor()); + comment.setVisible(note.getFlags() == NoteRecord.NOTE_VISIBLE); + comment.setString(txo.getStr()); + break; + } + } else if (rec instanceof ObjRecord){ + ObjRecord obj = (ObjRecord)rec; + SubRecord sub = (SubRecord)obj.getSubRecords().get(0); + if (sub instanceof CommonObjectDataSubRecord){ + CommonObjectDataSubRecord cmo = (CommonObjectDataSubRecord)sub; + if (cmo.getObjectType() == CommonObjectDataSubRecord.OBJECT_TYPE_COMMENT){ + //find the nearest TextObjectRecord which holds comment's text and map it to its shapeId + while(it.hasNext()) { + rec = ( Record ) it.next(); + if (rec instanceof TextObjectRecord) { + txshapes.put(new Integer(cmo.getObjectId()), rec); + break; + } + } + + } + } + } + } + } + return comment; + } } diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFComment.java b/src/java/org/apache/poi/hssf/usermodel/HSSFComment.java new file mode 100644 index 0000000000..d56db733a3 --- /dev/null +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFComment.java @@ -0,0 +1,143 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.hssf.usermodel; + +import org.apache.poi.hssf.record.EscherAggregate; +import org.apache.poi.hssf.record.NoteRecord; +import org.apache.poi.hssf.record.TextObjectRecord; +import org.apache.poi.ddf.*; + +import java.util.Map; +import java.util.List; +import java.util.Iterator; + +/** + * Represents a cell comment - a sticky note associated with a cell. + * + * @author Yegor Kolzlov + */ +public class HSSFComment extends HSSFTextbox { + + private boolean visible; + private short col, row; + private String author; + + /** + * Construct a new comment with the given parent and anchor. + * + * @param parent + * @param anchor defines position of this anchor in the sheet + */ + public HSSFComment( HSSFShape parent, HSSFAnchor anchor ) + { + super( parent, anchor ); + setShapeType(OBJECT_TYPE_COMMENT); + + //default color for comments + fillColor = 0x08000050; + + //by default comments are hidden + visible = false; + + author = ""; + } + + + /** + * Returns whether this comment is visible. + * + * @param visible true if the comment is visible, false otherwise + */ + public void setVisible(boolean visible){ + this.visible = visible; + } + + /** + * Sets whether this comment is visible. + * + * @return true if the comment is visible, false otherwise + */ + public boolean isVisible(){ + return this.visible; + } + + /** + * Return the row of the cell that contains the comment + * + * @return the 0-based row of the cell that contains the comment + */ + public int getRow(){ + return row; + } + + /** + * Set the row of the cell that contains the comment + * + * @param row the 0-based row of the cell that contains the comment + */ + public void setRow(int row){ + this.row = (short)row; + } + + /** + * Return the column of the cell that contains the comment + * + * @return the 0-based column of the cell that contains the comment + */ + public short getColumn(){ + return col; + } + + /** + * Set the column of the cell that contains the comment + * + * @param col the 0-based column of the cell that contains the comment + */ + public void setColumn(short col){ + this.col = col; + } + + /** + * Name of the original comment author + * + * @return the name of the original author of the comment + */ + public String getAuthor(){ + return author; + } + + /** + * Name of the original comment author + * + * @param author the name of the original author of the comment + */ + public void setAuthor(String author){ + this.author = author; + } + + /** + * Sets the rich text string used by this comment. + * + * @param string Sets the rich text string used by this object. + */ + public void setString( HSSFRichTextString string ) + { + //if font is not set we must set the default one implicitly + if (string.numFormattingRuns() == 0) string.applyFont((short)0); + super.setString(string); + } +} diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFPatriarch.java b/src/java/org/apache/poi/hssf/usermodel/HSSFPatriarch.java index bf97179ed0..4dfa700754 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFPatriarch.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFPatriarch.java @@ -125,6 +125,21 @@ public class HSSFPatriarch return shape; } + /** + * Constructs a cell comment. + * + * @param anchor the client anchor describes how this comment is attached + * to the sheet. + * @return the newly created comment. + */ + public HSSFComment createComment(HSSFAnchor anchor) + { + HSSFComment shape = new HSSFComment(null, anchor); + shape.anchor = anchor; + shapes.add(shape); + return shape; + } + /** * Returns a list of all shapes contained by the patriarch. */ diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFSimpleShape.java b/src/java/org/apache/poi/hssf/usermodel/HSSFSimpleShape.java index e2d64bac31..dca653ad8d 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFSimpleShape.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFSimpleShape.java @@ -47,7 +47,7 @@ public class HSSFSimpleShape // public final static short OBJECT_TYPE_LIST_BOX = 18; // public final static short OBJECT_TYPE_GROUP_BOX = 19; // public final static short OBJECT_TYPE_COMBO_BOX = 20; -// public final static short OBJECT_TYPE_COMMENT = 25; + public final static short OBJECT_TYPE_COMMENT = 25; // public final static short OBJECT_TYPE_MICROSOFT_OFFICE_DRAWING = 30; int shapeType = OBJECT_TYPE_LINE; @@ -65,6 +65,7 @@ public class HSSFSimpleShape * @see #OBJECT_TYPE_OVAL * @see #OBJECT_TYPE_RECTANGLE * @see #OBJECT_TYPE_PICTURE + * @see #OBJECT_TYPE_COMMENT */ public int getShapeType() { return shapeType; } @@ -77,6 +78,7 @@ public class HSSFSimpleShape * @see #OBJECT_TYPE_OVAL * @see #OBJECT_TYPE_RECTANGLE * @see #OBJECT_TYPE_PICTURE + * @see #OBJECT_TYPE_COMMENT */ public void setShapeType( int shapeType ){ this.shapeType = shapeType; } diff --git a/src/testcases/org/apache/poi/hssf/HSSFTests.java b/src/testcases/org/apache/poi/hssf/HSSFTests.java index d0fc8fe854..c26ba9beb3 100644 --- a/src/testcases/org/apache/poi/hssf/HSSFTests.java +++ b/src/testcases/org/apache/poi/hssf/HSSFTests.java @@ -110,6 +110,7 @@ import org.apache.poi.hssf.util.TestCellReference; import org.apache.poi.hssf.util.TestRKUtil; import org.apache.poi.hssf.util.TestRangeAddress; import org.apache.poi.hssf.util.TestSheetReferences; +import org.apache.poi.hssf.usermodel.TestHSSFComment; /** * Test Suite for running just HSSF tests. Mostly @@ -227,7 +228,8 @@ public class HSSFTests suite.addTest(new TestSuite(TestModelFactory.class)); suite.addTest(new TestSuite(TestDrawingManager.class)); suite.addTest(new TestSuite(TestSheet.class)); - + + suite.addTest(new TestSuite(TestHSSFComment.class)); //$JUnit-END$ return suite; } diff --git a/src/testcases/org/apache/poi/hssf/data/SimpleWithComments.xls b/src/testcases/org/apache/poi/hssf/data/SimpleWithComments.xls new file mode 100644 index 0000000000..0fbec16a63 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/SimpleWithComments.xls differ diff --git a/src/testcases/org/apache/poi/hssf/record/TestNoteRecord.java b/src/testcases/org/apache/poi/hssf/record/TestNoteRecord.java new file mode 100644 index 0000000000..5d45094924 --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/record/TestNoteRecord.java @@ -0,0 +1,82 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record; + + +import junit.framework.TestCase; + +import java.util.Arrays; + +/** + * Tests the serialization and deserialization of the NoteRecord + * class works correctly. Test data taken directly from a real + * Excel file. + * + * @author Yegor Kozlov + */ +public class TestNoteRecord + extends TestCase +{ + private byte[] data = new byte[] { + 0x06, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02, 0x04, 0x1A, 0x00, + 0x00, 0x41, 0x70, 0x61, 0x63, 0x68, 0x65, 0x20, 0x53, 0x6F, + 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x20, 0x46, 0x6F, 0x75, + 0x6E, 0x64, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x00 + }; + + public TestNoteRecord(String name) + { + super(name); + } + + public void testRead() + throws Exception + { + + NoteRecord record = new NoteRecord(new TestcaseRecordInputStream(NoteRecord.sid, (short)data.length, data)); + + assertEquals(NoteRecord.sid, record.getSid()); + record.validateSid(NoteRecord.sid); + assertEquals(6, record.getRow()); + assertEquals(1, record.getColumn()); + assertEquals(NoteRecord.NOTE_VISIBLE, record.getFlags()); + assertEquals(1026, record.getShapeId()); + assertEquals("Apache Software Foundation", record.getAuthor()); + + } + + public void testWrite() + { + NoteRecord record = new NoteRecord(); + assertEquals(NoteRecord.sid, record.getSid()); + record.validateSid(NoteRecord.sid); + + record.setRow((short)6); + record.setColumn((short)1); + record.setFlags(NoteRecord.NOTE_VISIBLE); + record.setShapeId((short)1026); + record.setAuthor("Apache Software Foundation"); + + byte [] ser = record.serialize(); + assertEquals(ser.length - 4, data.length); + + byte[] recdata = new byte[ser.length - 4]; + System.arraycopy(ser, 4, recdata, 0, recdata.length); + assertTrue(Arrays.equals(data, recdata)); + } +} diff --git a/src/testcases/org/apache/poi/hssf/record/TestNoteStructureSubRecord.java b/src/testcases/org/apache/poi/hssf/record/TestNoteStructureSubRecord.java new file mode 100644 index 0000000000..833aa66128 --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/record/TestNoteStructureSubRecord.java @@ -0,0 +1,68 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.hssf.record; + + +import junit.framework.TestCase; + +import java.util.Arrays; + +/** + * Tests the serialization and deserialization of the NoteRecord + * class works correctly. Test data taken directly from a real + * Excel file. + * + * @author Yegor Kozlov + */ +public class TestNoteStructureSubRecord + extends TestCase +{ + private byte[] data = new byte[] { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte)0x80, 0x00, 0x00, 0x00, + 0x00, 0x00, (byte)0xBF, 0x00, 0x00, 0x00, 0x00, 0x00, (byte)0x81, 0x01, + (byte)0xCC, (byte)0xEC + }; + + public TestNoteStructureSubRecord(String name) + { + super(name); + } + + public void testRead() + throws Exception + { + + NoteStructureSubRecord record = new NoteStructureSubRecord(new TestcaseRecordInputStream(NoteStructureSubRecord.sid, (short)data.length, data)); + + assertEquals(NoteStructureSubRecord.sid, record.getSid()); + record.validateSid(NoteStructureSubRecord.sid); + assertEquals(data.length + 4, record.getRecordSize()); + + } + + public void testWrite() + { + NoteStructureSubRecord record = new NoteStructureSubRecord(); + assertEquals(NoteStructureSubRecord.sid, record.getSid()); + record.validateSid(NoteStructureSubRecord.sid); + assertEquals(data.length + 4, record.getRecordSize()); + + byte [] ser = record.serialize(); + assertEquals(ser.length - 4, data.length); + + } +} diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFComment.java b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFComment.java new file mode 100644 index 0000000000..3f38747b02 --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFComment.java @@ -0,0 +1,120 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.hssf.usermodel; + +import junit.framework.TestCase; + +import java.io.*; + +/** + * Tests TestHSSFCellComment. + * + * @author Yegor Kozlov + */ + +public class TestHSSFComment extends TestCase { + + /** + * Test that we can create cells and add comments to it. + */ + public static void testWriteComments() throws Exception { + String cellText = "Hello, World"; + String commentText = "We can set comments in POI"; + String commentAuthor = "Apache Software Foundation"; + int cellRow = 3; + short cellColumn = 1; + + HSSFWorkbook wb = new HSSFWorkbook(); + + HSSFSheet sheet = wb.createSheet(); + + HSSFCell cell = sheet.createRow(cellRow).createCell(cellColumn); + cell.setCellValue(new HSSFRichTextString(cellText)); + assertNull(cell.getCellComment()); + + HSSFPatriarch patr = sheet.createDrawingPatriarch(); + HSSFClientAnchor anchor = new HSSFClientAnchor(); + anchor.setAnchor( (short)4, 2, 0, 0, (short) 6, 5, 0, 0); + HSSFComment comment = patr.createComment(anchor); + HSSFRichTextString string1 = new HSSFRichTextString(commentText); + comment.setString(string1); + comment.setAuthor(commentAuthor); + cell.setCellComment(comment); + + //verify our settings + assertEquals(HSSFSimpleShape.OBJECT_TYPE_COMMENT, comment.getShapeType()); + assertEquals(commentAuthor, comment.getAuthor()); + assertEquals(commentText, comment.getString().getString()); + assertEquals(cellRow, comment.getRow()); + assertEquals(cellColumn, comment.getColumn()); + + //serialize the workbook and read it again + ByteArrayOutputStream out = new ByteArrayOutputStream(); + wb.write(out); + out.close(); + + wb = new HSSFWorkbook(new ByteArrayInputStream(out.toByteArray())); + sheet = wb.getSheetAt(0); + cell = sheet.getRow(cellRow).getCell(cellColumn); + comment = cell.getCellComment(); + + assertNotNull(comment); + assertEquals(HSSFSimpleShape.OBJECT_TYPE_COMMENT, comment.getShapeType()); + assertEquals(commentAuthor, comment.getAuthor()); + assertEquals(commentText, comment.getString().getString()); + assertEquals(cellRow, comment.getRow()); + assertEquals(cellColumn, comment.getColumn()); + } + + /** + * test that we can read cell comments from an existing workbook. + */ + public static void testReadComments() throws Exception { + + String dir = System.getProperty("HSSF.testdata.path"); + FileInputStream is = new FileInputStream(new File(dir, "SimpleWithComments.xls")); + HSSFWorkbook wb = new HSSFWorkbook(is); + is.close(); + + HSSFSheet sheet = wb.getSheetAt(0); + + HSSFCell cell; + HSSFRow row; + HSSFComment comment; + + for (int rownum = 0; rownum < 3; rownum++) { + row = sheet.getRow(rownum); + cell = row.getCell((short)0); + comment = cell.getCellComment(); + assertNull("Cells in the first column are not commented", comment); + } + + for (int rownum = 0; rownum < 3; rownum++) { + row = sheet.getRow(rownum); + cell = row.getCell((short)1); + comment = cell.getCellComment(); + assertNotNull("Cells in the second column have comments", comment); + assertEquals(HSSFSimpleShape.OBJECT_TYPE_COMMENT, comment.getShapeType()); + assertEquals("Yegor Kozlov", comment.getAuthor()); + assertFalse("cells in the second column have not empyy notes", + "".equals(comment.getString().getString())); + assertEquals(rownum, comment.getRow()); + assertEquals(cell.getCellNum(), comment.getColumn()); + } + } + +}