All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.ttzero.excel.entity.style.Styles Maven / Gradle / Ivy

/*
 * Copyright (c) 2017, [email protected] All Rights Reserved.
 *
 * Licensed 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.ttzero.excel.entity.style;

import org.ttzero.excel.annotation.TopNS;
import org.ttzero.excel.entity.I18N;
import org.ttzero.excel.entity.Storageable;
import org.ttzero.excel.manager.Const;
import org.ttzero.excel.reader.ExcelReadException;
import org.ttzero.excel.util.FileUtil;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentFactory;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.awt.Color;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import static org.ttzero.excel.util.StringUtil.isEmpty;
import static org.ttzero.excel.util.StringUtil.isNotEmpty;

/**
 * Each excel style consists of {@link NumFmt}, {@link Font},
 * {@link Fill}, {@link Border}, {@link Verticals} and
 * {@link Horizontals}, each Worksheet introduced by subscript.
 * 

* EEC uses an Integer value to represent a complete pattern, * 0-8 bits are stored in NumFmt, 8-14 bits are stored in Font, * 14-20 bits are stored in Fill, 20-26 bits are stored in Border, * 26-29 bits are stored in Vertical and 29-32 bits are stored in * Horizontal. The Build-In number format does not write into styles. * * @author guanquan.wang on 2017/10/13. */ @TopNS(prefix = "", uri = Const.SCHEMA_MAIN, value = "styleSheet") public class Styles implements Storageable { private Map map; private Document document; private List fonts; private List numFmts; private List fills; private List borders; /** * Cache the data/time format style index. * It's use for fast test the cell value is a data or time value */ private Set dateFmtCache; private Styles() { map = new HashMap<>(); } /** * Returns the style index, if the style not exists it will * be insert into styles * * @param s the value of style * @return the style index */ public int of(int s) { int n = map.getOrDefault(s, 0); if (n == 0) { n = addStyle(s); map.put(s, n); } return n; } /** * Returns the number of styles * * @return the total styles */ public int size() { return map.size(); } static final int INDEX_NUMBER_FORMAT = 24; static final int INDEX_FONT = 18; static final int INDEX_FILL = 12; static final int INDEX_BORDER = 6; static final int INDEX_VERTICAL = 3; static final int INDEX_HORIZONTAL = 0; /** * Create a general style * * @param i18N the {@link I18N} * @return Styles */ public static Styles create(I18N i18N) { Styles self = new Styles(); DocumentFactory factory = DocumentFactory.getInstance(); TopNS ns = self.getClass().getAnnotation(TopNS.class); Element rootElement; if (ns != null) { rootElement = factory.createElement(ns.value(), ns.uri()[0]); } else { rootElement = factory.createElement("styleSheet", Const.SCHEMA_MAIN); } // number format rootElement.addElement("numFmts").addAttribute("count", "0"); // font rootElement.addElement("fonts").addAttribute("count", "0"); // fill rootElement.addElement("fills").addAttribute("count", "0"); // border rootElement.addElement("borders").addAttribute("count", "0"); // cellStyleXfs Element cellStyleXfs = rootElement.addElement("cellStyleXfs").addAttribute("count", "1"); cellStyleXfs.addElement("xf") // General style .addAttribute("borderId", "0") .addAttribute("fillId", "0") .addAttribute("fontId", "0") .addAttribute("numFmtId", "0") .addElement("alignment") .addAttribute("vertical", "center"); // cellXfs rootElement.addElement("cellXfs").addAttribute("count", "0"); // cellStyles Element cellStyles = rootElement.addElement("cellStyles").addAttribute("count", "1"); cellStyles.addElement("cellStyle") .addAttribute("builtinId", "0") .addAttribute("name", i18N.get("general")) .addAttribute("xfId", "0"); self.document = factory.createDocument(rootElement); self.numFmts = new ArrayList<>(); self.addNumFmt(new NumFmt("yyyy\\-mm\\-dd")); self.addNumFmt(new NumFmt("yyyy\\-mm\\-dd\\ hh:mm:ss")); self.fonts = new ArrayList<>(); Font font1 = new Font(i18N.get("en-font-family"), 11, Color.black); // en font1.setFamily(2); font1.setScheme("minor"); self.addFont(font1); String lang = Locale.getDefault().toLanguageTag(); // 添加中文默认字体 Font font2 = new Font(i18N.get("local-font-family"), 11); // cn font2.setFamily(3); font2.setScheme("minor"); if ("zh-CN".equals(lang)) { font2.setCharset(Charset.GB2312); } else if ("zh-TW".equals(lang)) { font2.setCharset(Charset.CHINESEBIG5); } // TODO other charset self.addFont(font2); self.fills = new ArrayList<>(); self.addFill(new Fill(PatternType.none)); self.addFill(new Fill(PatternType.gray125)); self.borders = new ArrayList<>(); self.addBorder(Border.parse("none")); self.addBorder(Border.parse("thin black")); // cellXfs self.of(0); // General return self; } /** * Load the style file from disk * * @param path the style file path * @return the {@link Styles} Object */ @SuppressWarnings("unchecked") public static Styles load(Path path) { // load workbook.xml SAXReader reader = new SAXReader(); Document document; try { document = reader.read(Files.newInputStream(path)); } catch (DocumentException | IOException e) { throw new ExcelReadException(e); } Styles self = new Styles(); Element root = document.getRootElement(); // Number format Element numFmts = root.element("numFmts"); // Break if there don't contains 'numFmts' tag if (numFmts == null) { return self; } List sub = numFmts.elements(); self.numFmts = new ArrayList<>(); for (Element e : sub) { String id = getAttr(e, "numFmtId"), code = getAttr(e, "formatCode"); self.numFmts.add(new NumFmt(Integer.parseInt(id), code)); } // Sort by id self.numFmts.sort(Comparator.comparingInt(NumFmt::getId)); /* Ignore other styles, there only parse number format It's use for Excel reader */ // Cell xf Element cellXfs = root.element("cellXfs"); sub = cellXfs.elements(); int i = 0; for (Element e : sub) { String applyNumberFormat = getAttr(e, "applyNumberFormat"); if (isNotEmpty(applyNumberFormat) && Integer.parseInt(applyNumberFormat) == 1) { String numFmtId = getAttr(e, "numFmtId"); int style = Integer.parseInt(numFmtId) << INDEX_NUMBER_FORMAT; self.map.put(i, style); } i++; } // Test number format for (Integer styleIndex : self.map.keySet()) { self.isDate(styleIndex); } return self; } /** * Add number format * * @param numFmt the {@link NumFmt} entry * @return the numFmt part value in style */ public final int addNumFmt(NumFmt numFmt) { // check and search default code if (numFmt.getId() < 0) { if (isEmpty(numFmt.getCode())) { throw new NullPointerException("NumFmt code"); } int index = BuiltInNumFmt.indexOf(numFmt.getCode()); if (index > -1) { // default code numFmt.setId(index); } else { int i = numFmts.indexOf(numFmt); if (i <= -1) { int id; if (numFmts.isEmpty()) { id = 176; // customer id } else { id = numFmts.get(numFmts.size() - 1).getId() + 1; } numFmt.setId(id); numFmts.add(numFmt); Element element = document.getRootElement().element("numFmts"); element.attribute("count").setValue(String.valueOf(numFmts.size())); numFmt.toDom4j(element); } else { numFmt.setId(numFmts.get(i).getId()); } } } return numFmt.getId() << INDEX_NUMBER_FORMAT; } /** * Add font * * @param font the {@link Font} entry * @return the font part value in style */ public final int addFont(Font font) { if (isEmpty(font.getName())) { throw new FontParseException("Font name not support."); } int i = fonts.indexOf(font); if (i <= -1) { fonts.add(font); i = fonts.size() - 1; Element element = document.getRootElement().element("fonts"); element.attribute("count").setValue(String.valueOf(fonts.size())); font.toDom4j(element); } return i << INDEX_FONT; } /** * Add fill * * @param fill the {@link Fill} entry * @return the fill part value in style */ public final int addFill(Fill fill) { int i = fills.indexOf(fill); if (i <= -1) { fills.add(fill); i = fills.size() - 1; Element element = document.getRootElement().element("fills"); element.attribute("count").setValue(String.valueOf(fills.size())); fill.toDom4j(element); } return i << INDEX_FILL; } /** * Add border * * @param border the {@link Border} entry * @return the border part value in style */ public final int addBorder(Border border) { int i = borders.indexOf(border); if (i <= -1) { borders.add(border); i = borders.size() - 1; Element element = document.getRootElement().element("borders"); element.attribute("count").setValue(String.valueOf(borders.size())); border.toDom4j(element); } return i << INDEX_BORDER; } private static int[] unpack(int style) { int[] styles = new int[6]; styles[0] = style >>> INDEX_NUMBER_FORMAT; styles[1] = style << 8 >>> (INDEX_FONT + 8); styles[2] = style << 14 >>> (INDEX_FILL + 14); styles[3] = style << 20 >>> (INDEX_BORDER + 20); styles[4] = style << 26 >>> (INDEX_VERTICAL + 26); styles[5] = style << 29 >>> (INDEX_HORIZONTAL + 29); return styles; } private static int pack(int[] styles) { return styles[0] << INDEX_NUMBER_FORMAT | styles[1] << INDEX_FONT | styles[2] << INDEX_FILL | styles[3] << INDEX_BORDER | styles[4] << INDEX_VERTICAL | styles[5] << INDEX_HORIZONTAL ; } static final String[] attrNames = {"numFmtId", "fontId", "fillId", "borderId", "vertical", "horizontal" , "applyNumberFormat", "applyFont", "applyFill", "applyBorder", "applyAlignment"}; /** * add style in document * * @param s style * @return style index in styles array. */ private synchronized int addStyle(int s) { int[] styles = unpack(s); Element root = document.getRootElement(); Element cellXfs = root.element("cellXfs"); int count; if (cellXfs == null) { cellXfs = root.addElement("cellXfs").addAttribute("count", "0"); count = 0; } else { count = Integer.parseInt(cellXfs.attributeValue("count")); } int n = cellXfs.elements().size(); Element newXf = cellXfs.addElement("xf"); newXf.addAttribute(attrNames[0], String.valueOf(styles[0])) .addAttribute(attrNames[1], String.valueOf(styles[1])) .addAttribute(attrNames[2], String.valueOf(styles[2])) .addAttribute(attrNames[3], String.valueOf(styles[3])) .addAttribute("xfId", "0") ; int start = 6; if (styles[0] > 0) { newXf.addAttribute(attrNames[start], "1"); } if (styles[1] > 0) { newXf.addAttribute(attrNames[start + 1], "1"); } if (styles[2] > 0) { newXf.addAttribute(attrNames[start + 2], "1"); } if (styles[3] > 0) { newXf.addAttribute(attrNames[start + 3], "1"); } if ((styles[4] | styles[5]) > 0) { newXf.addAttribute(attrNames[start + 4], "1"); } Element subEle = newXf.addElement("alignment").addAttribute(attrNames[4], Verticals.of(styles[4])); if (styles[5] > 0) { subEle.addAttribute(attrNames[5], Horizontals.of(styles[5])); } cellXfs.addAttribute("count", String.valueOf(count + 1)); return n; } /** * Write style to disk * * @param styleFile the storage path * @throws IOException if I/O error occur */ @Override public void writeTo(Path styleFile) throws IOException { if (document != null) { // Not null FileUtil.writeToDiskNoFormat(document, styleFile); } else { Files.copy(getClass().getClassLoader().getResourceAsStream("template/styles.xml"), styleFile); } } int indexOf(int[] array, int v) { for (int i = 0; i < array.length; i++) { if (array[i] == v) { return i; } } return -1; } ////////////////////////clear style/////////////////////////////// public static int clearNumfmt(int style) { return style & (-1 >>> 32 - INDEX_NUMBER_FORMAT); } public static int clearFont(int style) { return style & ~((-1 >>> 32 - (INDEX_NUMBER_FORMAT - INDEX_FONT)) << INDEX_FONT); } public static int clearFill(int style) { return style & ~((-1 >>> 32 - (INDEX_FONT - INDEX_FILL)) << INDEX_FILL); } public static int clearBorder(int style) { return style & ~((-1 >>> 32 - (INDEX_FILL - INDEX_BORDER)) << INDEX_BORDER); } public static int clearVertical(int style) { return style & ~((-1 >>> 32 - (INDEX_BORDER - INDEX_VERTICAL)) << INDEX_VERTICAL); } public static int clearHorizontal(int style) { return style & ~(-1 >>> 32 - (INDEX_VERTICAL - INDEX_HORIZONTAL)); } ////////////////////////reset style///////////////////////////// public static int reset(int style, int newStyle) { int[] sub = unpack(style), nsub = unpack(newStyle); for (int i = 0; i < sub.length; i++) { if (nsub[i] > 0) { sub[i] = nsub[i]; } } return pack(sub); } ////////////////////////default border style///////////////////////////// public static int defaultCharBorderStyle() { return (1 << INDEX_BORDER) | Horizontals.CENTER_CONTINUOUS; } public static int defaultStringBorderStyle() { return (1 << INDEX_FONT) | (1 << INDEX_BORDER) | Horizontals.LEFT; } public static int defaultIntBorderStyle() { return (1 << INDEX_NUMBER_FORMAT) | (1 << INDEX_BORDER) | Horizontals.RIGHT; } public static int defaultDateBorderStyle() { return (176 << INDEX_NUMBER_FORMAT) | (1 << INDEX_BORDER) | Horizontals.CENTER; } public static int defaultTimestampBorderStyle() { return (177 << INDEX_NUMBER_FORMAT) | (1 << INDEX_BORDER) | Horizontals.CENTER; } public static int defaultDoubleBorderStyle() { return (2 << INDEX_NUMBER_FORMAT) | (1 << INDEX_FONT) | (1 << INDEX_BORDER) | Horizontals.RIGHT; } ////////////////////////default style///////////////////////////// public static int defaultCharStyle() { return Horizontals.CENTER_CONTINUOUS; } public static int defaultStringStyle() { return (1 << INDEX_FONT) | Horizontals.LEFT; } public static int defaultIntStyle() { return (1 << INDEX_NUMBER_FORMAT) | Horizontals.RIGHT; } public static int defaultDateStyle() { return (176 << INDEX_NUMBER_FORMAT) | Horizontals.CENTER; } public static int defaultTimestampStyle() { return (177 << INDEX_NUMBER_FORMAT) | Horizontals.CENTER; } public static int defaultDoubleStyle() { return (2 << INDEX_NUMBER_FORMAT) | (1 << INDEX_FONT) | Horizontals.RIGHT; } ////////////////////////////////Check style//////////////////////////////// public static boolean hasNumFmt(int style) { return style >>> INDEX_NUMBER_FORMAT != 0; } public static boolean hasFont(int style) { return style << 8 >>> (INDEX_FONT + 8) != 0; } public static boolean hasFill(int style) { return style << 14 >>> (INDEX_FILL + 14) != 0; } public static boolean hasBorder(int style) { return style << 20 >>> (INDEX_BORDER + 20) != 0; } public static boolean hasVertical(int style) { return style << 26 >>> (INDEX_VERTICAL + 26) != 0; } public static boolean hasHorizontal(int style) { return style << 29 >>> (INDEX_HORIZONTAL + 29) != 0; } ////////////////////////////////To object////////////////////////////////// public NumFmt getNumFmt(int style) { return numFmts.get(style >>> INDEX_NUMBER_FORMAT); } public Fill getFill(int style) { return fills.get(style << 14 >>> (INDEX_FILL + 14)); } public Font getFont(int style) { return fonts.get(style << 8 >>> (INDEX_FONT + 8)); } public Border getBorder(int style) { return borders.get(style << 20 >>> (INDEX_BORDER + 20)); } public int getVertical(int style) { return style << 26 >>> (INDEX_VERTICAL + 26); } /** * Returns the attribute value from Element * * @param element current element * @param attr the attr name * @return the attr value */ public static String getAttr(Element element, String attr) { return element != null ? element.attributeValue(attr) : null; } /** * Test the style is data format * * @param styleIndex the style index * @return true if the style content data format */ public boolean isDate(int styleIndex) { // Test from cache if (fastTestDateFmt(styleIndex)) return true; Integer style = map.get(styleIndex); if (style == null) return false; int nf = style >> INDEX_NUMBER_FORMAT & 0xFF; boolean isDate = false; NumFmt numFmt; // All indexes from 0 to 163 are reserved for built-in formats. // The first user-defined format starts at 164. if (nf < 164) { isDate = (nf >= 14 && nf <= 22 || nf >= 27 && nf <= 36 || nf >= 45 && nf <= 47 || nf >= 50 && nf <= 58 || nf == 81); // Test by numFmt code } else if ((numFmt = findFmtById(nf)) != null && (isNotEmpty(numFmt.getCode()))) { isDate = testCodeIsDate(numFmt.getCode()); } // Put into data/time format cache // Ignore the style code, Uniform use of 'yyyy-mm-dd hh:mm:ss' format output if (isDate) { if (dateFmtCache == null) dateFmtCache = new HashSet<>(); dateFmtCache.add(styleIndex); } return isDate; } public static boolean testCodeIsDate(String code) { char[] chars = code.toCharArray(); int score = 0; byte[] byteScore = new byte[26]; for (int i = 0, size = chars.length; i < size; ) { char c = chars[i]; // To lower case if (c >= 65 && c <= 90) c += 32; int a = ++i; if (c == '[') { // Found the end char ']' for (; i < size && chars[i] != ']'; i++) ; int len = i - a + 1; // DBNum{n} if (len == 6 && chars[a] == 'D' && chars[a + 1] == 'B' && chars[a + 2] == 'N' && chars[a + 3] == 'u' && chars[a + 4] == 'm') { int n = chars[a + 5] - '0'; // If use "[DBNum{n}]" etc. as the Excel display format, you can use Chinese numerals. if (n != 1 && n != 2 && n != 3) break; } // Maybe is a LCID // [$-xxyyzzzz] // https://stackoverflow.com/questions/54134729/what-does-the-130000-in-excel-locale-code-130000-mean else if (i - a > 2 && chars[a] == '$' && chars[a + 1] == '-') { // Language & Calendar Identifier // String lcid = new String(chars, a + 2, i - a - 2); // Maybe the format is a data score = 50; } } else { switch (c) { case 'y': case 'm': case 'd': case 'h': case 's': for (; i < size && chars[i] == c; i++) ; byteScore[c - 'a'] += i - a + 1; break; case 'a': case 'p': if (a < size && chars[a] == 'm') byteScore[c - 'a'] += 1; break; default: } } } // Exclude case // y > 4 // h > 4 // s > 4 // am > 1 || pm > 1 if (byteScore[24] > 4 || byteScore[7] > 4 || byteScore[18] > 4 || byteScore[0] > 1 || byteScore[15] > 1) { return false; } // Calculating the score // Plus 5 points for each keywords score += byteScore[0] * 5; // am score += byteScore[3] * 5; // d score += byteScore[7] * 5; // h score += byteScore[12] * 5; // m score += byteScore[15] * 5; // pm score += byteScore[18] * 5; // s score += byteScore[24] * 5; // y // Addition calculation if consecutive keywords appear // y + m + d if (byteScore[24] > 0 && byteScore[12] > 0 && byteScore[3] > 0 && byteScore[24] + byteScore[12] + byteScore[3] >= 4) { score += 70; // y + m } else if (byteScore[24] > 0 && byteScore[12] > 0 && byteScore[24] + byteScore[12] >= 3) { score += 60; // m + d } else if (byteScore[12] > 0 && byteScore[3] > 0) { score += 60; // Code is yyyy or yy } else if (byteScore[24] == chars.length) { score += 60; } // h + m + s if (byteScore[7] > 0 && byteScore[12] > 0 && byteScore[18] > 0 && byteScore[7] + byteScore[12] + byteScore[18] > 3) { score += 70; // h + m } else if (byteScore[7] > 0 && byteScore[12] > 0) { score += 60; // m + s } else if (byteScore[12] > 0 && byteScore[18] > 0) { score += 60; } // am + pm if (byteScore[0] + byteScore[15] == 2) { score += 50; } return score >= 70; } // Find the number format in array private NumFmt findFmtById(int id) { if (numFmts == null || numFmts.isEmpty()) return null; NumFmt fmt = numFmts.get(0); // Found it if (fmt.getId() == id) return fmt; if (fmt.getId() < id) { int size = numFmts.size(); if (fmt.getId() < 164) { int index = 1; for (; index < size; ) { if ((fmt = numFmts.get(index++)).getId() >= 164) break; } } // The format is sorted and incremented // So get it by index, the index equals id subtract the first id int index = id - fmt.getId(); if (index >= size) { fmt = null; } else { fmt = numFmts.get(id - fmt.getId()); if (fmt.getId() == id) return fmt; else if (fmt.getId() < id) { // Loop check forward for (; index++ < size; ) { NumFmt nf = numFmts.get(index); if (nf.getId() == id) { fmt = nf; break; } } } else { // Loop check backward for (; index-- > 0; ) { NumFmt nf = numFmts.get(index); if (nf.getId() == id) { fmt = nf; break; } } } } } else fmt = null; return fmt; } /** * Fast test cell value is data/time value * * @param styleIndex the style index * @return true if the style content data format */ public boolean fastTestDateFmt(int styleIndex) { return dateFmtCache != null && dateFmtCache.contains(styleIndex); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy