Repository: wangguanquan/eec
Pull request #139 (2020-05-29 06:00:16)

Feature/fix#135
1. 增加Comment支持
2. Workbook放开更多属性，用户可以设置更多的文件属性(title,subject,keywords,category等)。


Patch:
diff --git a/src/main/java/org/ttzero/excel/annotation/ExcelColumn.java b/src/main/java/org/ttzero/excel/annotation/ExcelColumn.java
index d841cc7c..6862382b 100644
--- a/src/main/java/org/ttzero/excel/annotation/ExcelColumn.java
+++ b/src/main/java/org/ttzero/excel/annotation/ExcelColumn.java
@@ -34,16 +34,29 @@ import java.lang.annotation.Target;
 @Documented
 public @interface ExcelColumn {
     /**
-     * header cell
+     * Title of column
      *
-     * @return the column name
+     * @return the title of column
      */
     String value() default "";
 
     /**
-     * share body string
+     * Share body string
      *
      * @return true if shared
      */
     boolean share() default false;
+
+    /**
+     * Specify a comment in header column
+     * <p>
+     * If this annotation appears with {@code @HeaderComment} at
+     * the same time, the independent {@code @HeaderComment} annotation
+     * takes precedence
+     * <p>
+     * NOTE: This attribute only affects the header line
+     *
+     * @return a header {@link HeaderComment}
+     */
+    HeaderComment comment() default @HeaderComment;
 }
