diff --git a/src/java/org/apache/poi/ss/usermodel/ClientAnchor.java b/src/java/org/apache/poi/ss/usermodel/ClientAnchor.java index 702970c69..d11edc56a 100644 --- a/src/java/org/apache/poi/ss/usermodel/ClientAnchor.java +++ b/src/java/org/apache/poi/ss/usermodel/ClientAnchor.java @@ -20,10 +20,17 @@ import org.apache.poi.util.Internal; import org.apache.poi.util.Removal; /** - * A client anchor is attached to an excel worksheet. It anchors against a - * top-left and bottom-right cell. - * - * @author Yegor Kozlov + * A client anchor is attached to an excel worksheet. It anchors against + * absolute coordinates, a top-left cell and fixed height and width, or + * a top-left and bottom-right cell, depending on the {@link AnchorType}: + *
    + *
  1. {@link AnchorType#DONT_MOVE_AND_RESIZE} == absolute top-left coordinates and width/height, no cell references + *
  2. {@link AnchorType#MOVE_DONT_RESIZE} == fixed top-left cell reference, absolute width/height + *
  3. {@link AnchorType#MOVE_AND_RESIZE} == fixed top-left and bottom-right cell references, dynamic width/height + *
+ * Note this class only reports the current values for possibly calculated positions and sizes. + * If the sheet row/column sizes or positions shift, this needs updating via external calculations. + * */ public interface ClientAnchor { @@ -91,8 +98,9 @@ public interface ClientAnchor { *

* Specifies that the current drawing shall not move with its * row and column, but should be resized. This option is not normally - * used, but is included for completeness. + * used, but is included for completeness. *

+ * Note: Excel has no setting for this combination, nor does the ECMA standard. */ DONT_MOVE_DO_RESIZE(1), @@ -145,9 +153,10 @@ public interface ClientAnchor { } /** - * Returns the column (0 based) of the first cell. + * Returns the column (0 based) of the first cell, or -1 if there is no top-left anchor cell. + * This is the case for absolute positioning (AnchorType{@link #DONT_MOVE_AND_RESIZE}). * - * @return 0-based column of the first cell. + * @return 0-based column of the first cell or -1 if none. */ public short getCol1(); @@ -159,9 +168,11 @@ public interface ClientAnchor { public void setCol1(int col1); /** - * Returns the column (0 based) of the second cell. + * Returns the column (0 based) of the second cell, or -1 if there is no bottom-right anchor cell. + * This is the case for absolute positioning ({@link AnchorType#DONT_MOVE_AND_RESIZE}) + * and absolute sizing ({@link AnchorType#MOVE_DONT_RESIZE}. * - * @return 0-based column of the second cell. + * @return 0-based column of the second cell or -1 if none. */ public short getCol2(); @@ -173,9 +184,10 @@ public interface ClientAnchor { public void setCol2(int col2); /** - * Returns the row (0 based) of the first cell. + * Returns the row (0 based) of the first cell, or -1 if there is no bottom-right anchor cell. + * This is the case for absolute positioning ({@link AnchorType#DONT_MOVE_AND_RESIZE}). * - * @return 0-based row of the first cell. + * @return 0-based row of the first cell or -1 if none. */ public int getRow1(); @@ -187,9 +199,11 @@ public interface ClientAnchor { public void setRow1(int row1); /** - * Returns the row (0 based) of the second cell. + * Returns the row (0 based) of the second cell, or -1 if there is no bottom-right anchor cell. + * This is the case for absolute positioning ({@link AnchorType#DONT_MOVE_AND_RESIZE}) + * and absolute sizing ({@link AnchorType#MOVE_DONT_RESIZE}. * - * @return 0-based row of the second cell. + * @return 0-based row of the second cell or -1 if none. */ public int getRow2(); diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFClientAnchor.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFClientAnchor.java index 0f2c1a186..0fa3f96ff 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFClientAnchor.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFClientAnchor.java @@ -18,50 +18,73 @@ package org.apache.poi.xssf.usermodel; import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.Row; import org.apache.poi.util.Internal; import org.apache.poi.util.Removal; +import org.apache.poi.xssf.util.EMUUtils; +import org.openxmlformats.schemas.drawingml.x2006.main.CTPoint2D; +import org.openxmlformats.schemas.drawingml.x2006.main.CTPositiveSize2D; import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker; /** - * A client anchor is attached to an excel worksheet. It anchors against - * top-left and bottom-right cells. + * A client anchor is attached to an excel worksheet. It anchors against: + *
    + *
  1. A fixed position and fixed size + *
  2. A position relative to a cell (top-left) and a fixed size + *
  3. A position relative to a cell (top-left) and sized relative to another cell (bottom right) + *
* - * @author Yegor Kozlov + * which method is used is determined by the {@link AnchorType}. */ -public final class XSSFClientAnchor extends XSSFAnchor implements ClientAnchor { - private AnchorType DEFAULT_ANCHOR_TYPE = AnchorType.MOVE_AND_RESIZE; +public class XSSFClientAnchor extends XSSFAnchor implements ClientAnchor { + + /** + * placeholder for zeros when needed for dynamic position calculations + */ + private static final CTMarker EMPTY_MARKER = CTMarker.Factory.newInstance(); + private AnchorType anchorType; /** - * Starting anchor point + * Starting anchor point (top-left cell + relative offset) + * if left null recalculate as needed from point */ private CTMarker cell1; /** - * Ending anchor point + * Ending anchor point (bottom-right cell + relative offset) + * if left null, re-calculate as needed from size and cell1 */ private CTMarker cell2; + /** + * if present, fixed size of the object to use instead of cell2, which is inferred instead + */ + private CTPositiveSize2D size; + + /** + * if present, fixed top-left position to use instead of cell1, which is inferred instead + */ + private CTPoint2D position; + + /** + * sheet to base dynamic calculations on, if needed. Required if size and/or position or set. + * Not needed if cell1/2 are set explicitly (dynamic sizing and position relative to cells). + */ + private XSSFSheet sheet; + /** * Creates a new client anchor and defaults all the anchor positions to 0. + * Sets the type to {@link AnchorType#MOVE_AND_RESIZE} relative to cell range A1:A1. */ public XSSFClientAnchor() { - anchorType = DEFAULT_ANCHOR_TYPE; - cell1 = CTMarker.Factory.newInstance(); - cell1.setCol(0); - cell1.setColOff(0); - cell1.setRow(0); - cell1.setRowOff(0); - cell2 = CTMarker.Factory.newInstance(); - cell2.setCol(0); - cell2.setColOff(0); - cell2.setRow(0); - cell2.setRowOff(0); + this(0,0,0,0,0,0,0,0); } /** * Creates a new client anchor and sets the top-left and bottom-right - * coordinates of the anchor. + * coordinates of the anchor by cell references and offsets. + * Sets the type to {@link AnchorType#MOVE_AND_RESIZE}. * * @param dx1 the x coordinate within the first cell. * @param dy1 the y coordinate within the first cell. @@ -73,11 +96,13 @@ public final class XSSFClientAnchor extends XSSFAnchor implements ClientAnchor { * @param row2 the row (0 based) of the second cell. */ public XSSFClientAnchor(int dx1, int dy1, int dx2, int dy2, int col1, int row1, int col2, int row2) { - this(); + anchorType = AnchorType.MOVE_AND_RESIZE; + cell1 = CTMarker.Factory.newInstance(); cell1.setCol(col1); cell1.setColOff(dx1); cell1.setRow(row1); cell1.setRowOff(dy1); + cell2 = CTMarker.Factory.newInstance(); cell2.setCol(col2); cell2.setColOff(dx2); cell2.setRow(row2); @@ -85,77 +110,203 @@ public final class XSSFClientAnchor extends XSSFAnchor implements ClientAnchor { } /** - * Create XSSFClientAnchor from existing xml beans - * + * Create XSSFClientAnchor from existing xml beans, sized and positioned relative to a pair of cells. + * Sets the type to {@link AnchorType#MOVE_AND_RESIZE}. * @param cell1 starting anchor point * @param cell2 ending anchor point */ protected XSSFClientAnchor(CTMarker cell1, CTMarker cell2) { - anchorType = DEFAULT_ANCHOR_TYPE; + anchorType = AnchorType.MOVE_AND_RESIZE; this.cell1 = cell1; this.cell2 = cell2; } + /** + * Create XSSFClientAnchor from existing xml beans, sized and positioned relative to a pair of cells. + * Sets the type to {@link AnchorType#MOVE_DONT_RESIZE}. + * + * @param sheet needed to calculate ending point based on column/row sizes + * @param cell1 starting anchor point + * @param size object size, to calculate ending anchor point + */ + protected XSSFClientAnchor(XSSFSheet sheet, CTMarker cell1, CTPositiveSize2D size) { + anchorType = AnchorType.MOVE_DONT_RESIZE; + this.sheet = sheet; + this.size = size; + this.cell1 = cell1; +// this.cell2 = calcCell(sheet, cell1, size.getCx(), size.getCy()); + } + + /** + * Create XSSFClientAnchor from existing xml beans, sized and positioned relative to a pair of cells. + * Sets the type to {@link AnchorType#DONT_MOVE_AND_RESIZE}. + * + * @param sheet needed to calculate starting and ending points based on column/row sizes + * @param position starting absolute position + * @param size object size, to calculate ending position + */ + protected XSSFClientAnchor(XSSFSheet sheet, CTPoint2D position, CTPositiveSize2D size) { + anchorType = AnchorType.DONT_MOVE_AND_RESIZE; + this.sheet = sheet; + this.position = position; + this.size = size; + // zeros for row/col/offsets +// this.cell1 = calcCell(sheet, EMPTY_MARKER, position.getCx(), position.getCy()); +// this.cell2 = calcCell(sheet, cell1, size.getCx(), size.getCy()); + } + + /** + * + * @param sheet + * @param cell starting point and offsets (may be zeros) + * @param size dimensions to calculate relative to starting point + */ + private CTMarker calcCell(CTMarker cell, long w, long h) { + CTMarker c2 = CTMarker.Factory.newInstance(); + + int r = cell.getRow(); + int c = cell.getCol(); + + int cw = EMUUtils.EMUsFromColumnWidth(sheet.getColumnWidth(c)); + + // start with width - offset, then keep adding column widths until the next one puts us over w + long wPos = cw - cell.getColOff(); + + while (wPos < w) { + c++; + cw = EMUUtils.EMUsFromColumnWidth(sheet.getColumnWidth(c)); + wPos += cw; + } + // now wPos >= w, so end column = c, now figure offset + c2.setCol(c); + c2.setColOff(cw - (wPos - w)); + + int rh = EMUUtils.EMUsFromPoints(getRowHeight(sheet, r)); + // start with height - offset, then keep adding row heights until the next one puts us over h + long hPos = rh - cell.getRowOff(); + + while (hPos < h) { + r++; + rh = EMUUtils.EMUsFromPoints(getRowHeight(sheet, r)); + hPos += rh; + } + // now hPos >= h, so end row = r, now figure offset + c2.setRow(r); + c2.setRowOff(rh - (hPos - h)); + + return c2; + } + + /** + * @param sheet + * @param row + * @return height in twips (1/20th of point) for row or default + */ + private static float getRowHeight(XSSFSheet sheet, int row) { + XSSFRow r = sheet.getRow(row); + return r == null ? sheet.getDefaultRowHeightInPoints() : r.getHeightInPoints(); + } + + private CTMarker getCell1() { + return cell1 != null ? cell1 : calcCell(EMPTY_MARKER, position.getX(), position.getY()); + } + + private CTMarker getCell2() { + return cell2 != null ? cell2 : calcCell(getCell1(), size.getCx(), size.getCy()); + } + public short getCol1() { - return (short)cell1.getCol(); + return (short)getCell1().getCol(); } + /** + * @throws NullPointerException if cell1 is null (fixed position) + * @see org.apache.poi.ss.usermodel.ClientAnchor#setCol1(int) + */ public void setCol1(int col1) { cell1.setCol(col1); } public short getCol2() { - return (short)cell2.getCol(); + return (short) getCell2().getCol(); } + /** + * @throws NullPointerException if cell2 is null (fixed size) + * @see org.apache.poi.ss.usermodel.ClientAnchor#setCol2(int) + */ public void setCol2(int col2) { cell2.setCol(col2); } public int getRow1() { - return cell1.getRow(); + return getCell1().getRow(); } + /** + * @throws NullPointerException if cell1 is null (fixed position) + * @see org.apache.poi.ss.usermodel.ClientAnchor#setRow1(int) + */ public void setRow1(int row1) { cell1.setRow(row1); } public int getRow2() { - return cell2.getRow(); + return getCell2().getRow(); } + /** + * @throws NullPointerException if cell2 is null (fixed size) + * @see org.apache.poi.ss.usermodel.ClientAnchor#setRow2(int) + */ public void setRow2(int row2) { cell2.setRow(row2); } public int getDx1() { - return (int)cell1.getColOff(); + return (int) getCell1().getColOff(); } + /** + * @throws NullPointerException if cell1 is null (fixed position) + * @see org.apache.poi.ss.usermodel.ChildAnchor#setDx1(int) + */ public void setDx1(int dx1) { cell1.setColOff(dx1); } public int getDy1() { - return (int)cell1.getRowOff(); + return (int) getCell1().getRowOff(); } + /** + * @throws NullPointerException if cell1 is null (fixed position) + * @see org.apache.poi.ss.usermodel.ChildAnchor#setDy1(int) + */ public void setDy1(int dy1) { cell1.setRowOff(dy1); } public int getDy2() { - return (int)cell2.getRowOff(); + return (int) getCell2().getRowOff(); } + /** + * @throws NullPointerException if cell2 is null (fixed size) + * @see org.apache.poi.ss.usermodel.ChildAnchor#setDy2(int) + */ public void setDy2(int dy2) { cell2.setRowOff(dy2); } public int getDx2() { - return (int)cell2.getColOff(); + return (int) getCell2().getColOff(); } + /** + * @throws NullPointerException if cell2 is null (fixed size) + * @see org.apache.poi.ss.usermodel.ChildAnchor#setDx2(int) + */ public void setDx2(int dx2) { cell2.setColOff(dx2); } @@ -184,7 +335,7 @@ public final class XSSFClientAnchor extends XSSFAnchor implements ClientAnchor { @Override public String toString(){ - return "from : " + cell1 + "; to: " + cell2; + return "from : " + getCell1() + "; to: " + getCell2(); } /** @@ -194,7 +345,7 @@ public final class XSSFClientAnchor extends XSSFAnchor implements ClientAnchor { */ @Internal public CTMarker getFrom(){ - return cell1; + return getCell1(); } protected void setFrom(CTMarker from){ @@ -208,13 +359,47 @@ public final class XSSFClientAnchor extends XSSFAnchor implements ClientAnchor { */ @Internal public CTMarker getTo(){ - return cell2; + return getCell2(); } protected void setTo(CTMarker to){ cell2 = to; } + /** + * @return absolute top-left position, or null if position is determined from the "from" cell + * @since POI 3.17 beta 1 + */ + public CTPoint2D getPosition() { + return position; + } + + /** + * Sets the top-left absolute position of the object. To use this, "from" must be set to null. + * @param position + * @since POI 3.17 beta 1 + */ + public void setPosition(CTPoint2D position) { + this.position = position; + } + + /** + * + * @return size or null, if size is determined from the to and from cells + * @since POI 3.17 beta 1 + */ + public CTPositiveSize2D getSize() { + return size; + } + + /** + * Sets the size of the object. To use this, "to" must be set to null. + * @param size + * @since POI 3.17 beta 1 + */ + public void setSize(CTPositiveSize2D size) { + this.size = size; + } /** * Sets the anchor type @@ -250,7 +435,9 @@ public final class XSSFClientAnchor extends XSSFAnchor implements ClientAnchor { } public boolean isSet(){ - return !(cell1.getCol() == 0 && cell2.getCol() == 0 && - cell1.getRow() == 0 && cell2.getRow() == 0); + CTMarker c1 = getCell1(); + CTMarker c2 = getCell2(); + return !(c1.getCol() == 0 && c2.getCol() == 0 && + c1.getRow() == 0 && c2.getRow() == 0); } } diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFDrawing.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFDrawing.java index 42518fa9f..7078f3c09 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFDrawing.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFDrawing.java @@ -58,6 +58,7 @@ import org.openxmlformats.schemas.drawingml.x2006.main.CTNonVisualDrawingProps; import org.openxmlformats.schemas.drawingml.x2006.main.CTPoint2D; import org.openxmlformats.schemas.drawingml.x2006.main.CTPositiveSize2D; import org.openxmlformats.schemas.drawingml.x2006.main.CTTransform2D; +import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTAbsoluteAnchor; import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTConnector; import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTDrawing; import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTGraphicalObjectFrame; @@ -676,7 +677,10 @@ public final class XSSFDrawing extends POIXMLDocumentPart implements Drawing + * These are defined briefly in Wikipedia + * as a "rational" way to use an integer value to represent something that could be in + * inches, centimeters, points, or pixels. + * So now we get to convert between all those. + */ +public class EMUUtils { + public static final int EMUS_PER_INCH = 914400; + public static final int EMUS_PER_POINT = 12700; + public static final int EMUS_PER_CENTIMETER = 360000; + + // TODO: these could move here or something to standardize definitions + public static final int EMU_PER_PIXEL = XSSFShape.EMU_PER_PIXEL; + public static final int EMU_PER_POINT = XSSFShape.EMU_PER_POINT; + public static final int PIXEL_DPI = XSSFShape.PIXEL_DPI; + public static final int POINT_DPI = XSSFShape.POINT_DPI; + public static final int EMU_PER_CHARACTER = (int) (EMU_PER_PIXEL * XSSFWorkbook.DEFAULT_CHARACTER_WIDTH); + + /** + * @param columnWidth as (fractional # of characters) * 256 + * @return EMUs + */ + public static final int EMUsFromColumnWidth(int columnWidth) { + return (int) (columnWidth /256d * EMUUtils.EMU_PER_CHARACTER); + } + + /** + * @param twips (1/20th of a point) typically a row height + * @return EMUs + */ + public static final int EMUsFromTwips(short twips) { + return (int) (twips / 20d * EMU_PER_POINT); + } + + /** + * @param points (fractional) + * @return EMUs + */ + public static final int EMUsFromPoints(float points) { + return (int) (points * EMU_PER_POINT); + } +}