From 57f02339393c9997391b76ffcb22ae72fd0a45cb Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Tue, 14 Mar 2017 19:49:24 +0100 Subject: [PATCH] Perf improvement: covers - Library: do not load the covers automatically any more - UI: load the cover only the first time, resize it, then cache it --- changelog.md | 4 + src/be/nikiroo/fanfix/Cache.java | 43 ++++++++- src/be/nikiroo/fanfix/Library.java | 94 ++++++++++++++----- .../fanfix/reader/LocalReaderBook.java | 82 ++++++++++++---- src/be/nikiroo/fanfix/supported/Epub.java | 2 +- .../nikiroo/fanfix/supported/InfoReader.java | 17 ++-- src/be/nikiroo/fanfix/supported/InfoText.java | 5 +- 7 files changed, 190 insertions(+), 57 deletions(-) diff --git a/changelog.md b/changelog.md index 598fbcd..458c2e5 100644 --- a/changelog.md +++ b/changelog.md @@ -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) diff --git a/src/be/nikiroo/fanfix/Cache.java b/src/be/nikiroo/fanfix/Cache.java index d97a58a..7f603c1 100644 --- a/src/be/nikiroo/fanfix/Cache.java +++ b/src/be/nikiroo/fanfix/Cache.java @@ -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); } /** diff --git a/src/be/nikiroo/fanfix/Library.java b/src/be/nikiroo/fanfix/Library.java index bc9a4da..7819eff 100644 --- a/src/be/nikiroo/fanfix/Library.java +++ b/src/be/nikiroo/fanfix/Library.java @@ -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; *

* 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}. + *

+ * 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. + *

+ * 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. + *

+ * 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 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 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() { + 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}. * diff --git a/src/be/nikiroo/fanfix/reader/LocalReaderBook.java b/src/be/nikiroo/fanfix/reader/LocalReaderBook.java index 9991dff..0145559 100644 --- a/src/be/nikiroo/fanfix/reader/LocalReaderBook.java +++ b/src/be/nikiroo/fanfix/reader/LocalReaderBook.java @@ -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( "" @@ -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); } diff --git a/src/be/nikiroo/fanfix/supported/Epub.java b/src/be/nikiroo/fanfix/supported/Epub.java index 47da1ac..6cfe4f3 100644 --- a/src/be/nikiroo/fanfix/supported/Epub.java +++ b/src/be/nikiroo/fanfix/supported/Epub.java @@ -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); } diff --git a/src/be/nikiroo/fanfix/supported/InfoReader.java b/src/be/nikiroo/fanfix/supported/InfoReader.java index 6f1a7a5..ede84e3 100644 --- a/src/be/nikiroo/fanfix/supported/InfoReader.java +++ b/src/be/nikiroo/fanfix/supported/InfoReader.java @@ -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())); } diff --git a/src/be/nikiroo/fanfix/supported/InfoText.java b/src/be/nikiroo/fanfix/supported/InfoText.java index 8d4d97d..771d510 100644 --- a/src/be/nikiroo/fanfix/supported/InfoText.java +++ b/src/be/nikiroo/fanfix/supported/InfoText.java @@ -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(); -- 2.27.0