diff --git a/src/main/java/org/ttzero/excel/annotation/HeaderComment.java b/src/main/java/org/ttzero/excel/annotation/HeaderComment.java
new file mode 100644
index 00000000..dcaebf73
--- /dev/null
+++ b/src/main/java/org/ttzero/excel/annotation/HeaderComment.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2017-2020, guanquan.wang@yandex.com 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.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author guanquan.wang at 2020-05-21 16:43
+ */
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface HeaderComment {
+    /**
+     * Description detail
+     *
+     * @return the comment detail desc
+     */
+    String value() default "";
+
+    /**
+     * Specify a title, the title will be bold display
+     *
+     * @return sub-title of comment
+     */
+    String title() default "";
+
+
+}
diff --git a/src/main/java/org/ttzero/excel/entity/Comment.java b/src/main/java/org/ttzero/excel/entity/Comment.java
new file mode 100644
index 00000000..39d40ad6
--- /dev/null
+++ b/src/main/java/org/ttzero/excel/entity/Comment.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2017-2020, guanquan.wang@yandex.com 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;
+
+/**
+ * @author guanquan.wang at 2020-05-22 09:32
+ */
+public class Comment {
+    private String title;
+    private String value;
+
+    public Comment (String title, String value) {
+        this.title = title;
+        this.value = value;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+}
diff --git a/src/main/java/org/ttzero/excel/entity/Comments.java b/src/main/java/org/ttzero/excel/entity/Comments.java
new file mode 100644
index 00000000..73126037
--- /dev/null
+++ b/src/main/java/org/ttzero/excel/entity/Comments.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2017-2020, guanquan.wang@yandex.com 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;
+
+import org.ttzero.excel.annotation.TopNS;
+import org.ttzero.excel.entity.style.Font;
+import org.ttzero.excel.manager.Const;
+import org.ttzero.excel.reader.ExcelReader;
+import org.ttzero.excel.util.ExtBufferedWriter;
+import org.ttzero.excel.util.FileUtil;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.ttzero.excel.util.StringUtil.isNotEmpty;
+
+/**
+ * A simple implementation
+ *
+ * @author guanquan.wang at 2020-05-21 16:31
+ */
+@TopNS(prefix = "", value = "comments", uri = Const.SCHEMA_MAIN)
+public class Comments implements Storable, Closeable {
+
+    /** Comments Cache*/
+    private List<C> commentList;
+    private int id;
+    private String author;
+//    private final static int CACHE_SIZE = 20;
+
+    public Comments(int id, String author) {
+        this.id = id;
+        this.author = author;
+        commentList = new ArrayList<>();
+    }
+
+    public void addComment(String ref, String title, String value) {
+        C c = new C();
+        c.ref = ref;
+        c.text = new ArrayList<>();
+        if (isNotEmpty(title)) {
+            parse(title, true, c.text);
+        }
+        if (isNotEmpty(value)) {
+            parse(value, false, c.text);
+        }
+
+        commentList.add(c);
+
+//        if (commentList.size() >= CACHE_SIZE) {
+//            flush();
+//        }
+    }
+
+    private void parse(String val, boolean bold, List<R> list) {
+        // TODO
+        // a simple implement
+        R r = new R();
+        r.rPr = new Pr("宋体", 9);
+        if (bold) {
+            r.rPr.bold();
+            if (val.charAt(val.length() - 1) != 10) {
+                val += (char) 10;
+            }
+        }
+        r.t = val;
+        list.add(r);
+    }
+
+    public void flush() {
+        // TODO Write tmp and clear cache
+
+    }
+
+    @Override
+    public void close() {
+        // Ignore
+    }
+
+    @Override
+    public void writeTo(Path root) throws IOException {
+        if (commentList.isEmpty()) return;
+        try (ExtBufferedWriter writer = new ExtBufferedWriter(
+            Files.newBufferedWriter(root.resolve("comments" + id + Const.Suffix.XML)))) {
+            writer.write(Const.EXCEL_XML_DECLARATION);
+            writer.newLine();
+            TopNS topNS = this.getClass().getAnnotation(TopNS.class);
+            writer.write('<');
+            writer.write(topNS.value());
+            writer.write(" xmlns=\"");
+            writer.write(topNS.uri()[0]);
+            writer.write("\"><authors><author>");
+            writer.write(isNotEmpty(author) ? author : System.getProperty("user.name"));
+            writer.write("</author></authors><commentList>");
+
+            for (C c : commentList)
+                writer.write(c.toString());
+            writer.write("</commentList></comments>");
+        }
+
+        // Write vml
+        vml(root);
+    }
+
+    private void vml(Path root) throws IOException {
+        Path parent = root.resolve("drawings");
+        if (!Files.exists(parent)) {
+            FileUtil.mkdir(parent);
+        }
+
+        try (ExtBufferedWriter writer = new ExtBufferedWriter(
+            Files.newBufferedWriter(parent.resolve("vmlDrawing" + id + Const.Suffix.VML)))) {
+            writer.write("<xml xmlns:v=\"urn:schemas-microsoft-com:vml\"");
+            writer.write(" xmlns:o=\"urn:schemas-microsoft-com:office:office\"");
+            writer.write(" xmlns:x=\"urn:schemas-microsoft-com:office:excel\">");
+            writer.write(" <o:shapelayout v:ext=\"edit\">");
+            writer.write("  <o:idmap v:ext=\"edit\" data=\"1\"/>");
+            writer.write(" </o:shapelayout>");
+            writer.write(" <v:shapetype id=\"_x0000_t202\" coordsize=\"21600,21600\" o:spt=\"202\"");
+            writer.write("  path=\"m,l,21600r21600,l21600,xe\">");
+            writer.write("  <v:stroke joinstyle=\"miter\"/>");
+            writer.write("  <v:path gradientshapeok=\"t\" o:connecttype=\"rect\"/>");
+            writer.write(" </v:shapetype>");
+            int i = 1;
+            for (C c : commentList) {
+                long cr = ExcelReader.cellRangeToLong(c.ref);
+                writer.write(" <v:shape id=\"_x0000_s");writer.write(100 + i);
+                writer.write("\" type=\"#_x0000_t202\" style='width:100.8pt;height:60.6pt;z-index:");
+                writer.writeInt(i++);
+                writer.write(";  visibility:hidden' fillcolor=\"#ffffe1\" o:insetmode=\"auto\">");
+                writer.write("  <v:fill color2=\"#ffffe1\"/>");
+                writer.write("  <v:shadow on=\"t\" color=\"black\" obscured=\"t\"/>");
+                writer.write("  <v:path o:connecttype=\"none\"/>");
+                writer.write("  <v:textbox style='mso-direction-alt:auto'>");
+                writer.write("   <div style='text-align:left'></div>");
+                writer.write("  </v:textbox>");
+                writer.write("  <x:ClientData ObjectType=\"Note\">");
+                writer.write("   <x:MoveWithCells/>");
+                writer.write("   <x:SizeWithCells/>");
+                writer.write("   <x:Anchor/>");
+                writer.write("   <x:AutoFill>False</x:AutoFill>");
+                writer.write("   <x:Row>");writer.write((cr >> 16) - 1);writer.write("</x:Row>");
+                writer.write("   <x:Column>");writer.write((cr & 0x7FFF) - 1);writer.write("</x:Column>");
+                writer.write("  </x:ClientData>");
+                writer.write(" </v:shape>");
+            }
+            writer.write("</xml>");
+        }
+    }
+
+    private static class C {
+        private String ref;
+        private List<R> text;
+
+        @Override
+        public String toString() {
+            StringBuilder buf = new StringBuilder("<comment ref=\"")
+                .append(ref).append("\" authorId=\"0\"><text>");
+            for (R r : text)
+                buf.append(r);
+            buf.append("</text>").append("</comment>");
+            return buf.toString();
+        }
+    }
+
+    private static class R {
+        private Pr rPr;
+        private String t;
+
+        @Override
+        public String toString() {
+            return "<r>" + rPr + "<t" + (t.indexOf(10) > 0 ? " xml:space=\"preserve\">" : ">") +
+                t + "</t>" + "</r>";
+        }
+    }
+
+    private static class Pr extends Font {
+        private static final String[] STYLE = {"", "<u/>", "<b/>", "<u/><b/>", "<i/>", "<i/><u/>", "<b/><i/>", "<i/><b/><u/>"};
+        Pr(String name, int size) {
+            super(name, size);
+        }
+
+        @Override
+        public String toString() {
+            return "<rPr>" + STYLE[getStyle() & 0x07] +
+                "<sz val=\"" + getSize() + "\"/>" +
+                "<rFont val=\"" + getName() + "\"/>" +
+                "<charset val=\"" + getCharset() + "\"/></rPr>";
+        }
+    }
+}
diff --git a/src/main/java/org/ttzero/excel/entity/ListSheet.java b/src/main/java/org/ttzero/excel/entity/ListSheet.java
index c4c97678..c5a58ce7 100644
--- a/src/main/java/org/ttzero/excel/entity/ListSheet.java
+++ b/src/main/java/org/ttzero/excel/entity/ListSheet.java
@@ -17,6 +17,7 @@
 package org.ttzero.excel.entity;
 
 import org.ttzero.excel.annotation.ExcelColumn;
+import org.ttzero.excel.annotation.HeaderComment;
 import org.ttzero.excel.reader.Cell;
 import org.ttzero.excel.annotation.IgnoreExport;
 import org.ttzero.excel.util.StringUtil;
@@ -404,8 +405,10 @@ public class ListSheet<T> extends Sheet {
                     methods[i] = method;
                     ExcelColumn mec = method.getAnnotation(ExcelColumn.class);
                     if (mec != null && isNotEmpty(mec.value())) {
-                        list.add(new Column(mec.value(), field.getName(), method.getReturnType())
-                            .setShare(mec.share()));
+                        Column column = new Column(mec.value(), field.getName(), method.getReturnType())
+                            .setShare(mec.share());
+                        column.headerComment = createComment(method.getAnnotation(HeaderComment.class), mec.comment());
+                        list.add(column);
                         continue;
                     }
                 }
@@ -418,13 +421,17 @@ public class ListSheet<T> extends Sheet {
                 }
 
                 ExcelColumn ec = field.getAnnotation(ExcelColumn.class);
-
+                HeaderComment hc = field.getAnnotation(HeaderComment.class);
                 if (ec != null && isNotEmpty(ec.value())) {
-                    list.add(new Column(ec.value(), field.getName(), field.getType())
-                        .setShare(ec.share()));
+                    Column column = new Column(ec.value(), field.getName(), field.getType())
+                        .setShare(ec.share());
+                    column.headerComment = createComment(hc, ec.comment());
+                    list.add(column);
                 } else if (method != null) {
-                    list.add(new Column(field.getName(), field.getName(), field.getType())
-                        .setShare(ec != null && ec.share()));
+                    Column column = new Column(field.getName(), field.getName(), field.getType())
+                        .setShare(ec != null && ec.share());
+                    column.headerComment = createComment(hc, ec != null ? ec.comment() : null);
+                    list.add(column);
                 }
             }
             if (readLength > 0) {
@@ -433,8 +440,10 @@ public class ListSheet<T> extends Sheet {
                     readMethod.setAccessible(true);
                     methods[i++] = readMethod;
                     ExcelColumn mec = readMethod.getAnnotation(ExcelColumn.class);
-                    list.add(new Column(mec.value(), readMethod.getName(), readMethod.getReturnType())
-                        .setShare(mec.share()));
+                    Column column = new Column(mec.value(), readMethod.getName(), readMethod.getReturnType())
+                        .setShare(mec.share());
+                    column.headerComment = createComment(readMethod.getAnnotation(HeaderComment.class), mec.comment());
+                    list.add(column);
                 }
             }
             // No column to write
@@ -492,7 +501,14 @@ public class ListSheet<T> extends Sheet {
             }
             return columns.length;
         }
+    }
 
+    private Comment createComment(HeaderComment precedence, HeaderComment normal) {
+        HeaderComment comment = precedence != null ? precedence : normal;
+        if (comment != null && (isNotEmpty(comment.value()) || isNotEmpty(comment.title()))) {
+            return new Comment(comment.title(), comment.value());
+        }
+        return null;
     }
 
     /**
diff --git a/src/main/java/org/ttzero/excel/entity/RowBlock.java b/src/main/java/org/ttzero/excel/entity/RowBlock.java
index 48aa1f95..c5720ce6 100644
--- a/src/main/java/org/ttzero/excel/entity/RowBlock.java
+++ b/src/main/java/org/ttzero/excel/entity/RowBlock.java
@@ -95,7 +95,7 @@ public class RowBlock implements Iterator<Row> {
      *
      * @return true if end of file
      */
-    public boolean isEof() {
+    public boolean isEOF() {
         return eof;
     }
 
diff --git a/src/main/java/org/ttzero/excel/entity/Sheet.java b/src/main/java/org/ttzero/excel/entity/Sheet.java
index 25d9d6b9..95b3cb19 100644
--- a/src/main/java/org/ttzero/excel/entity/Sheet.java
+++ b/src/main/java/org/ttzero/excel/entity/Sheet.java
@@ -100,6 +100,10 @@ public abstract class Sheet implements Cloneable, Storable {
     protected WaterMark waterMark;
     protected RelManager relManager;
     protected int id;
+    /**
+     * The header column comments
+     */
+    protected Comments comments;
     /**
      * To mark the cell auto-width
      */
@@ -242,6 +246,7 @@ public abstract class Sheet implements Cloneable, Storable {
         public double width;
         public int o;
         public Styles styles;
+        public Comment headerComment, cellComment;
 
         /**
          * Constructor Column
@@ -949,6 +954,31 @@ public abstract class Sheet implements Cloneable, Storable {
         return this;
     }
 
+    /**
+     * Returns the header column {@link Comments}
+     *
+     * @return Columns instance if exists
+     */
+    public Comments getComments() {
+        return comments;
+    }
+
+    /**
+     * Create a {@link Comments} and add relationship
+     *
+     * @return a comment instance
+     */
+    public Comments createComments() {
+        if (comments == null) {
+            comments = new Comments(id, workbook.getCreator());
+            // FIXME Removed at excel version 2013
+            addRel(new Relationship("../drawings/vmlDrawing" + id + Const.Suffix.VML, Const.Relationship.VMLDRAWING));
+
+            addRel(new Relationship("../comments" + id + Const.Suffix.XML, Const.Relationship.COMMENTS));
+        }
+        return comments;
+    }
+
     /**
      * Returns the header column info
      * <p>
@@ -1071,7 +1101,7 @@ public abstract class Sheet implements Cloneable, Storable {
         return this;
     }
 
-    public Relationship find(String key) {
+    public Relationship findRel(String key) {
         return relManager.likeByTarget(key);
     }
 
diff --git a/src/main/java/org/ttzero/excel/entity/Workbook.java b/src/main/java/org/ttzero/excel/entity/Workbook.java
index d2f4ed65..fd7de752 100644
--- a/src/main/java/org/ttzero/excel/entity/Workbook.java
+++ b/src/main/java/org/ttzero/excel/entity/Workbook.java
@@ -22,6 +22,7 @@ import org.ttzero.excel.entity.csv.CSVWorkbookWriter;
 import org.ttzero.excel.entity.e7.XMLWorkbookWriter;
 import org.ttzero.excel.entity.style.Fill;
 import org.ttzero.excel.entity.style.Styles;
+import org.ttzero.excel.manager.docProps.Core;
 import org.ttzero.excel.processor.ParamProcessor;
 import org.ttzero.excel.processor.Watch;
 import org.ttzero.excel.util.FileUtil;
@@ -101,7 +102,15 @@ public class Workbook implements Storable {
      * Automatic interlacing fill, default fill color is '#E2EDDA'
      */
     private int autoOdd = 0;
-    private String creator, company;
+    /**
+     * Author
+     */
+    private String creator;
+    private Core core;
+    /**
+     * Specify a company name(null able)
+     */
+    private String company;
     /**
      * The fill
      */
@@ -226,6 +235,25 @@ public class Workbook implements Storable {
         return size;
     }
 
+    /**
+     * Returns the basic information about workbook
+     *
+     * @return the {@link Core} instance
+     */
+    public Core getCore() {
+        return core;
+    }
+
+    /**
+     * Setting basic information,such as title, subject, keyword, category...
+     *
+     * @param core the {@link Core} instance
+     */
+    public Workbook setCore(Core core) {
+        this.core = core;
+        return this;
+    }
+
     /**
      * Returns the Shared String Table
      *
diff --git a/src/main/java/org/ttzero/excel/entity/csv/CSVWorksheetWriter.java b/src/main/java/org/ttzero/excel/entity/csv/CSVWorksheetWriter.java
index 170c9662..c2feb908 100644
--- a/src/main/java/org/ttzero/excel/entity/csv/CSVWorksheetWriter.java
+++ b/src/main/java/org/ttzero/excel/entity/csv/CSVWorksheetWriter.java
@@ -88,7 +88,7 @@ public class CSVWorksheetWriter implements IWorksheetWriter {
                 // write row-block data
                 writeRow(rowBlock.next());
                 // end of row
-                if (rowBlock.isEof()) break;
+                if (rowBlock.isEOF()) break;
             } while ((rowBlock = supplier.get()) != null);
         }
         // Write some final info
@@ -126,7 +126,7 @@ public class CSVWorksheetWriter implements IWorksheetWriter {
                 // write row-block data
                 for (; rowBlock.hasNext(); writeRow(rowBlock.next())) ;
                 // end of row
-                if (rowBlock.isEof()) break;
+                if (rowBlock.isEOF()) break;
                 // Get the next block
                 rowBlock = sheet.nextBlock();
             }
diff --git a/src/main/java/org/ttzero/excel/entity/e7/XMLWorkbookWriter.java b/src/main/java/org/ttzero/excel/entity/e7/XMLWorkbookWriter.java
index 0643036c..270ba428 100644
--- a/src/main/java/org/ttzero/excel/entity/e7/XMLWorkbookWriter.java
+++ b/src/main/java/org/ttzero/excel/entity/e7/XMLWorkbookWriter.java
@@ -22,6 +22,7 @@ import org.dom4j.Element;
 import org.dom4j.Namespace;
 import org.dom4j.QName;
 import org.ttzero.excel.annotation.TopNS;
+import org.ttzero.excel.entity.Comments;
 import org.ttzero.excel.entity.ExcelWriteException;
 import org.ttzero.excel.entity.IWorkbookWriter;
 import org.ttzero.excel.entity.IWorksheetWriter;
@@ -45,7 +46,6 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.lang.reflect.InvocationTargetException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -182,7 +182,16 @@ public class XMLWorkbookWriter implements IWorkbookWriter {
         }
 
         for (int i = 0; i < size; i++) {
-            contentType.add(new ContentType.Override(Const.ContentType.SHEET, "/xl/worksheets/sheet" + workbook.getSheetAt(i).getId() + Const.Suffix.XML));
+            Sheet sheet;
+            contentType.add(new ContentType.Override(Const.ContentType.SHEET
+                , "/xl/worksheets/sheet" + (sheet = workbook.getSheetAt(i)).getId() + Const.Suffix.XML));
+            Comments comments = sheet.getComments();
+            if (comments != null) {
+                comments.writeTo(root);
+                contentType.add(new ContentType.Override(Const.ContentType.COMMENTS
+                    , "/xl/comments" + sheet.getId() + Const.Suffix.XML));
+                contentType.add(new ContentType.Default(Const.ContentType.VMLDRAWING, "vml"));
+            }
         } // END
 
 
@@ -234,34 +243,27 @@ public class XMLWorkbookWriter implements IWorkbookWriter {
         }
         app.setTitlePards(titleParts);
 
-        try {
-            app.writeTo(root.getParent() + "/docProps/app.xml");
-            contentType.add(new ContentType.Override(Const.ContentType.APP, "/docProps/app.xml"));
-            contentType.addRel(new Relationship("docProps/app.xml", Const.Relationship.APP));
-        } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
-            throw new ExcelWriteException(e);
-        }
+        app.writeTo(root.getParent().resolve("docProps/app.xml"));
+        contentType.add(new ContentType.Override(Const.ContentType.APP, "/docProps/app.xml"));
+        contentType.addRel(new Relationship("docProps/app.xml", Const.Relationship.APP));
     }
 
     private void writeCore(Path root, ContentType contentType) throws IOException {
-        Core core = new Core();
-        core.setCreated(new Date());
-        if (workbook.getCreator() != null) {
-            core.setCreator(workbook.getCreator());
-        } else {
-            core.setCreator(System.getProperty("user.name"));
+        Core core = workbook.getCore() != null ? workbook.getCore() : new Core();
+        if (StringUtil.isEmpty(core.getCreator())) {
+            if (workbook.getCreator() != null) {
+                core.setCreator(workbook.getCreator());
+            } else {
+                core.setCreator(System.getProperty("user.name"));
+            }
         }
-        core.setTitle(workbook.getName());
-
-        core.setModified(new Date());
+        if (StringUtil.isEmpty(core.getTitle())) core.setTitle(workbook.getName());
+        if (core.getCreated() == null) core.setCreated(new Date());
+        if (core.getModified() == null) core.setModified(new Date());
 
-        try {
-            core.writeTo(root.getParent() + "/docProps/core.xml");
-            contentType.add(new ContentType.Override(Const.ContentType.CORE, "/docProps/core.xml"));
-            contentType.addRel(new Relationship("docProps/core.xml", Const.Relationship.CORE));
-        } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
-            throw new ExcelWriteException(e);
-        }
+        core.writeTo(root.getParent().resolve("docProps/core.xml"));
+        contentType.add(new ContentType.Override(Const.ContentType.CORE, "/docProps/core.xml"));
+        contentType.addRel(new Relationship("docProps/core.xml", Const.Relationship.CORE));
     }
 
     private void madeMark(Path parent) throws IOException {
@@ -310,7 +312,7 @@ public class XMLWorkbookWriter implements IWorkbookWriter {
             uris = topNs.uri();
             rootName = topNs.value();
             for (int i = 0; i < prefixs.length; i++) {
-                if (prefixs[i].length() == 0) { // 创建前缀为空的命名空间
+                if (prefixs[i].length() == 0) {
                     rootElement = factory.createElement(rootName, uris[i]);
                     break;
                 }
@@ -376,7 +378,8 @@ public class XMLWorkbookWriter implements IWorkbookWriter {
             }
             // Interlace changes color as default
             if (sheet.getAutoOdd() == 0) {
-                sheet.setOddFill(workbook.getOddFill() == null ? new Fill(PatternType.solid, new Color(226, 237, 218)) : workbook.getOddFill());
+                sheet.setOddFill(workbook.getOddFill() == null
+                    ? new Fill(PatternType.solid, new Color(226, 237, 218)) : workbook.getOddFill());
             }
             sheet.setId(i + 1);
             // default worksheet name
diff --git a/src/main/java/org/ttzero/excel/entity/e7/XMLWorksheetWriter.java b/src/main/java/org/ttzero/excel/entity/e7/XMLWorksheetWriter.java
index 9c8b9b1f..41bc3300 100644
--- a/src/main/java/org/ttzero/excel/entity/e7/XMLWorksheetWriter.java
+++ b/src/main/java/org/ttzero/excel/entity/e7/XMLWorksheetWriter.java
@@ -17,6 +17,7 @@
 package org.ttzero.excel.entity.e7;
 
 import org.ttzero.excel.annotation.TopNS;
+import org.ttzero.excel.entity.Comments;
 import org.ttzero.excel.entity.ExcelWriteException;
 import org.ttzero.excel.entity.IWorksheetWriter;
 import org.ttzero.excel.entity.Relationship;
@@ -89,6 +90,7 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
     private Sheet sheet;
     private Sheet.Column[] columns;
     private SharedStrings sst;
+    private Comments comments;
 
     public XMLWorksheetWriter(Sheet sheet) {
         this.sheet = sheet;
@@ -117,14 +119,14 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
                     // write row-block data auto size
                     writeAutoSizeRowBlock(rowBlock);
                     // end of row
-                    if (rowBlock.isEof()) break;
+                    if (rowBlock.isEOF()) break;
                 } while ((rowBlock = supplier.get()) != null);
             } else {
                 do {
                     // write row-block data
                     writeRowBlock(rowBlock);
                     // end of row
-                    if (rowBlock.isEof()) break;
+                    if (rowBlock.isEOF()) break;
                 } while ((rowBlock = supplier.get()) != null);
             }
         }
@@ -167,7 +169,7 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
                     // write row-block data
                     writeAutoSizeRowBlock(rowBlock);
                     // end of row
-                    if (rowBlock.isEof()) break;
+                    if (rowBlock.isEOF()) break;
                     // Get the next block
                     rowBlock = sheet.nextBlock();
                 }
@@ -176,7 +178,7 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
                     // write row-block data
                     writeRowBlock(rowBlock);
                     // end of row
-                    if (rowBlock.isEof()) break;
+                    if (rowBlock.isEOF()) break;
                     // Get the next block
                     rowBlock = sheet.nextBlock();
                 }
@@ -399,6 +401,17 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
                 writeString(isNotEmpty(hc.getName()) ? hc.getName() : hc.key, row, c++, defaultStyle);
             }
         }
+
+        // Write header comments
+        c = 0;
+        for (Sheet.Column hc : columns) {
+            c++;
+            if (hc.headerComment != null) {
+                if (comments == null) comments = sheet.createComments();
+                comments.addComment(new String(int2Col(c)) + row
+                    , hc.headerComment.getTitle(), hc.headerComment.getValue());
+            }
+        }
         bw.write("</row>");
     }
 
@@ -415,13 +428,20 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
         // background image
         if (sheet.getWaterMark() != null) {
             // relationship
-            Relationship r = sheet.find("media/image"); // only one background image
+            Relationship r = sheet.findRel("media/image"); // only one background image
             if (r != null) {
                 bw.write("<picture r:id=\"");
                 bw.write(r.getId());
                 bw.write("\"/>");
             }
         }
+        // vmlDrawing
+        Relationship r = sheet.findRel("vmlDrawing");
+        if (r != null) {
+            bw.write("<legacyDrawing r:id=\"");
+            bw.write(r.getId());
+            bw.write("\"/>");
+        }
         // End target
         if (getClass().isAnnotationPresent(TopNS.class)) {
             TopNS topNS = getClass().getAnnotation(TopNS.class);
diff --git a/src/main/java/org/ttzero/excel/manager/Const.java b/src/main/java/org/ttzero/excel/manager/Const.java
index 13024264..27333b46 100644
--- a/src/main/java/org/ttzero/excel/manager/Const.java
+++ b/src/main/java/org/ttzero/excel/manager/Const.java
@@ -61,6 +61,10 @@ public interface Const {
             , SHEET = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
             , THEME = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"
             , RELATIONSHIP = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
+            , COMMENTS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
+            , DRAWINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
+            , VMLDRAWING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"
+            , CHARTS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"
             ;
     }
 
@@ -84,6 +88,10 @@ public interface Const {
             , CORE = "application/vnd.openxmlformats-package.core-properties+xml"
             , SHAREDSTRING = "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"
             , PRINTSETTING = "application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings"
+            , COMMENTS ="application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"
+            , DRAWINGS = "application/vnd.openxmlformats-officedocument.drawing+xml"
+            , VMLDRAWING = "application/vnd.openxmlformats-officedocument.vmlDrawing"
+            , CHARTS = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
             ;
     }
 
@@ -145,6 +153,10 @@ public interface Const {
          * Zip
          */
         String ZIP = ".zip";
+        /**
+         * VML
+         */
+        String VML = ".vml";
     }
 
     /**
diff --git a/src/main/java/org/ttzero/excel/manager/docProps/Core.java b/src/main/java/org/ttzero/excel/manager/docProps/Core.java
index 8432b11a..dcfabdfc 100644
--- a/src/main/java/org/ttzero/excel/manager/docProps/Core.java
+++ b/src/main/java/org/ttzero/excel/manager/docProps/Core.java
@@ -31,30 +31,63 @@ import java.util.Date;
     , "http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
     , "http://purl.org/dc/terms/"}, value = "cp:coreProperties")
 public class Core extends XmlEntity {
+    /**
+     * Title of workbook(null able)
+     */
     @NS("dc")
-    private String title;   // 标题
+    private String title;
+    /**
+     * Subject of workbook(null able)
+     */
     @NS("dc")
-    private String subject; // 主题
+    private String subject;
+    /**
+     * Author, default get the system login user
+     */
     @NS("dc")
-    private String creator; // 作者
+    private String creator;
+    /**
+     * Description(null able)
+     */
     @NS("dc")
-    private String description; // 描述
+    private String description;
+    /**
+     * List keyword about this workbook, Multiple keywords are separated by ';'(null able)
+     */
     @NS("cp")
-    private String keywords;        // 标记
+    private String keywords;
+    /**
+     * The last modify user(null able)
+     */
     @NS("cp")
-    private String lastModifiedBy;  // 最后一次保存者
+    private String lastModifiedBy;
+    /**
+     * The file version(null able)
+     */
     @NS("cp")
-    private String version;         // 版本号
+    private String version;
+    /**
+     * The file reversion(null able)
+     */
     @NS("cp")
-    private String revision;        // 修订号
+    private String revision;
+    /**
+     * Specify category about this workbook, Multiple keywords are separated by ';'(null able)
+     */
     @NS("cp")
-    private String category;        // 类别
+    private String category;
+    /**
+     * Create time(notnull)
+     */
     @NS("dcterms")
     @Attr(name = "type", value = "dcterms:W3CDTF", namespace = @NS(value = "xsi", uri = "http://www.w3.org/2001/XMLSchema-instance"))
-    private Date created;         // 创建时间
+    private Date created;
+    /**
+     * The last modify time(notnull)
+     */
     @NS("dcterms")
     @Attr(name = "type", value = "dcterms:W3CDTF", namespace = @NS(value = "xsi", uri = "http://www.w3.org/2001/XMLSchema-instance"))
-    private Date modified;       // 最后更新时间
+    private Date modified;
 
 
     public void setTitle(String title) {
diff --git a/src/main/java/org/ttzero/excel/manager/docProps/XmlEntity.java b/src/main/java/org/ttzero/excel/manager/docProps/XmlEntity.java
index ba10a90c..b2cd787a 100644
--- a/src/main/java/org/ttzero/excel/manager/docProps/XmlEntity.java
+++ b/src/main/java/org/ttzero/excel/manager/docProps/XmlEntity.java
@@ -24,7 +24,9 @@ import org.dom4j.QName;
 import org.ttzero.excel.annotation.Attr;
 import org.ttzero.excel.annotation.NS;
 import org.ttzero.excel.annotation.TopNS;
+import org.ttzero.excel.entity.ExcelWriteException;
 import org.ttzero.excel.entity.NameValue;
+import org.ttzero.excel.entity.Storable;
 import org.ttzero.excel.util.DateUtil;
 import org.ttzero.excel.util.FileUtil;
 import org.ttzero.excel.util.StringUtil;
@@ -33,7 +35,7 @@ import java.io.IOException;
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
-import java.nio.file.Paths;
+import java.nio.file.Path;
 import java.util.Collection;
 import java.util.Date;
 import java.util.Iterator;
@@ -42,12 +44,11 @@ import java.util.List;
 /**
  * @author guanquan.wang on 2017/9/21.
  */
-class XmlEntity {
+class XmlEntity implements Storable {
 
     private String[] prefixs, uris;
 
-    public void writeTo(String path) throws IllegalAccessException, NoSuchMethodException
-        , InvocationTargetException, IOException {
+    public void writeTo(Path path) throws IOException {
         DocumentFactory factory = DocumentFactory.getInstance();
         //use the factory to create a root element
         Element rootElement = null;
@@ -58,7 +59,7 @@ class XmlEntity {
             prefixs = topNs.prefix();
             uris = topNs.uri();
             for (int i = 0; i < prefixs.length; i++) {
-                if (prefixs[i].length() == 0) { // 创建前缀为空的命名空间
+                if (prefixs[i].length() == 0) {
                     rootElement = factory.createElement(topNs.value(), uris[i]);
                     break;
                 }
@@ -80,10 +81,15 @@ class XmlEntity {
                 }
             }
         }
-        toXML(rootElement, this);
+
+        try {
+            toXML(rootElement, this);
+        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
+            throw new ExcelWriteException("Create Node failed.", e);
+        }
 
         Document doc = factory.createDocument(rootElement);
-        FileUtil.writeToDiskNoFormat(doc, Paths.get(path)); // write to desk
+        FileUtil.writeToDiskNoFormat(doc, path); // write to desk
     }
 
     public void toXML(Element doc, Object o) throws IllegalAccessException
diff --git a/src/main/java/org/ttzero/excel/reader/ExcelReader.java b/src/main/java/org/ttzero/excel/reader/ExcelReader.java
index ac34cf53..d07b5a1a 100644
--- a/src/main/java/org/ttzero/excel/reader/ExcelReader.java
+++ b/src/main/java/org/ttzero/excel/reader/ExcelReader.java
@@ -780,7 +780,7 @@ public class ExcelReader implements Closeable {
      * @param r the range string of cell
      * @return long value
      */
-    static long cellRangeToLong(String r) {
+    public static long cellRangeToLong(String r) {
         char[] values = r.toCharArray();
         long v = 0L;
         int n = 0;


Test patch:
diff --git a/src/test/java/org/ttzero/excel/entity/CommentTest.java b/src/test/java/org/ttzero/excel/entity/CommentTest.java
new file mode 100644
index 00000000..3907512d
--- /dev/null
+++ b/src/test/java/org/ttzero/excel/entity/CommentTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2017-2020, guanquan.wang@yandex.com 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;
+
+import org.junit.Test;
+import org.ttzero.excel.annotation.HeaderComment;
+import org.ttzero.excel.annotation.ExcelColumn;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * @author guanquan.wang at 2020-05-21 16:52
+ */
+public class CommentTest extends WorkbookTest {
+    @Test public void testComment() throws IOException {
+        new Workbook("comment test")
+            .addSheet(new ListSheet<>(Student.randomTestData()))
+            .writeTo(defaultTestPath);
+    }
+
+    /**
+     * Annotation Object
+     */
+    public static class Student {
+        @ExcelColumn(value = "SID", comment = @HeaderComment(title = "王老师：", value = "学生ID"))
+        private int id;
+        @ExcelColumn(value = "姓名", comment = @HeaderComment(title = "王老师：", value = "学生姓名"))
+        private String name;
+        @ExcelColumn(value = "成绩")
+        @HeaderComment(title = "王老师：", value = "低于60分显示\"不及格\"")
+        private int score;
+
+        public Student() { }
+
+        public Student(int id, String name, int score) {
+            this.id = id;
+            this.name = name;
+            this.score = score;
+        }
+
+        public int getId() {
+            return id;
+        }
+
+        public void setId(int id) {
+            this.id = id;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public int getScore() {
+            return score;
+        }
+
+        public void setScore(int score) {
+            this.score = score;
+        }
+
+        public static List<Student> randomTestData(int pageNo, int limit) {
+            List<Student> list = new ArrayList<>(limit);
+            for (int i = pageNo * limit, n = i + limit; i < n; i++) {
+               Student e = new Student(i, getRandomString(), random.nextInt(50) + 50);
+                list.add(e);
+            }
+            return list;
+        }
+
+        public static List<Student> randomTestData(int n) {
+            return randomTestData(0, n);
+        }
+
+        public static List<Student> randomTestData() {
+            int n = random.nextInt(100) + 1;
+            return randomTestData(n);
+        }
+
+        @Override
+        public String toString() {
+            return "id: " + id + ", name: " + name + ", score: " + score;
+        }
+    }
+}
diff --git a/src/test/java/org/ttzero/excel/entity/ListObjectSheetTest.java b/src/test/java/org/ttzero/excel/entity/ListObjectSheetTest.java
index c1791252..e8700045 100644
--- a/src/test/java/org/ttzero/excel/entity/ListObjectSheetTest.java
+++ b/src/test/java/org/ttzero/excel/entity/ListObjectSheetTest.java
@@ -23,6 +23,7 @@ import org.ttzero.excel.annotation.IgnoreExport;
 import org.ttzero.excel.entity.style.Fill;
 import org.ttzero.excel.entity.style.PatternType;
 import org.ttzero.excel.entity.style.Styles;
+import org.ttzero.excel.manager.docProps.Core;
 import org.ttzero.excel.processor.IntConversionProcessor;
 import org.ttzero.excel.processor.StyleProcessor;
 import org.ttzero.excel.reader.ExcelReaderTest;
@@ -51,7 +52,7 @@ import static org.ttzero.excel.reader.ExcelReaderTest.testResourceRoot;
 /**
  * @author guanquan.wang at 2019-04-28 19:17
  */
-public class ListObjectSheetTest extends WorkbookTest{
+public class ListObjectSheetTest extends WorkbookTest {
 
     @Test
     public void testWrite() throws IOException {
@@ -428,6 +429,23 @@ public class ListObjectSheetTest extends WorkbookTest{
         }).writeTo(defaultTestPath);
     }
 
+    @Test public void testSpecifyCore() throws IOException {
+        Core core = new Core();
+        core.setCreator("一名光荣的测试人员");
+        core.setTitle("空白文件");
+        core.setSubject("主题");
+        core.setCategory("IT;木工");
+        core.setDescription("为了艾尔");
+        core.setKeywords("机枪兵;光头");
+        core.setVersion("1.0");
+        core.setRevision("1.2");
+        core.setLastModifiedBy("TTT");
+        new Workbook("Specify Core")
+            .setCore(core)
+            .addSheet(new ListSheet<>(Collections.singletonList(new NotSharedObject(getRandomString()))))
+                .writeTo(defaultTestPath);
+    }
+
     @Test public void testLarge() throws IOException {
         new Workbook("large07").addSheet(new ListSheet<ExcelReaderTest.LargeData>() {
             private int i = 0, n;