Perf improvement: covers
authorNiki Roo <niki@nikiroo.be>
Tue, 14 Mar 2017 18:49:24 +0000 (19:49 +0100)
committerNiki Roo <niki@nikiroo.be>
Tue, 14 Mar 2017 18:49:24 +0000 (19:49 +0100)
- Library: do not load the covers automatically any more
- UI: load the cover only the first time, resize it, then cache it

changelog.md
src/be/nikiroo/fanfix/Cache.java
src/be/nikiroo/fanfix/Library.java
src/be/nikiroo/fanfix/reader/LocalReaderBook.java
src/be/nikiroo/fanfix/supported/Epub.java
src/be/nikiroo/fanfix/supported/InfoReader.java
src/be/nikiroo/fanfix/supported/InfoText.java

index 598fbcd4dc4407bbe5a18e54f74084f21b8c563c..458c2e536cdc2860beafbbb38d9d9441f9233c77 100644 (file)
@@ -1,5 +1,9 @@
 # Fanfix
 
+## Version wip
+- Library: perf improvement when retrieving the stories (cover not loaded when not needed)
+- UI: perf improvement when displaying books (cover resized then cached)
+
 ## Version 1.4.2
 
 - New Options menu in UI to configure the program (minimalist for now)
index d97a58a66a13a67751ea94f876e9c36c4f1b6a27..7f603c1ba15f1c7e9d812d76941ce616e9d419b3 100644 (file)
@@ -3,6 +3,7 @@ package be.nikiroo.fanfix;
 import java.io.BufferedOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -345,11 +346,32 @@ public class Cache {
         *             in case of I/O error
         */
        public File addToCache(InputStream in, String uniqueID) throws IOException {
-               File file = getCached(new File(uniqueID).toURI().toURL());
+               File file = getCached(uniqueID);
                IOUtils.write(in, file);
                return file;
        }
 
+       /**
+        * Return the {@link InputStream} corresponding to the given unique ID, or
+        * NULL if none found.
+        * 
+        * @param uniqueID
+        *            the unique ID
+        *
+        * @return the content or NULL
+        */
+       public InputStream getFromCache(String uniqueID) {
+               File file = getCached(uniqueID);
+               if (file.exists()) {
+                       try {
+                               return new MarkableFileInputStream(new FileInputStream(file));
+                       } catch (FileNotFoundException e) {
+                       }
+               }
+
+               return null;
+       }
+
        /**
         * Clean the cache (delete the cached items).
         * 
@@ -500,15 +522,28 @@ public class Cache {
         */
        private File getCached(URL url) {
                String name = url.getHost();
-               if (name == null || name.length() == 0) {
+               if (name == null || name.isEmpty()) {
                        name = url.getFile();
                } else {
                        name = url.toString();
                }
 
-               name = name.replace('/', '_').replace(':', '_');
+               return getCached(name);
+       }
+
+       /**
+        * Get the cache resource from the cache if it is present for this unique
+        * ID.
+        * 
+        * @param url
+        *            the url
+        * @return the cached version if present, NULL if not
+        */
+       private File getCached(String uniqueID) {
+               uniqueID = uniqueID.replace('/', '_').replace(':', '_')
+                               .replace("\\", "_");
 
-               return new File(dir, name);
+               return new File(dir, uniqueID);
        }
 
        /**
index bc9a4da4532d5787ac4b29a82dcf7a07cf0edbac..7819eff65d15e665a97b124ac412260e79821927 100644 (file)
@@ -1,5 +1,6 @@
 package be.nikiroo.fanfix;
 
+import java.awt.image.BufferedImage;
 import java.io.File;
 import java.io.FileFilter;
 import java.io.IOException;
@@ -27,6 +28,9 @@ import be.nikiroo.utils.Progress;
  * <p>
  * Each {@link Story} object will be associated with a (local to the library)
  * unique ID, the LUID, which will be used to identify the {@link Story}.
+ * <p>
+ * Most of the {@link Library} functions work on either the LUID or a partial
+ * (cover not included) {@link MetaData} object.
  * 
  * @author niki
  */
@@ -106,6 +110,8 @@ public class Library {
        /**
         * List all the stories of the given author in the {@link Library}, or all
         * the stories if NULL is passed as an author.
+        * <p>
+        * Cover images not included.
         * 
         * @param author
         *            the author of the stories to retrieve, or NULL for all
@@ -128,6 +134,8 @@ public class Library {
        /**
         * List all the stories of the given source type in the {@link Library}, or
         * all the stories if NULL is passed as a type.
+        * <p>
+        * Cover images not included.
         * 
         * @param type
         *            the type of story to retrieve, or NULL for all
@@ -148,7 +156,8 @@ public class Library {
        }
 
        /**
-        * Retrieve a {@link File} corresponding to the given {@link Story}.
+        * Retrieve a {@link File} corresponding to the given {@link Story}, cover
+        * image not included.
         * 
         * @param luid
         *            the Library UID of the story
@@ -187,6 +196,29 @@ public class Library {
                return null;
        }
 
+       /**
+        * Return the cover image associated to this story.
+        * 
+        * @param luid
+        *            the Library UID of the story
+        * 
+        * @return the cover image
+        */
+       public synchronized BufferedImage getCover(String luid) {
+               MetaData meta = getInfo(luid);
+               if (meta != null) {
+                       try {
+                               File infoFile = new File(getFile(meta).getPath() + ".info");
+                               meta = readMeta(infoFile, true).getKey();
+                               return meta.getCover();
+                       } catch (IOException e) {
+                               Instance.syserr(e);
+                       }
+               }
+
+               return null;
+       }
+
        /**
         * Retrieve a specific {@link Story}.
         * 
@@ -477,12 +509,12 @@ public class Library {
                        Progress pgDirs = new Progress(0, 100 * dirs.length);
                        pg.addProgress(pgDirs, 100);
 
-                       final String ext = ".info";
                        for (File dir : dirs) {
                                File[] files = dir.listFiles(new FileFilter() {
                                        public boolean accept(File file) {
                                                return file != null
-                                                               && file.getPath().toLowerCase().endsWith(ext);
+                                                               && file.getPath().toLowerCase()
+                                                                               .endsWith(".info");
                                        }
                                });
 
@@ -491,34 +523,22 @@ public class Library {
                                pgDirs.setName("Loading from: " + dir.getName());
 
                                for (File file : files) {
+                                       pgFiles.setName(file.getName());
                                        try {
-                                               pgFiles.setName(file.getName());
-                                               MetaData meta = InfoReader.readMeta(file);
+                                               Entry<MetaData, File> entry = readMeta(file, false);
                                                try {
-                                                       int id = Integer.parseInt(meta.getLuid());
+                                                       int id = Integer.parseInt(entry.getKey().getLuid());
                                                        if (id > lastId) {
                                                                lastId = id;
                                                        }
 
-                                                       // Replace .info with whatever is needed:
-                                                       String path = file.getPath();
-                                                       path = path.substring(0,
-                                                                       path.length() - ext.length());
-
-                                                       String newExt = getOutputType(meta)
-                                                                       .getDefaultExtension(true);
-
-                                                       file = new File(path + newExt);
-                                                       //
-
-                                                       stories.put(meta, file);
-
+                                                       stories.put(entry.getKey(), entry.getValue());
                                                } catch (Exception e) {
                                                        // not normal!!
-                                                       Instance.syserr(new IOException(
+                                                       throw new IOException(
                                                                        "Cannot understand the LUID of "
                                                                                        + file.getPath() + ": "
-                                                                                       + meta.getLuid(), e));
+                                                                                       + entry.getKey().getLuid(), e);
                                                }
                                        } catch (IOException e) {
                                                // We should not have not-supported files in the
@@ -526,9 +546,8 @@ public class Library {
                                                Instance.syserr(new IOException(
                                                                "Cannot load file from library: "
                                                                                + file.getPath(), e));
-                                       } finally {
-                                               pgFiles.add(1);
                                        }
+                                       pgFiles.add(1);
                                }
 
                                pgFiles.setName(null);
@@ -540,6 +559,35 @@ public class Library {
                return stories;
        }
 
+       private Entry<MetaData, File> readMeta(File infoFile, boolean withCover)
+                       throws IOException {
+
+               final MetaData meta = InfoReader.readMeta(infoFile, withCover);
+
+               // Replace .info with whatever is needed:
+               String path = infoFile.getPath();
+               path = path.substring(0, path.length() - ".info".length());
+
+               String newExt = getOutputType(meta).getDefaultExtension(true);
+
+               File targetFile = new File(path + newExt);
+
+               final File ffile = targetFile;
+               return new Entry<MetaData, File>() {
+                       public File setValue(File value) {
+                               return null;
+                       }
+
+                       public File getValue() {
+                               return ffile;
+                       }
+
+                       public MetaData getKey() {
+                               return meta;
+                       }
+               };
+       }
+
        /**
         * Return the {@link OutputType} for this {@link Story}.
         * 
index 9991dfff902e463c6dbae46e56f4f46eca7631de..0145559cd04d15ed189548cb6ef909e712e25a81 100644 (file)
@@ -9,17 +9,25 @@ import java.awt.Rectangle;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.EventListener;
 import java.util.List;
 
+import javax.imageio.ImageIO;
 import javax.swing.ImageIcon;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 
+import be.nikiroo.fanfix.Instance;
 import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.data.Story;
+import be.nikiroo.utils.IOUtils;
 import be.nikiroo.utils.ui.UIUtils;
 
 /**
@@ -117,8 +125,7 @@ class LocalReaderBook extends JPanel {
                        optSecondary = "(" + optSecondary + ")";
                }
 
-               icon = new JLabel(generateCoverIcon(meta.getCover()));
-
+               icon = new JLabel(generateCoverIcon(meta));
                title = new JLabel(
                                String.format(
                                                "<html>"
@@ -339,29 +346,64 @@ class LocalReaderBook extends JPanel {
        }
 
        /**
-        * Generate a cover icon based upon the given cover image (which may be
-        * NULL).
+        * Generate a cover icon based upon the given {@link MetaData}.
         * 
-        * @param image
-        *            the cover image, or NULL for none
+        * @param meta
+        *            the {@link MetaData} about the target {@link Story}
         * 
         * @return the icon
         */
-       private ImageIcon generateCoverIcon(BufferedImage image) {
-               BufferedImage resizedImage = new BufferedImage(SPINE_WIDTH
-                               + COVER_WIDTH, SPINE_HEIGHT + COVER_HEIGHT + HOFFSET,
-                               BufferedImage.TYPE_4BYTE_ABGR);
-               Graphics2D g = resizedImage.createGraphics();
-               g.setColor(Color.white);
-               g.fillRect(0, HOFFSET, COVER_WIDTH, COVER_HEIGHT);
-               if (image != null) {
-                       g.drawImage(image, 0, HOFFSET, COVER_WIDTH, COVER_HEIGHT, null);
-               } else {
-                       g.setColor(Color.black);
-                       g.drawLine(0, HOFFSET, COVER_WIDTH, HOFFSET + COVER_HEIGHT);
-                       g.drawLine(COVER_WIDTH, HOFFSET, 0, HOFFSET + COVER_HEIGHT);
+       private ImageIcon generateCoverIcon(MetaData meta) {
+               String id = meta.getUuid() + ".thumb_" + SPINE_WIDTH + "x"
+                               + COVER_WIDTH + "+" + SPINE_HEIGHT + "+" + COVER_HEIGHT + "@"
+                               + HOFFSET;
+               BufferedImage resizedImage = null;
+
+               InputStream in = Instance.getCache().getFromCache(id);
+               if (in != null) {
+                       try {
+                               resizedImage = IOUtils.toImage(in);
+                               in.close();
+                               in = null;
+                       } catch (IOException e) {
+                               Instance.syserr(e);
+                       }
+               }
+
+               if (resizedImage == null) {
+                       try {
+                               BufferedImage cover = Instance.getLibrary().getCover(
+                                               meta.getLuid());
+
+                               resizedImage = new BufferedImage(SPINE_WIDTH + COVER_WIDTH,
+                                               SPINE_HEIGHT + COVER_HEIGHT + HOFFSET,
+                                               BufferedImage.TYPE_4BYTE_ABGR);
+                               Graphics2D g = resizedImage.createGraphics();
+                               g.setColor(Color.white);
+                               g.fillRect(0, HOFFSET, COVER_WIDTH, COVER_HEIGHT);
+                               if (cover != null) {
+                                       g.drawImage(cover, 0, HOFFSET, COVER_WIDTH, COVER_HEIGHT,
+                                                       null);
+                               } else {
+                                       g.setColor(Color.black);
+                                       g.drawLine(0, HOFFSET, COVER_WIDTH, HOFFSET + COVER_HEIGHT);
+                                       g.drawLine(COVER_WIDTH, HOFFSET, 0, HOFFSET + COVER_HEIGHT);
+                               }
+                               g.dispose();
+
+                               ByteArrayOutputStream out = new ByteArrayOutputStream();
+                               ImageIO.write(resizedImage, "png", out);
+                               byte[] imageBytes = out.toByteArray();
+                               in = new ByteArrayInputStream(imageBytes);
+                               Instance.getCache().addToCache(in, id);
+                               in.close();
+                               in = null;
+                       } catch (MalformedURLException e) {
+                               Instance.syserr(e);
+                       } catch (IOException e) {
+                               Instance.syserr(e);
+                       }
                }
-               g.dispose();
 
                return new ImageIcon(resizedImage);
        }
index 47da1ac2259abbb313788583ba4420ba1dae8134..6cfe4f3d38de3b26b6b4392e1d55fedfb82a32c2 100644 (file)
@@ -140,7 +140,7 @@ class Epub extends InfoText {
                }
 
                if (tmpInfo.exists()) {
-                       meta = InfoReader.readMeta(tmpInfo);
+                       meta = InfoReader.readMeta(tmpInfo, true);
                        if (cover != null) {
                                meta.setCover(cover);
                        }
index 6f1a7a50002281710613293125731f35ed0f75e0..ede84e39a5c4f945a49f9fbc1c512ab7b84c4ba1 100644 (file)
@@ -14,7 +14,8 @@ import be.nikiroo.utils.MarkableFileInputStream;
 
 // not complete: no "description" tag
 public class InfoReader {
-       public static MetaData readMeta(File infoFile) throws IOException {
+       public static MetaData readMeta(File infoFile, boolean withCover)
+                       throws IOException {
                if (infoFile == null) {
                        throw new IOException("File is null");
                }
@@ -23,7 +24,7 @@ public class InfoReader {
                        InputStream in = new MarkableFileInputStream(new FileInputStream(
                                        infoFile));
                        try {
-                               return createMeta(infoFile.toURI().toURL(), in);
+                               return createMeta(infoFile.toURI().toURL(), in, withCover);
                        } finally {
                                in.close();
                                in = null;
@@ -35,8 +36,8 @@ public class InfoReader {
                }
        }
 
-       private static MetaData createMeta(URL sourceInfoFile, InputStream in)
-                       throws IOException {
+       private static MetaData createMeta(URL sourceInfoFile, InputStream in,
+                       boolean withCover) throws IOException {
                MetaData meta = new MetaData();
 
                meta.setTitle(getInfoTag(in, "TITLE"));
@@ -52,8 +53,10 @@ public class InfoReader {
                meta.setSubject(getInfoTag(in, "SUBJECT"));
                meta.setType(getInfoTag(in, "TYPE"));
                meta.setImageDocument(getInfoTagBoolean(in, "IMAGES_DOCUMENT", false));
-               meta.setCover(BasicSupport.getImage(null, sourceInfoFile,
-                               getInfoTag(in, "COVER")));
+               if (withCover) {
+                       meta.setCover(BasicSupport.getImage(null, sourceInfoFile,
+                                       getInfoTag(in, "COVER")));
+               }
                try {
                        meta.setWords(Long.parseLong(getInfoTag(in, "WORDCOUNT")));
                } catch (NumberFormatException e) {
@@ -62,7 +65,7 @@ public class InfoReader {
                meta.setCreationDate(getInfoTag(in, "CREATION_DATE"));
                meta.setFakeCover(Boolean.parseBoolean(getInfoTag(in, "FAKE_COVER")));
 
-               if (meta.getCover() == null) {
+               if (withCover && meta.getCover() == null) {
                        meta.setCover(BasicSupport.getDefaultCover(meta.getSubject()));
                }
 
index 8d4d97d935dc4ed263c80b20a836c790bcf683bd..771d5102e8a10fb416747eaa18cb2339a75fc321 100644 (file)
@@ -27,8 +27,9 @@ class InfoText extends Text {
        @Override
        protected MetaData getMeta(URL source, InputStream in) throws IOException {
                try {
-                       MetaData meta = InfoReader.readMeta(new File(new File(source
-                                       .toURI()).getPath() + ".info"));
+                       MetaData meta = InfoReader.readMeta(
+                                       new File(new File(source.toURI()).getPath() + ".info"),
+                                       true);
 
                        // Some old .info files don't have those now required fields...
                        String test = meta.getTitle() == null ? "" : meta.getTitle();