Repository: wangguanquan/eec
Pull request #360 (2023-09-05 13:04:02)

ExcelReader性能优化
RT


Patch:
diff --git a/CHANGELOG b/CHANGELOG
index 02f10772..e9372473 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,7 @@
+Version 0.5.10 ()
+-------------
+- 优化ExcelReader性能，性能有2倍以上提升
+
 Version 0.5.10 (2023-08-20)
 -------------
 - 修复单元格长度过长导致内容错位的异常(#354)
diff --git a/src/main/java/org/ttzero/excel/entity/Sheet.java b/src/main/java/org/ttzero/excel/entity/Sheet.java
index 064cb428..fb3012ba 100644
--- a/src/main/java/org/ttzero/excel/entity/Sheet.java
+++ b/src/main/java/org/ttzero/excel/entity/Sheet.java
@@ -1327,11 +1327,10 @@ public abstract class Sheet implements Cloneable, Storable {
      * @return column string
      */
     public static char[] int2Col(int n) {
-        char[][] cache_col = cache.get();
         char[] c;
         char A = 'A';
         if (n <= 26) {
-            c = cache_col[0];
+            c = cache[0];
             c[0] = (char) (n - 1 + A);
         } else if (n <= 702) {
             int t = n / 26, w = n % 26;
@@ -1339,7 +1338,7 @@ public abstract class Sheet implements Cloneable, Storable {
                 t--;
                 w = 26;
             }
-            c = cache_col[1];
+            c = cache[1];
             c[0] = (char) (t - 1 + A);
             c[1] = (char) (w - 1 + A);
         } else {
@@ -1352,7 +1351,7 @@ public abstract class Sheet implements Cloneable, Storable {
                 t--;
                 m += 26;
             }
-            c = cache_col[2];
+            c = cache[2];
             c[0] = (char) (t - 1 + A);
             c[1] = (char) (m - 1 + A);
             c[2] = (char) (w - 1 + A);
@@ -1360,8 +1359,7 @@ public abstract class Sheet implements Cloneable, Storable {
         return c;
     }
 
-    private static final ThreadLocal<char[][]> cache
-        = ThreadLocal.withInitial(() -> new char[][]{ {65}, {65, 65}, {65, 65, 65} });
+    private static final char[][] cache = new char[][]{ {65}, {65, 65}, {65, 65, 65} };
 
 //    /**
 //     * Check empty header row
diff --git a/src/main/java/org/ttzero/excel/entity/style/Styles.java b/src/main/java/org/ttzero/excel/entity/style/Styles.java
index 7e1bfd5b..df939927 100644
--- a/src/main/java/org/ttzero/excel/entity/style/Styles.java
+++ b/src/main/java/org/ttzero/excel/entity/style/Styles.java
@@ -32,8 +32,8 @@ import org.ttzero.excel.util.StringUtil;
 
 import java.awt.Color;
 import java.io.IOException;
+import java.io.InputStream;
 import java.lang.reflect.Field;
-import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -238,16 +238,16 @@ public class Styles implements Storable {
     /**
      * Load the style file from disk
      *
-     * @param path the style file path
+     * @param is the style {@code InputStream}
      * @return the {@link Styles} Object
      */
-    public static Styles load(Path path) {
+    public static Styles load(InputStream is) {
         // load styles.xml
         SAXReader reader = SAXReader.createDefault();
         Document document;
         try {
-            document = reader.read(Files.newInputStream(path));
-        } catch (DocumentException | IOException e) {
+            document = reader.read(is);
+        } catch (DocumentException e) {
             LOGGER.warn("Read the style failed and ignore the style to continue.", e);
             Styles self = forReader();
             // Add a default font
@@ -256,8 +256,6 @@ public class Styles implements Storable {
         }
 
         Styles self = new Styles();
-        Path themePath = path.getParent().resolve("theme/theme1.xml");
-        if (Files.exists(themePath) && !Files.isDirectory(themePath)) Theme.load(themePath);
         Element root = document.getRootElement();
 
         // Parse Number format
diff --git a/src/main/java/org/ttzero/excel/entity/style/Theme.java b/src/main/java/org/ttzero/excel/entity/style/Theme.java
index 767f8421..3d58d344 100644
--- a/src/main/java/org/ttzero/excel/entity/style/Theme.java
+++ b/src/main/java/org/ttzero/excel/entity/style/Theme.java
@@ -25,8 +25,7 @@ import org.slf4j.LoggerFactory;
 import org.ttzero.excel.manager.TopNS;
 
 import java.awt.Color;
-import java.nio.file.Files;
-import java.nio.file.Path;
+import java.io.InputStream;
 import java.util.List;
 
 import static org.ttzero.excel.entity.style.Styles.getAttr;
@@ -47,13 +46,13 @@ public class Theme {
     // theme colors
     private ClrScheme[] clrs;
 
-    public static Theme load(Path path) {
+    public static Theme load(InputStream is) {
         Theme self = new Theme();
         // load theme1.xml
         SAXReader reader = SAXReader.createDefault();
         Document document;
         try {
-            document = reader.read(Files.newInputStream(path));
+            document = reader.read(is);
             Element root = document.getRootElement();
             Element clrScheme = root.element("themeElements").element("clrScheme");
             List<Element> clrSchemes;
diff --git a/src/main/java/org/ttzero/excel/reader/ExcelReader.java b/src/main/java/org/ttzero/excel/reader/ExcelReader.java
index 78e8fedb..ec3101da 100644
--- a/src/main/java/org/ttzero/excel/reader/ExcelReader.java
+++ b/src/main/java/org/ttzero/excel/reader/ExcelReader.java
@@ -24,6 +24,7 @@ import org.dom4j.QName;
 import org.dom4j.io.SAXReader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.ttzero.excel.entity.style.Theme;
 import org.ttzero.excel.manager.NS;
 import org.ttzero.excel.manager.TopNS;
 import org.ttzero.excel.entity.IWorkbookWriter;
@@ -35,9 +36,9 @@ import org.ttzero.excel.manager.ExcelType;
 import org.ttzero.excel.manager.RelManager;
 import org.ttzero.excel.manager.docProps.App;
 import org.ttzero.excel.manager.docProps.Core;
+import org.ttzero.excel.util.DateUtil;
 import org.ttzero.excel.util.FileUtil;
 import org.ttzero.excel.util.StringUtil;
-import org.ttzero.excel.util.ZipUtil;
 
 import java.io.Closeable;
 import java.io.FileNotFoundException;
@@ -54,6 +55,7 @@ import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
+import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -63,6 +65,8 @@ import java.util.Spliterator;
 import java.util.Spliterators;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
 
 import static org.ttzero.excel.reader.SharedStrings.toInt;
 import static org.ttzero.excel.util.FileUtil.exists;
@@ -106,12 +110,14 @@ public class ExcelReader implements Closeable {
 
     protected ExcelReader() { }
 
-    protected Path self;
     protected Sheet[] sheets;
     private Path temp;
     private ExcelType type;
     private AppInfo appInfo;
-
+    /**
+     * Store temporary files (Copy images to temp directory)
+     */
+    protected Path tempDir;
     /**
      * The Shared String Table
      */
@@ -143,6 +149,10 @@ public class ExcelReader implements Closeable {
      * A global styles
      */
     protected Styles styles;
+    /**
+     * Xlsx src file
+     */
+    protected ZipFile zipFile;
 
     /**
      * Constructor Excel Reader
@@ -218,21 +228,6 @@ public class ExcelReader implements Closeable {
         return read(stream, bufferSize, 0, option);
     }
 
-    /**
-     * Constructor Excel Reader
-     *
-     * @param path       the excel path
-     * @param bufferSize the {@link SharedStrings} buffer size. default is 512
-     *                   This parameter affects the number of read times.
-     * @param cacheSize  the {@link Cache} size, default is 512
-     * @param option the reader option.
-     * @return the {@link ExcelReader}
-     * @throws IOException if path not exists or I/O error occur
-     */
-    public static ExcelReader read(Path path, int bufferSize, int cacheSize, int option) throws IOException {
-        return read(path, bufferSize, cacheSize, false, option);
-    }
-
     /**
      * Constructor Excel Reader
      *
@@ -250,7 +245,9 @@ public class ExcelReader implements Closeable {
             throw new IOException("Create temp directory error. Please check your permission");
         }
         FileUtil.cp(stream, temp);
-        return read(temp, bufferSize, cacheSize, true, option);
+        ExcelReader reader = read(temp, bufferSize, cacheSize, option);
+        reader.temp = temp;
+        return reader;
     }
 
     /**
@@ -349,21 +346,21 @@ public class ExcelReader implements Closeable {
     @Override
     public void close() throws IOException {
         // Close all opened sheet
-        for (Sheet st : sheets) {
-            st.close();
+        if (sheets != null) {
+            for (Sheet st : sheets) st.close();
         }
 
         // Close Shared String Table
-        if (sst != null)
-            sst.close();
+        if (sst != null) sst.close();
 
-        // Delete temp files
-        if (self != null) {
-            FileUtil.rm_rf(self.toFile(), true);
-        }
-        if (temp != null) {
-            FileUtil.rm(temp);
-        }
+        // Remove temp file
+        if (temp != null) FileUtil.rm(temp);
+
+        // Remove temp dir
+        if (tempDir != null) FileUtil.rm_rf(tempDir);
+
+        // Close src file
+        if (zipFile != null) zipFile.close();
     }
 
     /**
@@ -372,7 +369,7 @@ public class ExcelReader implements Closeable {
      * @return the information
      */
     public AppInfo getAppInfo() {
-        return appInfo != null ? appInfo : (appInfo = getGeneralInfo(self));
+        return appInfo != null ? appInfo : (appInfo = getGeneralInfo());
     }
 
     /**
@@ -397,8 +394,15 @@ public class ExcelReader implements Closeable {
      */
     public ExcelReader parseFormula() {
         if (hasFormula) {
-            // Formula string if exists
-            long[][] calcArray = parseCalcChain(self);
+            ZipEntry entry = getEntry(zipFile, "xl/calcChain.xml");
+            if (entry == null) return this;
+            long[][] calcArray = null;
+            try (InputStream is = zipFile.getInputStream(entry)) {
+                // Formula string if exists
+                calcArray = parseCalcChain(is);
+            } catch (IOException ex) {
+                LOGGER.warn("Part of `calcChain` has be damaged, It will be ignore all formulas.", ex);
+            }
 
             if (calcArray == null) return this;
             int i = 0;
@@ -453,14 +457,19 @@ public class ExcelReader implements Closeable {
     public static final Set<String> MUST_CHECK_PART = new HashSet<>(Arrays.asList(Const.ContentType.WORKBOOK
             , Const.ContentType.SHAREDSTRING, Const.ContentType.SHEET, Const.ContentType.STYLE));
 
-    protected ContentType checkContentType(Path root) {
+    protected ContentType checkContentType() {
+        // Read [Content_Types].xml
+        ZipEntry entry = getEntry(zipFile, "[Content_Types].xml");
+        if (entry == null) {
+            if (temp != null) FileUtil.rm(temp);
+            throw new ExcelReadException("The file format is incorrect or corrupted. [[Content_Types].xml]");
+        }
         SAXReader reader = SAXReader.createDefault();
         Document document;
-        // Read [Content_Types].xml
         try {
-            document = reader.read(Files.newInputStream(root.resolve("[Content_Types].xml")));
+            document = reader.read(zipFile.getInputStream(entry));
         } catch (DocumentException | IOException e) {
-            FileUtil.rm_rf(root.toFile(), true);
+            if (temp != null) FileUtil.rm(temp);
             throw new ExcelReadException("The file format is incorrect or corrupted. [[Content_Types].xml]");
         }
         ContentType contentType = new ContentType();
@@ -468,9 +477,10 @@ public class ExcelReader implements Closeable {
         for (Element e : list) {
             if ("Override".equals(e.getName())) {
                 ContentType.Override override = new ContentType.Override(e.attributeValue("ContentType"), e.attributeValue("PartName"));
-                if (!Files.exists(root.resolve(override.getPartName().substring(1)))) {
+                entry = getEntry(zipFile, override.getPartName());
+                if (entry == null) {
                     if (MUST_CHECK_PART.contains(override.getContentType())) {
-                        FileUtil.rm_rf(root.toFile(), true);
+                        if (temp != null) FileUtil.rm(temp);
                         throw new ExcelReadException("The file format is incorrect or corrupted. [" + override.getPartName() + "]");
                     } else {
                         LOGGER.warn("{} is configured in [Content_Types].xml, but the corresponding file is missing.", override.getKey());
@@ -513,35 +523,58 @@ public class ExcelReader implements Closeable {
     }
 
     protected ExcelReader init(Path path, int bufferSize, int cacheSize, int option) throws IOException {
-        // Store template stream as zip file
-        Path tmp = FileUtil.mktmp(Const.EEC_PREFIX);
-        LOGGER.debug("Unzip file to：{}", tmp);
-        ZipUtil.unzip(Files.newInputStream(path), tmp);
-        LOGGER.debug("Finished decompress. start to check the file integrity.");
+        this.zipFile = new ZipFile(path.toFile());
+        LOGGER.debug("Check file integrity.");
 
         // Check content-type
-        ContentType contentType = checkContentType(tmp);
+        ContentType contentType = checkContentType();
         if (contentType.hasDrawings()) {
             this.drawings = new XMLDrawings(this);
         }
 
-        // Check the file format and parse general information
-        try {
-            appInfo = getGeneralInfo(tmp);
-        } catch (Exception e) {
-            FileUtil.rm_rf(tmp.toFile(), true);
-            throw e;
-        }
+//        // Check the file format and parse general information
+//        appInfo = getGeneralInfo();
 
         // load workbook.xml
         SAXReader reader = SAXReader.createDefault();
         Document document;
+
+        // Load SharedString
+        ZipEntry entry = getEntry(zipFile, "xl/sharedStrings.xml");
+        if (entry != null) {
+            sst = new SharedStrings(zipFile.getInputStream(entry), bufferSize, cacheSize).load();
+        }
+
+        // Load Styles
+        entry = getEntry(zipFile, "xl/styles.xml");
+        if (entry != null) {
+            try {
+                styles = Styles.load(zipFile.getInputStream(entry));
+                // Load Theme style
+                ZipEntry themeEntry = getEntry(zipFile, "theme/theme1.xml");
+                if (themeEntry != null) Theme.load(zipFile.getInputStream(themeEntry));
+            } catch (Exception ex) {
+                LOGGER.warn("Parse style failed.", ex);
+            }
+        }
+        // Construct a empty Styles
+        if (styles == null) {
+            styles = Styles.forReader();
+        }
+
+        this.option = option;
+        hasFormula = getEntry(zipFile, "xl/calcChain.xml") != null;
+
+        entry = getEntry(zipFile, "xl/_rels/workbook.xml.rels");
+        if (entry == null)
+            throw new ExcelReadException("The file format is incorrect or corrupted. [xl/_rels/workbook.xml.rels]");
+
         try {
-            document = reader.read(Files.newInputStream(tmp.resolve("xl/_rels/workbook.xml.rels")));
+            document = reader.read(zipFile.getInputStream(entry));
         } catch (DocumentException | IOException e) {
-            FileUtil.rm_rf(tmp.toFile(), true);
             throw new ExcelReadException("The file format is incorrect or corrupted. [xl/_rels/workbook.xml.rels]");
         }
+
         List<Element> list = document.getRootElement().elements();
         Relationship[] rels = new Relationship[list.size()];
         int i = 0;
@@ -550,46 +583,16 @@ public class ExcelReader implements Closeable {
         }
         RelManager relManager = RelManager.of(rels);
 
+        entry = getEntry(zipFile, "xl/workbook.xml");
+        if (entry == null)
+            throw new ExcelReadException("The file format is incorrect or corrupted. [xl/workbook.xml]");
         try {
-            document = reader.read(Files.newInputStream(tmp.resolve("xl/workbook.xml")));
+            document = reader.read(zipFile.getInputStream(entry));
         } catch (DocumentException | IOException e) {
-            // read style file fail.
-            FileUtil.rm_rf(tmp.toFile(), true);
             throw new ExcelReadException("The file format is incorrect or corrupted. [xl/workbook.xml]");
         }
         Element root = document.getRootElement();
         Namespace ns = root.getNamespaceForPrefix("r");
-
-        // Load SharedString
-        Path ss = tmp.resolve("xl/sharedStrings.xml");
-        if (exists(ss)) {
-            sst = new SharedStrings(ss, bufferSize, cacheSize).load();
-        }
-
-        // Load Styles
-        Path s = tmp.resolve("xl/styles.xml");
-
-        if (exists(s)) {
-            try {
-                styles = Styles.load(s);
-            } catch (Exception ex) {
-                LOGGER.warn("Parse style failed.", ex);
-            }
-        }
-//        else {
-//            FileUtil.rm_rf(tmp.toFile(), true);
-//            throw new ExcelReadException("The file format is incorrect or corrupted. [xl/styles.xml]");
-//        }
-        // Construct a empty Styles
-        if (styles == null) {
-            styles = Styles.forReader();
-        }
-
-        // TODO Parse theme
-
-        this.option = option;
-        hasFormula = exists(tmp.resolve("xl/calcChain.xml"));
-
         List<Sheet> sheets = new ArrayList<>();
         Iterator<Element> sheetIter = root.element("sheets").elementIterator();
         int index = 0;
@@ -602,11 +605,17 @@ public class ExcelReader implements Closeable {
             sheet.setHidden("hidden".equals(state));
             Relationship r = relManager.getById(e.attributeValue(QName.get("id", ns)));
             if (r == null) {
-                FileUtil.rm_rf(tmp.toFile(), true);
                 sheet.close();
                 throw new ExcelReadException("The file format is incorrect or corrupted.");
             }
-            sheet.setPath(tmp.resolve("xl").resolve(r.getTarget()));
+            sheet.setPath("xl/" + r.getTarget());
+            entry = getEntry(zipFile, "xl/" + r.getTarget());
+            if (entry == null) {
+                sheet.close();
+                throw new ExcelReadException("The file format is incorrect or corrupted.");
+            }
+            sheet.setZipFile(zipFile);
+            sheet.setZipEntry(entry);
             // put shared string
             sheet.setSst(sst);
             // Setting styles
@@ -617,19 +626,13 @@ public class ExcelReader implements Closeable {
             sheets.add(sheet);
         }
 
-        if (sheets.isEmpty()) {
-            FileUtil.rm_rf(tmp.toFile(), true);
+        if (sheets.isEmpty())
             throw new ExcelReadException("The file format is incorrect or corrupted. [There has no worksheet]");
-        }
-
-        // sort by sheet index
-//        sheets.sort(Comparator.comparingInt(Sheet::getIndex));
 
         Sheet[] sheets1 = new Sheet[sheets.size()];
         sheets.toArray(sheets1);
 
         this.sheets = sheets1;
-        self = tmp;
 
         if ((option >> 1 & 1) == 1) {
             parseFormula();
@@ -644,13 +647,12 @@ public class ExcelReader implements Closeable {
      * @param bufferSize the {@link SharedStrings} buffer size. default is 512
      *                   This parameter affects the number of read times.
      * @param cacheSize  the {@link Cache} size, default is 512
-     * @param rmSource   remove the source files
      * @param option the reader option.
      * @return the {@link ExcelReader}
      * @throws FileNotFoundException if the path not exists or no permission to read
      * @throws IOException if I/O error occur
      */
-    private static ExcelReader read(Path path, int bufferSize, int cacheSize, boolean rmSource, int option) throws IOException {
+    public static ExcelReader read(Path path, int bufferSize, int cacheSize, int option) throws IOException {
         if (!exists(path)) {
             throw new FileNotFoundException(path.toString());
         }
@@ -686,11 +688,6 @@ public class ExcelReader implements Closeable {
         }
         er.type = type;
 
-        // storage source path
-        if (rmSource) {
-            er.temp = path;
-        }
-
         return er;
     }
 
@@ -758,73 +755,84 @@ public class ExcelReader implements Closeable {
         return excelType;
     }
 
-    protected AppInfo getGeneralInfo(Path tmp) {
-        // load workbook.xml
+    protected AppInfo getGeneralInfo() {
+        // load app.xml
         SAXReader reader = SAXReader.createDefault();
-        Document document;
-        try {
-            document = reader.read(Files.newInputStream(tmp.resolve("docProps/app.xml")));
-        } catch (DocumentException | IOException e) {
-            throw new ExcelReadException("The file format is incorrect or corrupted. [docProps/app.xml]");
-        }
-        Element root = document.getRootElement();
-        App app = new App();
-        app.setCompany(root.elementText("Company"));
-        app.setApplication(root.elementText("Application"));
-        String v = root.elementText("AppVersion");
-        if (StringUtil.isNotEmpty(v)) app.setAppVersion(v);
+        App app = null;
+        Core core = null;
+        ZipEntry entry = getEntry(zipFile, "docProps/app.xml");
+        if (entry != null) {
+            Document document = null;
+            try {
+                document = reader.read(zipFile.getInputStream(entry));
+            } catch (DocumentException | IOException e) {
+                LOGGER.warn("The file format is incorrect or corrupted. [docProps/app.xml]");
+            }
+            if (document != null) {
+                Element root = document.getRootElement();
+                app = new App();
+                app.setCompany(root.elementText("Company"));
+                app.setApplication(root.elementText("Application"));
+                String v = root.elementText("AppVersion");
+                if (StringUtil.isNotEmpty(v)) app.setAppVersion(v);
+            }
+        } else LOGGER.warn("The file format is incorrect or corrupted. [docProps/app.xml]");
 
-        try {
-            document = reader.read(Files.newInputStream(tmp.resolve("docProps/core.xml")));
-        } catch (DocumentException | IOException e) {
-            throw new ExcelReadException("The file format is incorrect or corrupted. [docProps/core.xml]");
-        }
-        root = document.getRootElement();
-        Core core = new Core();
-        Class<Core> clazz = Core.class;
-        TopNS topNS = clazz.getAnnotation(TopNS.class);
-        String[] prefixs = topNS.prefix(), urls = topNS.uri();
-        Field[] fields = clazz.getDeclaredFields();
-        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'");
-        for (Field f : fields) {
-            NS ns = f.getAnnotation(NS.class);
-            if (ns == null) continue;
-
-            f.setAccessible(true);
-            int nsIndex = StringUtil.indexOf(prefixs, ns.value());
-            if (nsIndex < 0) continue;
-
-            Namespace namespace = new Namespace(ns.value(), urls[nsIndex]);
-            Class<?> type = f.getType();
-            v = root.elementText(new QName(f.getName(), namespace));
-            if (isEmpty(v)) continue;
-            if (type == String.class) {
-                try {
-                    f.set(core, v);
-                } catch (IllegalAccessException e) {
-                    LOGGER.warn("Set field ({}) error.", f);
-                }
-            } else if (type == Date.class) {
-                try {
-                    f.set(core, format.parse(v));
-                } catch (ParseException | IllegalAccessException e) {
-                    LOGGER.warn("Set field ({}) error.", f);
+        entry = getEntry(zipFile, "docProps/core.xml");
+        if (entry != null) {
+            Document document = null;
+            try {
+                document = reader.read(zipFile.getInputStream(entry));
+            } catch (DocumentException | IOException e) {
+                LOGGER.warn("The file format is incorrect or corrupted. [docProps/core.xml]");
+            }
+            if (document != null) {
+                Element root = document.getRootElement();
+                core = new Core();
+                Class<Core> clazz = Core.class;
+                TopNS topNS = clazz.getAnnotation(TopNS.class);
+                String[] prefixs = topNS.prefix(), urls = topNS.uri();
+                Field[] fields = clazz.getDeclaredFields();
+                SimpleDateFormat format = DateUtil.utcDateTimeFormat.get();
+                for (Field f : fields) {
+                    NS ns = f.getAnnotation(NS.class);
+                    if (ns == null) continue;
+
+                    f.setAccessible(true);
+                    int nsIndex = StringUtil.indexOf(prefixs, ns.value());
+                    if (nsIndex < 0) continue;
+
+                    Namespace namespace = new Namespace(ns.value(), urls[nsIndex]);
+                    Class<?> type = f.getType();
+                    String v = root.elementText(new QName(f.getName(), namespace));
+                    if (isEmpty(v)) continue;
+                    if (type == String.class) {
+                        try {
+                            f.set(core, v);
+                        } catch (IllegalAccessException e) {
+                            LOGGER.warn("Set field ({}) error.", f);
+                        }
+                    } else if (type == Date.class) {
+                        try {
+                            f.set(core, format.parse(v));
+                        } catch (ParseException | IllegalAccessException e) {
+                            LOGGER.warn("Set field ({}) error.", f);
+                        }
+                    }
                 }
             }
-        }
+        } else LOGGER.warn("The file format is incorrect or corrupted. [docProps/core.xml]");
 
         return new AppInfo(app, core);
     }
 
     /* Parse `calcChain` */
-    static long[][] parseCalcChain(Path root) {
-        Path calcPath = root.resolve("xl/calcChain.xml");
-        if (!FileUtil.exists(calcPath)) return null;
+    static long[][] parseCalcChain(InputStream is) {
+        SAXReader reader = SAXReader.createDefault();
         Element calcChain;
         try {
-            SAXReader reader = SAXReader.createDefault();
-            calcChain = reader.read(Files.newInputStream(calcPath)).getRootElement();
-        } catch (DocumentException | IOException e) {
+            calcChain = reader.read(is).getRootElement();
+        } catch (DocumentException e) {
             LOGGER.warn("Part of `calcChain` has be damaged, It will be ignore all formulas.");
             return null;
         }
@@ -894,12 +902,12 @@ public class ExcelReader implements Closeable {
             if (value >= 'A' && value <= 'Z') {
                 v = v * 26 + value - 'A' + 1;
             }
-            else if (value >= 'a' && value <= 'z') {
-                v = v * 26 + value - 'a' + 1;
-            }
             else if (value >= '0' && value <= '9') {
                 n = n * 10 + value - '0';
             }
+            else if (value >= 'a' && value <= 'z') {
+                v = v * 26 + value - 'a' + 1;
+            }
             else
                 throw new ExcelReadException("Column mark out of range: " + r);
         }
@@ -923,4 +931,45 @@ public class ExcelReader implements Closeable {
     public Styles getStyles() {
         return styles;
     }
+
+    /**
+     * Find {@code ZipEntry} by name
+     *
+     * @param zipFile src zip file
+     * @param name entry name
+     * @return {@code ZipEntry} if exist, otherwise null
+     */
+    public static ZipEntry getEntry(ZipFile zipFile, String name) {
+        char c0 = name.charAt(0);
+        if (c0 == '/' || c0 == '\\') name = name.substring(1);
+        ZipEntry entry = zipFile.getEntry(name);
+        if (entry == null) entry = zipFile.getEntry(name.replace('/', '\\'));
+        if (entry == null) {
+            // Iterator entries
+            Enumeration<? extends ZipEntry> entries = zipFile.entries();
+            while (entries.hasMoreElements()) {
+                ZipEntry e = entries.nextElement();
+                String k = e.getName().replace('\\', '/');
+                if (k.equalsIgnoreCase(name)) {
+                    entry = e;
+                    break;
+                }
+            }
+        }
+        return entry;
+    }
+
+    /**
+     * Simple convert to zip path
+     *
+     * @param path file path
+     * @return zip path
+     */
+    static String toZipPath(String path) {
+        int i = 0;
+        if (path.startsWith("../") || path.startsWith("..\\")) i = 3;
+        else if (path.startsWith("./") || path.startsWith(".\\")) i = 2;
+        else if (path.charAt(0) == '/' || path.charAt(0) == '\\') i = 1;
+        return i > 0 ? path.substring(i) : path;
+    }
 }
diff --git a/src/main/java/org/ttzero/excel/reader/FixSizeLRUCache.java b/src/main/java/org/ttzero/excel/reader/FixSizeLRUCache.java
index 057b477b..fd65448f 100644
--- a/src/main/java/org/ttzero/excel/reader/FixSizeLRUCache.java
+++ b/src/main/java/org/ttzero/excel/reader/FixSizeLRUCache.java
@@ -84,7 +84,7 @@ public class FixSizeLRUCache<K, V> implements Cache<K, V> {
     }
 
     /**
-     * Create a fix size cache witch size is {@code 1 &lt;&lt; 9}
+     * Create a fix size cache witch size is {@code 512}
      */
     private FixSizeLRUCache() {
         this(1 << 9);
@@ -93,7 +93,7 @@ public class FixSizeLRUCache<K, V> implements Cache<K, V> {
     private FixSizeLRUCache(int limit) {
         this.limit = limit;
         // Create double limit size
-        table = new HashMap<>(Math.round(limit * 1.25f));
+        table = new HashMap<>(limit);
     }
 
     /**
diff --git a/src/main/java/org/ttzero/excel/reader/IndexSharedStringTable.java b/src/main/java/org/ttzero/excel/reader/IndexSharedStringTable.java
index 8a9d2c11..6ab29b6c 100644
--- a/src/main/java/org/ttzero/excel/reader/IndexSharedStringTable.java
+++ b/src/main/java/org/ttzero/excel/reader/IndexSharedStringTable.java
@@ -30,7 +30,6 @@ import java.nio.file.Paths;
 import java.nio.file.StandardOpenOption;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.ttzero.excel.util.FileUtil.exists;
 
 /**
  * @author guanquan.wang at 2019-05-10 20:06
@@ -106,9 +105,6 @@ public class IndexSharedStringTable extends SharedStringTable {
      */
     IndexSharedStringTable(Path path) throws IOException {
         super(Paths.get(path.toString().substring(0, path.toString().length() - 4)));
-        if (!exists(path)) {
-            throw new IOException("The index path [" + path + "] not exists.");
-        }
         temp = path;
         channel = Files.newByteChannel(temp, StandardOpenOption.WRITE, StandardOpenOption.READ);
         channel.position(channel.size());
diff --git a/src/main/java/org/ttzero/excel/reader/SharedStrings.java b/src/main/java/org/ttzero/excel/reader/SharedStrings.java
index 71e80248..8ef6f982 100644
--- a/src/main/java/org/ttzero/excel/reader/SharedStrings.java
+++ b/src/main/java/org/ttzero/excel/reader/SharedStrings.java
@@ -18,39 +18,33 @@ package org.ttzero.excel.reader;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.ttzero.excel.entity.ExcelWriteException;
 
-import java.io.BufferedReader;
 import java.io.Closeable;
 import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 
+import static java.lang.Character.highSurrogate;
+import static java.lang.Character.isBmpCodePoint;
+import static java.lang.Character.isValidCodePoint;
+import static java.lang.Character.lowSurrogate;
 import static java.lang.Integer.numberOfTrailingZeros;
-import static org.ttzero.excel.util.FileUtil.exists;
+import static org.ttzero.excel.manager.Const.Limit.MAX_CHARACTERS_PER_CELL;
 import static org.ttzero.excel.util.StringUtil.EMPTY;
 
 /**
  * Read sharedString data
  * <p>
- * For large files, it is impossible to load all data into the
- * memory and get it by index. The current practice is to partition
- * and divide the data into three areas: forward, backward, and hot.
- * The newly read data is placed in the forward area, if not found
- * in forward area, go to the backward area to find, finally go to
- * the hot area to find it. If not found in the three areas, press
- * the ward will be re-load in to the forward area. The original
- * forwarding area data is copied to the backward area. The blocks
- * loaded twice will be marked, the marked blocks will be placed in
- * the hot area when they are repeatedly read, and the hot area will
- * be eliminated by the LRU page replacement algorithm.
+ * This record contains a list of all strings used anywhere in the workbook.
+ * Each string occurs only once. The workbook uses indexes into the list to reference the strings
  *
  * @author guanquan.wang at 2018-09-27 14:28
  */
 public class SharedStrings implements Closeable {
     private final Logger LOGGER = LoggerFactory.getLogger(getClass());
-    private Path sstPath;
 
     /**
      * The maximum capacity, used if a higher value is implicitly specified
@@ -69,14 +63,12 @@ public class SharedStrings implements Closeable {
         max = data.length;
         offset_forward = 0;
         status = 1;
-        if (max <= page) {
+        if (max <= 512) {
             forward = new String[max];
             System.arraycopy(data, offset_forward, forward, 0, max);
             limit_forward = max;
         } else {
-            if (max > page << 1) {
-                page = max >> 1;
-            }
+            page = (max + 1) >> 1;
             status <<= 1;
             forward = new String[page];
             limit_forward = page;
@@ -92,12 +84,12 @@ public class SharedStrings implements Closeable {
      * Constructs a SharedString with the xml path, please call
      * {@link SharedStrings#load()} after instance
      *
-     * @param sstPath   the xml file path
+     * @param is   the xml file path
      * @param cacheSize the number of word per load
      * @param hotSize   the number of high frequency word
      */
-    public SharedStrings(Path sstPath, int cacheSize, int hotSize) {
-        this.sstPath = sstPath;
+    public SharedStrings(InputStream is, int cacheSize, int hotSize) {
+        this.reader = new InputStreamReader(is, StandardCharsets.UTF_8);
         if (cacheSize > 0) {
             this.page = tableSizeFor(cacheSize);
         }
@@ -135,11 +127,11 @@ public class SharedStrings implements Closeable {
     /**
      * Number of word per load
      */
-    private int page = 512;
+    private int page;
     /**
      * The word total
      */
-    private int max = -1, vt = 0, offsetM = 0;
+    private int max = -1, offsetM = 0;
     /**
      * The forward offset
      */
@@ -171,19 +163,15 @@ public class SharedStrings implements Closeable {
     /**
      * Main reader
      */
-    private BufferedReader reader;
+    private Reader reader;
     /**
      * Buffered
      */
     private char[] cb;
     /**
-     * offset of cb[]
-     */
-    private int offset;
-    /**
-     * escape buffer
+     * length of cb[]
      */
-    private StringBuilder escapeBuf;
+    private int nChar, length;
     /**
      * Shared string table
      */
@@ -195,6 +183,10 @@ public class SharedStrings implements Closeable {
      * 4: large model/unknown size
      */
     private int status;
+    /**
+     * Buffer
+     */
+    StringBuilder buf = null;
 
     // For debug
     private int total, total_forward, total_backward, total_hot, total_sst;
@@ -230,31 +222,26 @@ public class SharedStrings implements Closeable {
      * @throws IOException if io error occur
      */
     public SharedStrings load() throws IOException {
-        if (!exists(sstPath)) {
-            max = 0;
-            return this;
-        }
-
         // Get unique count
         max = uniqueCount();
         LOGGER.debug("Size of SharedString: {}", max);
         //
         init();
-        escapeBuf = new StringBuilder();
         return this;
     }
 
     /* */
     private void init() throws IOException {
         status = 1;
-        // Unknown size or greater than {@code page * 2}
-        if (max < 0 || max > page << 1) {
+        // Unknown size or greater than {@code 8192}
+        if (max < 0 || max > 1 << 14) {
+            if (page <= 0) page = 16;
             status <<= 2;
             forward = new String[page];
             backward = new String[page];
 
             // Cache 8KB binary, it will store 1^16 strings.
-            tester = new Tester.FixBinaryTester(Math.min(max, 1 << 16));
+            tester = new Tester.BinaryTester(max > 0 ? Math.min(max, 1 << 16) : 1 << 16);
 
             if (hotSize > 0) hot = FixSizeLRUCache.create(hotSize);
             else hot = FixSizeLRUCache.create();
@@ -264,12 +251,14 @@ public class SharedStrings implements Closeable {
                 sst.setShortSectorSize(numberOfTrailingZeros(page));
             }
         }
-        else if (max > page) {
+        else if (max > 512) {
             status <<= 1;
+            page = (max + 1) >> 1;
             forward = new String[page];
             backward = new String[page];
-        } else {
-            forward = new String[page];
+        }
+        else {
+            forward = new String[page = Math.max(16, max)];
         }
     }
 
@@ -281,25 +270,22 @@ public class SharedStrings implements Closeable {
      */
     private int uniqueCount() throws IOException {
         int off = -1;
-        reader = Files.newBufferedReader(sstPath);
         cb = new char[1 << 12];
-        offset = 0;
-        offset = reader.read(cb);
+        length = reader.read(cb);
 
-//        int i = 0, len = offset - 4;
-//        for (; i < len && (cb[i] != '<' || cb[i + 1] != 's' || cb[i + 2] != 'i' || cb[i + 3] != '>'); i++) ;
-//        if (i == len) return 0; // Empty
+        // Empty Shared String Table
+        if (length <= 0) return status = 0;
 
-        String line = new String(cb, 0, cb.length);
+        String line = new String(cb, 0, Math.min(256, length));
         // Microsoft Excel
         String uniqueCount = " uniqueCount=";
         int index = line.indexOf(uniqueCount)
-            , end = index > 0 ? line.indexOf('"'
-            , index += (uniqueCount.length() + 1)) : -1;
+            , end = index > 0 ? line.indexOf('"', index += (uniqueCount.length() + 1)) : -1;
         if (end > 0) {
             off = Integer.parseInt(line.substring(index, end));
-            // WPS
-        } else {
+        }
+        // WPS
+        else {
             String count = " count=";
             index = line.indexOf(count);
             end = index > 0 ? line.indexOf('"', index += (count.length() + 1)) : -1;
@@ -308,8 +294,7 @@ public class SharedStrings implements Closeable {
             }
         }
 
-        vt = ++end;
-        System.arraycopy(cb, vt, cb, 0, offset -= vt);
+        if (end > 0) nChar = end + 1;
 
         return off;
     }
@@ -321,15 +306,13 @@ public class SharedStrings implements Closeable {
      * @return string
      */
     public String get(int index) {
-        checkBound(index);
+//        checkBound(index);
         total++;
 
         // Load first
         if (offset_forward == -1) {
             offset_forward = index / page * page;
 
-            if (vt < 0) vt = 0;
-
             readMore();
         }
 
@@ -372,7 +355,7 @@ public class SharedStrings implements Closeable {
                     // Load from SharedStringTable
                     limit_forward = sst.get(offset_forward, forward);
                 } catch (IOException e) {
-                    throw new ExcelWriteException(e);
+                    throw new ExcelReadException(e);
                 }
                 total_sst++;
             } else {
@@ -403,12 +386,12 @@ public class SharedStrings implements Closeable {
             && offset_backward + limit_backward > index;
     }
 
-    // Check the current index if out of bound
-    private void checkBound(int index) {
-        if (index < 0 || max > -1 && max <= index) {
-            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + max);
-        }
-    }
+//    // Check the current index if out of bound
+//    private void checkBound(int index) {
+//        if (index < 0 || max > -1 && max <= index) {
+//            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + max);
+//        }
+//    }
 
     // Check the current index has been loaded twice
     private boolean test(int index) {
@@ -416,7 +399,9 @@ public class SharedStrings implements Closeable {
     }
 
     private void copyToBackward() {
-        System.arraycopy(forward, 0, backward, 0, limit_forward);
+        String[] tmp = backward;
+        backward = forward;
+        forward = tmp;
         offset_backward = offset_forward;
         limit_backward = limit_forward;
     }
@@ -449,38 +434,34 @@ public class SharedStrings implements Closeable {
      */
     protected int readData() throws IOException {
         // Read forward area data
-        int n = 0, length, nChar;
-        for (; (length = reader.read(cb, offset, cb.length - offset)) > 0 || offset > 0; ) {
-            length += offset;
-            nChar = offset &= 0;
-            int len0 = length - 3, len1 = len0 - 1;
-            int[] t = findT(cb, nChar, length, len0, len1, n);
+        int n = 0, len = length, offset;
+        for (; ;) {
+            int len0 = len - 3, len1 = len0 - 1;
+            int[] t = findT(cb, nChar, len, len0, len1, n);
 
             nChar = t[0];
-            n = t[1];
+            limit_forward = n = t[1];
 
-            limit_forward = n;
+            // A page Or EOF
+            if (n == page || len < cb.length && nChar == len - 6) {
+                ++offsetM; // out of index range
+                break;
+            }
 
             // If cell value(character value) length greater than buffer size
             if (nChar == 0) {
                 cb = Arrays.copyOf(cb, cb.length << 1);
-                offset = length;
-            } else if (nChar < length) {
-                System.arraycopy(cb, nChar, cb, 0, offset = length - nChar);
+                offset = len;
             }
+            else if (nChar < len) System.arraycopy(cb, nChar, cb, 0, offset = len - nChar);
+            else offset = 0;
 
-            // A page
-            if (n == page) {
-                ++offsetM;
-                break;
-            } else if (length < cb.length && nChar == length - 6) { // EOF '</sst>'
-//                if (max == -1) { // Reset totals when unknown size
-//                    max = offsetM * page + n;
-//                }
-                ++offsetM; // out of index range
-                break;
-            }
+            // Read more
+            if ((len = reader.read(cb, offset, cb.length - offset)) <= 0) break;
+            len += offset;
+            nChar = 0;
         }
+
         // Reset totals when unknown size
         if (max < n) {
             max = offsetM * page + n;
@@ -488,6 +469,8 @@ public class SharedStrings implements Closeable {
         return n; // Returns the word count
     }
 
+    // [0]: nChar
+    // [1]: number of string
     private int[] findT(char[] cb, int nChar, int length, int len0, int len1, int n) throws IOException {
         int cursor;
         for (; nChar < length && n < page; ) {
@@ -509,13 +492,13 @@ public class SharedStrings implements Closeable {
             if (a == -1) break;
             nChar = subT[1];
 
-            String tmp = unescape(escapeBuf, cb, a, nChar);
+            String tmp = escape(cb, a, nChar);
 
              // Skip the end tag of 't'
             nChar += 4;
 
             // Test the next tag
-            for (; nChar < len1 && (cb[nChar] != '<'); ++nChar);
+            if (cb[nChar] != '<') for (; nChar < len1 && (cb[nChar] != '<'); ++nChar);
 
             // End of <si>
             if (nChar < len1 && cb[nChar + 1] == '/' && cb[nChar + 2] == 's' && cb[nChar + 3] == 'i' && cb[nChar + 4] == '>') {
@@ -523,7 +506,6 @@ public class SharedStrings implements Closeable {
                 if (status == 4) sst.push(forward[n - 1]);
                 nChar += 5;
             } else {
-                StringBuilder buf = new StringBuilder(tmp);
                 int t = nChar;
                 // Find the end tag of 'si'
                 for (; nChar < len1 && (cb[nChar] != '<' || cb[nChar + 1] != '/'
@@ -534,6 +516,7 @@ public class SharedStrings implements Closeable {
                 }
                 int end = nChar;
                 nChar = t;
+                boolean shouldClear = true;
                 // Loop and join
                 for (; ; ) {
                     subT = subT(cb, nChar, end, end - 1);
@@ -541,10 +524,18 @@ public class SharedStrings implements Closeable {
                     if (a == -1) break;
                     nChar = subT[1];
 
-                    buf.append(unescape(escapeBuf, cb, a, nChar));
+                    if (buf == null) {
+                        shouldClear = false;
+                        buf = new StringBuilder(tmp);
+                    }
+                    else if (shouldClear) {
+                        shouldClear = false;
+                        buf.delete(0, buf.length());
+                    }
+                    buf.append(escape(cb, a, nChar));
                     nChar += 4;
                 }
-                forward[n++] = buf.toString();
+                forward[n++] = shouldClear ? tmp : buf.toString();
                 if (status == 4) sst.push(forward[n - 1]);
                 nChar = end + 5;
             }
@@ -602,79 +593,102 @@ public class SharedStrings implements Closeable {
         return new int[] { a, nChar };
     }
 
+    // Buffer cache (Maximum 64K)
+    private static char[] charBuffer = {};
+
     /**
-     * unescape
+     * escape
+     *
+     * @param cb source char buffer
+     * @param from starting position in the source array.
+     * @param to ending position in the source array.
+     * @return Escape xml string
      */
-    static String unescape(StringBuilder escapeBuf, char[] cb, int from, int to) {
-        if (from == to) return EMPTY;
-        int idx_38 = indexOf(cb, '&', from)
-            , idx_59 = idx_38 > -1 && idx_38 < to ? indexOf(cb, ';', idx_38 + 1) : -1;
+    public static String escape(char[] cb, int from, int to) {
+        int n = to - from;
+        if (n == 0) return EMPTY;
+        int idx_38 = indexOf(cb, '&', from, to)
+            , idx_59 = idx_38 > -1 && idx_38 < to ? indexOf(cb, ';', idx_38 + 1, Math.min(idx_38 + 9, to)) : -1;
 
-        if (idx_38 <= 0 || idx_38 >= idx_59 || idx_59 > to) {
-            return new String(cb, from, to - from);
-        }
-        escapeBuf.delete(0, escapeBuf.length());
+        if (idx_38 < from || idx_38 >= idx_59 || idx_59 > to) return new String(cb, from, to - from);
+
+        char[] buf;
+        if (n <= charBuffer.length) buf = charBuffer;
+        else if (n <= MAX_CHARACTERS_PER_CELL) charBuffer = buf = new char[Math.min(n + 100, MAX_CHARACTERS_PER_CELL)];
+        else buf = new char[n];
+
+        int offset = 0;
         do {
-            escapeBuf.append(cb, from, idx_38 - from);
+            System.arraycopy(cb, from, buf, offset, n = idx_38 - from);
+            offset += n;
             // ASCII
             if (cb[idx_38 + 1] == '#') {
-                int n = toInt(cb, idx_38 + 2, idx_59);
-                // byte range
-//                if (n < 0 || n > 127) {
-//                    LOGGER.warn("Unknown escape [{}]", new String(cb, idx_38, idx_59 - idx_38 + 1));
-//                }
-                // Unicode char
-                escapeBuf.append((char) n);
+                char c = cb[idx_38 + 2];
+                if (c == 'x') offset += toChars(toIntH(cb, idx_38 + 3, idx_59), buf, offset);
+                else if (c >= '0' && c <= '9') offset += toChars(toInt(cb, idx_38 + 2, idx_59), buf, offset);
+                else {
+                    System.arraycopy(cb, idx_38, buf, offset, n = idx_59 - idx_38 + 1);
+                    offset += n;
+                }
             }
             // desc
             else {
-                String name = new String(cb, idx_38 + 1, idx_59 - idx_38 - 1);
-                switch (name) {
-                    case "lt":
-                        escapeBuf.append('<');
-                        break;
-                    case "gt":
-                        escapeBuf.append('>');
-                        break;
-                    case "amp":
-                        escapeBuf.append('&');
-                        break;
-                    case "quot":
-                        escapeBuf.append('"');
-                        break;
-                    case "nbsp":
-                        escapeBuf.append(' ');
-                        break;
-                    default: // Unknown escape
-//                        LOGGER.warn("Unknown escape [&{}]", name);
-                        escapeBuf.append(cb, idx_38, idx_59 - idx_38 + 1);
+                n = idx_59 - idx_38 - 1;
+                if (n == 2 && cb[idx_38 + 1] == 'l' && cb[idx_38 + 2] == 't') buf[offset++] = '<';
+                else if (n == 2 && cb[idx_38 + 1] == 'g' && cb[idx_38 + 2] == 't') buf[offset++] = '>';
+                else if (n == 3 && cb[idx_38 + 1] == 'a' && cb[idx_38 + 2] == 'm' && cb[idx_38 + 3] == 'p') buf[offset++] = '&';
+                else if (n == 4 && cb[idx_38 + 1] == 'n' && cb[idx_38 + 2] == 'b' && cb[idx_38 + 3] == 's' && cb[idx_38 + 4] == 'p') buf[offset++] = ' ';
+                else if (n == 4 && cb[idx_38 + 1] == 'q' && cb[idx_38 + 2] == 'u' && cb[idx_38 + 3] == 'o' && cb[idx_38 + 4] == 't') buf[offset++] = '"';
+                else {
+                    System.arraycopy(cb, idx_38, buf, offset, n = idx_59 - idx_38 + 1);
+                    offset += n;
                 }
             }
             from = ++idx_59;
-            idx_59 = (idx_38 = indexOf(cb, '&', idx_59)) > -1 && idx_38 < to ? indexOf(cb, ';', idx_38 + 1) : -1;
+            idx_59 = (idx_38 = indexOf(cb, '&', idx_59, to)) > -1 && idx_38 < to ? indexOf(cb, ';', idx_38 + 1, Math.min(idx_38 + 9, to)) : -1;
         } while (idx_38 > -1 && idx_59 > idx_38 && idx_59 <= to);
 
         if (from < to) {
-            escapeBuf.append(cb, from, to - from);
+            System.arraycopy(cb, from, buf, offset, n = to - from);
+            offset += n;
         }
-        return escapeBuf.toString();
+
+        return new String(buf, 0, offset);
     }
 
-    private static int indexOf(char[] cb, char c, int from) {
-        for (; from < cb.length; from++) {
-            if (cb[from] == c) return from;
-        }
-        return -1;
+    private static int indexOf(char[] cb, char c, int from, int to) {
+        for (; from < to && cb[from] != c; from++);
+        return from < to ? from : -1;
     }
 
     static int toInt(char[] cb, int a, int b) {
-        boolean _n;
-        if (_n = cb[a] == '-') a++;
-        int n = cb[a++] - '0';
-        for (; b > a; ) {
-            n = n * 10 + cb[a++] - '0';
+        int n = 0;
+        boolean negative = cb[a] == '-';
+        for (int i = negative ? a + 1 : a; b > i; n = n * 10 + cb[i++] - '0');
+        return negative ? -n : n;
+    }
+
+    // Hex value
+    static int toIntH(char[] cb, int a, int b) {
+        int n = 0;
+        for (int c; b > a; n = n * 16 + ((c = cb[a++]) <= '9' ? c - '0' : (c >= 'a' ? c - 32 : c) - '7'));
+        return n;
+    }
+
+    static int toChars(int codePoint, char[] dst, int i) {
+        int n;
+        if (isBmpCodePoint(codePoint)) {
+            dst[i] = (char) codePoint;
+            n = 1;
+        } else if (isValidCodePoint(codePoint)) {
+            dst[i + 1] = lowSurrogate(codePoint);
+            dst[i] = highSurrogate(codePoint);
+            n = 2;
+        } else {
+           dst[i] = 0xFFFD; // Illegal value �
+           n = 1;
         }
-        return _n ? -n : n;
+        return n;
     }
 
     /**
@@ -684,8 +698,8 @@ public class SharedStrings implements Closeable {
     public void close() throws IOException {
         if (reader != null) {
             // Debug hit rate
-            LOGGER.debug("Count: {}， uniqueCount: {}， Repetition rate: {}", total, max
-                , (total > 0 ? (total - max) * 100.0/ total : 0) + "%");
+            LOGGER.debug("Count: {}， uniqueCount: {}， Repetition rate: {}%", total, max
+                , (total > 0 ? (total - max) * 100.0/ total : 0));
             LOGGER.debug("Forward: {}, Backward: {}, SST: {}, Hot: {}, Tester: {Resize: {}, Size: {}}"
                 , total_forward, total_backward, total_sst, total_hot
                 , tester != null ? tester.analysis() : 0, tester != null ? tester.size() : 0);
@@ -697,7 +711,6 @@ public class SharedStrings implements Closeable {
         if (tester != null) {
             tester = null;
         }
-        escapeBuf = null;
         if (sst != null) {
             sst.close();
         }
@@ -736,16 +749,16 @@ interface Tester {
 
     int analysis();
 
-    class FixBinaryTester implements Tester {
+    class BinaryTester implements Tester {
         private int start;
         private int limit;
         private final int initial_size;
-        private long[] marks;
+        private final long[] marks;
 //        private static final int LIMIT = (1 << 25) - 1;
 
         private int total_resize; // For debug
 
-        FixBinaryTester(int expectedInsertions) {
+        BinaryTester(int expectedInsertions) {
             marks = new long[initial_size = ((expectedInsertions - 1) >> 6) + 1];
             limit = (initial_size << 6) - 1;
         }
@@ -754,7 +767,7 @@ interface Tester {
         public boolean test(int i) {
             if (i < start) return true;
             // Check bound of bit-set
-            if (i > limit) resize(i);
+            if (i > limit && !resize(i)) return false;
             i = i - start;
             int n = i >> 6, m = i - (n << 6);
             boolean a = ((marks[n] >> (63 - m)) & 1) == 1;
@@ -777,31 +790,25 @@ interface Tester {
             return total_resize;
         }
 
-        private void resize(int i) {
+        private boolean resize(int i) {
             total_resize++;
             int ii = 0, n = marks.length, l = ((i - start) >> 6) + 1;
 
             for (; ii < n && marks[ii] == -1; ii++) ;
+            if (l - ii > initial_size)
+                for (; ii < n && (Long.bitCount(marks[ii]) > 48 || marks[ii] == 0); ii++) ;
 
             if (l - ii <= initial_size) {
                 // Clean old mark
-                if (ii > 0) {
-                    int j = 0;
-                    for (int m = ii; m < n; marks[j++] = marks[m++]) ;
-                    for (; j < n; marks[j++] &= 0) ;
-                    start += (ii << 6);
-                }
-//                else {
-//                    marks = Arrays.copyOf(marks, l + (l >> 1));
-//                }
-            } else {
-                // TODO Limit Tester length to prevent infinite expansion
-                long[] newMarks = new long[(l - ii) + ((l - ii) >> 1)];
-                System.arraycopy(marks, ii, newMarks, 0, marks.length - ii);
-                marks = newMarks;
+                int j = n - ii;
+                if (j > 0) System.arraycopy(marks, ii, marks, 0, j);
+
+                for (; j < n; marks[j++] = 0L) ;
                 start += (ii << 6);
+                limit = (marks.length << 6) + start - 1;
+                return true;
             }
-            limit = (marks.length << 6) + start - 1;
+            return false;
         }
     }
 }
\ No newline at end of file
diff --git a/src/main/java/org/ttzero/excel/reader/XMLDrawings.java b/src/main/java/org/ttzero/excel/reader/XMLDrawings.java
index 2dda0c1a..dc8dbf0f 100644
--- a/src/main/java/org/ttzero/excel/reader/XMLDrawings.java
+++ b/src/main/java/org/ttzero/excel/reader/XMLDrawings.java
@@ -33,6 +33,11 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import static org.ttzero.excel.reader.ExcelReader.getEntry;
+import static org.ttzero.excel.reader.ExcelReader.toZipPath;
 
 /**
  * Drawings resources
@@ -77,35 +82,64 @@ public class XMLDrawings implements Drawings {
         // Empty excel, maybe throw exception here
         if (excelReader.sheets == null) return null;
 
+        ZipFile zipFile = excelReader.zipFile;
+        if (zipFile == null) return null;
+
         SAXReader reader = SAXReader.createDefault();
         Document document;
 
         List<Picture> pictures = new ArrayList<>();
         for (Sheet sheet : excelReader.sheets) {
             XMLSheet xmlSheet = (XMLSheet) sheet;
-            Path relsPath = xmlSheet.path.getParent().resolve("_rels/" + xmlSheet.path.getFileName() + ".rels");
-            if (!Files.exists(relsPath)) continue;
+            int i = xmlSheet.path.lastIndexOf('/');
+            if (i < 0) i = xmlSheet.path.lastIndexOf('\\');
+            String fileName = xmlSheet.path.substring(i + 1);
+            ZipEntry entry = getEntry(zipFile, "xl/worksheets/_rels/" + fileName + ".rels");
+            if (entry == null) continue;
             try {
-                document = reader.read(Files.newInputStream(relsPath));
+                document = reader.read(zipFile.getInputStream(entry));
             } catch (DocumentException | IOException e) {
-                FileUtil.rm_rf(excelReader.self.toFile(), true);
-                throw new ExcelReadException("The file format is incorrect or corrupted. [/xl/worksheets/_rels/" + xmlSheet.path.getFileName() + ".rels]");
+                throw new ExcelReadException("The file format is incorrect or corrupted. [" + entry.getName() + ".rels]");
             }
 
+            if (excelReader.tempDir == null) {
+                try {
+                    excelReader.tempDir = FileUtil.mktmp("eec+");
+                } catch (IOException e) {
+                    throw new ExcelReadException("Create temp directory failed.", e);
+                }
+            }
+            Path imagesPath = excelReader.tempDir.resolve("media");
+            if (!Files.exists(imagesPath)) {
+                // Create media path
+                try {
+                    Files.createDirectory(imagesPath);
+                } catch (IOException e) {
+                    throw new ExcelReadException("Create temp directory failed.", e);
+                }
+            }
             List<Element> list = document.getRootElement().elements();
             for (Element e : list) {
                 String target = e.attributeValue("Target"), type = e.attributeValue("Type");
+                entry = getEntry(zipFile, "xl/" + toZipPath(target));
                 // Background
                 if (Const.Relationship.IMAGE.equals(type)) {
                     Picture picture = new Picture();
                     pictures.add(picture);
                     picture.sheet = sheet;
                     picture.background = true;
-                    picture.localPath = xmlSheet.path.getParent().resolve(target);
+                    // Copy image to tmp file
+                    try {
+                        Path targetPath = imagesPath.resolve(target);
+                        Files.copy(zipFile.getInputStream(entry), targetPath);
+                        picture.localPath = targetPath;
+                    } catch (IOException ioException) {
+                        ioException.printStackTrace();
+                    }
+
                     // Drawings
                 } else if (Const.Relationship.DRAWINGS.equals(type)) {
-                    Path drawingsPath = xmlSheet.path.getParent().resolve(target);
-                    List<Picture> subPictures = parseDrawings(drawingsPath);
+                    List<Picture> subPictures = parseDrawings(zipFile, entry, imagesPath);
                     if (subPictures != null) {
                         for (Picture picture : subPictures) {
                             picture.sheet = sheet;
@@ -120,28 +154,36 @@ public class XMLDrawings implements Drawings {
     }
 
     // Parse drawings.xml
-    protected List<Picture> parseDrawings(Path path) {
+    protected List<Picture> parseDrawings(ZipFile zipFile, ZipEntry entry, Path imagesPath) {
+        int i = entry.getName().lastIndexOf('/');
+        String relsKey;
+        if (i > 0)
+            relsKey = entry.getName().substring(0, i) + "/_rels" + entry.getName().substring(i);
+        else if ((i = entry.getName().lastIndexOf('\\')) > 0)
+            relsKey = entry.getName().substring(0, i) + "\\_rels" + entry.getName().substring(i);
+        else relsKey = entry.getName();
+        String key = relsKey + ".rels";
+        ZipEntry entry1 = getEntry(zipFile, key);
+        if (entry1 == null) throw new ExcelReadException("The file format is incorrect or corrupted. [" + key + "]");
         SAXReader reader = SAXReader.createDefault();
         Document document;
         try {
-            document = reader.read(Files.newInputStream(path.getParent().resolve("_rels/" + path.getFileName() + ".rels")));
+            document = reader.read(zipFile.getInputStream(entry1));
         } catch (DocumentException | IOException e) {
-            FileUtil.rm_rf(excelReader.self.toFile(), true);
-            throw new ExcelReadException("The file format is incorrect or corrupted. [/xl/drawings/_rels/" + path.getFileName() + ".rels]");
+            throw new ExcelReadException("The file format is incorrect or corrupted. [" + key + "]");
         }
         List<Element> list = document.getRootElement().elements();
         Relationship[] rels = new Relationship[list.size()];
-        int i = 0;
+        i = 0;
         for (Element e : list) {
             rels[i++] = new Relationship(e.attributeValue("Id"), e.attributeValue("Target"), e.attributeValue("Type"));
         }
         RelManager relManager = RelManager.of(rels);
 
         try {
-            document = reader.read(Files.newInputStream(path));
+            document = reader.read(zipFile.getInputStream(entry));
         } catch (DocumentException | IOException e) {
-            FileUtil.rm_rf(excelReader.self.toFile(), true);
-            throw new ExcelReadException("The file format is incorrect or corrupted. [/xl/drawings/" + path.getFileName() + "]");
+            throw new ExcelReadException("The file format is incorrect or corrupted. [" + entry.getName() + "]");
         }
 
         Element root = document.getRootElement();
@@ -166,7 +208,17 @@ public class XMLDrawings implements Drawings {
             if (r != null && Const.Relationship.IMAGE.equals(rel.getType())) {
                 Picture picture = new Picture();
                 pictures.add(picture);
-                picture.localPath = path.getParent().resolve(rel.getTarget());
+                // Copy image to tmp path
+                entry = getEntry(zipFile, "xl/" + toZipPath(rel.getTarget()));
+                if (entry != null) {
+                    try {
+                        Path targetPath = imagesPath.resolve(rel.getTarget());
+                        Files.copy(zipFile.getInputStream(entry), targetPath);
+                        picture.localPath = targetPath;
+                    } catch (IOException ioException) {
+                        ioException.printStackTrace();
+                    }
+                }
                 picture.dimension = dimension(e, xdr);
 
                 Element extLst = blip.element(QName.get("extLst", a));
@@ -187,7 +239,7 @@ public class XMLDrawings implements Drawings {
         return !pictures.isEmpty() ? pictures : null;
     }
 
-    protected Dimension dimension(Element e, Namespace xdr) {
+    protected static Dimension dimension(Element e, Namespace xdr) {
         Element fromEle = e.element(QName.get("from", xdr));
         int[] f = dimEle(fromEle, xdr);
         Element toEle = e.element(QName.get("to", xdr));
@@ -196,7 +248,7 @@ public class XMLDrawings implements Drawings {
         return new Dimension(f[0] + 1, (short) (f[1] + 1), t[0] + 1, (short) (t[1] + 1));
     }
 
-    protected int[] dimEle(Element e, Namespace xdr) {
+    protected static int[] dimEle(Element e, Namespace xdr) {
         int c = 0, r = 0;
         if (e != null) {
             String col = e.element(QName.get("col", xdr)).getText(), row = e.element(QName.get("row", xdr)).getText();
diff --git a/src/main/java/org/ttzero/excel/reader/XMLRow.java b/src/main/java/org/ttzero/excel/reader/XMLRow.java
index 11958899..5c5f584d 100644
--- a/src/main/java/org/ttzero/excel/reader/XMLRow.java
+++ b/src/main/java/org/ttzero/excel/reader/XMLRow.java
@@ -27,7 +27,7 @@ import static org.ttzero.excel.reader.Cell.SST;
 import static org.ttzero.excel.reader.Cell.INLINESTR;
 import static org.ttzero.excel.reader.Cell.UNALLOCATED;
 import static org.ttzero.excel.reader.SharedStrings.toInt;
-import static org.ttzero.excel.reader.SharedStrings.unescape;
+import static org.ttzero.excel.reader.SharedStrings.escape;
 import static org.ttzero.excel.util.StringUtil.swap;
 
 /**
@@ -44,7 +44,6 @@ import static org.ttzero.excel.util.StringUtil.swap;
  */
 public class XMLRow extends Row {
     protected int startRow;
-    protected StringBuilder buf;
 
     /**
      * The number of row. (one base)
@@ -62,11 +61,9 @@ public class XMLRow extends Row {
     @SuppressWarnings("unused")
     public XMLRow() {
         this.startRow = 1;
-        buf = new StringBuilder();
     }
 
     public XMLRow(SharedStrings sst, Styles styles, int startRow) {
-        buf = new StringBuilder();
         init(sst, styles, startRow);
     }
 
@@ -161,12 +158,7 @@ public class XMLRow extends Row {
         unknownLength = lc < 0;
 
         // Parse cell value
-//        if (unknownLength) {
         for (Cell cell; (cell = nextCell()) != null; parseCellValue(cell)) ;
-//        } else {
-//            int index = 0;
-//            for (Cell cell; index++ < lc && (cell = nextCell()) != null; parseCellValue(cell)) ;
-//        }
     }
 
     /**
@@ -238,7 +230,7 @@ public class XMLRow extends Row {
         return cell;
     }
 
-    protected long toLong(int a, int b) {
+    protected static long toLong(char[] cb, int a, int b) {
         boolean _n;
         if (_n = cb[a] == '-') a++;
         long n = cb[a++] - '0';
@@ -248,15 +240,15 @@ public class XMLRow extends Row {
         return _n ? -n : n;
     }
 
-    protected String toString(int a, int b) {
+    protected static String toString(char[] cb, int a, int b) {
         return new String(cb, a, b - a);
     }
 
-    protected double toDouble(int a, int b) {
-        return Double.parseDouble(toString(a, b));
+    protected static double toDouble(char[] cb, int a, int b) {
+        return Double.parseDouble(toString(cb, a, b));
     }
 
-    protected boolean isNumber(int a, int b) {
+    protected static boolean isNumber(char[] cb, int a, int b) {
         if (a == b) return false;
         if (cb[a] == '-') a++;
         for (; a < b; ) {
@@ -266,7 +258,7 @@ public class XMLRow extends Row {
         return a == b;
     }
 
-    protected boolean isDouble(int a, int b) {
+    protected static boolean isDouble(char[] cb, int a, int b) {
         if (a == b) return false;
         if (cb[a] == '-') a++;
         for (char i = 0, e = 0; a < b; ) {
@@ -281,49 +273,26 @@ public class XMLRow extends Row {
 
     /* Found specify target  */
     protected int get(char c) {
-        // Ignore all attributes
-        return get(null, c, null);
-    }
-
-    /**
-     * Parses the value and attributes of the specified tag
-     *
-     * @param cell current cell
-     * @param c the specified tag
-     * @param attrConsumer an attribute consumer
-     * @return the start index of value
-     */
-    protected int get(Cell cell, char c, Attribute attrConsumer) {
         for (; cursor < e && (cb[cursor] != '<' || cb[cursor + 1] != c
             || cb[cursor + 2] != '>' && cb[cursor + 2] > ' ' && cb[cursor + 2] != '/'); cursor++) ;
         if (cursor == e) return cursor;
 
         int a;
-        if (cb[cursor + 2] == '>') {
-            a = cursor += 3;
-        }
+        if (cb[cursor + 2] == '>') a = cursor += 3;
+
         // Some other attributes
         else if (cb[cursor + 2] == ' ') {
-            int i = cursor + 3;
             for (; cursor < e && cb[cursor] != '>'; cursor++) ;
-            // If parse attributes
-            if (attrConsumer != null)
-                attrConsumer.accept(cell, cb, i, cb[cursor - 1] != '/' ? cursor : cursor - 1);
 
             cursor++;
-            if (cb[cursor - 2] == '/' || cursor == e) {
-                return cursor;
-            }
+            if (cb[cursor - 2] == '/' || cursor == e) return cursor;
             a = cursor;
         }
         // Empty tag
         else if (cb[cursor + 2] == '/') {
             cursor += 3;
             return cursor;
-        }
-        else {
-            a = cursor += 3;
-        }
+        } else a = cursor += 3;
 
         // Found end tag
         for (; cursor < e && (cb[cursor] != '<' || cb[cursor + 1] != '/'
@@ -368,7 +337,7 @@ public class XMLRow extends Row {
             case INLINESTR: // inner string
                 a = getT();
                 if (a < cursor) {
-                    cell.setSv(unescape(buf, cb, a, cursor));
+                    cell.setSv(escape(cb, a, cursor));
                 } else { // null value
                     cell.blank(); // Reset type to BLANK if null value
                 }
@@ -387,7 +356,7 @@ public class XMLRow extends Row {
             case FUNCTION: // function string
                 a = getV();
                 if (a < cursor) {
-                    cell.setSv(unescape(buf, cb, a, cursor));
+                    cell.setSv(escape(cb, a, cursor));
                 } else { // null value
                     cell.blank(); // Reset type to BLANK if null value
                 }
@@ -396,17 +365,17 @@ public class XMLRow extends Row {
             default:
                 a = getV();
                 if (a < cursor) {
-                    if (isNumber(a, cursor)) {
-                        long l = toLong(a, cursor);
+                    if (isNumber(cb, a, cursor)) {
+                        long l = toLong(cb, a, cursor);
                         if (l <= Integer.MAX_VALUE && l >= Integer.MIN_VALUE) {
                             cell.setNv((int) l);
                         } else {
                             cell.setLv(l);
                         }
-                    } else if (isDouble(a, cursor)) {
-                        cell.setDv(toDouble(a, cursor));
+                    } else if (isDouble(cb, a, cursor)) {
+                        cell.setDv(toDouble(cb, a, cursor));
                     } else {
-                        cell.setSv(toString(a, cursor));
+                        cell.setSv(toString(cb, a, cursor));
                     }
                 }
                 // Maybe the cell should be merged
@@ -425,21 +394,6 @@ public class XMLRow extends Row {
         return !(this instanceof XMLMergeRow) ? new XMLMergeRow(this) : (XMLMergeRow) this;
     }
 
-    /**
-     * Attribute consumer
-     */
-    @FunctionalInterface
-    interface Attribute {
-        /**
-         * Performs this operation on the given argument.
-         *
-         * @param cell current cell
-         * @param cb characters for the entire attribute
-         * @param a start index
-         * @param b end index
-         */
-        void accept(Cell cell, char[] cb, int a, int b);
-    }
 }
 
 /**
@@ -453,7 +407,6 @@ class XMLCalcRow extends XMLRow {
         this.sst = sst;
         this.styles = styles;
         this.startRow = startRow;
-        this.buf = new StringBuilder();
         this.calcFun = calcFun;
         this.hasCalcFunc = calcFun != null;
     }
@@ -462,7 +415,6 @@ class XMLCalcRow extends XMLRow {
         this.sst = row.sst;
         this.styles = row.styles;
         this.startRow = row.startRow;
-        this.buf = row.buf;
     }
 
     XMLCalcRow setCalcFun(MergeCalcFunc calcFun) {
@@ -486,11 +438,7 @@ class XMLCalcRow extends XMLRow {
         }
 
         // Parse cell value
-//        if (unknownLength) {
         for (Cell cell; (cell = nextCell()) != null; parseCellValue(cell)) ;
-//        } else {
-//            for(Cell cell; (cell = nextCell()) != null; parseCellValue(cell)) ;
-//        }
     }
 
     /**
@@ -526,7 +474,7 @@ class XMLCalcRow extends XMLRow {
         }
         // Inner text
         if (a < cursor) {
-            cell.fv = unescape(buf, cb, a, cursor);
+            cell.fv = escape(cb, a, cursor);
             if (cell.si > -1) setCalc(cell.si, cell.fv);
         }
         // Function string is shared
@@ -545,7 +493,35 @@ class XMLCalcRow extends XMLRow {
      * @return the end index of function value
      */
     private int getF(Cell cell) {
-        return get(cell, 'f', this::parseFunAttr);
+        for (; cursor < e && (cb[cursor] != '<' || cb[cursor + 1] != 'f'
+            || cb[cursor + 2] != '>' && cb[cursor + 2] > ' ' && cb[cursor + 2] != '/'); cursor++) ;
+        if (cursor == e) return cursor;
+
+        int a;
+        if (cb[cursor + 2] == '>') a = cursor += 3;
+
+        // Some other attributes
+        else if (cb[cursor + 2] == ' ') {
+            int i = cursor + 3;
+            for (; cursor < e && cb[cursor] != '>'; cursor++) ;
+            // If parse attributes
+            parseFunAttr(cell, cb, i, cb[cursor - 1] != '/' ? cursor : cursor - 1);
+
+            cursor++;
+            if (cb[cursor - 2] == '/' || cursor == e) return cursor;
+            a = cursor;
+        }
+        // Empty tag
+        else if (cb[cursor + 2] == '/') {
+            cursor += 3;
+            return cursor;
+        } else a = cursor += 3;
+
+        // Found end tag
+        for (; cursor < e && (cb[cursor] != '<' || cb[cursor + 1] != '/'
+            || cb[cursor + 2] != 'f' || cb[cursor + 3] != '>'); cursor++) ;
+
+        return a;
     }
 
     /* Parse function tag's attribute */
@@ -621,7 +597,6 @@ class XMLMergeRow extends XMLRow {
         this.sst = row.sst;
         this.styles = row.styles;
         this.startRow = row.startRow;
-        this.buf = row.buf;
     }
 
     XMLMergeRow setCopyValueFunc(Grid mergeCells, MergeValueFunc func) {
diff --git a/src/main/java/org/ttzero/excel/reader/XMLSheet.java b/src/main/java/org/ttzero/excel/reader/XMLSheet.java
index a15cbfa0..b1cdb55b 100644
--- a/src/main/java/org/ttzero/excel/reader/XMLSheet.java
+++ b/src/main/java/org/ttzero/excel/reader/XMLSheet.java
@@ -20,21 +20,23 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.ttzero.excel.entity.style.Styles;
 
-import java.io.BufferedReader;
 import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.channels.SeekableByteChannel;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
 import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import static org.ttzero.excel.reader.ExcelReader.getEntry;
 
 /**
  * The open-xml format Worksheet
@@ -69,12 +71,14 @@ public class XMLSheet implements Sheet {
         this.lastRowMark = sheet.lastRowMark;
         this.hrf = sheet.hrf;
         this.hrl = sheet.hrl;
+        this.zipFile = sheet.zipFile;
+        this.entry = sheet.entry;
     }
 
     protected String name;
     protected int index; // per sheet index of workbook
 //        , size = -1; // size of rows per sheet
-    protected Path path;
+    protected String path;
     protected int id;
     /**
      * The Shared String Table
@@ -94,7 +98,9 @@ public class XMLSheet implements Sheet {
     protected Drawings drawings;
     // Header row
     protected int hrf, hrl;
-
+    // Data Source
+    protected ZipFile zipFile;
+    protected ZipEntry entry;
 
     /**
      * Setting the worksheet name
@@ -110,10 +116,28 @@ public class XMLSheet implements Sheet {
      *
      * @param path the temp path
      */
-    protected void setPath(Path path) {
+    protected void setPath(String path) {
         this.path = path;
     }
 
+    /**
+     * Setting the source zip file
+     *
+     * @param zipFile source data
+     */
+    protected void setZipFile(ZipFile zipFile) {
+        this.zipFile = zipFile;
+    }
+
+    /**
+     * Setting the worksheet zip entry
+     *
+     * @param entry source data
+     */
+    protected void setZipEntry(ZipEntry entry) {
+        this.entry = entry;
+    }
+
     /**
      * Setting the Shared String Table
      *
@@ -195,7 +219,8 @@ public class XMLSheet implements Sheet {
     @Deprecated
     @Override
     public int getSize() {
-        return dimension != null ? dimension.lastRow - dimension.firstRow + 1 : -1;
+        Dimension d = getDimension();
+        return d != null ? d.lastRow - d.firstRow + 1 : -1;
     }
 
     /**
@@ -208,7 +233,7 @@ public class XMLSheet implements Sheet {
      */
     @Override
     public Dimension getDimension() {
-        return dimension;
+        return dimension != null ? dimension : (dimension = parseDimension());
     }
 
     /**
@@ -374,11 +399,11 @@ public class XMLSheet implements Sheet {
 
     @Override
     public String toString() {
-        return "Sheet id: " + getId() + ", name: " + getName() + ", dimension: " + getDimension();
+        return "Sheet id: " + getId() + ", name: " + getName();// + ", dimension: " + getDimension();
     }
 
     /////////////////////////////////Read sheet file/////////////////////////////////
-    protected BufferedReader reader;
+    protected Reader reader;
     protected char[] cb; // buffer
     protected int nChar, length;
     protected boolean eof = false, heof = false; // OPTIONS = false
@@ -401,8 +426,8 @@ public class XMLSheet implements Sheet {
             reset();
             return this;
         }
-        LOGGER.debug("Load {}", path.toString());
-        reader = Files.newBufferedReader(path);
+        LOGGER.debug("Load {}", path);
+        reader = new InputStreamReader(zipFile.getInputStream(entry), StandardCharsets.UTF_8);
         cb = new char[8192];
         nChar = 0;
         int left = 0;
@@ -432,7 +457,7 @@ public class XMLSheet implements Sheet {
                 }
             }
             // find index of <sheetData>
-            for (; nChar < length - 12; nChar++) {
+            for (int len = length - 12; nChar < len; nChar++) {
                 if (cb[nChar] == '<' && cb[nChar + 1] == 's' && cb[nChar + 2] == 'h'
                     && cb[nChar + 3] == 'e' && cb[nChar + 4] == 'e' && cb[nChar + 5] == 't'
                     && cb[nChar + 6] == 'D' && cb[nChar + 7] == 'a' && cb[nChar + 8] == 't'
@@ -454,21 +479,13 @@ public class XMLSheet implements Sheet {
         if (cb[nChar] == '>') {
             mark += length;
             eof = true;
+            if (dimension == null) dimension = new Dimension(1, (short) 1);
         } else {
             eof = false;
             mark += nChar;
             sRow = createRow().init(sst, styles, this.startRow > 0 ? this.startRow : 1);
         }
-
-        // Deep read if dimension information not write in header
-        if (!eof && dimension == null) {
-            parseDimension();
-        }
-        // Create a empty worksheet dimension
-        if (dimension == null) {
-            dimension = new Dimension(1, (short) 1);
-        }
-        LOGGER.debug("Dimension-Range: {}", dimension);
+        if (dimension != null) LOGGER.debug("Dimension-Range: {}", dimension);
 
         return this;
     }
@@ -516,6 +533,8 @@ public class XMLSheet implements Sheet {
                     reader.close(); // close reader
                     reader = null; // wait GC
                     LOGGER.debug("end of file.");
+                    if (dimension == null)
+                        dimension = new Dimension(1, (short) Math.max(sRow.fc, 1), Math.max(sRow.index, 1), (short) Math.max(sRow.lc, 1));
                     return null;
                 }
             } catch (IOException e) {
@@ -534,13 +553,12 @@ public class XMLSheet implements Sheet {
         char[] cb = new char[8192];
         int nChar = 0, length;
         // reload file
-        try (BufferedReader reader = Files.newBufferedReader(path)) {
+        try (Reader reader = new InputStreamReader(zipFile.getInputStream(entry), StandardCharsets.UTF_8)) {
             if (mark > 0) {
                 reader.skip(mark);
                 length = reader.read(cb);
             } else {
-                loopA:
-                for (; ; ) {
+                loopA: for (; ; ) {
                     length = reader.read(cb);
                     // find index of <sheetData>
                     for (; nChar < length - 12; nChar++) {
@@ -666,7 +684,7 @@ public class XMLSheet implements Sheet {
      */
     @Override
     public XMLSheet reset() {
-        LOGGER.debug("Reset {}", path.toString());
+        LOGGER.debug("Reset {}", path);
         try {
             hrf = 0;
             hrl = 0;
@@ -681,7 +699,7 @@ public class XMLSheet implements Sheet {
                 return this.load();
             }
             // Reload
-            reader = Files.newBufferedReader(path);
+            reader = new InputStreamReader(zipFile.getInputStream(entry), StandardCharsets.UTF_8);
             reader.skip(mark);
             length = reader.read(cb);
             nChar = 0;
@@ -703,135 +721,60 @@ public class XMLSheet implements Sheet {
     Read from tail and look at the line number of the last line
     to confirm the scope of the entire worksheet.
      */
-    void parseDimension() {
-        try (SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
-            long position = Files.size(path);
-            final int block = (int) Math.min(1 << 11, position), c = 7;
-            ByteBuffer buffer = ByteBuffer.allocate(block);
-            byte[] left = null;
-            int left_size = 0, i;
-
-            boolean eof, getit = false;
-            for (; ;) {
-                position -= (block - left_size);
-                channel.position(Math.max(0, position));
-                channel.read(buffer);
-                if (left_size > 0) {
-                    buffer.limit(block);
-                    buffer.put(left, 0, left_size);
+    Dimension parseDimension() {
+        try (InputStream is = zipFile.getInputStream(entry)) {
+            // Skips specified number of bytes of uncompressed data.
+            if (lastRowMark > 0L) is.skip(lastRowMark);
+
+            // Mark
+            long mark = 0L, mark0 = 0L;
+            int n, offset = 0, limit = 1 << 14, i, len, f, size = 0;
+            byte[] buf = new byte[limit], bytes = new byte[10];
+            while ((n = is.read(buf, offset, limit - offset)) > 0) {
+                mark += n;
+                if ((len = n + offset) < 11) {
+                    offset = len;
+                    continue;
                 }
-                buffer.flip();
-                eof = buffer.limit() < block;
-
-                int limit = buffer.limit();
-                i = limit - 1;
-
-                // <row r="
-                for (; i >= c && (buffer.get(i) != '"' || buffer.get(i - 1) != '='
-                    || buffer.get(i - 2) != 'r' || buffer.get(i - 3) > ' '
-                    || buffer.get(i - 4) != 'w' || buffer.get(i - 5) != 'o'
-                    || buffer.get(i - 6) != 'r' || buffer.get(i - 7) != '<'); i--)
-                    ;
-
-                // Not Found
-                if (i < c) {
-                    if (eof || (eof = position <= 0)) break;
-                    for (; i < limit && buffer.get(i) != '>'; i++) ;
-                    i++;
-                    if (i < limit - 1) {
-                        buffer.position(i);
-                        left_size = i;
-                        if (left == null || left_size > left.length) {
-                            left = new byte[left_size];
-                        }
-                        buffer.position(0);
-                        buffer.get(left, 0, left_size);
-                    } else left_size = 0;
+                i = len - 1;
+                // Look up from tail
+                for (; i > 1 && (buf[i--] != ' ' || buf[i--] != 'c' || buf[i] != '<'); ) ;
+                if (i <= 1 && (buf[i] != '<' || buf[i + 1] != 'c')) {
+                    offset = 0;
+                    continue;
                 }
-                // Found the last row or empty worksheet
-                else {
-                    getit = true;
-                    buffer.position(i);
-                    // Align channel and buffer position
-                    if (left_size > 0) channel.position(channel.position() + left_size);
-                    lastRowMark = channel.position() - buffer.remaining();
-                    break;
+                if (i <= len - 9) {
+                    n = i;
+                    i += 3;
+                    for (; i < len && buf[i] != 'r' && buf[i + 1] != '=' && buf[i + 2] != '"'; i++);
+                    if (i < len - 3) {
+                        f = i += 3;
+                        for (; i < len && buf[i] != '"'; i++);
+                        if (i < len) {
+                            System.arraycopy(buf, f, bytes, 0, size = i - f);
+                            if (buf[len - 1] == '<') {
+                                buf[0] = '<';
+                                offset = 1;
+                            } else offset = 0;
+                            mark0 = mark - (len - n);
+                            continue;
+                        }
+                    }
+                    i = n;
                 }
-                buffer.position(0);
-                buffer.limit(buffer.capacity() - left_size);
+                if (i < len) System.arraycopy(buf, i, buf, 0, offset = len - i);
             }
-
-            // Empty worksheet
-            if (!getit) {
-                dimension = Dimension.of("A1");
-                return;
+            if (lastRowMark < mark0) lastRowMark = mark0;
+            if (size > 0) {
+                long cr = ExcelReader.cellRangeToLong(new String(bytes, 0, size, StandardCharsets.US_ASCII));
+                return new Dimension(1, (short) 1, (int) (cr >> 16), (short) (cr & 0x7FFF));
             }
-
-            // Dimension
-            parseDim(channel, buffer);
         } catch (IOException e) {
             // Ignore error
-            LOGGER.debug("", e);
-        }
-    }
-
-    private int[] innerParse(ByteBuffer buffer, int i) {
-        int row = 0;
-        for (; buffer.get(i) != '"'; i++)
-            row = row * 10 + (buffer.get(i) - '0');
-        // spans="
-        i++;
-        for (; buffer.limit() - i >= 7 && (buffer.get(i) != 's' || buffer.get(i + 1) != 'p'
-            || buffer.get(i + 2) != 'a' || buffer.get(i + 3) != 'n'
-            || buffer.get(i + 4) != 's' || buffer.get(i + 5) != '='
-            || buffer.get(i + 6) != '"'); i++) ;
-        i += 7;
-        int cs = 0, ls = 0;
-        if (buffer.limit() <= i) {
-            eof = true; // EOF
-            return new int[] { row, 1, 1 };
+            LOGGER.warn("", e);
         }
-        for (; buffer.get(i) != ':'; i++)
-            cs = cs * 10 + (buffer.get(i) - '0');
-        i++;
-        for (; buffer.get(i) != '"'; i++)
-            ls = ls * 10 + (buffer.get(i) - '0');
-        return new int[] { row, cs, ls };
-    }
 
-    private void parseDim(SeekableByteChannel channel, ByteBuffer buffer) throws IOException {
-        long rr = 0L;
-        int rc, i = buffer.position();
-
-        int[] info = innerParse(buffer, ++i);
-        rc = (info[2] << 16) | (info[1] & 0x7FFF);
-        rr |= ((long) info[0]) << 32;
-
-        // Skip if the first row is known
-        if (mark > 0) {
-            channel.position(mark);
-            buffer.clear();
-            channel.read(buffer);
-            buffer.flip();
-
-            i = 0;
-            for (; buffer.get(i) != '<' || buffer.get(i + 1) != 'r'
-                || buffer.get(i + 2) != 'o' || buffer.get(i + 3) != 'w'
-                || buffer.get(i + 4) > ' ' || buffer.get(i + 5) != 'r'
-                || buffer.get(i + 6) != '=' || buffer.get(i + 7) != '"'; i++) ;
-            i += 8;
-
-            info = innerParse(buffer, i);
-            rr |= info[0];
-            if (info[1] > (rc & 0x7FFF)) {
-                rc |= info[1] & 0x7FFF;
-            }
-            if (info[2] < (rc >>> 16)) {
-                rc |= info[2] << 16;
-            }
-        } else rr |= 1;
-
-        dimension = new Dimension((int) rr, (short) rc, (int) (rr >>> 32), (short) (rc >>> 16));
+        return Dimension.of("A1");
     }
 
     Row createHeader(char[] cb, int start, int n) {
@@ -862,14 +805,6 @@ public class XMLSheet implements Sheet {
         Row accept(char[] cb, int start, int n);
     }
 
-    // For debug
-    static void watch(ByteBuffer buffer) {
-        int p = buffer.position();
-        byte[] bytes = new byte[Math.min(1 << 7, buffer.remaining())];
-        buffer.get(bytes, 0, bytes.length);
-        System.out.println(new String(bytes, 0, bytes.length, StandardCharsets.US_ASCII));
-        buffer.position(p);
-    }
 }
 
 /**
@@ -900,6 +835,8 @@ class XMLCalcSheet extends XMLSheet implements CalcSheet {
         this.mark = sheet.mark;
         this.sRow = sheet.sRow;
         this.lastRowMark = sheet.lastRowMark;
+        this.zipFile = sheet.zipFile;
+        this.entry = sheet.entry;
 
         if (this.path != null) {
 
@@ -925,8 +862,14 @@ class XMLCalcSheet extends XMLSheet implements CalcSheet {
     void load0() {
         if (ready) return;
 
-        // Parse calc.xml
-        long[][] calcArray = ExcelReader.parseCalcChain(path.getParent());
+        // Parse calcChain.xml
+        ZipEntry entry = getEntry(zipFile, "xl/calcChain.xml");
+        long[][] calcArray = null;
+        try {
+            calcArray = entry != null ? ExcelReader.parseCalcChain(zipFile.getInputStream(entry)) : null;
+        } catch (IOException e) {
+            LOGGER.warn("Parse calcChain failed, formula will be ignored");
+        }
         if (calcArray != null && calcArray.length >= id) setCalc(calcArray[id - 1]);
 
         if (!eof && !(sRow instanceof XMLCalcRow)) {
@@ -983,6 +926,7 @@ class XMLMergeSheet extends XMLSheet implements MergeSheet {
     // A merge cells grid
     private Grid mergeCells;
     boolean ready;
+    List<Dimension> dimensions;
 
     XMLMergeSheet(XMLSheet sheet) {
         this.name = sheet.name;
@@ -1005,6 +949,8 @@ class XMLMergeSheet extends XMLSheet implements MergeSheet {
         this.mark = sheet.mark;
         this.sRow = sheet.sRow;
         this.lastRowMark = sheet.lastRowMark;
+        this.zipFile = sheet.zipFile;
+        this.entry = sheet.entry;
 
         if (path != null) {
             if (reader != null && !ready) this.load0();
@@ -1034,8 +980,9 @@ class XMLMergeSheet extends XMLSheet implements MergeSheet {
 
             if (mergeCells != null && !mergeCells.isEmpty()) {
                 this.mergeCells = GridFactory.create(mergeCells);
-                LOGGER.debug("Grid: Size: {} ==> {}", this.mergeCells.size(), this.mergeCells.getClass());
-            }
+                LOGGER.debug("Grid: {} ===> Size: {}", this.mergeCells.getClass(), this.mergeCells.size());
+                this.dimensions = mergeCells;
+            } else this.dimensions = Collections.emptyList();
         }
 
         if (!eof && !(sRow instanceof XMLMergeRow) && mergeCells != null) {
@@ -1048,128 +995,102 @@ class XMLMergeSheet extends XMLSheet implements MergeSheet {
     Parse `mergeCells` tag
      */
     List<Dimension> parseMerge() {
-        try (SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
-            long position = Files.size(path);
-            final int block = (int) Math.min(1 << 11, position), c = 12;
-            ByteBuffer buffer = ByteBuffer.allocate(block);
-            byte[] left;
-            int left_size = 0, i, limit;
-            boolean eof, getit = false;
-            // Skip if marked
-            if (lastRowMark > 0L) {
-                channel.position(lastRowMark);
-                channel.read(buffer);
-                buffer.flip();
-                getit = true;
-            } else {
-                left = new byte[12];
-                for (; ; ) {
-                    channel.position(Math.max(0, position -= block - left_size));
-                    channel.read(buffer);
-                    if (left_size > 0) {
-                        buffer.limit(block);
-                        buffer.put(left, 0, left_size);
-                    }
-                    buffer.flip();
-                    eof = buffer.limit() < block;
-
-                    limit = buffer.limit();
-                    i = limit - 1;
-
-                    // </sheetData>
-                    for (; i >= c && (buffer.get(i) != '>' || buffer.get(i - 1) != 'a'
-                        || buffer.get(i - 2) != 't' || buffer.get(i - 3) != 'a'
-                        || buffer.get(i - 4) != 'D' || buffer.get(i - 5) != 't'
-                        || buffer.get(i - 6) != 'e' || buffer.get(i - 7) != 'e'
-                        || buffer.get(i - 8) != 'h' || buffer.get(i - 9) != 's'
-                        || buffer.get(i - 10) != '/' || buffer.get(i - 11) != '<'); i--)
-                        ;
-
-                    // Not Found
-                    if (i < c) {
-                        if (eof) break;
-                        buffer.position(0);
-                        left_size = 12;
-                        buffer.get(left, 0, left_size);
-                        buffer.clear();
-                        buffer.limit(block - left_size);
-                    }
-                    // Found the last row or empty worksheet
-                    else {
-                        buffer.position(i + 1);
-                        getit = true;
-                        // Align channel and buffer position
-                        channel.position(channel.position() + left_size);
-                        break;
+        List<Dimension> list = null;
+        try (InputStream is = zipFile.getInputStream(entry)) {
+            // Skips specified number of bytes of uncompressed data.
+            if (lastRowMark > 0L) is.skip(lastRowMark);
+
+            int n, offset = 0, limit = 1 << 14, i, len;
+            byte[] buf = new byte[limit];
+            while ((n = is.read(buf, offset, limit - offset)) > 0) {
+                if ((len = n + offset) < 11) {
+                    offset = len;
+                    continue;
+                }
+                i = 0; len--;
+                for (; i < len && (buf[i++] != '<' || buf[i] != 'm'); ) ;
+                // Compact
+                if (i >= len) {
+                    if (buf[i] == '<') {
+                        buf[0] = '<';
+                        offset = 1;
+                    } else if (buf[i - 1] == '<' && buf[i] == 'm') {
+                        buf[0] = '<';
+                        buf[1] = 'm';
+                        offset = 2;
+                    } else offset = 0;
+                    continue;
+                }
+                // Get it
+                len++;
+                if (len - i < 11) {
+                    System.arraycopy(buf, i, buf, 0, offset = len - i);
+                    if ((n = is.read(buf, offset, limit - offset)) <= 0)
+                        return null;
+                    len = n + offset;
+                    if (len < 11) {
+                        while (((n = is.read(buf, offset, limit - offset)) > 0)) {
+                            if ((len = n + offset) < 11) offset = len;
+                            else break;
+                        }
                     }
+                    i = 0;
                 }
-            }
+                if (len < 11 || buf[i] != 'm' || buf[i + 1] != 'e' || buf[i + 2] != 'r'
+                    || buf[i + 3] != 'g' || buf[i + 4] != 'e' || buf[i + 5] != 'C' || buf[i + 6] != 'e'
+                    || buf[i + 7] != 'l' || buf[i + 8] != 'l' || buf[i + 9] != 's' || buf[i + 10] > ' ')
+                    return null;
+                i += 11;
+                n = len;
+                list = new ArrayList<>();
+                int f, t;
+                do {
+                    if (n < 11) {
+                        offset += n;
+                        continue;
+                    }
+                    for (; ;) {
+                        for (; i < n - 11 && (buf[i] != '<' || buf[i + 1] != 'm'
+                            || buf[i + 2] != 'e' || buf[i + 3] != 'r'
+                            || buf[i + 4] != 'g' || buf[i + 5] != 'e'
+                            || buf[i + 6] != 'C' || buf[i + 7] != 'e'
+                            || buf[i + 8] != 'l' || buf[i + 9] != 'l'
+                            || buf[i + 10] > ' '); i++) ;
+
+                        if (i >= n - 11) {
+                            System.arraycopy(buf, i, buf, 0, offset = n - i);
+                            break;
+                        }
 
-            if (getit) {
-                // Find mergeCells tag
-                return parseMerge(channel, buffer, block);
-            }
-        } catch (IOException e) {
-            // Ignore error
-            LOGGER.warn("", e);
-        }
-        return null;
-    }
+                        t = i;
+                        i += 11;
 
-    // Find mergeCell tags
-    List<Dimension> parseMerge(SeekableByteChannel channel, ByteBuffer buffer, int block) throws IOException {
-        List<Dimension> mergeCells = new ArrayList<>();
-        boolean eof = false;
-        int limit = buffer.limit(), i = buffer.position(), f, n;
-        byte[] bytes = new byte[32];
-        for (; ;) {
-            for (; i < limit - 11 && (buffer.get(i) != '<' || buffer.get(i + 1) != 'm'
-                || buffer.get(i + 2) != 'e' || buffer.get(i + 3) != 'r'
-                || buffer.get(i + 4) != 'g' || buffer.get(i + 5) != 'e'
-                || buffer.get(i + 6) != 'C' || buffer.get(i + 7) != 'e'
-                || buffer.get(i + 8) != 'l' || buffer.get(i + 9) != 'l'
-                || buffer.get(i + 10) > ' '); i++) ;
-
-            if (i >= limit - 22) {
-                if (eof) break;
-                buffer.position(limit - 22);
-                for (; i < buffer.limit() && buffer.get() != '<'; i++) ;
-                if (buffer.get(buffer.position() - 1) == '<') buffer.position(buffer.position() - 1);
-                buffer.compact();
-                channel.read(buffer);
-                buffer.flip();
-                if ((limit = buffer.limit()) <= 0) break;
-                i = 0;
-                eof = limit < block;
-            } else {
-                i += 11;
-                for (; i < limit - 5 && (buffer.get(i) != 'r' || buffer.get(i + 1) != 'e'
-                    || buffer.get(i + 2) != 'f' || buffer.get(i + 3) != '='
-                    || buffer.get(i + 4) != '"'); i++) ;
-                f = i += 5;
-                for (; i < limit && buffer.get(i) != '"'; i++) ;
-                if (i >= limit) {
-                    buffer.compact();
-                    channel.read(buffer);
-                    buffer.flip();
-                    if ((limit = buffer.limit()) <= 0) break;
+                        for (; i < n - 5 && (buf[i] != 'r' || buf[i + 1] != 'e' || buf[i + 2] != 'f'
+                            || buf[i + 3] != '=' || buf[i + 4] != '"'); i++) ;
 
-                    i = 0;
-                    eof = limit < block;
-                    continue;
-                }
-                n = i - f;
-                if (n > bytes.length) bytes = new byte[n];
+                        if (i >= n - 5) {
+                            System.arraycopy(buf, t, buf, 0, offset = n - t);
+                            break;
+                        }
 
-                buffer.position(f);
-                buffer.get(bytes, 0, n);
+                        f = i += 5;
+                        for (; i < n && buf[i] != '"'; i++) ;
 
-                mergeCells.add(Dimension.of(new String(bytes, 0, n, StandardCharsets.US_ASCII)));
-                buffer.get(); // Skip '"'
+                        if (i >= n) {
+                            System.arraycopy(buf, t, buf, 0, offset = n - t);
+                            break;
+                        }
+
+                        list.add(Dimension.of(new String(buf, f, i - f, StandardCharsets.US_ASCII)));
+                    }
+                    i = 0;
+                } while ((n = is.read(buf, offset, limit - offset)) > 0 && (n += offset) > 0);
             }
+        } catch (IOException e) {
+            // Ignore error
+            LOGGER.warn("", e);
         }
-
-        return mergeCells;
+        return list;
     }
 
     private void mergeCell(int row, Cell cell) {
@@ -1183,6 +1104,6 @@ class XMLMergeSheet extends XMLSheet implements MergeSheet {
 
     @Override
     public List<Dimension> getMergeCells() {
-        return parseMerge();
+        return dimensions != null ? dimensions : (dimensions = parseMerge());
     }
 }
\ No newline at end of file


Test patch:
diff --git a/src/test/java/org/ttzero/excel/entity/ListObjectSheetTest.java b/src/test/java/org/ttzero/excel/entity/ListObjectSheetTest.java
index 7d9e9f8d..c4ace262 100644
--- a/src/test/java/org/ttzero/excel/entity/ListObjectSheetTest.java
+++ b/src/test/java/org/ttzero/excel/entity/ListObjectSheetTest.java
@@ -16,7 +16,6 @@
 
 package org.ttzero.excel.entity;
 
-import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.ttzero.excel.Print;
@@ -32,6 +31,7 @@ import org.ttzero.excel.manager.Const;
 import org.ttzero.excel.manager.docProps.Core;
 import org.ttzero.excel.processor.ConversionProcessor;
 import org.ttzero.excel.processor.StyleProcessor;
+import org.ttzero.excel.reader.AppInfo;
 import org.ttzero.excel.reader.Cell;
 import org.ttzero.excel.reader.Dimension;
 import org.ttzero.excel.reader.ExcelReader;
@@ -58,6 +58,7 @@ import java.util.Collections;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
@@ -84,9 +85,9 @@ import static org.ttzero.excel.reader.ExcelReaderTest.testResourceRoot;
 public class ListObjectSheetTest extends WorkbookTest {
 
     @BeforeClass
-    public static void setUp() throws Exception {
+    public static void setUp() {
         // 删除之前的测试文件，防止重复执行不正确
-        List<String> randomExcelNameList = new ArrayList<String>();
+        List<String> randomExcelNameList = new ArrayList<>();
         randomExcelNameList.add("testForceExportOnWorkbook.xlsx");
         randomExcelNameList.add("testForceExportOnWorkbook2.xlsx");
         randomExcelNameList.add("testForceExportOnWorkSheet.xlsx");
@@ -511,10 +512,21 @@ public class ListObjectSheetTest extends WorkbookTest {
         core.setVersion("1.0");
 //        core.setRevision("1.2");
         core.setLastModifiedBy("TTT");
-        new Workbook("Specify Core")
-            .setCore(core)
+        new Workbook().setCore(core)
             .addSheet(new ListSheet<>(Collections.singletonList(new NotSharedObject(getRandomString()))))
-                .writeTo(defaultTestPath);
+                .writeTo(defaultTestPath.resolve("Specify Core.xlsx"));
+
+        try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("Specify Core.xlsx"))) {
+            AppInfo info = reader.getAppInfo();
+            assert core.getCreator().equals(info.getCreator());
+            assert core.getTitle().equals(info.getTitle());
+            assert core.getSubject().equals(info.getSubject());
+            assert core.getCategory().equals(info.getCategory());
+            assert core.getDescription().equals(info.getDescription());
+            assert core.getKeywords().equals(info.getKeywords());
+            assert core.getVersion().equals(info.getVersion());
+            assert core.getLastModifiedBy().equals(info.getLastModifiedBy());
+        }
     }
 
     @Test public void testLarge() throws IOException {
@@ -560,7 +572,14 @@ public class ListObjectSheetTest extends WorkbookTest {
         }).writeTo(defaultTestPath.resolve("large07.xlsx"));
 
         try (ExcelReader reader = ExcelReader.read(defaultTestPath.resolve("large07.xlsx"))) {
-            assert Dimension.of("A1:Y50001").equals(reader.sheet(0).getDimension());
+            org.ttzero.excel.reader.Sheet sheet = reader.sheet(0);
+            assert Dimension.of("A1:Y50001").equals(sheet.getDimension());
+            int i = 0;
+            for (Iterator<org.ttzero.excel.reader.Row> iter = sheet.header(1).iterator(); iter.hasNext(); i++) {
+                Map<String, Object> map = iter.next().toMap();
+                assert map.get("str1").equals("str1-" + i);
+                assert map.get("str25").equals("str25-" + i);
+            }
         }
     }
 
diff --git a/src/test/java/org/ttzero/excel/entity/style/StylesTest.java b/src/test/java/org/ttzero/excel/entity/style/StylesTest.java
index 424b0832..07f0dab9 100644
--- a/src/test/java/org/ttzero/excel/entity/style/StylesTest.java
+++ b/src/test/java/org/ttzero/excel/entity/style/StylesTest.java
@@ -22,6 +22,7 @@ import org.ttzero.excel.entity.I18N;
 
 import java.awt.Color;
 import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 
 import static org.ttzero.excel.entity.WorkbookTest.getOutputTestPath;
@@ -120,7 +121,7 @@ public class StylesTest {
         Path storagePath = getOutputTestPath().resolve("styles.xml");
         styles.writeTo(storagePath);
 
-        Styles styles = Styles.load(storagePath);
+        Styles styles = Styles.load(Files.newInputStream(storagePath));
         for (int i = 0, size = styles.size(); i < size; i++) {
            boolean isDate = styles.fastTestDateFmt(i);
            if (i == 0 || i == 5 || i >= 7 && i <= 9) assert !isDate;
diff --git a/src/test/java/org/ttzero/excel/reader/CacheTesterTest.java b/src/test/java/org/ttzero/excel/reader/CacheTesterTest.java
index 0fe2a6d8..7359aff1 100644
--- a/src/test/java/org/ttzero/excel/reader/CacheTesterTest.java
+++ b/src/test/java/org/ttzero/excel/reader/CacheTesterTest.java
@@ -24,7 +24,7 @@ import org.junit.Test;
  */
 public class CacheTesterTest {
     @Test public void test() {
-        Tester tester = new Tester.FixBinaryTester(1024);
+        Tester tester = new Tester.BinaryTester(1024);
         assert !tester.test(0);
         assert !tester.test(1);
         assert !tester.test(9);
diff --git a/src/test/java/org/ttzero/excel/reader/ExcelReaderTest.java b/src/test/java/org/ttzero/excel/reader/ExcelReaderTest.java
index 0995bc01..8e5cf29d 100644
--- a/src/test/java/org/ttzero/excel/reader/ExcelReaderTest.java
+++ b/src/test/java/org/ttzero/excel/reader/ExcelReaderTest.java
@@ -16,7 +16,6 @@
 
 package org.ttzero.excel.reader;
 
-import org.junit.Ignore;
 import org.junit.Test;
 import org.ttzero.excel.Print;
 import org.ttzero.excel.annotation.ExcelColumn;
@@ -24,7 +23,6 @@ import org.ttzero.excel.annotation.IgnoreExport;
 import org.ttzero.excel.annotation.IgnoreImport;
 import org.ttzero.excel.annotation.RowNum;
 import org.ttzero.excel.entity.ListObjectSheetTest;
-import org.ttzero.excel.entity.WorkbookTest;
 import org.ttzero.excel.util.DateUtil;
 import org.ttzero.excel.util.FileUtil;
 import org.ttzero.excel.util.StringUtil;
@@ -103,7 +101,7 @@ public class ExcelReaderTest {
                 Row row = it.next();
                 println(row.getRowNum()
                     + " | " + row.getFirstColumnIndex()
-                    + " | " + row.getLastColumnIndex()
+                    + " - " + row.getLastColumnIndex()
                     + " => " + row);
             }
         }
@@ -356,14 +354,6 @@ public class ExcelReaderTest {
         }
     }
 
-    @Ignore
-    @Test public void testReaderLarge() throws IOException {
-        try (ExcelReader reader = ExcelReader.read(WorkbookTest.getOutputTestPath().resolve("large07.xlsx"))) {
-            long n = reader.sheets().flatMap(Sheet::dataRows).map(row -> row.too(LargeData.class)).count();
-            assert n == 500000;
-        }
-    }
-
     @Test public void testAllType() throws IOException {
         try (ExcelReader reader = ExcelReader.read(testResourceRoot().resolve("all type.xlsx"))) {
             reader.sheets().flatMap(Sheet::dataRows)
diff --git a/src/test/java/org/ttzero/excel/reader/ExcelReaderTest2.java b/src/test/java/org/ttzero/excel/reader/ExcelReaderTest2.java
index a484bf98..0c78bbac 100644
--- a/src/test/java/org/ttzero/excel/reader/ExcelReaderTest2.java
+++ b/src/test/java/org/ttzero/excel/reader/ExcelReaderTest2.java
@@ -58,4 +58,59 @@ public class ExcelReaderTest2 {
         }
     }
 
+    @Test public void testMerge() throws IOException {
+        try (ExcelReader reader = ExcelReader.read(testResourceRoot().resolve("merge.xlsx"))) {
+            MergeSheet sheet = reader.sheet(0).asMergeSheet();
+            List<Dimension> list = sheet.getMergeCells();
+            assert list.size() == 4;
+            assert list.get(0).equals(Dimension.of("B2:C2"));
+            assert list.get(1).equals(Dimension.of("E5:F8"));
+            assert list.get(2).equals(Dimension.of("A13:A20"));
+            assert list.get(3).equals(Dimension.of("B16:E17"));
+
+            sheet = reader.sheet(1).asMergeSheet();
+            list = sheet.getMergeCells();
+            assert list.size() == 2;
+            assert list.get(0).equals(Dimension.of("BM2:BQ11"));
+            assert list.get(1).equals(Dimension.of("A1:B26"));
+
+            sheet = reader.sheet(2).asMergeSheet();
+            list = sheet.getMergeCells();
+            assert list.size() == 2;
+            assert list.get(0).equals(Dimension.of("A16428:D16437"));
+            assert list.get(1).equals(Dimension.of("A1:K3"));
+
+            sheet = reader.sheet(3).asMergeSheet();
+            list = sheet.getMergeCells();
+            assert list.size() == 1;
+            assert list.get(0).equals(Dimension.of("A1:CF1434"));
+        }
+    }
+
+    @Test public void testLargeMerge() throws IOException {
+        try (ExcelReader reader = ExcelReader.read(testResourceRoot().resolve("largeMerged.xlsx"))) {
+            MergeSheet sheet = reader.sheet(0).asMergeSheet();
+            List<Dimension> list = sheet.getMergeCells();
+            assert list.size() == 2608;
+            assert list.get(0).equals(Dimension.of("C3:F3"));
+            assert list.get(1).equals(Dimension.of("J2:J3"));
+            assert list.get(2).equals(Dimension.of("B2:B3"));
+            assert list.get(3).equals(Dimension.of("C5:F5"));
+
+            assert list.get(98).equals(Dimension.of("C82:F82"));
+            assert list.get(120).equals(Dimension.of("A104:A106"));
+            assert list.get(210).equals(Dimension.of("C176:F176"));
+            assert list.get(984).equals(Dimension.of("C821:F821"));
+
+            assert list.get(1626).equals(Dimension.of("B1362:B1371"));
+            assert list.get(1627).equals(Dimension.of("J1362:J1363"));
+            assert list.get(2381).equals(Dimension.of("B2006:B2007"));
+            assert list.get(2396).equals(Dimension.of("J2019:J2020"));
+
+            assert list.get(2596).equals(Dimension.of("C2190:F2190"));
+            assert list.get(2601).equals(Dimension.of("J2195:J2196"));
+            assert list.get(2605).equals(Dimension.of("C2198:F2198"));
+            assert list.get(2607).equals(Dimension.of("C2200:F2200"));
+        }
+    }
 }
diff --git a/src/test/java/org/ttzero/excel/reader/SharedStringsTest.java b/src/test/java/org/ttzero/excel/reader/SharedStringsTest.java
index 02f77947..7657081a 100644
--- a/src/test/java/org/ttzero/excel/reader/SharedStringsTest.java
+++ b/src/test/java/org/ttzero/excel/reader/SharedStringsTest.java
@@ -50,19 +50,27 @@ public class SharedStringsTest {
     @Test public void testGeneral() throws IOException {
         List<String> list = Arrays.asList("abc", "中文");
         writeTestData(list);
-        try (SharedStrings sst = new SharedStrings(path, 0, 0).load()) {
+        try (SharedStrings sst = new SharedStrings(Files.newInputStream(path), 0, 0).load()) {
             checkTrue(sst, list);
         }
     }
 
     @Test public void testEscape() throws IOException {
-        List<String> list = Arrays.asList("<row>", "\"abc\"", "&nbsp;");
+        List<String> list = Arrays.asList("<row>", "\"abc\"", "&nbsp;", "<tag>",
+            "random&more", "one'one", "with\"signs\"", "random&more",
+            "<this will be escaped \ud83d\ude01>", "nothing+!()happens", "An 😀awesome 😃string with a few 😉emojis!");
         writeTestData(list);
-        try (SharedStrings sst = new SharedStrings(path, 0, 0).load()) {
+        try (SharedStrings sst = new SharedStrings(Files.newInputStream(path), 0, 0).load()) {
             checkTrue(sst, list);
         }
     }
 
+    @Test public void testEscape2() {
+        char[] chars = "&lt;tag&gt;,random&amp;more,with&quot;signs&quot;,random&amp;more,&abcd;352,&lt;this will be escaped &#x1f601;&gt;,An &#128512;awesome &#128515;string with a few &#x1f609;emojis!".toCharArray();
+        String desc = SharedStrings.escape(chars, 0, chars.length);
+        assert desc.equals("<tag>,random&more,with\"signs\",random&more,&abcd;352,<this will be escaped \uD83D\uDE01>,An 😀awesome 😃string with a few 😉emojis!");
+    }
+
     private void checkTrue(SharedStrings sst, List<String> list) {
         for (int i = 0, size = list.size(); i < size; i++) {
             assertEquals(list.get(i), sst.get(i));