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>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<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.scalatest.version>2.2.1</lib.scalatest.version>
<lib.logback.version>1.1.2</lib.logback.version>
<lib.slf4j.version>1.7.7</lib.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.jta.version>1.1</lib.jta.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.ehcache.version>2.6.6</lib.hibernate.ehcache.version>
<lib.markdown4j.version>2.2-cj-1.0</lib.markdown4j.version>
<lib.scala-arm.version>1.4</lib.scala-arm.version>
</properties>
<dependencies>
@ -57,7 +59,7 @@
</dependency>
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_2.11</artifactId>
<artifactId>scalatest_${scala.binary.version}</artifactId>
<version>${lib.scalatest.version}</version>
<scope>test</scope>
</dependency>
@ -83,7 +85,7 @@
</dependency>
<dependency>
<groupId>org.clapper</groupId>
<artifactId>grizzled-slf4j_2.11</artifactId>
<artifactId>grizzled-slf4j_${scala.binary.version}</artifactId>
<version>${lib.grizzled-slf4j.version}</version>
</dependency>
<dependency>
@ -133,7 +135,7 @@
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-slf4j_2.11</artifactId>
<artifactId>akka-slf4j_${scala.binary.version}</artifactId>
<version>${lib.akka.version}</version>
</dependency>
<dependency>
@ -166,6 +168,11 @@
<artifactId>markdown4j</artifactId>
<version>${lib.markdown4j.version}</version>
</dependency>
<dependency>
<groupId>com.jsuereth</groupId>
<artifactId>scala-arm_${scala.binary.version}</artifactId>
<version>${lib.scala-arm.version}</version>
</dependency>
</dependencies>
<build>

2
src/includes/LICENSE

@ -1,6 +1,6 @@
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
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.ActorSystem;
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 org.apache.commons.cli.*;
import org.slf4j.Logger;

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

