Browse Source

Fixes for unmanaged FileInputStreams hogging file descriptors.

Misc UI fixes.
Temporary fallback to the sequential engine as it works on remote files and consistently.
Massive cleanup work.
Need to go through code and do a little more cleanup.
Need to inspect why the concurrent engine chokes on remote files and multiple runs. (Nothing helpful in logs as of yet.)
master
Drew Short 10 years ago
parent
commit
fabace8005
  1. 15
      pom.xml
  2. 2
      src/includes/LICENSE
  3. 3
      src/main/java/com/sothr/imagetools/AppCLI.java
  4. 30
      src/main/java/com/sothr/imagetools/AppConfig.java
  5. 2
      src/main/java/com/sothr/imagetools/errors/ImageToolsException.java
  6. 5
      src/main/java/com/sothr/imagetools/util/ResourceLoader.java
  7. 6
      src/main/resources/application.conf
  8. 1
      src/main/resources/ehcache.xml
  9. 15
      src/main/resources/fxml/mainapp/MainApp.fxml
  10. 16
      src/main/scala/com/sothr/imagetools/dao/HibernateUtil.scala
  11. 6
      src/main/scala/com/sothr/imagetools/dao/ImageDAO.scala
  12. 3
      src/main/scala/com/sothr/imagetools/dto/ImageHashDTO.scala
  13. 36
      src/main/scala/com/sothr/imagetools/engine/ConcurrentEngine.scala
  14. 19
      src/main/scala/com/sothr/imagetools/engine/Engine.scala
  15. 13
      src/main/scala/com/sothr/imagetools/engine/SequentialEngine.scala
  16. 4
      src/main/scala/com/sothr/imagetools/hash/DHash.scala
  17. 36
      src/main/scala/com/sothr/imagetools/hash/HashService.scala
  18. 4
      src/main/scala/com/sothr/imagetools/hash/PerceptualHasher.scala
  19. 7
      src/main/scala/com/sothr/imagetools/image/Image.scala
  20. 5
      src/main/scala/com/sothr/imagetools/image/ImageFilter.scala
  21. 26
      src/main/scala/com/sothr/imagetools/image/ImageService.scala
  22. 4
      src/main/scala/com/sothr/imagetools/image/SimilarImages.scala
  23. 3
      src/main/scala/com/sothr/imagetools/ui/component/ImageTile.scala
  24. 54
      src/main/scala/com/sothr/imagetools/ui/component/ImageTileFactory.scala
  25. 101
      src/main/scala/com/sothr/imagetools/ui/controller/AppController.scala
  26. 4
      src/main/scala/com/sothr/imagetools/util/DirectoryFilter.scala
  27. 59
      src/main/scala/com/sothr/imagetools/util/PropertiesService.scala
  28. 10
      src/main/scala/com/sothr/imagetools/util/PropertyEnum.scala
  29. 46
      src/main/scala/com/sothr/imagetools/util/Version.scala
  30. 6
      src/test/resources/application.conf
  31. 4
      src/test/scala/com/sothr/imagetools/BaseTest.scala
  32. 4
      src/test/scala/com/sothr/imagetools/EngineTest.scala
  33. 2
      src/test/scala/com/sothr/imagetools/ScalaAppTest.scala
  34. 18
      src/test/scala/com/sothr/imagetools/hash/HashServiceTest.scala
  35. 5
      src/test/scala/com/sothr/imagetools/image/ImageFilterTest.scala

15
pom.xml

