You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SheetUtil.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. /* ====================================================================
  2. Licensed to the Apache Software Foundation (ASF) under one or more
  3. contributor license agreements. See the NOTICE file distributed with
  4. this work for additional information regarding copyright ownership.
  5. The ASF licenses this file to You under the Apache License, Version 2.0
  6. (the "License"); you may not use this file except in compliance with
  7. the License. You may obtain a copy of the License at
  8. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. ==================================================================== */
  15. package org.apache.poi.ss.util;
  16. import java.awt.font.FontRenderContext;
  17. import java.awt.font.TextAttribute;
  18. import java.awt.font.TextLayout;
  19. import java.awt.geom.AffineTransform;
  20. import java.awt.geom.Rectangle2D;
  21. import java.text.AttributedString;
  22. import java.util.List;
  23. import java.util.Locale;
  24. import java.util.Map;
  25. import org.apache.poi.ss.usermodel.Cell;
  26. import org.apache.poi.ss.usermodel.CellStyle;
  27. import org.apache.poi.ss.usermodel.CellType;
  28. import org.apache.poi.ss.usermodel.CellValue;
  29. import org.apache.poi.ss.usermodel.DataFormatter;
  30. import org.apache.poi.ss.usermodel.Font;
  31. import org.apache.poi.ss.usermodel.FormulaEvaluator;
  32. import org.apache.poi.ss.usermodel.RichTextString;
  33. import org.apache.poi.ss.usermodel.Row;
  34. import org.apache.poi.ss.usermodel.Sheet;
  35. import org.apache.poi.ss.usermodel.Workbook;
  36. import org.apache.poi.util.Internal;
  37. import org.apache.poi.util.Removal;
  38. /**
  39. * Helper methods for when working with Usermodel sheets
  40. *
  41. * @author Yegor Kozlov
  42. */
  43. public class SheetUtil {
  44. /**
  45. * Excel measures columns in units of 1/256th of a character width
  46. * but the docs say nothing about what particular character is used.
  47. * '0' looks to be a good choice.
  48. */
  49. private static final char defaultChar = '0';
  50. /**
  51. * This is the multiple that the font height is scaled by when determining the
  52. * boundary of rotated text.
  53. */
  54. private static final double fontHeightMultiple = 2.0;
  55. /**
  56. * Dummy formula evaluator that does nothing.
  57. * YK: The only reason of having this class is that
  58. * {@link org.apache.poi.ss.usermodel.DataFormatter#formatCellValue(org.apache.poi.ss.usermodel.Cell)}
  59. * returns formula string for formula cells. Dummy evaluator makes it to format the cached formula result.
  60. *
  61. * See Bugzilla #50021
  62. */
  63. private static final FormulaEvaluator dummyEvaluator = new FormulaEvaluator() {
  64. @Override
  65. public void clearAllCachedResultValues(){}
  66. @Override
  67. public void notifySetFormula(Cell cell) {}
  68. @Override
  69. public void notifyDeleteCell(Cell cell) {}
  70. @Override
  71. public void notifyUpdateCell(Cell cell) {}
  72. @Override
  73. public CellValue evaluate(Cell cell) {return null; }
  74. @Override
  75. public Cell evaluateInCell(Cell cell) { return null; }
  76. @Override
  77. public void setupReferencedWorkbooks(Map<String, FormulaEvaluator> workbooks) {}
  78. @Override
  79. public void setDebugEvaluationOutputForNextEval(boolean value) {}
  80. @Override
  81. public void setIgnoreMissingWorkbooks(boolean ignore) {}
  82. @Override
  83. public void evaluateAll() {}
  84. @Override
  85. public CellType evaluateFormulaCell(Cell cell) { return cell.getCachedFormulaResultType(); }
  86. /**
  87. * @since POI 3.15 beta 3
  88. * @deprecated POI 3.15 beta 3. Will be deleted when we make the CellType enum transition. See bug 59791.
  89. */
  90. @Deprecated
  91. @Removal(version = "4.2")
  92. @Internal(since="POI 3.15 beta 3")
  93. @Override
  94. public CellType evaluateFormulaCellEnum(Cell cell) { return evaluateFormulaCell(cell); }
  95. };
  96. /**
  97. * drawing context to measure text
  98. */
  99. private static final FontRenderContext fontRenderContext = new FontRenderContext(null, true, true);
  100. /**
  101. * Compute width of a single cell
  102. *
  103. * @param cell the cell whose width is to be calculated
  104. * @param defaultCharWidth the width of a single character
  105. * @param formatter formatter used to prepare the text to be measured
  106. * @param useMergedCells whether to use merged cells
  107. * @return the width in pixels or -1 if cell is empty
  108. */
  109. public static double getCellWidth(Cell cell, int defaultCharWidth, DataFormatter formatter, boolean useMergedCells) {
  110. List<CellRangeAddress> mergedRegions = cell.getSheet().getMergedRegions();
  111. return getCellWidth(cell, defaultCharWidth, formatter, useMergedCells, mergedRegions);
  112. }
  113. /**
  114. * Compute width of a single cell
  115. *
  116. * This method receives the list of merged regions as querying it from the cell/sheet
  117. * is time-consuming and thus caching the list across cells speeds up certain operations
  118. * considerably.
  119. *
  120. * @param cell the cell whose width is to be calculated
  121. * @param defaultCharWidth the width of a single character
  122. * @param formatter formatter used to prepare the text to be measured
  123. * @param useMergedCells whether to use merged cells
  124. * @param mergedRegions The list of merged regions as received via cell.getSheet().getMergedRegions()
  125. * @return the width in pixels or -1 if cell is empty
  126. */
  127. public static double getCellWidth(Cell cell, int defaultCharWidth, DataFormatter formatter, boolean useMergedCells,
  128. List<CellRangeAddress> mergedRegions) {
  129. Sheet sheet = cell.getSheet();
  130. Workbook wb = sheet.getWorkbook();
  131. Row row = cell.getRow();
  132. int column = cell.getColumnIndex();
  133. // FIXME: this looks very similar to getCellWithMerges below. Consider consolidating.
  134. // We should only be checking merged regions if useMergedCells is true. Why are we doing this for-loop?
  135. int colspan = 1;
  136. for (CellRangeAddress region : mergedRegions) {
  137. if (region.isInRange(row.getRowNum(), column)) {
  138. if (!useMergedCells) {
  139. // If we're not using merged cells, skip this one and move on to the next.
  140. return -1;
  141. }
  142. cell = row.getCell(region.getFirstColumn());
  143. colspan = 1 + region.getLastColumn() - region.getFirstColumn();
  144. }
  145. }
  146. CellStyle style = cell.getCellStyle();
  147. CellType cellType = cell.getCellType();
  148. // for formula cells we compute the cell width for the cached formula result
  149. if (cellType == CellType.FORMULA)
  150. cellType = cell.getCachedFormulaResultType();
  151. Font font = wb.getFontAt(style.getFontIndexAsInt());
  152. double width = -1;
  153. if (cellType == CellType.STRING) {
  154. RichTextString rt = cell.getRichStringCellValue();
  155. String[] lines = rt.getString().split("\\n");
  156. for (String line : lines) {
  157. String txt = line + defaultChar;
  158. AttributedString str = new AttributedString(txt);
  159. copyAttributes(font, str, 0, txt.length());
  160. /*if (rt.numFormattingRuns() > 0) {
  161. // TODO: support rich text fragments
  162. }*/
  163. width = getCellWidth(defaultCharWidth, colspan, style, width, str);
  164. }
  165. } else {
  166. String sval = null;
  167. if (cellType == CellType.NUMERIC) {
  168. // Try to get it formatted to look the same as excel
  169. try {
  170. sval = formatter.formatCellValue(cell, dummyEvaluator);
  171. } catch (Exception e) {
  172. sval = String.valueOf(cell.getNumericCellValue());
  173. }
  174. } else if (cellType == CellType.BOOLEAN) {
  175. sval = String.valueOf(cell.getBooleanCellValue()).toUpperCase(Locale.ROOT);
  176. }
  177. if(sval != null) {
  178. String txt = sval + defaultChar;
  179. AttributedString str = new AttributedString(txt);
  180. copyAttributes(font, str, 0, txt.length());
  181. width = getCellWidth(defaultCharWidth, colspan, style, width, str);
  182. }
  183. }
  184. return width;
  185. }
  186. /**
  187. * Calculate the best-fit width for a cell
  188. * If a merged cell spans multiple columns, evenly distribute the column width among those columns
  189. *
  190. * @param defaultCharWidth the width of a character using the default font in a workbook
  191. * @param colspan the number of columns that is spanned by the cell (1 if the cell is not part of a merged region)
  192. * @param style the cell style, which contains text rotation and indention information needed to compute the cell width
  193. * @param minWidth the minimum best-fit width. This algorithm will only return values greater than or equal to the minimum width.
  194. * @param str the text contained in the cell
  195. * @return the best fit cell width
  196. */
  197. private static double getCellWidth(int defaultCharWidth, int colspan,
  198. CellStyle style, double minWidth, AttributedString str) {
  199. TextLayout layout = new TextLayout(str.getIterator(), fontRenderContext);
  200. final Rectangle2D bounds;
  201. if(style.getRotation() != 0){
  202. /*
  203. * Transform the text using a scale so that it's height is increased by a multiple of the leading,
  204. * and then rotate the text before computing the bounds. The scale results in some whitespace around
  205. * the unrotated top and bottom of the text that normally wouldn't be present if unscaled, but
  206. * is added by the standard Excel autosize.
  207. */
  208. AffineTransform trans = new AffineTransform();
  209. trans.concatenate(AffineTransform.getRotateInstance(style.getRotation()*2.0*Math.PI/360.0));
  210. trans.concatenate(
  211. AffineTransform.getScaleInstance(1, fontHeightMultiple)
  212. );
  213. bounds = layout.getOutline(trans).getBounds();
  214. } else {
  215. bounds = layout.getBounds();
  216. }
  217. // frameWidth accounts for leading spaces which is excluded from bounds.getWidth()
  218. final double frameWidth = bounds.getX() + bounds.getWidth();
  219. return Math.max(minWidth, ((frameWidth / colspan) / defaultCharWidth) + style.getIndention());
  220. }
  221. /**
  222. * Compute width of a column and return the result
  223. *
  224. * @param sheet the sheet to calculate
  225. * @param column 0-based index of the column
  226. * @param useMergedCells whether to use merged cells
  227. * @return the width in pixels or -1 if all cells are empty
  228. */
  229. public static double getColumnWidth(Sheet sheet, int column, boolean useMergedCells) {
  230. return getColumnWidth(sheet, column, useMergedCells, sheet.getFirstRowNum(), sheet.getLastRowNum());
  231. }
  232. /**
  233. * Compute width of a column based on a subset of the rows and return the result
  234. *
  235. * @param sheet the sheet to calculate
  236. * @param column 0-based index of the column
  237. * @param useMergedCells whether to use merged cells
  238. * @param firstRow 0-based index of the first row to consider (inclusive)
  239. * @param lastRow 0-based index of the last row to consider (inclusive)
  240. * @return the width in pixels or -1 if cell is empty
  241. */
  242. public static double getColumnWidth(Sheet sheet, int column, boolean useMergedCells, int firstRow, int lastRow){
  243. DataFormatter formatter = new DataFormatter();
  244. int defaultCharWidth = getDefaultCharWidth(sheet.getWorkbook());
  245. List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
  246. double width = -1;
  247. for (int rowIdx = firstRow; rowIdx <= lastRow; ++rowIdx) {
  248. Row row = sheet.getRow(rowIdx);
  249. if( row != null ) {
  250. double cellWidth = getColumnWidthForRow(row, column, defaultCharWidth, formatter, useMergedCells, mergedRegions);
  251. width = Math.max(width, cellWidth);
  252. }
  253. }
  254. return width;
  255. }
  256. /**
  257. * Get default character width using the Workbook's default font
  258. *
  259. * @param wb the workbook to get the default character width from
  260. * @return default character width in pixels
  261. */
  262. @Internal
  263. public static int getDefaultCharWidth(final Workbook wb) {
  264. Font defaultFont = wb.getFontAt( 0);
  265. AttributedString str = new AttributedString(String.valueOf(defaultChar));
  266. copyAttributes(defaultFont, str, 0, 1);
  267. TextLayout layout = new TextLayout(str.getIterator(), fontRenderContext);
  268. return (int) layout.getAdvance();
  269. }
  270. /**
  271. * Compute width of a single cell in a row
  272. * Convenience method for {@link #getCellWidth}
  273. *
  274. * @param row the row that contains the cell of interest
  275. * @param column the column number of the cell whose width is to be calculated
  276. * @param defaultCharWidth the width of a single character
  277. * @param formatter formatter used to prepare the text to be measured
  278. * @param useMergedCells whether to use merged cells
  279. * @return the width in pixels or -1 if cell is empty
  280. */
  281. private static double getColumnWidthForRow(
  282. Row row, int column, int defaultCharWidth, DataFormatter formatter, boolean useMergedCells,
  283. List<CellRangeAddress> mergedRegions) {
  284. if( row == null ) {
  285. return -1;
  286. }
  287. Cell cell = row.getCell(column);
  288. if (cell == null) {
  289. return -1;
  290. }
  291. return getCellWidth(cell, defaultCharWidth, formatter, useMergedCells, mergedRegions);
  292. }
  293. /**
  294. * Check if the Fonts are installed correctly so that Java can compute the size of
  295. * columns.
  296. *
  297. * If a Cell uses a Font which is not available on the operating system then Java may
  298. * fail to return useful Font metrics and thus lead to an auto-computed size of 0.
  299. *
  300. * This method allows to check if computing the sizes for a given Font will succeed or not.
  301. *
  302. * @param font The Font that is used in the Cell
  303. * @return true if computing the size for this Font will succeed, false otherwise
  304. */
  305. public static boolean canComputeColumnWidth(Font font) {
  306. // not sure what is the best value sample-here, only "1" did not work on some platforms...
  307. AttributedString str = new AttributedString("1w");
  308. copyAttributes(font, str, 0, "1w".length());
  309. TextLayout layout = new TextLayout(str.getIterator(), fontRenderContext);
  310. return (layout.getBounds().getWidth() > 0);
  311. }
  312. /**
  313. * Copy text attributes from the supplied Font to Java2D AttributedString
  314. */
  315. private static void copyAttributes(Font font, AttributedString str, @SuppressWarnings("SameParameterValue") int startIdx, int endIdx) {
  316. str.addAttribute(TextAttribute.FAMILY, font.getFontName(), startIdx, endIdx);
  317. str.addAttribute(TextAttribute.SIZE, (float)font.getFontHeightInPoints());
  318. if (font.getBold()) str.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD, startIdx, endIdx);
  319. if (font.getItalic() ) str.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE, startIdx, endIdx);
  320. if (font.getUnderline() == Font.U_SINGLE ) str.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON, startIdx, endIdx);
  321. }
  322. /**
  323. * Return the cell, without taking account of merged regions.
  324. * <p>
  325. * Use {@link #getCellWithMerges(Sheet, int, int)} if you want the top left
  326. * cell from merged regions instead when the reference is a merged cell.
  327. * <p>
  328. * Use this where you want to know if the given cell is explicitly defined
  329. * or not.
  330. *
  331. * @param sheet The workbook sheet to look at.
  332. * @param rowIx The 0-based index of the row.
  333. * @param colIx The 0-based index of the cell.
  334. * @return cell at the given location, or null if not defined
  335. * @throws NullPointerException if sheet is null
  336. */
  337. public static Cell getCell(Sheet sheet, int rowIx, int colIx) {
  338. Row r = sheet.getRow(rowIx);
  339. if (r != null) {
  340. return r.getCell(colIx);
  341. }
  342. return null;
  343. }
  344. /**
  345. * Return the cell, taking account of merged regions. Allows you to find the
  346. * cell who's contents are shown in a given position in the sheet.
  347. *
  348. * <p>If the cell at the given co-ordinates is a merged cell, this will
  349. * return the primary (top-left) most cell of the merged region.
  350. * <p>If the cell at the given co-ordinates is not in a merged region,
  351. * then will return the cell itself.
  352. * <p>If there is no cell defined at the given co-ordinates, will return
  353. * null.
  354. *
  355. * @param sheet The workbook sheet to look at.
  356. * @param rowIx The 0-based index of the row.
  357. * @param colIx The 0-based index of the cell.
  358. * @return cell at the given location, its base merged cell, or null if not defined
  359. * @throws NullPointerException if sheet is null
  360. */
  361. public static Cell getCellWithMerges(Sheet sheet, int rowIx, int colIx) {
  362. final Cell c = getCell(sheet, rowIx, colIx);
  363. if (c != null) return c;
  364. for (CellRangeAddress mergedRegion : sheet.getMergedRegions()) {
  365. if (mergedRegion.isInRange(rowIx, colIx)) {
  366. // The cell wanted is in this merged range
  367. // Return the primary (top-left) cell for the range
  368. Row r = sheet.getRow(mergedRegion.getFirstRow());
  369. if (r != null) {
  370. return r.getCell(mergedRegion.getFirstColumn());
  371. }
  372. }
  373. }
  374. // If we get here, then the cell isn't defined, and doesn't
  375. // live within any merged regions
  376. return null;
  377. }
  378. }