@ -1,23 +1,19 @@
package com.sothr.imagetools;
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.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
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.util.Properties;
public class AppConfig {
@ -37,7 +33,7 @@ public class AppConfig {
private static Boolean configuredCache = false;
// General Akka Actor System
private static ActorSystem appSystem = ActorSystem.create("ITActorSystem");
private static final ActorSystem appSystem = ActorSystem.create("ITActorSystem");
// The Main App
private static Stage primaryStage = null;
@ -55,7 +51,7 @@ public class AppConfig {
configCache();
}
public static void configLogging(String location) {
private static void configLogging(String location) {
//Logging Config
//remove previous configuration if it exists
Logger rootLogger = (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
@ -91,11 +87,11 @@ public class AppConfig {
}
String message = fromFile ? "From File" : "From Defaults";
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
public static void configLogging() {
private static void configLogging() {
if (!configuredLogging) {
configLogging(LOGSETTINGSFILE);
configuredLogging = true;
@ -103,7 +99,7 @@ public class AppConfig {
}
}
public static void loadProperties() {
private static void loadProperties() {
if (!loadedProperties) {
File file = new File(USERPROPERTIESFILE);
if (file.exists()) {
@ -116,7 +112,7 @@ public class AppConfig {
}
}
public static void configCache() {
private static void configCache() {
if (!configuredCache) {
cacheManager = CacheManager.newInstance();
configuredCache = true;
@ -129,7 +125,7 @@ public class AppConfig {
HibernateUtil.getSessionFactory().close();
}
public static void saveProperties() {
private static void saveProperties() {
PropertiesService.saveConf(USERPROPERTIESFILE);
logger.debug("Saved properties");
}

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

@ -1,6 +1,8 @@
package com.sothr.imagetools.errors;
/**
* Simple Exception
*
* Created by drew on 12/31/13.
*/
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.LoggerFactory;
import java.io.InputStream;
import java.net.URL;
/**
* Seamlessly handle resource loading
*
* Created by drew on 1/5/14.
*/
public class 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() {
logger.info("Created Resource Loader");

6
src/main/resources/application.conf

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

1
src/main/resources/ehcache.xml

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

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

@ -1,11 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<!--suppress ALL -->
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?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">
<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">
@ -62,8 +61,12 @@
<bottom>
<FlowPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="60.0" prefWidth="220.0" BorderPane.alignment="CENTER">
<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>
</FlowPane>
</bottom>
@ -110,7 +113,7 @@
</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">
<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>
<VBox.margin>
<Insets />

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

@ -1,25 +1,31 @@
package com.sothr.imagetools.dao
import com.sothr.imagetools.util.{PropertiesService, PropertyEnum}
import grizzled.slf4j.Logging
import org.hibernate.SessionFactory
import org.hibernate.boot.registry.StandardServiceRegistryBuilder
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.
*/
object HibernateUtil extends Logging {
private val sessionFactory:SessionFactory = buildSessionFactory()
private var serviceRegistry:ServiceRegistry = null
private def buildSessionFactory():SessionFactory = {
try {
// Create the SessionFactory from hibernate.cfg.xml
val configuration = new Configuration().configure("hibernate.cfg.xml")
//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 {
case ex:Throwable =>
// 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
}

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

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

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

@ -1,8 +1,9 @@
package com.sothr.imagetools.dto
import grizzled.slf4j.Logging
import javax.persistence._
import grizzled.slf4j.Logging
@Entity
@Table(name = "ImageHash")
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.util.concurrent.TimeUnit
import akka.actor._
import akka.routing.{Broadcast, RoundRobinRouter, SmallestMailboxRouter}
import akka.pattern.ask
import akka.routing.{Broadcast, RoundRobinRouter, SmallestMailboxRouter}
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.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 akka.routing.Broadcast
import scala.concurrent.Await
class ConcurrentEngine extends Engine with grizzled.slf4j.Logging {
val engineProcessingController = system.actorOf(Props[ConcurrentEngineProcessingController], name = "EngineProcessingController")
@ -127,7 +127,7 @@ case object EngineActorReactivate
class ConcurrentEngineProcessingController extends Actor with ActorLogging {
val numOfRouters = {
val max = PropertiesService.get(PropertiesEnum.ConcurrentProcessingLimit.toString).toInt
val max = PropertiesService.get(PropertyEnum.ConcurrentProcessingLimit.toString).toInt
val processors = Runtime.getRuntime.availableProcessors()
var threads = 0
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 EngineNoMoreFiles => requestWrapup()
case EngineActorProcessingFinished => actorProcessingFinished()
case EngineIsProcessingFinished => isProcessingFinished()
case EngineGetProcessingResults => getResults()
case EngineIsProcessingFinished => checkIfProcessingIsFinished()
case EngineGetProcessingResults => checkForResults()
case _ => log.info("received unknown message")
}
@ -203,7 +203,7 @@ class ConcurrentEngineProcessingController extends Actor with ActorLogging {
/*
* Check if processing is done
*/
def isProcessingFinished() = {
def checkIfProcessingIsFinished() = {
try {
if (processorsFinished >= numOfRouters) sender ! true else sender ! false
} catch {
@ -216,7 +216,7 @@ class ConcurrentEngineProcessingController extends Actor with ActorLogging {
/*
* Get the results of the processing
*/
def getResults() = {
def checkForResults() = {
try {
processorsFinished = 0
toProcess = 0
@ -269,7 +269,7 @@ case object EngineActorCompareImagesFinished
class ConcurrentEngineSimilarityController extends Actor with ActorLogging {
val numOfRouters = {
val max = PropertiesService.get(PropertiesEnum.ConcurrentSimiliartyLimit.toString).toInt
val max = PropertiesService.get(PropertyEnum.ConcurrentSimilarityLimit.toString).toInt
val processors = Runtime.getRuntime.availableProcessors()
var threads = 0
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 EngineNoMoreComparisons => requestWrapup()
case EngineActorCompareImagesFinished => actorProcessingFinished()
case EngineIsSimilarityFinished => isProcessingFinished()
case EngineGetSimilarityResults => getResults()
case EngineIsSimilarityFinished => checkIfProcessingIsFinished()
case EngineGetSimilarityResults => checkForResults()
case _ => log.info("received unknown message")
}
@ -352,7 +352,7 @@ class ConcurrentEngineSimilarityController extends Actor with ActorLogging {
/*
* Check if processing is done
*/
def isProcessingFinished() = {
def checkIfProcessingIsFinished() = {
try {
log.debug("Processors Finished {}/{}", processorsFinished, numOfRouters)
if (processorsFinished >= numOfRouters) sender ! true else sender ! false
@ -366,7 +366,7 @@ class ConcurrentEngineSimilarityController extends Actor with ActorLogging {
/*
* Get the results of the processing
*/
def getResults() = {
def checkForResults() = {
try {
processorsFinished = 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 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 akka.actor.{ActorRef, ActorSystem, ActorLogging, Actor}
import scala.collection.mutable
/**
* Engine definition
*
* Created by drew on 1/26/14.
*/
abstract class Engine extends Logging {
@ -44,12 +49,12 @@ abstract class Engine extends Logging {
/**
* 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
*/
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)

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 grizzled.slf4j.Logging
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.
*/
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
/**
* Created by dev on 1/22/14.
* DHash algorithm class
*
* Created by Drew on 1/22/14.
*/
object DHash extends PerceptualHasher with Logging {
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
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.io.{File, FileInputStream}
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 com.sothr.imagetools.image.Image
import resource._
/**
* A service that exposes the ability to construct perceptive hashes from an
@ -33,14 +34,14 @@ object HashService extends Logging {
//Get Image Data
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)
@ -57,7 +58,7 @@ object HashService extends Logging {
} else {
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)
AHash.getHash(imageData)
}
@ -70,7 +71,7 @@ object HashService extends Logging {
} else {
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)
DHash.getHash(imageData)
}
@ -83,13 +84,16 @@ object HashService extends Logging {
} else {
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)
PHash.getHash(imageData)
}
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 = {

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

@ -1,7 +1,9 @@
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 {

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

@ -1,9 +1,10 @@
package com.sothr.imagetools.image
import javax.persistence._
import com.sothr.imagetools.dto.ImageHashDTO
import com.sothr.imagetools.hash.HashService
import grizzled.slf4j.Logging
import javax.persistence._
@Entity
@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
def getName():String = {
def getName:String = {
if(this.imageName.length < 1) {
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 = {
return new Image(imagePath,thumbnailPath,imageSize,hashes.cloneHashes)
new Image(imagePath,thumbnailPath,imageSize,hashes.cloneHashes)
}
override def toString:String = {

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

@ -1,9 +1,14 @@
package com.sothr.imagetools.image
import java.io.{File, FilenameFilter}
import scala.collection.immutable.HashSet
/**
* Filter for file names
*
* Used to detect image files based on extension
*
* Created by drew on 1/26/14.
*/
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 java.awt.image.{DataBufferByte, BufferedImage, ColorConvertOp}
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 com.sothr.imagetools.util.{PropertiesEnum, PropertiesService}
import com.sothr.imagetools.dao.ImageDAO
object ImageService extends Logging {
@ -80,7 +80,7 @@ object ImageService extends Logging {
def calculateThumbPath(md5:String):String = {
//break the path down into 4 char parts
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 {
val dir = new File(path)
if (!dir.exists()) dir.mkdirs()
@ -105,7 +105,7 @@ object ImageService extends Logging {
def getThumbnail(image:BufferedImage, md5:String):String = {
//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
val path = calculateThumbPath(md5)
// save thumbnail to path
@ -129,7 +129,7 @@ object ImageService extends Logging {
/**
* Quickly convert an image to grayscale
*
* @param image
* @param image image to convert to greyscale
* @return
*/
def convertToGray(image:BufferedImage):BufferedImage = {
@ -162,7 +162,7 @@ object ImageService extends Logging {
/**
* Convert a buffered image into a 2d pixel data array
*
* @param image
* @param image image to convert without using RGB
* @return
*/
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
/**
* Similar Image payload class
*
* Created by drew on 1/26/14.
*/
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 = {
s"""RootImage: ${rootImage.imagePath}
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
import javafx.scene.layout.VBox
import com.sothr.imagetools.image.Image
/**
* ImageTile class that is a special VBox
*
* Created by drew on 8/22/14.
*/
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
import java.io.FileInputStream
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.layout.{Background, BackgroundFill, VBox}
import javafx.scene.paint.Color
import grizzled.slf4j.Logging
import resource._
/**
* Created by drew on 8/6/14.
*
* 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 = {
val imageTile = new ImageTile()
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.addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler[MouseEvent] {
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
}
}
@ -31,16 +45,30 @@ object ImageTileFactory {
// Image
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)
imageTile.getChildren.add(genImageView)
//Label
val imageLabel = new Label()
imageLabel.setText(image.getName())
imageLabel.setText(s"${image.getHeight}x${image.getWidth}")
imageLabel.setWrapText(true)
//Tooltip
val tooltip = new Tooltip()
tooltip.setText(s"${image.getName}")
imageLabel.setTooltip(tooltip)
imageTile.getChildren.add(imageLabel)
imageTile

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

@ -1,21 +1,24 @@
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.util
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.util.ResourceLoader
import com.sothr.imagetools.util.{PropertiesService, ResourceLoader}
import grizzled.slf4j.Logging
import javafx.scene.web.WebView
import org.markdown4j.Markdown4jProcessor
import javafx.collections.{FXCollections}
/**
* Main Application controller
*
* Created by drew on 12/31/13.
*/
class AppController extends Logging {
@ -29,19 +32,30 @@ class AppController extends Logging {
// Labels
@FXML var selectedDirectoryLabel: javafx.scene.control.Label = null
// Engine
val engine:Engine = new SequentialEngine()
// Current State
var currentDirectory:String = "."
@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
@ -75,7 +89,7 @@ class AppController extends Logging {
}
@FXML
def closeAction(event:ActionEvent ) = {
def closeAction(event:ActionEvent) = {
debug("Closing application from the menu bar")
val stage:Stage = this.rootMenuBar.getScene.getWindow.asInstanceOf[Stage]
stage.close()
@ -85,12 +99,39 @@ class AppController extends Logging {
def browseFolders(event:ActionEvent) = {
val chooser = new DirectoryChooser()
chooser.setTitle("ImageTools Browser")
val defaultDirectory = new File(".")
val defaultDirectory = new File(currentDirectory)
chooser.setInitialDirectory(defaultDirectory)
val window = this.rootPane.getScene.getWindow
val selectedDirectory = chooser.showDialog(window)
info(s"Selected Directory: ${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
@ -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.
*
* @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) = {
val dialog:Stage = new Stage()
@ -156,9 +197,9 @@ class AppController extends Logging {
/**
* 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,
message:String,
@ -188,6 +229,6 @@ class AppController extends Logging {
}
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}
/**
* Filter directories
*
* Created by drew on 1/26/14.
*/
class DirectoryFilter extends FilenameFilter {
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
import java.io.{File, FileOutputStream, PrintStream}
import java.util.Properties
import com.typesafe.config.{Config, ConfigFactory}
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
@ -41,48 +41,47 @@ object PropertiesService extends Logging {
*/
def loadProperties(defaultLocation:String, userLocation:String = null) = {
info(s"Attempting to load properties from: $defaultLocation")
defaultConf = ConfigFactory.load(defaultLocation);
defaultConf = ConfigFactory.load(defaultLocation)
if (userLocation != null) {
userConf = ConfigFactory.parseFile(new File(userLocation));
userConf = ConfigFactory.parseFile(new File(userLocation))
} else {
userConf = ConfigFactory.empty
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")
//load special properties
TimingEnabled = get(PropertiesEnum.Timed.toString).toBoolean
TimingEnabled = get(PropertyEnum.Timed.toString).toBoolean
//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
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
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")
}
private def cleanAndPrepareNewUserProperties():Properties = {
//insert special keys here
newUserConf.setProperty(PropertiesEnum.PreviousVersion.toString, version.parsableToString())
newUserConf.setProperty(PropertyEnum.PreviousVersion.toString, version.parsableToString())
//remove special keys here
newUserConf.remove(PropertiesEnum.Version.toString)
newUserConf.remove(PropertyEnum.Version.toString)
newUserConf
}
private def getCleanedMergedUserConf():Config = {
ConfigFactory.parseProperties(cleanAndPrepareNewUserProperties()) withFallback(userConf)
private def getCleanedMergedUserConf:Config = {
ConfigFactory.parseProperties(cleanAndPrepareNewUserProperties()) withFallback userConf
}
def saveConf(location:String) = {
@ -95,6 +94,16 @@ object PropertiesService extends Logging {
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 = {
var result:String = defaultValue
//check the latest properties
@ -109,7 +118,7 @@ object PropertiesService extends Logging {
else if (defaultConf.hasPath(key)) {
result = defaultConf.getString(key)
}
return result
result
}
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
object PropertiesEnum extends Enumeration {
object PropertyEnum extends Enumeration {
type PropertiesEnum = Value
val Version = Value("app.version.current")
val PreviousVersion = Value("app.version.previous")
//default app settings
val Timed = Value("app.timed")
//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")
//default image settings
val ImageDifferenceThreshold = Value("app.image.differenceThreshold")
@ -15,15 +15,15 @@ object PropertiesEnum extends Enumeration {
val UseAhash = Value("app.image.ahash.use")
val AhashWeight = Value("app.image.ahash.weight")
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 DhashWeight = Value("app.image.dhash.weight")
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 PhashWeight = Value("app.image.phash.weight")
val PhashPrecision = Value("app.image.phash.precision")
val PhashTolerance = Value("app.image.phash.tolerence")
val PhashTolerance = Value("app.image.phash.tolerance")
//Default Thumbnail Settings
val ThumbnailDirectory = Value("app.thumbnail.directory")
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
import grizzled.slf4j.Logging
import java.lang.NumberFormatException
/**
* Class to handle version detection and evaluation
*
* Created by drew on 1/6/14.
*/
class Version(val versionString:String) extends Logging{
//parse version into parts
//typical version string i.e. 0.1.0-DEV-27-060aec7
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 {
val splitVersion = versionString.split("""\.""")
val splitType = splitVersion(splitVersion.length-1).split("""-""")
@ -33,30 +34,39 @@ class Version(val versionString:String) extends Logging{
* 4 = this.buildTag != that.buildTag
*/
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){
return -1
//major is the same
-1
// major is the same
} else {
// This is at least a minor version ahead
if (this.minor > that.minor) {
return 2
2
// This is at least a minor version behind
} else if (this.minor < that.minor) {
return -2
//major.minor are the same
-2
// major.minor are the same
} else {
// This is at least a patch version ahead
if (this.patch > that.patch) {
return 3
3
// This is at least a patch version version
} else if (this.patch < that.patch) {
return -3
-3
//major.minor.patch are all the same
} else {
// This is a different build
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"
}
override def toString():String = {
override def toString:String = {
s"$major.$minor.$patch-$buildTag build:$buildNumber code:$buildHash"
}
override def hashCode(): Int = {
val prime:Int = 37;
val prime:Int = 37
val result:Int = 255
var hash:Int = major
hash += minor
hash += patch
hash += buildTag.hashCode
return prime * result + hash
prime * result + hash
}
}

6
src/test/resources/application.conf

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

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

@ -1,8 +1,8 @@
package com.sothr.imagetools
import grizzled.slf4j.Logging
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 {

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

@ -1,6 +1,10 @@
package com.sothr.imagetools
import com.sothr.imagetools.engine.{ConcurrentEngine, Engine, SequentialEngine}
/**
* Basic Test of the engines
*
* Created by drew on 1/26/14.
*/
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 {
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
import com.sothr.imagetools.{AppConfig, BaseTest, TestParams}
import javax.imageio.ImageIO
import java.io.File
import javax.imageio.ImageIO
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
/**
* Test the Hash service and make sure it is consistent
*
* Created by dev on 1/23/14.
*/
class HashServiceTest extends BaseTest {
@ -60,7 +64,7 @@ class HashServiceTest extends BaseTest {
Array(64,63,62,61,60,59,58,57))
val hash = DHash.getHash(testData)
debug(s"Hash of test array: $hash")
assert(hash == (Long.MaxValue))
assert(hash == Long.MaxValue)
}
test("Calculate DHash Large Sample Image 1") {
@ -389,21 +393,21 @@ class HashServiceTest extends BaseTest {
test("Calculate ImageHash Large Sample Image 1") {
debug("Starting 'Calculate ImageHash Large Sample Image 1' test")
val hash = HashService.getImageHashes(TestParams.LargeSampleImage1)
debug(s"Testing that ${hash.hashCode} = -812844858")
debug(s"Testing that ${hash.hashCode()} = -812844858")
assert(hash.hashCode == -812844858)
}
test("Calculate ImageHash Medium Sample Image 1") {
debug("Starting 'Calculate ImageHash Medium Sample Image 1' test")
val hash = HashService.getImageHashes(TestParams.MediumSampleImage1)
debug(s"Testing that ${hash.hashCode} = -812836666")
debug(s"Testing that ${hash.hashCode()} = -812836666")
assert(hash.hashCode == -812836666)
}
test("Calculate ImageHash Small Sample Image 1") {
debug("Starting 'Calculate ImageHash Small Sample Image 1' test")
val hash = HashService.getImageHashes(TestParams.SmallSampleImage1)
debug(s"Testing that ${hash.hashCode} = -812840762")
debug(s"Testing that ${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
import com.sothr.imagetools.BaseTest
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.
*/
class ImageFilterTest extends BaseTest{

Loading…
Cancel
Save