@ -28,12 +28,13 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jdk.version>1.8</jdk.version> <jdk.version>1.8</jdk.version>
<scala.binary.version>2.11</scala.binary.version>
<lib.scala-library.version>2.11.2</lib.scala-library.version>
<lib.junit.version>4.11</lib.junit.version> <lib.junit.version>4.11</lib.junit.version>
<lib.scalatest.version>2.2.1</lib.scalatest.version> <lib.scalatest.version>2.2.1</lib.scalatest.version>
<lib.logback.version>1.1.2</lib.logback.version> <lib.logback.version>1.1.2</lib.logback.version>
<lib.slf4j.version>1.7.7</lib.slf4j.version> <lib.slf4j.version>1.7.7</lib.slf4j.version>
<lib.grizzled-slf4j.version>1.0.2</lib.grizzled-slf4j.version> <lib.grizzled-slf4j.version>1.0.2</lib.grizzled-slf4j.version>
<lib.scala-library.version>2.11.2</lib.scala-library.version>
<lib.akka.version>2.3.5</lib.akka.version> <lib.akka.version>2.3.5</lib.akka.version>
<lib.jta.version>1.1</lib.jta.version> <lib.jta.version>1.1</lib.jta.version>
<lib.ehcache.version>2.8.0</lib.ehcache.version> <lib.ehcache.version>2.8.0</lib.ehcache.version>
@ -46,6 +47,7 @@
<lib.hibernate.version>4.3.0.Final</lib.hibernate.version> <lib.hibernate.version>4.3.0.Final</lib.hibernate.version>
<lib.hibernate.ehcache.version>2.6.6</lib.hibernate.ehcache.version> <lib.hibernate.ehcache.version>2.6.6</lib.hibernate.ehcache.version>
<lib.markdown4j.version>2.2-cj-1.0</lib.markdown4j.version> <lib.markdown4j.version>2.2-cj-1.0</lib.markdown4j.version>
<lib.scala-arm.version>1.4</lib.scala-arm.version>
</properties> </properties>
<dependencies> <dependencies>
@ -57,7 +59,7 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.scalatest</groupId> <groupId>org.scalatest</groupId>
<artifactId>scalatest_2.11</artifactId>
<artifactId>scalatest_${scala.binary.version}</artifactId>
<version>${lib.scalatest.version}</version> <version>${lib.scalatest.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
@ -83,7 +85,7 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.clapper</groupId> <groupId>org.clapper</groupId>
<artifactId>grizzled-slf4j_2.11</artifactId>
<artifactId>grizzled-slf4j_${scala.binary.version}</artifactId>
<version>${lib.grizzled-slf4j.version}</version> <version>${lib.grizzled-slf4j.version}</version>
</dependency> </dependency>
<dependency> <dependency>
@ -133,7 +135,7 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.typesafe.akka</groupId> <groupId>com.typesafe.akka</groupId>
<artifactId>akka-slf4j_2.11</artifactId>
<artifactId>akka-slf4j_${scala.binary.version}</artifactId>
<version>${lib.akka.version}</version> <version>${lib.akka.version}</version>
</dependency> </dependency>
<dependency> <dependency>
@ -166,6 +168,11 @@
<artifactId>markdown4j</artifactId> <artifactId>markdown4j</artifactId>
<version>${lib.markdown4j.version}</version> <version>${lib.markdown4j.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.jsuereth</groupId>
<artifactId>scala-arm_${scala.binary.version}</artifactId>
<version>${lib.scala-arm.version}</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

2
src/includes/LICENSE

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013 Drew Short
Copyright (c) 2014 Drew Short
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in

3
src/main/java/com/sothr/imagetools/AppCLI.java

@ -3,6 +3,9 @@ package com.sothr.imagetools;
import akka.actor.ActorRef; import akka.actor.ActorRef;
import akka.actor.ActorSystem; import akka.actor.ActorSystem;
import akka.actor.Props; import akka.actor.Props;
import com.sothr.imagetools.engine.CLIEngineListener;
import com.sothr.imagetools.engine.ConcurrentEngine;
import com.sothr.imagetools.engine.Engine;
import com.sothr.imagetools.image.SimilarImages; import com.sothr.imagetools.image.SimilarImages;
import org.apache.commons.cli.*; import org.apache.commons.cli.*;
import org.slf4j.Logger; import org.slf4j.Logger;

30
src/main/java/com/sothr/imagetools/AppConfig.java

@ -1,23 +1,19 @@
package com.sothr.imagetools; package com.sothr.imagetools;
import akka.actor.ActorSystem; import akka.actor.ActorSystem;
import com.sothr.imagetools.dao.HibernateUtil;
import com.sothr.imagetools.util.ResourceLoader;
import com.sothr.imagetools.util.PropertiesService;
import com.sothr.imagetools.util.PropertiesEnum;
import javafx.stage.Stage;
import net.sf.ehcache.CacheManager;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter; import ch.qos.logback.core.util.StatusPrinter;
import com.sothr.imagetools.dao.HibernateUtil;
import com.sothr.imagetools.util.PropertiesService;
import com.sothr.imagetools.util.ResourceLoader;
import javafx.stage.Stage;
import net.sf.ehcache.CacheManager;
import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.util.Properties;
public class AppConfig { public class AppConfig {
@ -37,7 +33,7 @@ public class AppConfig {
private static Boolean configuredCache = false; private static Boolean configuredCache = false;
// General Akka Actor System // General Akka Actor System
private static ActorSystem appSystem = ActorSystem.create("ITActorSystem");
private static final ActorSystem appSystem = ActorSystem.create("ITActorSystem");
// The Main App // The Main App
private static Stage primaryStage = null; private static Stage primaryStage = null;
@ -55,7 +51,7 @@ public class AppConfig {
configCache(); configCache();
} }
public static void configLogging(String location) {
private static void configLogging(String location) {
//Logging Config //Logging Config
//remove previous configuration if it exists //remove previous configuration if it exists
Logger rootLogger = (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); Logger rootLogger = (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
@ -91,11 +87,11 @@ public class AppConfig {
} }
String message = fromFile ? "From File" : "From Defaults"; String message = fromFile ? "From File" : "From Defaults";
logger.info(String.format("Configured Logger %s", message)); logger.info(String.format("Configured Logger %s", message));
logger.info("Detected Version: %s of Image Tools".format(PropertiesService.getVersion().toString()));
logger.info(String.format("Detected Version: %s of Image Tools", PropertiesService.getVersion().toString()));
} }
//Only configure logging from the default file once //Only configure logging from the default file once
public static void configLogging() {
private static void configLogging() {
if (!configuredLogging) { if (!configuredLogging) {
configLogging(LOGSETTINGSFILE); configLogging(LOGSETTINGSFILE);
configuredLogging = true; configuredLogging = true;
@ -103,7 +99,7 @@ public class AppConfig {
} }
} }
public static void loadProperties() {
private static void loadProperties() {
if (!loadedProperties) { if (!loadedProperties) {
File file = new File(USERPROPERTIESFILE); File file = new File(USERPROPERTIESFILE);
if (file.exists()) { if (file.exists()) {
@ -116,7 +112,7 @@ public class AppConfig {
} }
} }
public static void configCache() {
private static void configCache() {
if (!configuredCache) { if (!configuredCache) {
cacheManager = CacheManager.newInstance(); cacheManager = CacheManager.newInstance();
configuredCache = true; configuredCache = true;
@ -129,7 +125,7 @@ public class AppConfig {
HibernateUtil.getSessionFactory().close(); HibernateUtil.getSessionFactory().close();
} }
public static void saveProperties() {
private static void saveProperties() {
PropertiesService.saveConf(USERPROPERTIESFILE); PropertiesService.saveConf(USERPROPERTIESFILE);
logger.debug("Saved properties"); logger.debug("Saved properties");
} }

2
src/main/java/com/sothr/imagetools/errors/ImageToolsException.java

@ -1,6 +1,8 @@
package com.sothr.imagetools.errors; package com.sothr.imagetools.errors;
/** /**
* Simple Exception
*
* Created by drew on 12/31/13. * Created by drew on 12/31/13.
*/ */
public class ImageToolsException extends Exception { public class ImageToolsException extends Exception {

5
src/main/java/com/sothr/imagetools/util/ResourceLoader.java

@ -2,17 +2,20 @@ package com.sothr.imagetools.util;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
/** /**
* Seamlessly handle resource loading
*
* Created by drew on 1/5/14. * Created by drew on 1/5/14.
*/ */
public class ResourceLoader { public class ResourceLoader {
private static final ResourceLoader instance = new ResourceLoader(); private static final ResourceLoader instance = new ResourceLoader();
private Logger logger = LoggerFactory.getLogger(this.getClass());
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private ResourceLoader() { private ResourceLoader() {
logger.info("Created Resource Loader"); logger.info("Created Resource Loader");

6
src/main/resources/application.conf

@ -29,20 +29,20 @@ app {
use = true use = true
weight = 0.70 weight = 0.70
precision = 8 precision = 8
tolerence = 8
tolerance = 8
} }
dhash { dhash {
use = true use = true
weight = 0.85 weight = 0.85
precision = 8 precision = 8
tolerence = 8
tolerance = 8
} }
phash { phash {
//set to false if hashing images is taking too long //set to false if hashing images is taking too long
use = true use = true
weight = 1.0 weight = 1.0
precision = 32 precision = 32
tolerence = 8
tolerance = 8
} }
} }
//Default Thumbnail Settings //Default Thumbnail Settings

1
src/main/resources/ehcache.xml

@ -1,4 +1,5 @@
<ehcache maxBytesLocalHeap="512M" maxBytesLocalDisk="5g" > <ehcache maxBytesLocalHeap="512M" maxBytesLocalDisk="5g" >
<diskStore path=".cache/ehcache"/>
<cache name="images" <cache name="images"
timeToLiveSeconds="100"> timeToLiveSeconds="100">
</cache> </cache>

15
src/main/resources/fxml/mainapp/MainApp.fxml

@ -1,11 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<!--suppress ALL -->
<?import javafx.geometry.*?> <?import javafx.geometry.*?>
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import javafx.scene.text.Font?>
<AnchorPane fx:id="rootPane" minHeight="600.0" minWidth="1024.0" prefHeight="600.0" prefWidth="1024.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sothr.imagetools.ui.controller.AppController"> <AnchorPane fx:id="rootPane" minHeight="600.0" minWidth="1024.0" prefHeight="600.0" prefWidth="1024.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sothr.imagetools.ui.controller.AppController">
<children> <children>
<MenuBar fx:id="rootMenuBar" minWidth="-Infinity" prefHeight="30.0" prefWidth="600.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> <MenuBar fx:id="rootMenuBar" minWidth="-Infinity" prefHeight="30.0" prefWidth="600.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
@ -62,8 +61,12 @@
<bottom> <bottom>
<FlowPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="60.0" prefWidth="220.0" BorderPane.alignment="CENTER"> <FlowPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="60.0" prefWidth="220.0" BorderPane.alignment="CENTER">
<children> <children>
<Button maxWidth="1.7976931348623157E308" minWidth="220.0" mnemonicParsing="false" text="Show All Images" />
<Button maxWidth="200.0" minWidth="220.0" mnemonicParsing="false" text="Show Similar Images" />
<Button maxWidth="1.7976931348623157E308" minWidth="220.0" mnemonicParsing="false" onAction="#showAllImages" text="Show All Images">
<FlowPane.margin>
<Insets bottom="5.0" />
</FlowPane.margin>
</Button>
<Button maxWidth="200.0" minWidth="220.0" mnemonicParsing="false" onAction="#showSimilarImages" text="Show Similar Images" />
</children> </children>
</FlowPane> </FlowPane>
</bottom> </bottom>
@ -110,7 +113,7 @@
</ToolBar> </ToolBar>
<ScrollPane id="ScrollPane" fitToHeight="true" fitToWidth="true" minWidth="600.0" pannable="false" prefViewportHeight="567.0" prefViewportWidth="766.0" vbarPolicy="AS_NEEDED" VBox.vgrow="ALWAYS"> <ScrollPane id="ScrollPane" fitToHeight="true" fitToWidth="true" minWidth="600.0" pannable="false" prefViewportHeight="567.0" prefViewportWidth="766.0" vbarPolicy="AS_NEEDED" VBox.vgrow="ALWAYS">
<content> <content>
<TilePane fx:id="imageTilePane" hgap="5.0" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefColumns="6" prefHeight="-1.0" prefTileHeight="128.0" prefTileWidth="128.0" prefWidth="-1.0" tileAlignment="TOP_LEFT" vgap="5.0" />
<TilePane fx:id="imageTilePane" hgap="5.0" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefColumns="6" prefHeight="-1.0" prefTileHeight="160.0" prefTileWidth="160.0" prefWidth="-1.0" tileAlignment="TOP_LEFT" vgap="5.0" />
</content> </content>
<VBox.margin> <VBox.margin>
<Insets /> <Insets />

16
src/main/scala/com/sothr/imagetools/dao/HibernateUtil.scala

@ -1,25 +1,31 @@
package com.sothr.imagetools.dao package com.sothr.imagetools.dao
import com.sothr.imagetools.util.{PropertiesService, PropertyEnum}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import org.hibernate.SessionFactory import org.hibernate.SessionFactory
import org.hibernate.boot.registry.StandardServiceRegistryBuilder
import org.hibernate.cfg.Configuration import org.hibernate.cfg.Configuration
import com.sothr.imagetools.util.{PropertiesEnum, PropertiesService}
import org.hibernate.service.ServiceRegistry
/** /**
* Utility class to interface with hibernate
*
* Created by drew on 2/8/14. * Created by drew on 2/8/14.
*/ */
object HibernateUtil extends Logging { object HibernateUtil extends Logging {
private val sessionFactory:SessionFactory = buildSessionFactory() private val sessionFactory:SessionFactory = buildSessionFactory()
private var serviceRegistry:ServiceRegistry = null
private def buildSessionFactory():SessionFactory = { private def buildSessionFactory():SessionFactory = {
try { try {
// Create the SessionFactory from hibernate.cfg.xml // Create the SessionFactory from hibernate.cfg.xml
val configuration = new Configuration().configure("hibernate.cfg.xml") val configuration = new Configuration().configure("hibernate.cfg.xml")
//set the database location //set the database location
info(s"Connecting to database at: \'${PropertiesService.get(PropertiesEnum.DatabaseConnectionURL.toString)}\'")
configuration.setProperty("hibernate.connection.url", PropertiesService.get(PropertiesEnum.DatabaseConnectionURL.toString))
return configuration.buildSessionFactory
info(s"Connecting to database at: \'${PropertiesService.get(PropertyEnum.DatabaseConnectionURL.toString)}\'")
configuration.setProperty("hibernate.connection.url", PropertiesService.get(PropertyEnum.DatabaseConnectionURL.toString))
serviceRegistry = new StandardServiceRegistryBuilder().applySettings(configuration.getProperties).build
configuration.buildSessionFactory(serviceRegistry)
} catch { } catch {
case ex:Throwable => case ex:Throwable =>
// Make sure you log the exception, as it might be swallowed // Make sure you log the exception, as it might be swallowed
@ -28,7 +34,7 @@ object HibernateUtil extends Logging {
} }
} }
def getSessionFactory():SessionFactory = {
def getSessionFactory:SessionFactory = {
sessionFactory sessionFactory
} }

6
src/main/scala/com/sothr/imagetools/dao/ImageDAO.scala

@ -1,14 +1,16 @@
package com.sothr.imagetools.dao package com.sothr.imagetools.dao
import org.hibernate.{Session, SessionFactory}
import com.sothr.imagetools.image.Image import com.sothr.imagetools.image.Image
import org.hibernate.{Session, SessionFactory}
/** /**
* Interact with stored images
*
* Created by drew on 2/8/14. * Created by drew on 2/8/14.
*/ */
class ImageDAO { class ImageDAO {
private val sessionFactory:SessionFactory = HibernateUtil.getSessionFactory()
private val sessionFactory:SessionFactory = HibernateUtil.getSessionFactory
def find(path:String):Image = { def find(path:String):Image = {
val session:Session = sessionFactory.getCurrentSession val session:Session = sessionFactory.getCurrentSession

3
src/main/scala/com/sothr/imagetools/dto/ImageHashDTO.scala

@ -1,8 +1,9 @@
package com.sothr.imagetools.dto package com.sothr.imagetools.dto
import grizzled.slf4j.Logging
import javax.persistence._ import javax.persistence._
import grizzled.slf4j.Logging
@Entity @Entity
@Table(name = "ImageHash") @Table(name = "ImageHash")
class ImageHashDTO(var ahash:Long, var dhash:Long, var phash:Long, var md5:String) extends Serializable with Logging { class ImageHashDTO(var ahash:Long, var dhash:Long, var phash:Long, var md5:String) extends Serializable with Logging {

36
src/main/scala/com/sothr/imagetools/ConcurrentEngine.scala → src/main/scala/com/sothr/imagetools/engine/ConcurrentEngine.scala

@ -1,18 +1,18 @@
package com.sothr.imagetools
package com.sothr.imagetools.engine
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit
import akka.actor._ import akka.actor._
import akka.routing.{Broadcast, RoundRobinRouter, SmallestMailboxRouter}
import akka.pattern.ask import akka.pattern.ask
import akka.routing.{Broadcast, RoundRobinRouter, SmallestMailboxRouter}
import akka.util.Timeout import akka.util.Timeout
import java.util.concurrent.TimeUnit
import com.sothr.imagetools.image.{SimilarImages, Image}
import com.sothr.imagetools.hash.HashService import com.sothr.imagetools.hash.HashService
import com.sothr.imagetools.util.{PropertiesEnum, PropertiesService}
import scala.concurrent.Await
import java.lang.Thread
import com.sothr.imagetools.image.{Image, ImageService, SimilarImages}
import com.sothr.imagetools.util._
import scala.collection.mutable import scala.collection.mutable
import akka.routing.Broadcast
import scala.concurrent.Await
class ConcurrentEngine extends Engine with grizzled.slf4j.Logging { class ConcurrentEngine extends Engine with grizzled.slf4j.Logging {
val engineProcessingController = system.actorOf(Props[ConcurrentEngineProcessingController], name = "EngineProcessingController") val engineProcessingController = system.actorOf(Props[ConcurrentEngineProcessingController], name = "EngineProcessingController")
@ -127,7 +127,7 @@ case object EngineActorReactivate
class ConcurrentEngineProcessingController extends Actor with ActorLogging { class ConcurrentEngineProcessingController extends Actor with ActorLogging {
val numOfRouters = { val numOfRouters = {
val max = PropertiesService.get(PropertiesEnum.ConcurrentProcessingLimit.toString).toInt
val max = PropertiesService.get(PropertyEnum.ConcurrentProcessingLimit.toString).toInt
val processors = Runtime.getRuntime.availableProcessors() val processors = Runtime.getRuntime.availableProcessors()
var threads = 0 var threads = 0
if (processors > max) threads = max else if (processors > 1) threads = processors - 1 else threads = 1 if (processors > max) threads = max else if (processors > 1) threads = processors - 1 else threads = 1
@ -161,8 +161,8 @@ class ConcurrentEngineProcessingController extends Actor with ActorLogging {
case command:EngineFileProcessed => fileProcessed(command) case command:EngineFileProcessed => fileProcessed(command)
case EngineNoMoreFiles => requestWrapup() case EngineNoMoreFiles => requestWrapup()
case EngineActorProcessingFinished => actorProcessingFinished() case EngineActorProcessingFinished => actorProcessingFinished()
case EngineIsProcessingFinished => isProcessingFinished()
case EngineGetProcessingResults => getResults()
case EngineIsProcessingFinished => checkIfProcessingIsFinished()
case EngineGetProcessingResults => checkForResults()
case _ => log.info("received unknown message") case _ => log.info("received unknown message")
} }
@ -203,7 +203,7 @@ class ConcurrentEngineProcessingController extends Actor with ActorLogging {
/* /*
* Check if processing is done * Check if processing is done
*/ */
def isProcessingFinished() = {
def checkIfProcessingIsFinished() = {
try { try {
if (processorsFinished >= numOfRouters) sender ! true else sender ! false if (processorsFinished >= numOfRouters) sender ! true else sender ! false
} catch { } catch {
@ -216,7 +216,7 @@ class ConcurrentEngineProcessingController extends Actor with ActorLogging {
/* /*
* Get the results of the processing * Get the results of the processing
*/ */
def getResults() = {
def checkForResults() = {
try { try {
processorsFinished = 0 processorsFinished = 0
toProcess = 0 toProcess = 0
@ -269,7 +269,7 @@ case object EngineActorCompareImagesFinished
class ConcurrentEngineSimilarityController extends Actor with ActorLogging { class ConcurrentEngineSimilarityController extends Actor with ActorLogging {
val numOfRouters = { val numOfRouters = {
val max = PropertiesService.get(PropertiesEnum.ConcurrentSimiliartyLimit.toString).toInt
val max = PropertiesService.get(PropertyEnum.ConcurrentSimilarityLimit.toString).toInt
val processors = Runtime.getRuntime.availableProcessors() val processors = Runtime.getRuntime.availableProcessors()
var threads = 0 var threads = 0
if (processors > max) threads = max else if (processors > 1) threads = processors - 1 else threads = 1 if (processors > max) threads = max else if (processors > 1) threads = processors - 1 else threads = 1
@ -304,8 +304,8 @@ class ConcurrentEngineSimilarityController extends Actor with ActorLogging {
case command:EngineCompareImagesComplete => similarityProcessed(command) case command:EngineCompareImagesComplete => similarityProcessed(command)
case EngineNoMoreComparisons => requestWrapup() case EngineNoMoreComparisons => requestWrapup()
case EngineActorCompareImagesFinished => actorProcessingFinished() case EngineActorCompareImagesFinished => actorProcessingFinished()
case EngineIsSimilarityFinished => isProcessingFinished()
case EngineGetSimilarityResults => getResults()
case EngineIsSimilarityFinished => checkIfProcessingIsFinished()
case EngineGetSimilarityResults => checkForResults()
case _ => log.info("received unknown message") case _ => log.info("received unknown message")
} }
@ -352,7 +352,7 @@ class ConcurrentEngineSimilarityController extends Actor with ActorLogging {
/* /*
* Check if processing is done * Check if processing is done
*/ */
def isProcessingFinished() = {
def checkIfProcessingIsFinished() = {
try { try {
log.debug("Processors Finished {}/{}", processorsFinished, numOfRouters) log.debug("Processors Finished {}/{}", processorsFinished, numOfRouters)
if (processorsFinished >= numOfRouters) sender ! true else sender ! false if (processorsFinished >= numOfRouters) sender ! true else sender ! false
@ -366,7 +366,7 @@ class ConcurrentEngineSimilarityController extends Actor with ActorLogging {
/* /*
* Get the results of the processing * Get the results of the processing
*/ */
def getResults() = {
def checkForResults() = {
try { try {
processorsFinished = 0 processorsFinished = 0
toProcess = 0 toProcess = 0

19
src/main/scala/com/sothr/imagetools/Engine.scala → src/main/scala/com/sothr/imagetools/engine/Engine.scala

@ -1,13 +1,18 @@
package com.sothr.imagetools
package com.sothr.imagetools.engine
import com.sothr.imagetools.image.{SimilarImages, ImageFilter, Image}
import com.sothr.imagetools.util.DirectoryFilter
import scala.collection.mutable
import java.io.File import java.io.File
import akka.actor.{Actor, ActorLogging, ActorRef, ActorSystem}
import com.sothr.imagetools.AppConfig
import com.sothr.imagetools.image.{Image, ImageFilter, SimilarImages}
import com.sothr.imagetools.util.DirectoryFilter
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import akka.actor.{ActorRef, ActorSystem, ActorLogging, Actor}
import scala.collection.mutable
/** /**
* Engine definition
*
* Created by drew on 1/26/14. * Created by drew on 1/26/14.
*/ */
abstract class Engine extends Logging { abstract class Engine extends Logging {
@ -44,12 +49,12 @@ abstract class Engine extends Logging {
/** /**
* Get all images for a directory with hashes * Get all images for a directory with hashes
*/ */
def getImagesForDirectory(directoryPath:String, recursive:Boolean=false, recursiveDepth:Int=500):List[Image];
def getImagesForDirectory(directoryPath:String, recursive:Boolean=false, recursiveDepth:Int=500):List[Image]
/** /**
* Get all similar images for a directory with hashes * Get all similar images for a directory with hashes
*/ */
def getSimilarImagesForDirectory(directoryPath:String, recursive:Boolean=false, recursiveDepth:Int=500):List[SimilarImages];
def getSimilarImagesForDirectory(directoryPath:String, recursive:Boolean=false, recursiveDepth:Int=500):List[SimilarImages]
} }
case class SubmitMessage(message:String) case class SubmitMessage(message:String)

13
src/main/scala/com/sothr/imagetools/SequentialEngine.scala → src/main/scala/com/sothr/imagetools/engine/SequentialEngine.scala

@ -1,12 +1,17 @@
package com.sothr.imagetools
package com.sothr.imagetools.engine
import com.sothr.imagetools.image.{SimilarImages, ImageFilter, Image}
import scala.collection.mutable
import java.io.File import java.io.File
import grizzled.slf4j.Logging
import akka.actor.{ActorRef, Props} import akka.actor.{ActorRef, Props}
import com.sothr.imagetools.image.{Image, ImageService, SimilarImages}
import grizzled.slf4j.Logging
import scala.collection.mutable
/** /**
* Engine that works sequentially
* Very Slow, but consistent. Excellent for testing
*
* Created by drew on 1/26/14. * Created by drew on 1/26/14.
*/ */
class SequentialEngine extends Engine with Logging { class SequentialEngine extends Engine with Logging {

4
src/main/scala/com/sothr/imagetools/hash/DHash.scala

@ -3,7 +3,9 @@ package com.sothr.imagetools.hash
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
/** /**
* Created by dev on 1/22/14.
* DHash algorithm class
*
* Created by Drew on 1/22/14.
*/ */
object DHash extends PerceptualHasher with Logging { object DHash extends PerceptualHasher with Logging {
def getHash(imageData: Array[Array[Int]]): Long = { def getHash(imageData: Array[Array[Int]]): Long = {

36
src/main/scala/com/sothr/imagetools/hash/HashService.scala

@ -1,14 +1,15 @@
package com.sothr.imagetools.hash package com.sothr.imagetools.hash
import grizzled.slf4j.Logging
import com.sothr.imagetools.dto.ImageHashDTO
import com.sothr.imagetools.util.{PropertiesEnum, PropertiesService, Hamming}
import com.sothr.imagetools.ImageService
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.io.{File, FileInputStream}
import javax.imageio.ImageIO import javax.imageio.ImageIO
import java.io.{FileInputStream, File}
import com.sothr.imagetools.dto.ImageHashDTO
import com.sothr.imagetools.image.ImageService
import com.sothr.imagetools.util.{Hamming, PropertiesService}
import grizzled.slf4j.Logging
import org.apache.commons.codec.digest.DigestUtils import org.apache.commons.codec.digest.DigestUtils
import com.sothr.imagetools.image.Image
import resource._
/** /**
* A service that exposes the ability to construct perceptive hashes from an * A service that exposes the ability to construct perceptive hashes from an
@ -33,14 +34,14 @@ object HashService extends Logging {
//Get Image Data //Get Image Data
val grayImage = ImageService.convertToGray(image) val grayImage = ImageService.convertToGray(image)
if (PropertiesService.useAhash == true) {
ahash = getAhash(grayImage, true)
if (PropertiesService.useAhash) {
ahash = getAhash(grayImage, alreadyGray = true)
} }
if (PropertiesService.useDhash == true) {
dhash = getDhash(grayImage, true)
if (PropertiesService.useDhash) {
dhash = getDhash(grayImage, alreadyGray = true)
} }
if (PropertiesService.usePhash == true) {
phash = getPhash(grayImage, true)
if (PropertiesService.usePhash) {
phash = getPhash(grayImage, alreadyGray = true)
} }
val hashes = new ImageHashDTO(ahash, dhash, phash, md5) val hashes = new ImageHashDTO(ahash, dhash, phash, md5)
@ -57,7 +58,7 @@ object HashService extends Logging {
} else { } else {
grayImage = ImageService.convertToGray(image) grayImage = ImageService.convertToGray(image)
} }
val resizedImage = ImageService.resize(grayImage, PropertiesService.aHashPrecision, true)
val resizedImage = ImageService.resize(grayImage, PropertiesService.aHashPrecision, forced = true)
val imageData = ImageService.getImageData(resizedImage) val imageData = ImageService.getImageData(resizedImage)
AHash.getHash(imageData) AHash.getHash(imageData)
} }
@ -70,7 +71,7 @@ object HashService extends Logging {
} else { } else {
grayImage = ImageService.convertToGray(image) grayImage = ImageService.convertToGray(image)
} }
val resizedImage = ImageService.resize(grayImage, PropertiesService.dHashPrecision, true)
val resizedImage = ImageService.resize(grayImage, PropertiesService.dHashPrecision, forced = true)
val imageData = ImageService.getImageData(resizedImage) val imageData = ImageService.getImageData(resizedImage)
DHash.getHash(imageData) DHash.getHash(imageData)
} }
@ -83,13 +84,16 @@ object HashService extends Logging {
} else { } else {
grayImage = ImageService.convertToGray(image) grayImage = ImageService.convertToGray(image)
} }
val resizedImage = ImageService.resize(grayImage, PropertiesService.pHashPrecision, true)
val resizedImage = ImageService.resize(grayImage, PropertiesService.pHashPrecision, forced = true)
val imageData = ImageService.getImageData(resizedImage) val imageData = ImageService.getImageData(resizedImage)
PHash.getHash(imageData) PHash.getHash(imageData)
} }
def getMD5(filePath:String):String = { def getMD5(filePath:String):String = {
DigestUtils.md5Hex(new FileInputStream(filePath))
managed(new FileInputStream(filePath)) acquireAndGet {
input =>
DigestUtils.md5Hex(input)
}
} }
def areAhashSimilar(ahash1:Long, ahash2:Long):Boolean = { def areAhashSimilar(ahash1:Long, ahash2:Long):Boolean = {

4
src/main/scala/com/sothr/imagetools/hash/PerceptualHasher.scala

@ -1,7 +1,9 @@
package com.sothr.imagetools.hash package com.sothr.imagetools.hash
/** /**
* Created by dev on 1/22/14.
* Interface for perceptual hashing
*
* Created by drew on 1/22/14.
*/ */
trait PerceptualHasher { trait PerceptualHasher {

7
src/main/scala/com/sothr/imagetools/image/Image.scala

@ -1,9 +1,10 @@
package com.sothr.imagetools.image package com.sothr.imagetools.image
import javax.persistence._
import com.sothr.imagetools.dto.ImageHashDTO import com.sothr.imagetools.dto.ImageHashDTO
import com.sothr.imagetools.hash.HashService import com.sothr.imagetools.hash.HashService
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import javax.persistence._
@Entity @Entity
@Table(name = "Image") @Table(name = "Image")
@ -36,7 +37,7 @@ class Image(val image:String, val thumbnail:String, val size:(Int, Int), val ima
var imageType:ImageType = ImageType.SingleFrameImage var imageType:ImageType = ImageType.SingleFrameImage
def getName():String = {
def getName:String = {
if(this.imageName.length < 1) { if(this.imageName.length < 1) {
this.imageName = this.getImagePath.split('/').last this.imageName = this.getImagePath.split('/').last
} }
@ -57,7 +58,7 @@ class Image(val image:String, val thumbnail:String, val size:(Int, Int), val ima
}*/ }*/
def cloneImage:Image = { def cloneImage:Image = {
return new Image(imagePath,thumbnailPath,imageSize,hashes.cloneHashes)
new Image(imagePath,thumbnailPath,imageSize,hashes.cloneHashes)
} }
override def toString:String = { override def toString:String = {

5
src/main/scala/com/sothr/imagetools/image/ImageFilter.scala

@ -1,9 +1,14 @@
package com.sothr.imagetools.image package com.sothr.imagetools.image
import java.io.{File, FilenameFilter} import java.io.{File, FilenameFilter}
import scala.collection.immutable.HashSet import scala.collection.immutable.HashSet
/** /**
* Filter for file names
*
* Used to detect image files based on extension
*
* Created by drew on 1/26/14. * Created by drew on 1/26/14.
*/ */
class ImageFilter extends FilenameFilter { class ImageFilter extends FilenameFilter {

26
src/main/scala/com/sothr/imagetools/image/ImageService.scala

@ -1,16 +1,16 @@
package com.sothr.imagetools
package com.sothr.imagetools.image
import java.awt.image.{BufferedImage, ColorConvertOp, DataBufferByte}
import java.io.{File, IOException}
import javax.imageio.ImageIO
import com.sothr.imagetools.AppConfig
import com.sothr.imagetools.dao.ImageDAO
import com.sothr.imagetools.hash.HashService
import com.sothr.imagetools.util.{PropertiesService, PropertyEnum}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import java.awt.image.{DataBufferByte, BufferedImage, ColorConvertOp}
import net.coobird.thumbnailator.Thumbnails import net.coobird.thumbnailator.Thumbnails
import java.io.File
import com.sothr.imagetools.image.Image
import com.sothr.imagetools.hash.HashService
import javax.imageio.ImageIO
import java.io.IOException
import net.sf.ehcache.Element import net.sf.ehcache.Element
import com.sothr.imagetools.util.{PropertiesEnum, PropertiesService}
import com.sothr.imagetools.dao.ImageDAO
object ImageService extends Logging { object ImageService extends Logging {
@ -80,7 +80,7 @@ object ImageService extends Logging {
def calculateThumbPath(md5:String):String = { def calculateThumbPath(md5:String):String = {
//break the path down into 4 char parts //break the path down into 4 char parts
val subPath = md5.substring(0, 3) val subPath = md5.substring(0, 3)
var path:String = s"${PropertiesService.get(PropertiesEnum.ThumbnailDirectory.toString)}${PropertiesService.get(PropertiesEnum.ThumbnailSize.toString)}/$subPath/"
var path:String = s"${PropertiesService.get(PropertyEnum.ThumbnailDirectory.toString)}${PropertiesService.get(PropertyEnum.ThumbnailSize.toString)}/$subPath/"
try { try {
val dir = new File(path) val dir = new File(path)
if (!dir.exists()) dir.mkdirs() if (!dir.exists()) dir.mkdirs()
@ -105,7 +105,7 @@ object ImageService extends Logging {
def getThumbnail(image:BufferedImage, md5:String):String = { def getThumbnail(image:BufferedImage, md5:String):String = {
//create thumbnail //create thumbnail
val thumb = resize(image, PropertiesService.get(PropertiesEnum.ThumbnailSize.toString).toInt, forced=false)
val thumb = resize(image, PropertiesService.get(PropertyEnum.ThumbnailSize.toString).toInt, forced=false)
//calculate path //calculate path
val path = calculateThumbPath(md5) val path = calculateThumbPath(md5)
// save thumbnail to path // save thumbnail to path
@ -129,7 +129,7 @@ object ImageService extends Logging {
/** /**
* Quickly convert an image to grayscale * Quickly convert an image to grayscale
* *
* @param image
* @param image image to convert to greyscale
* @return * @return
*/ */
def convertToGray(image:BufferedImage):BufferedImage = { def convertToGray(image:BufferedImage):BufferedImage = {
@ -162,7 +162,7 @@ object ImageService extends Logging {
/** /**
* Convert a buffered image into a 2d pixel data array * Convert a buffered image into a 2d pixel data array
* *
* @param image
* @param image image to convert without using RGB
* @return * @return
*/ */
private def convertTo2DWithoutUsingGetRGB(image:BufferedImage):Array[Array[Int]] = { private def convertTo2DWithoutUsingGetRGB(image:BufferedImage):Array[Array[Int]] = {

4
src/main/scala/com/sothr/imagetools/image/SimilarImages.scala

@ -3,6 +3,8 @@ package com.sothr.imagetools.image
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
/** /**
* Similar Image payload class
*
* Created by drew on 1/26/14. * Created by drew on 1/26/14.
*/ */
class SimilarImages(val rootImage:Image, val similarImages:List[Image]) extends Logging { class SimilarImages(val rootImage:Image, val similarImages:List[Image]) extends Logging {
@ -28,7 +30,7 @@ class SimilarImages(val rootImage:Image, val similarImages:List[Image]) extends
override def toString:String = { override def toString:String = {
s"""RootImage: ${rootImage.imagePath} s"""RootImage: ${rootImage.imagePath}
Similar Images: Similar Images:
${getPrettySimilarImagesList}""".stripMargin
$getPrettySimilarImagesList""".stripMargin
} }
} }

3
src/main/scala/com/sothr/imagetools/ui/component/ImageTile.scala

@ -1,9 +1,12 @@
package com.sothr.imagetools.ui.component package com.sothr.imagetools.ui.component
import javafx.scene.layout.VBox import javafx.scene.layout.VBox
import com.sothr.imagetools.image.Image import com.sothr.imagetools.image.Image
/** /**
* ImageTile class that is a special VBox
*
* Created by drew on 8/22/14. * Created by drew on 8/22/14.
*/ */
class ImageTile extends VBox{ class ImageTile extends VBox{

54
src/main/scala/com/sothr/imagetools/ui/component/ImageTileFactory.scala

@ -1,29 +1,43 @@
package com.sothr.imagetools.ui.component package com.sothr.imagetools.ui.component
import java.io.FileInputStream
import javafx.event.EventHandler import javafx.event.EventHandler
import javafx.geometry.Pos
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.image.{ImageView, Image}
import javafx.geometry.{Insets, Pos}
import javafx.scene.control.{Label, Tooltip}
import javafx.scene.image.{Image, ImageView}
import javafx.scene.input.MouseEvent import javafx.scene.input.MouseEvent
import javafx.scene.layout.{Background, BackgroundFill, VBox}
import javafx.scene.paint.Color
import grizzled.slf4j.Logging
import resource._
/** /**
* Created by drew on 8/6/14. * Created by drew on 8/6/14.
* *
* Creates pre-generated image tiles that can be rendered to a scene * Creates pre-generated image tiles that can be rendered to a scene
*/ */
object ImageTileFactory {
object ImageTileFactory extends Logging {
def get(image:com.sothr.imagetools.image.Image):ImageTile = { def get(image:com.sothr.imagetools.image.Image):ImageTile = {
val imageTile = new ImageTile() val imageTile = new ImageTile()
imageTile.setImageData(image) imageTile.setImageData(image)
imageTile.setPrefSize(192.0d,192.0d)
//set tile size
imageTile.setPrefSize(160.0d,160.0d)
imageTile.setMinSize(160.0d,160.0d)
imageTile.setMaxSize(160.0d,160.0d)
//set padding
imageTile.setPadding(new Insets(2,2,2,2))
//imageTile.setSpacing(5.0d)
imageTile.setAlignment(Pos.TOP_CENTER) imageTile.setAlignment(Pos.TOP_CENTER)
imageTile.addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler[MouseEvent] { imageTile.addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler[MouseEvent] {
override def handle(event: MouseEvent): Unit = { override def handle(event: MouseEvent): Unit = {
if (event.isSecondaryButtonDown()) {
if (event.isPrimaryButtonDown) {
//double click
if (event.getClickCount == 2) {
} else {
}
} else if (event.isSecondaryButtonDown) {
//right click context menu //right click context menu
} }
} }
@ -31,16 +45,30 @@ object ImageTileFactory {
// Image // Image
val genImageView = new ImageView() val genImageView = new ImageView()
val thumbnail = new Image(image.getThumbnailPath)
genImageView.setImage(thumbnail)
genImageView.setFitWidth(128.0)
debug(s"Getting thumbnail from: ${image.getThumbnailPath}")
managed(new FileInputStream(image.getThumbnailPath)) acquireAndGet {
thumbSource =>
val thumbnail = new Image(thumbSource)
genImageView.setImage(thumbnail)
if (thumbnail.getHeight > thumbnail.getWidth) {
genImageView.setFitHeight(128.0)
} else {
genImageView.setFitWidth(128.0)
}
}
genImageView.setPreserveRatio(true) genImageView.setPreserveRatio(true)
imageTile.getChildren.add(genImageView) imageTile.getChildren.add(genImageView)
//Label //Label
val imageLabel = new Label() val imageLabel = new Label()
imageLabel.setText(image.getName())
imageLabel.setText(s"${image.getHeight}x${image.getWidth}")
imageLabel.setWrapText(true) imageLabel.setWrapText(true)
//Tooltip
val tooltip = new Tooltip()
tooltip.setText(s"${image.getName}")
imageLabel.setTooltip(tooltip)
imageTile.getChildren.add(imageLabel) imageTile.getChildren.add(imageLabel)
imageTile imageTile

101
src/main/scala/com/sothr/imagetools/ui/controller/AppController.scala

@ -1,21 +1,24 @@
package com.sothr.imagetools.ui.controller package com.sothr.imagetools.ui.controller
import javafx.fxml.FXML
import javafx.event.ActionEvent
import javafx.stage.{DirectoryChooser, StageStyle, Stage}
import javafx.scene.{Scene,Group}
import javafx.scene.text.{TextAlignment, Text}
import java.io.{File, IOException} import java.io.{File, IOException}
import java.util
import java.util.Scanner import java.util.Scanner
import com.sothr.imagetools.image.Image
import javafx.event.ActionEvent
import javafx.fxml.FXML
import javafx.scene.text.{Text, TextAlignment}
import javafx.scene.web.WebView
import javafx.scene.{Group, Node, Scene}
import javafx.stage.{DirectoryChooser, Stage, StageStyle}
import com.sothr.imagetools.engine.{Engine, SequentialEngine}
import com.sothr.imagetools.ui.component.ImageTileFactory import com.sothr.imagetools.ui.component.ImageTileFactory
import com.sothr.imagetools.util.ResourceLoader
import com.sothr.imagetools.util.{PropertiesService, ResourceLoader}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import javafx.scene.web.WebView
import org.markdown4j.Markdown4jProcessor import org.markdown4j.Markdown4jProcessor
import javafx.collections.{FXCollections}
/** /**
* Main Application controller
*
* Created by drew on 12/31/13. * Created by drew on 12/31/13.
*/ */
class AppController extends Logging { class AppController extends Logging {
@ -29,19 +32,30 @@ class AppController extends Logging {
// Labels // Labels
@FXML var selectedDirectoryLabel: javafx.scene.control.Label = null @FXML var selectedDirectoryLabel: javafx.scene.control.Label = null
// Engine
val engine:Engine = new SequentialEngine()
// Current State
var currentDirectory:String = "."
@FXML def initialize() = { @FXML def initialize() = {
//test
val testImage = new Image()
testImage.setThumbnailPath("test.jpg")
testImage.setImagePath("test.jpg")
for (i <- 1 to 100) {
imageTilePane.getChildren.add(ImageTileFactory.get(testImage))
}
val list = FXCollections.observableArrayList[String]()
for (i <- 1 to 100) {
list.add(s"test-item ${i}")
if (PropertiesService.has("lastPath")) {
currentDirectory = PropertiesService.get("lastPath", ".")
selectedDirectoryLabel.setText(PropertiesService.get("lastPath", ""))
} }
tagListView.setItems(list)
//test
//val testImage = new Image()
//testImage.setThumbnailPath("test.jpg")
//testImage.setImagePath("test.jpg")
//for (i <- 1 to 100) {
// imageTilePane.getChildren.add(ImageTileFactory.get(testImage))
//}
//val list = FXCollections.observableArrayList[String]()
//for (i <- 1 to 100) {
// list.add(s"test-item ${i}")
//}
//tagListView.setItems(list)
} }
//region MenuItem Actions //region MenuItem Actions
@ -75,7 +89,7 @@ class AppController extends Logging {
} }
@FXML @FXML
def closeAction(event:ActionEvent ) = {
def closeAction(event:ActionEvent) = {
debug("Closing application from the menu bar") debug("Closing application from the menu bar")
val stage:Stage = this.rootMenuBar.getScene.getWindow.asInstanceOf[Stage] val stage:Stage = this.rootMenuBar.getScene.getWindow.asInstanceOf[Stage]
stage.close() stage.close()
@ -85,12 +99,39 @@ class AppController extends Logging {
def browseFolders(event:ActionEvent) = { def browseFolders(event:ActionEvent) = {
val chooser = new DirectoryChooser() val chooser = new DirectoryChooser()
chooser.setTitle("ImageTools Browser") chooser.setTitle("ImageTools Browser")
val defaultDirectory = new File(".")
val defaultDirectory = new File(currentDirectory)
chooser.setInitialDirectory(defaultDirectory) chooser.setInitialDirectory(defaultDirectory)
val window = this.rootPane.getScene.getWindow val window = this.rootPane.getScene.getWindow
val selectedDirectory = chooser.showDialog(window) val selectedDirectory = chooser.showDialog(window)
info(s"Selected Directory: ${selectedDirectory.getAbsolutePath}") info(s"Selected Directory: ${selectedDirectory.getAbsolutePath}")
selectedDirectoryLabel.setText(selectedDirectory.getAbsolutePath) selectedDirectoryLabel.setText(selectedDirectory.getAbsolutePath)
currentDirectory = selectedDirectory.getAbsolutePath
PropertiesService.set("lastPath",selectedDirectory.getAbsolutePath)
}
@FXML
def showAllImages(event:ActionEvent) = {
imageTilePane.getChildren.setAll(new util.ArrayList[Node]())
val images = engine.getImagesForDirectory(currentDirectory)
info(s"Displaying ${images.length} images")
for (image <- images) {
debug(s"Adding image ${image.toString} to app")
imageTilePane.getChildren.add(ImageTileFactory.get(image))
}
}
@FXML
def showSimilarImages(event:ActionEvent) = {
imageTilePane.getChildren.setAll(new util.ArrayList[Node]())
val similarImages = engine.getSimilarImagesForDirectory(currentDirectory)
info(s"Displaying ${similarImages.length} similar images")
for (similarImage <- similarImages) {
debug(s"Adding similar images ${similarImage.rootImage.toString} to app")
imageTilePane.getChildren.add(ImageTileFactory.get(similarImage.rootImage))
similarImage.similarImages.foreach( image => imageTilePane.getChildren.add(ImageTileFactory.get(image)))
}
} }
//endregion //endregion
@ -106,10 +147,10 @@ class AppController extends Logging {
/** /**
* Render HTML content to a utility dialog. No input or output, just raw rendered content through a webkit engine. * Render HTML content to a utility dialog. No input or output, just raw rendered content through a webkit engine.
* *
* @param title
* @param htmlBody
* @param width
* @param height
* @param title Title of the dialog
* @param htmlBody Body to render
* @param width Desired width of the dialog
* @param height Desired height of the dialog
*/ */
def showHTMLUtilityDialog(title:String, htmlBody:String, width:Double = 800.0, height:Double = 600.0) = { def showHTMLUtilityDialog(title:String, htmlBody:String, width:Double = 800.0, height:Double = 600.0) = {
val dialog:Stage = new Stage() val dialog:Stage = new Stage()
@ -156,9 +197,9 @@ class AppController extends Logging {
/** /**
* Show a plain text utility dialog * Show a plain text utility dialog
* *
* @param message
* @param wrapWidth
* @param alignment
* @param message Message to display
* @param wrapWidth When to wrap
* @param alignment How it should be aligned
*/ */
def showUtilityDialog(title:String, def showUtilityDialog(title:String,
message:String, message:String,
@ -188,6 +229,6 @@ class AppController extends Logging {
} }
def print():String = { def print():String = {
return "This method works"
"This method works"
} }
} }

4
src/main/scala/com/sothr/imagetools/util/DirectoryFilter.scala

@ -3,11 +3,13 @@ package com.sothr.imagetools.util
import java.io.{File, FilenameFilter} import java.io.{File, FilenameFilter}
/** /**
* Filter directories
*
* Created by drew on 1/26/14. * Created by drew on 1/26/14.
*/ */
class DirectoryFilter extends FilenameFilter { class DirectoryFilter extends FilenameFilter {
def accept(dir: File, name: String): Boolean = { def accept(dir: File, name: String): Boolean = {
return new File(dir, name).isDirectory();
new File(dir, name).isDirectory
} }
} }

59
src/main/scala/com/sothr/imagetools/util/PropertiesService.scala

@ -1,10 +1,10 @@
package com.sothr.imagetools.util package com.sothr.imagetools.util
import java.io.{File, FileOutputStream, PrintStream}
import java.util.Properties
import com.typesafe.config.{Config, ConfigFactory} import com.typesafe.config.{Config, ConfigFactory}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import java.io.{File, PrintStream, FileOutputStream}
import java.util.Properties
import scala.collection.JavaConversions._
/* /*
* Service for loading and interacting with the properties file * Service for loading and interacting with the properties file
@ -41,48 +41,47 @@ object PropertiesService extends Logging {
*/ */
def loadProperties(defaultLocation:String, userLocation:String = null) = { def loadProperties(defaultLocation:String, userLocation:String = null) = {
info(s"Attempting to load properties from: $defaultLocation") info(s"Attempting to load properties from: $defaultLocation")
defaultConf = ConfigFactory.load(defaultLocation);
defaultConf = ConfigFactory.load(defaultLocation)
if (userLocation != null) { if (userLocation != null) {
userConf = ConfigFactory.parseFile(new File(userLocation));
userConf = ConfigFactory.parseFile(new File(userLocation))
} else { } else {
userConf = ConfigFactory.empty userConf = ConfigFactory.empty
info("No user properties file exists to load from") info("No user properties file exists to load from")
} }
version = new Version(get(PropertiesEnum.Version.toString));
version = new Version(get(PropertyEnum.Version.toString))
info(s"Detected Version: $version") info(s"Detected Version: $version")
//load special properties //load special properties
TimingEnabled = get(PropertiesEnum.Timed.toString).toBoolean
TimingEnabled = get(PropertyEnum.Timed.toString).toBoolean
//ahash //ahash
aHashPrecision = get(PropertiesEnum.AhashPrecision.toString).toInt
aHashTolerance = get(PropertiesEnum.AhashTolerance.toString).toInt
aHashWeight = get(PropertiesEnum.AhashWeight.toString).toFloat
useAhash = get(PropertiesEnum.UseAhash.toString).toBoolean
aHashPrecision = get(PropertyEnum.AhashPrecision.toString).toInt
aHashTolerance = get(PropertyEnum.AhashTolerance.toString).toInt
aHashWeight = get(PropertyEnum.AhashWeight.toString).toFloat
useAhash = get(PropertyEnum.UseAhash.toString).toBoolean
//dhash //dhash
dHashPrecision = get(PropertiesEnum.DhashPrecision.toString).toInt
dHashTolerance = get(PropertiesEnum.DhashTolerance.toString).toInt
dHashWeight = get(PropertiesEnum.DhashWeight.toString).toFloat
useDhash = get(PropertiesEnum.UseDhash.toString).toBoolean
dHashPrecision = get(PropertyEnum.DhashPrecision.toString).toInt
dHashTolerance = get(PropertyEnum.DhashTolerance.toString).toInt
dHashWeight = get(PropertyEnum.DhashWeight.toString).toFloat
useDhash = get(PropertyEnum.UseDhash.toString).toBoolean
//phash //phash
pHashPrecision = get(PropertiesEnum.PhashPrecision.toString).toInt
pHashTolerance = get(PropertiesEnum.PhashTolerance.toString).toInt
pHashWeight = get(PropertiesEnum.PhashWeight.toString).toFloat
usePhash = get(PropertiesEnum.UsePhash.toString).toBoolean
pHashPrecision = get(PropertyEnum.PhashPrecision.toString).toInt
pHashTolerance = get(PropertyEnum.PhashTolerance.toString).toInt
pHashWeight = get(PropertyEnum.PhashWeight.toString).toFloat
usePhash = get(PropertyEnum.UsePhash.toString).toBoolean
info("Loaded Special Properties") info("Loaded Special Properties")
} }
private def cleanAndPrepareNewUserProperties():Properties = { private def cleanAndPrepareNewUserProperties():Properties = {
//insert special keys here //insert special keys here
newUserConf.setProperty(PropertiesEnum.PreviousVersion.toString, version.parsableToString())
newUserConf.setProperty(PropertyEnum.PreviousVersion.toString, version.parsableToString())
//remove special keys here //remove special keys here
newUserConf.remove(PropertiesEnum.Version.toString)
newUserConf.remove(PropertyEnum.Version.toString)
newUserConf newUserConf
} }
private def getCleanedMergedUserConf():Config = {
ConfigFactory.parseProperties(cleanAndPrepareNewUserProperties()) withFallback(userConf)
private def getCleanedMergedUserConf:Config = {
ConfigFactory.parseProperties(cleanAndPrepareNewUserProperties()) withFallback userConf
} }
def saveConf(location:String) = { def saveConf(location:String) = {
@ -95,6 +94,16 @@ object PropertiesService extends Logging {
out.close() out.close()
} }
def has(key:String):Boolean = {
var result = false
if (newUserConf.containsKey(key)
|| userConf.hasPath(key)
|| defaultConf.hasPath(key)) {
result = true
}
result
}
def get(key:String, defaultValue:String=null):String = { def get(key:String, defaultValue:String=null):String = {
var result:String = defaultValue var result:String = defaultValue
//check the latest properties //check the latest properties
@ -109,7 +118,7 @@ object PropertiesService extends Logging {
else if (defaultConf.hasPath(key)) { else if (defaultConf.hasPath(key)) {
result = defaultConf.getString(key) result = defaultConf.getString(key)
} }
return result
result
} }
def set(key:String, value:String) = { def set(key:String, value:String) = {

10
src/main/scala/com/sothr/imagetools/util/PropertiesEnum.scala → src/main/scala/com/sothr/imagetools/util/PropertyEnum.scala

@ -1,13 +1,13 @@
package com.sothr.imagetools.util package com.sothr.imagetools.util
object PropertiesEnum extends Enumeration {
object PropertyEnum extends Enumeration {
type PropertiesEnum = Value type PropertiesEnum = Value
val Version = Value("app.version.current") val Version = Value("app.version.current")
val PreviousVersion = Value("app.version.previous") val PreviousVersion = Value("app.version.previous")
//default app settings //default app settings
val Timed = Value("app.timed") val Timed = Value("app.timed")
//default engine concurrency settings //default engine concurrency settings
val ConcurrentSimiliartyLimit = Value("app.engine.concurrent.similarity.limit")
val ConcurrentSimilarityLimit = Value("app.engine.concurrent.similarity.limit")
val ConcurrentProcessingLimit = Value("app.engine.concurrent.processing.limit") val ConcurrentProcessingLimit = Value("app.engine.concurrent.processing.limit")
//default image settings //default image settings
val ImageDifferenceThreshold = Value("app.image.differenceThreshold") val ImageDifferenceThreshold = Value("app.image.differenceThreshold")
@ -15,15 +15,15 @@ object PropertiesEnum extends Enumeration {
val UseAhash = Value("app.image.ahash.use") val UseAhash = Value("app.image.ahash.use")
val AhashWeight = Value("app.image.ahash.weight") val AhashWeight = Value("app.image.ahash.weight")
val AhashPrecision = Value("app.image.ahash.precision") val AhashPrecision = Value("app.image.ahash.precision")
val AhashTolerance = Value("app.image.ahash.tolerence")
val AhashTolerance = Value("app.image.ahash.tolerance")
val UseDhash = Value("app.image.dhash.use") val UseDhash = Value("app.image.dhash.use")
val DhashWeight = Value("app.image.dhash.weight") val DhashWeight = Value("app.image.dhash.weight")
val DhashPrecision = Value("app.image.dhash.precision") val DhashPrecision = Value("app.image.dhash.precision")
val DhashTolerance = Value("app.image.dhash.tolerence")
val DhashTolerance = Value("app.image.dhash.tolerance")
val UsePhash = Value("app.image.phash.use") val UsePhash = Value("app.image.phash.use")
val PhashWeight = Value("app.image.phash.weight") val PhashWeight = Value("app.image.phash.weight")
val PhashPrecision = Value("app.image.phash.precision") val PhashPrecision = Value("app.image.phash.precision")
val PhashTolerance = Value("app.image.phash.tolerence")
val PhashTolerance = Value("app.image.phash.tolerance")
//Default Thumbnail Settings //Default Thumbnail Settings
val ThumbnailDirectory = Value("app.thumbnail.directory") val ThumbnailDirectory = Value("app.thumbnail.directory")
val ThumbnailSize = Value("app.thumbnail.size") val ThumbnailSize = Value("app.thumbnail.size")

46
src/main/scala/com/sothr/imagetools/util/Version.scala

@ -1,16 +1,17 @@
package com.sothr.imagetools.util package com.sothr.imagetools.util
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import java.lang.NumberFormatException
/** /**
* Class to handle version detection and evaluation
*
* Created by drew on 1/6/14. * Created by drew on 1/6/14.
*/ */
class Version(val versionString:String) extends Logging{ class Version(val versionString:String) extends Logging{
//parse version into parts //parse version into parts
//typical version string i.e. 0.1.0-DEV-27-060aec7 //typical version string i.e. 0.1.0-DEV-27-060aec7
val (major,minor,patch,buildTag,buildNumber,buildHash) = { val (major,minor,patch,buildTag,buildNumber,buildHash) = {
var version:Tuple6[Int,Int,Int,String,Int,String] = (0,0,0,"DEV",0,"asdfzxcv")
var version:(Int, Int, Int, String, Int, String) = (0,0,0,"DEV",0,"asdfzxcv")
try { try {
val splitVersion = versionString.split("""\.""") val splitVersion = versionString.split("""\.""")
val splitType = splitVersion(splitVersion.length-1).split("""-""") val splitType = splitVersion(splitVersion.length-1).split("""-""")
@ -33,30 +34,39 @@ class Version(val versionString:String) extends Logging{
* 4 = this.buildTag != that.buildTag * 4 = this.buildTag != that.buildTag
*/ */
def compare(that:Version):Integer = { def compare(that:Version):Integer = {
if (this.hashCode == that.hashCode) return 0
if (this.major > that.major) {
return 1
//Identical Versions
if (this.hashCode == that.hashCode) {
0
// This is at least a major version ahead
} else if (this.major > that.major) {
1
// This is at least a major version behind
} else if (this.major < that.major){ } else if (this.major < that.major){
return -1
//major is the same
-1
// major is the same
} else { } else {
// This is at least a minor version ahead
if (this.minor > that.minor) { if (this.minor > that.minor) {
return 2
2
// This is at least a minor version behind
} else if (this.minor < that.minor) { } else if (this.minor < that.minor) {
return -2
//major.minor are the same
-2
// major.minor are the same
} else { } else {
// This is at least a patch version ahead
if (this.patch > that.patch) { if (this.patch > that.patch) {
return 3
3
// This is at least a patch version version
} else if (this.patch < that.patch) { } else if (this.patch < that.patch) {
return -3
-3
//major.minor.patch are all the same //major.minor.patch are all the same
} else { } else {
// This is a different build
if (this.buildTag != that.buildTag) { if (this.buildTag != that.buildTag) {
return 4
4
} }
//should be caught by the first if, but incase not
return 0
//should be caught by the first if, but in case not
0
} }
} }
} }
@ -66,17 +76,17 @@ class Version(val versionString:String) extends Logging{
s"$major.$minor.$patch-$buildTag-$buildNumber-$buildHash" s"$major.$minor.$patch-$buildTag-$buildNumber-$buildHash"
} }
override def toString():String = {
override def toString:String = {
s"$major.$minor.$patch-$buildTag build:$buildNumber code:$buildHash" s"$major.$minor.$patch-$buildTag build:$buildNumber code:$buildHash"
} }
override def hashCode(): Int = { override def hashCode(): Int = {
val prime:Int = 37;
val prime:Int = 37
val result:Int = 255 val result:Int = 255
var hash:Int = major var hash:Int = major
hash += minor hash += minor
hash += patch hash += patch
hash += buildTag.hashCode hash += buildTag.hashCode
return prime * result + hash
prime * result + hash
} }
} }

6
src/test/resources/application.conf

@ -29,20 +29,20 @@ app {
use = true use = true
weight = 0.70 weight = 0.70
precision = 8 precision = 8
tolerence = 8
tolerance = 8
} }
dhash { dhash {
use = true use = true
weight = 0.85 weight = 0.85
precision = 8 precision = 8
tolerence = 8
tolerance = 8
} }
phash { phash {
//set to false if hashing images is taking too long //set to false if hashing images is taking too long
use = true use = true
weight = 1.0 weight = 1.0
precision = 32 precision = 32
tolerence = 8
tolerance = 8
} }
} }
//Default Thumbnail Settings //Default Thumbnail Settings

4
src/test/scala/com/sothr/imagetools/BaseTest.scala

@ -1,8 +1,8 @@
package com.sothr.imagetools package com.sothr.imagetools
import grizzled.slf4j.Logging
import com.sothr.imagetools.util.Timing import com.sothr.imagetools.util.Timing
import org.scalatest.{FunSuite,Matchers,OptionValues,Inside,Inspectors,BeforeAndAfter}
import grizzled.slf4j.Logging
import org.scalatest.{BeforeAndAfter, FunSuite, Inside, Inspectors, Matchers, OptionValues}
abstract class BaseTest extends FunSuite with Matchers with OptionValues with Inside with Inspectors with BeforeAndAfter with Logging with Timing { abstract class BaseTest extends FunSuite with Matchers with OptionValues with Inside with Inspectors with BeforeAndAfter with Logging with Timing {

4
src/test/scala/com/sothr/imagetools/EngineTest.scala

@ -1,6 +1,10 @@
package com.sothr.imagetools package com.sothr.imagetools
import com.sothr.imagetools.engine.{ConcurrentEngine, Engine, SequentialEngine}
/** /**
* Basic Test of the engines
*
* Created by drew on 1/26/14. * Created by drew on 1/26/14.
*/ */
class EngineTest extends BaseTest{ class EngineTest extends BaseTest{

2
src/test/scala/com/sothr/imagetools/ScalaAppTest.scala

@ -3,7 +3,7 @@ package com.sothr.imagetools
class ScalaAppTest extends BaseTest { class ScalaAppTest extends BaseTest {
test("I Do Nothing Just Make Sure The Framework Works") { test("I Do Nothing Just Make Sure The Framework Works") {
assert(true);
assert(true)
} }
} }

18
src/test/scala/com/sothr/imagetools/hash/HashServiceTest.scala

@ -1,13 +1,17 @@
package com.sothr.imagetools.hash package com.sothr.imagetools.hash
import com.sothr.imagetools.{AppConfig, BaseTest, TestParams}
import javax.imageio.ImageIO
import java.io.File import java.io.File
import javax.imageio.ImageIO
import com.sothr.imagetools.dto.ImageHashDTO import com.sothr.imagetools.dto.ImageHashDTO
import net.sf.ehcache.{Cache, Element}
import com.sothr.imagetools.{AppConfig, BaseTest, TestParams}
import net.sf.ehcache.Element
import scala.collection.mutable import scala.collection.mutable
/** /**
* Test the Hash service and make sure it is consistent
*
* Created by dev on 1/23/14. * Created by dev on 1/23/14.
*/ */
class HashServiceTest extends BaseTest { class HashServiceTest extends BaseTest {
@ -60,7 +64,7 @@ class HashServiceTest extends BaseTest {
Array(64,63,62,61,60,59,58,57)) Array(64,63,62,61,60,59,58,57))
val hash = DHash.getHash(testData) val hash = DHash.getHash(testData)
debug(s"Hash of test array: $hash") debug(s"Hash of test array: $hash")
assert(hash == (Long.MaxValue))
assert(hash == Long.MaxValue)
} }
test("Calculate DHash Large Sample Image 1") { test("Calculate DHash Large Sample Image 1") {
@ -389,21 +393,21 @@ class HashServiceTest extends BaseTest {
test("Calculate ImageHash Large Sample Image 1") { test("Calculate ImageHash Large Sample Image 1") {
debug("Starting 'Calculate ImageHash Large Sample Image 1' test") debug("Starting 'Calculate ImageHash Large Sample Image 1' test")
val hash = HashService.getImageHashes(TestParams.LargeSampleImage1) val hash = HashService.getImageHashes(TestParams.LargeSampleImage1)
debug(s"Testing that ${hash.hashCode} = -812844858")
debug(s"Testing that ${hash.hashCode()} = -812844858")
assert(hash.hashCode == -812844858) assert(hash.hashCode == -812844858)
} }
test("Calculate ImageHash Medium Sample Image 1") { test("Calculate ImageHash Medium Sample Image 1") {
debug("Starting 'Calculate ImageHash Medium Sample Image 1' test") debug("Starting 'Calculate ImageHash Medium Sample Image 1' test")
val hash = HashService.getImageHashes(TestParams.MediumSampleImage1) val hash = HashService.getImageHashes(TestParams.MediumSampleImage1)
debug(s"Testing that ${hash.hashCode} = -812836666")
debug(s"Testing that ${hash.hashCode()} = -812836666")
assert(hash.hashCode == -812836666) assert(hash.hashCode == -812836666)
} }
test("Calculate ImageHash Small Sample Image 1") { test("Calculate ImageHash Small Sample Image 1") {
debug("Starting 'Calculate ImageHash Small Sample Image 1' test") debug("Starting 'Calculate ImageHash Small Sample Image 1' test")
val hash = HashService.getImageHashes(TestParams.SmallSampleImage1) val hash = HashService.getImageHashes(TestParams.SmallSampleImage1)
debug(s"Testing that ${hash.hashCode} = -812840762")
debug(s"Testing that ${hash.hashCode()} = -812840762")
assert(hash.hashCode == -812840762) assert(hash.hashCode == -812840762)
} }

5
src/test/scala/com/sothr/imagetools/image/ImageFilterTest.scala

@ -1,9 +1,12 @@
package com.sothr.imagetools.image package com.sothr.imagetools.image
import com.sothr.imagetools.BaseTest
import java.io.File import java.io.File
import com.sothr.imagetools.BaseTest
/** /**
* Test to make sure that the image filters work
*
* Created by drew on 1/26/14. * Created by drew on 1/26/14.
*/ */
class ImageFilterTest extends BaseTest{ class ImageFilterTest extends BaseTest{

Loading…
Cancel
Save