Browse Source

Refactor: Code Style Cleanup

* Whitespace fixes (tabs vs spaces for portability)
* EOF whitespace removal
master
Drew Short 7 years ago
parent
commit
031133caaa
  1. 226
      cli/pom.xml
  2. 144
      cli/src/includes/logback.xml
  3. 180
      cli/src/main/java/com/sothr/imagetools/cli/AppCLI.java
  4. 430
      engine/pom.xml
  5. 250
      engine/src/main/java/com/sothr/imagetools/engine/AppConfig.java
  6. 24
      engine/src/main/java/com/sothr/imagetools/engine/errors/ImageToolsException.java
  7. 2
      engine/src/main/java/com/sothr/imagetools/engine/image/ImageType.java
  8. 32
      engine/src/main/java/com/sothr/imagetools/engine/util/ResourceLoader.java
  9. 14
      engine/src/main/resources/ehcache.xml
  10. 80
      engine/src/main/resources/hibernate.cfg.xml
  11. 26
      engine/src/main/resources/hibernate/Image.hbm.xml
  12. 28
      engine/src/main/resources/hibernate/ImageHash.hbm.xml
  13. 40
      engine/src/main/resources/logback-minimum-config.xml
  14. 762
      engine/src/main/scala/com/sothr/imagetools/engine/ConcurrentEngine.scala
  15. 348
      engine/src/main/scala/com/sothr/imagetools/engine/Engine.scala
  16. 148
      engine/src/main/scala/com/sothr/imagetools/engine/SequentialEngine.scala
  17. 52
      engine/src/main/scala/com/sothr/imagetools/engine/dao/HibernateUtil.scala
  18. 69
      engine/src/main/scala/com/sothr/imagetools/engine/dao/ImageDAO.scala
  19. 193
      engine/src/main/scala/com/sothr/imagetools/engine/image/Image.scala
  20. 24
      engine/src/main/scala/com/sothr/imagetools/engine/image/ImageFilter.scala
  21. 254
      engine/src/main/scala/com/sothr/imagetools/engine/image/ImageService.scala
  22. 56
      engine/src/main/scala/com/sothr/imagetools/engine/image/SimilarImages.scala
  23. 14
      engine/src/main/scala/com/sothr/imagetools/engine/util/DirectoryFilter.scala
  24. 24
      engine/src/main/scala/com/sothr/imagetools/engine/util/Hamming.scala
  25. 226
      engine/src/main/scala/com/sothr/imagetools/engine/util/PropertiesService.scala
  26. 56
      engine/src/main/scala/com/sothr/imagetools/engine/util/PropertyEnum.scala
  27. 49
      engine/src/main/scala/com/sothr/imagetools/engine/util/Timing.scala
  28. 162
      engine/src/main/scala/com/sothr/imagetools/engine/util/Version.scala
  29. 74
      engine/src/main/scala/com/sothr/imagetools/engine/vo/ImageHashVO.scala
  30. 40
      engine/src/test/java/com/sothr/imagetools/engine/AppTest.java
  31. 12
      engine/src/test/resources/ehcache.xml
  32. 74
      engine/src/test/resources/hibernate.cfg.xml
  33. 26
      engine/src/test/resources/hibernate/Image.hbm.xml
  34. 28
      engine/src/test/resources/hibernate/ImageHash.hbm.xml
  35. 112
      engine/src/test/resources/logback-minimum-config.xml
  36. 6
      engine/src/test/scala/com/sothr/imagetools/engine/BaseTest.scala
  37. 72
      engine/src/test/scala/com/sothr/imagetools/engine/EngineTest.scala
  38. 6
      engine/src/test/scala/com/sothr/imagetools/engine/ScalaAppTest.scala
  39. 6
      engine/src/test/scala/com/sothr/imagetools/engine/TestParams.scala
  40. 60
      engine/src/test/scala/com/sothr/imagetools/engine/image/ImageFilterTest.scala
  41. 234
      gui/pom.xml
  42. 144
      gui/src/includes/logback.xml
  43. 136
      gui/src/main/java/com/sothr/imagetools/ui/App.java
  44. 395
      gui/src/main/resources/fxml/mainapp/MainApp.fxml
  45. 184
      gui/src/main/scala/com/sothr/imagetools/ui/component/ImageTile.scala
  46. 23
      gui/src/main/scala/com/sothr/imagetools/ui/component/ImageTileFactory.scala
  47. 491
      gui/src/main/scala/com/sothr/imagetools/ui/component/ImageTilePane.scala
  48. 841
      gui/src/main/scala/com/sothr/imagetools/ui/controller/AppController.scala
  49. 32
      gui/src/main/scala/com/sothr/imagetools/ui/util/FileUtil.scala
  50. 4
      hash/pom.xml
  51. 32
      hash/src/main/scala/com/sothr/imagetools/hash/HashService.scala
  52. 892
      parent/pom.xml
  53. 42
      pom.xml

226
cli/pom.xml

@ -1,122 +1,122 @@
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.sothr.imagetools</groupId>
<artifactId>parent</artifactId>
<version>1.0.1</version>
<relativePath>../parent</relativePath>
</parent>
<parent>
<groupId>com.sothr.imagetools</groupId>
<artifactId>parent</artifactId>
<version>1.0.1</version>
<relativePath>../parent</relativePath>
</parent>
<artifactId>cli</artifactId>
<version>0.1.1</version>
<packaging>jar</packaging>
<artifactId>cli</artifactId>
<version>0.1.1</version>
<packaging>jar</packaging>
<name>ImageTools-CLI</name>
<description>The Command Line Interface for Image-Tools</description>
<url>http://imagetools.sothr.com</url>
<organization>
<name>Sothr Software</name>
</organization>
<name>ImageTools-CLI</name>
<description>The Command Line Interface for Image-Tools</description>
<url>http://imagetools.sothr.com</url>
<organization>
<name>Sothr Software</name>
</organization>
<dependencies>
<dependency>
<groupId>com.sothr.imagetools</groupId>
<artifactId>engine</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.clapper</groupId>
<artifactId>grizzled-slf4j_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
</dependency>
</dependencies>
<dependencies>
<dependency>
<groupId>com.sothr.imagetools</groupId>
<artifactId>engine</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.clapper</groupId>
<artifactId>grizzled-slf4j_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Packaging Configuration -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>com.sothr.imagetools.cli.AppCLI</mainClass>
</manifest>
</archive>
<outputDirectory>
${project.build.directory}/release
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<id>prepare</id>
<phase>process-resources</phase>
<configuration>
<tasks>
<copy file="${project.build.directory}/version.info" toFile="${basedir}/version.info"
overwrite="true"/>
<copy file="${project.build.directory}/name.info" toFile="${basedir}/name.info"
overwrite="true"/>
<copy file="${project.build.directory}/LICENSE" toFile="${basedir}/LICENSE"
overwrite="true"/>
<chmod file="${project.build.directory}/startCLI.sh" perm="755"/>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
<execution>
<id>package</id>
<phase>package</phase>
<configuration>
<tasks>
<!-- set permissions on run files -->
<chmod file="${project.build.directory}/release/startCLI.sh" perm="755"/>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<build>
<plugins>
<!-- Packaging Configuration -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>com.sothr.imagetools.cli.AppCLI</mainClass>
</manifest>
</archive>
<outputDirectory>
${project.build.directory}/release
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<id>prepare</id>
<phase>process-resources</phase>
<configuration>
<tasks>
<copy file="${project.build.directory}/version.info" toFile="${basedir}/version.info"
overwrite="true"/>
<copy file="${project.build.directory}/name.info" toFile="${basedir}/name.info"
overwrite="true"/>
<copy file="${project.build.directory}/LICENSE" toFile="${basedir}/LICENSE"
overwrite="true"/>
<chmod file="${project.build.directory}/startCLI.sh" perm="755"/>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
<execution>
<id>package</id>
<phase>package</phase>
<configuration>
<tasks>
<!-- set permissions on run files -->
<chmod file="${project.build.directory}/release/startCLI.sh" perm="755"/>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project> </project>

144
cli/src/includes/logback.xml

@ -1,75 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<configuration> <configuration>
<logger name="org.hibernate" level="WARN"/>
<logger name="net.sf.ehcache" level="WARN"/>
<appender name="C" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- Sorry Windows Users -->
<withJansi>false</withJansi>
<pattern>[%date{HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<appender name="DL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.debug</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.debug.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>5MB</MaxFileSize>
</triggeringPolicy>
</appender>
<appender name="IL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.info</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.info.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>500KB</MaxFileSize>
</triggeringPolicy>
</appender>
<appender name="EL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.err</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%.16thread] [%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.err.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>500KB</MaxFileSize>
</triggeringPolicy>
</appender>
<root level="DEBUG">
<appender-ref ref="C"/>
<appender-ref ref="DL"/>
<appender-ref ref="IL"/>
<appender-ref ref="EL"/>
</root>
<logger name="org.hibernate" level="WARN"/>
<logger name="net.sf.ehcache" level="WARN"/>
<appender name="C" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- Sorry Windows Users -->
<withJansi>false</withJansi>
<pattern>[%date{HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<appender name="DL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.debug</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.debug.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>5MB</MaxFileSize>
</triggeringPolicy>
</appender>
<appender name="IL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.info</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.info.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>500KB</MaxFileSize>
</triggeringPolicy>
</appender>
<appender name="EL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.err</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%.16thread] [%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.err.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>500KB</MaxFileSize>
</triggeringPolicy>
</appender>
<root level="DEBUG">
<appender-ref ref="C"/>
<appender-ref ref="DL"/>
<appender-ref ref="IL"/>
<appender-ref ref="EL"/>
</root>
</configuration> </configuration>

180
cli/src/main/java/com/sothr/imagetools/cli/AppCLI.java

@ -8,7 +8,13 @@ import com.sothr.imagetools.engine.CLIEngineListener;
import com.sothr.imagetools.engine.ConcurrentEngine; import com.sothr.imagetools.engine.ConcurrentEngine;
import com.sothr.imagetools.engine.Engine; import com.sothr.imagetools.engine.Engine;
import com.sothr.imagetools.engine.image.SimilarImages; import com.sothr.imagetools.engine.image.SimilarImages;
import org.apache.commons.cli.*;
import org.apache.commons.cli.BasicParser;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import scala.collection.immutable.List; import scala.collection.immutable.List;
@ -18,101 +24,101 @@ import scala.collection.immutable.List;
*/ */
class AppCLI { class AppCLI {
private static final String HEADER = "Process images and search for duplicates and similar images heuristically";
private static final String FOOTER = "Please report issues to...";
private static Logger logger;
private static final String HEADER = "Process images and search for duplicates and similar images heuristically";
private static final String FOOTER = "Please report issues to...";
private static Logger logger;
public static void main(String[] args) {
try {
Options options = getOptions();
CommandLineParser parser = new BasicParser();
CommandLine cmd = parser.parse(options, args);
if (cmd.hasOption('h') || cmd.getOptions().length < 1 || cmd.getArgs().length > 0) {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("Image-Tools CLI", HEADER, options, FOOTER, true);
} else {
AppConfig.configureApp();
logger = LoggerFactory.getLogger(AppCLI.class);
logger.info("Started Image Tools CLI");
process(cmd);
AppConfig.shutdown();
}
System.exit(0);
} catch (Exception ex) {
logger.error("Unhandled exception in AppCLI", ex);
}
}
public static void main(String[] args) {
try {
Options options = getOptions();
CommandLineParser parser = new BasicParser();
CommandLine cmd = parser.parse(options, args);
if (cmd.hasOption('h') || cmd.getOptions().length < 1 || cmd.getArgs().length > 0) {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("Image-Tools CLI", HEADER, options, FOOTER, true);
} else {
AppConfig.configureApp();
logger = LoggerFactory.getLogger(AppCLI.class);
logger.info("Started Image Tools CLI");
process(cmd);
AppConfig.shutdown();
}
System.exit(0);
} catch (Exception ex) {
logger.error("Unhandled exception in AppCLI", ex);
}
}
private static Options getOptions() {
//scan a list of directories
Options options = new Options();
private static Options getOptions() {
//scan a list of directories
Options options = new Options();
//show help
Option helpOption = OptionBuilder.create('h');
helpOption.setLongOpt("help");
helpOption.setDescription("Display this help dialog");
options.addOption(helpOption);
//show help
Option helpOption = OptionBuilder.create('h');
helpOption.setLongOpt("help");
helpOption.setDescription("Display this help dialog");
options.addOption(helpOption);
//scan directories
Option scanOption = OptionBuilder.create('s');
scanOption.setLongOpt("scan");
scanOption.setDescription("Scan directories for a list of similar images");
scanOption.setArgs(1);
scanOption.setArgName("DIRECTORY");
options.addOption(scanOption);
//scan directories
Option scanOption = OptionBuilder.create('s');
scanOption.setLongOpt("scan");
scanOption.setDescription("Scan directories for a list of similar images");
scanOption.setArgs(1);
scanOption.setArgName("DIRECTORY");
options.addOption(scanOption);
//scan directories in a recursive manner
Option recursiveOption = OptionBuilder.create('r');
recursiveOption.setLongOpt("recursive");
recursiveOption.setDescription("Scan directories recursively");
options.addOption(recursiveOption);
//scan directories in a recursive manner
Option recursiveOption = OptionBuilder.create('r');
recursiveOption.setLongOpt("recursive");
recursiveOption.setDescription("Scan directories recursively");
options.addOption(recursiveOption);
//depth limit
Option depthOption = OptionBuilder.create('d');
depthOption.setLongOpt("depth");
depthOption.setDescription("Limit the maximum depth of the recursive search");
depthOption.setArgs(1);
depthOption.setArgName("INTEGER");
options.addOption(depthOption);
return options;
}
//depth limit
Option depthOption = OptionBuilder.create('d');
depthOption.setLongOpt("depth");
depthOption.setDescription("Limit the maximum depth of the recursive search");
depthOption.setArgs(1);
depthOption.setArgName("INTEGER");
options.addOption(depthOption);
return options;
}
private static void process(CommandLine cmd) {
//scan a comma separated list of paths to search for image similarities
try {
Engine engine = new ConcurrentEngine();
private static void process(CommandLine cmd) {
//scan a comma separated list of paths to search for image similarities
try {
Engine engine = new ConcurrentEngine();
//create the listeners that will be passed onto the actors
ActorSystem system = AppConfig.getAppActorSystem();
Props cliListenerProps = Props.create(CLIEngineListener.class);
ActorRef cliListener = system.actorOf(cliListenerProps);
//create the listeners that will be passed onto the actors
ActorSystem system = AppConfig.getAppActorSystem();
Props cliListenerProps = Props.create(CLIEngineListener.class);
ActorRef cliListener = system.actorOf(cliListenerProps);
//set the listeners
engine.setProcessedListener(cliListener);
engine.setSimilarityListener(cliListener);
//set the listeners
engine.setProcessedListener(cliListener);
engine.setSimilarityListener(cliListener);
if (cmd.hasOption('s')) {
Boolean recursive = false;
Integer recursiveDepth = 500;
if (cmd.hasOption('r')) {
recursive = true;
}
if (cmd.hasOption('d')) {
recursiveDepth = Integer.parseInt(cmd.getOptionValue('d'));
}
String scanList = cmd.getOptionValue('s');
String[] paths = scanList.split(",");
for (String path : paths) {
List<SimilarImages> similarImages = engine.getSimilarImagesForDirectory(path, recursive, recursiveDepth);
for (int index = 0; index < similarImages.length(); index++) {
SimilarImages similar = similarImages.apply(index);
System.out.println(similar.toString());
}
}
}
} catch (Exception ex) {
throw new IllegalArgumentException("One or more arguments could not be parsed correctly", ex);
}
}
if (cmd.hasOption('s')) {
Boolean recursive = false;
Integer recursiveDepth = 500;
if (cmd.hasOption('r')) {
recursive = true;
}
if (cmd.hasOption('d')) {
recursiveDepth = Integer.parseInt(cmd.getOptionValue('d'));
}
String scanList = cmd.getOptionValue('s');
String[] paths = scanList.split(",");
for (String path : paths) {
List<SimilarImages> similarImages = engine.getSimilarImagesForDirectory(path, recursive, recursiveDepth);
for (int index = 0; index < similarImages.length(); index++) {
SimilarImages similar = similarImages.apply(index);
System.out.println(similar.toString());
}
}
}
} catch (Exception ex) {
throw new IllegalArgumentException("One or more arguments could not be parsed correctly", ex);
}
}
} }

430
engine/pom.xml

@ -1,223 +1,223 @@
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.sothr.imagetools</groupId>
<artifactId>parent</artifactId>
<version>1.0.1</version>
<relativePath>../parent</relativePath>
</parent>
<parent>
<groupId>com.sothr.imagetools</groupId>
<artifactId>parent</artifactId>
<version>1.0.1</version>
<relativePath>../parent</relativePath>
</parent>
<artifactId>engine</artifactId>
<version>0.1.3</version>
<packaging>jar</packaging>
<name>ImageTools-Engine</name>
<description>An image collection management utility</description>
<url>http://imagetools.sothr.com</url>
<organization>
<name>Sothr Software</name>
</organization>
<artifactId>engine</artifactId>
<version>0.1.3</version>
<packaging>jar</packaging>
<name>ImageTools-Engine</name>
<description>An image collection management utility</description>
<url>http://imagetools.sothr.com</url>
<organization>
<name>Sothr Software</name>
</organization>
<dependencies>
<dependency>
<groupId>com.sothr.imagetools</groupId>
<artifactId>hash</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_${scala.binary.version}</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.clapper</groupId>
<artifactId>grizzled-slf4j_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
</dependency>
<dependency>
<groupId>com.jsuereth</groupId>
<artifactId>scala-arm_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
</dependency>
<dependency>
<groupId>com.typesafe</groupId>
<artifactId>config</artifactId>
</dependency>
<dependency>
<groupId>net.sourceforge.jtransforms</groupId>
<artifactId>jtransforms</artifactId>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-slf4j_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-c3p0</artifactId>
</dependency>
<dependency>
<groupId>org.commonjava.googlecode.markdown4j</groupId>
<artifactId>markdown4j</artifactId>
</dependency>
<dependency>
<groupId>com.jsuereth</groupId>
<artifactId>scala-arm_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-jpeg</artifactId>
</dependency>
</dependencies>
<dependencies>
<dependency>
<groupId>com.sothr.imagetools</groupId>
<artifactId>hash</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_${scala.binary.version}</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.clapper</groupId>
<artifactId>grizzled-slf4j_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
</dependency>
<dependency>
<groupId>com.jsuereth</groupId>
<artifactId>scala-arm_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
</dependency>
<dependency>
<groupId>com.typesafe</groupId>
<artifactId>config</artifactId>
</dependency>
<dependency>
<groupId>net.sourceforge.jtransforms</groupId>
<artifactId>jtransforms</artifactId>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-slf4j_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-c3p0</artifactId>
</dependency>
<dependency>
<groupId>org.commonjava.googlecode.markdown4j</groupId>
<artifactId>markdown4j</artifactId>
</dependency>
<dependency>
<groupId>com.jsuereth</groupId>
<artifactId>scala-arm_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-jpeg</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- enable surefire for java tests-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.7</version>
<configuration>
<skipTests>false</skipTests>
</configuration>
</plugin>
<!-- enable scalatest for scala tests-->
<plugin>
<groupId>org.scalatest</groupId>
<artifactId>scalatest-maven-plugin</artifactId>
<version>1.0-RC2</version>
<configuration>
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
<junitxml>.</junitxml>
<filereports>WDF TestSuite.txt</filereports>
<argLine>-Xmx128m</argLine>
</configuration>
<executions>
<execution>
<id>test</id>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Packaging Configuration -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
</manifest>
</archive>
<outputDirectory>
${project.build.directory}/release
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- General Ant Tasks
Mostly Moving Files -->
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<id>prepare</id>
<phase>process-resources</phase>
<configuration>
<tasks>
<!-- Copy hibernate configuration files -->
<copy todir="${basedir}/src/test/resources/hibernate">
<fileset dir="${basedir}/src/main/resources/hibernate" includes="**/*"/>
</copy>
<copy file="${project.build.directory}/LICENSE" toFile="${basedir}/LICENSE"
overwrite="true"/>
<copy file="${project.build.directory}/version.info" toFile="${basedir}/../version.info"
overwrite="true"/>
<copy file="${project.build.directory}/README.md" toFile="${basedir}/../README.md"
overwrite="true"/>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<build>
<plugins>
<!-- enable surefire for java tests-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.7</version>
<configuration>
<skipTests>false</skipTests>
</configuration>
</plugin>
<!-- enable scalatest for scala tests-->
<plugin>
<groupId>org.scalatest</groupId>
<artifactId>scalatest-maven-plugin</artifactId>
<version>1.0-RC2</version>
<configuration>
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
<junitxml>.</junitxml>
<filereports>WDF TestSuite.txt</filereports>
<argLine>-Xmx128m</argLine>
</configuration>
<executions>
<execution>
<id>test</id>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Packaging Configuration -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
</manifest>
</archive>
<outputDirectory>
${project.build.directory}/release
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- General Ant Tasks
Mostly Moving Files -->
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<id>prepare</id>
<phase>process-resources</phase>
<configuration>
<tasks>
<!-- Copy hibernate configuration files -->
<copy todir="${basedir}/src/test/resources/hibernate">
<fileset dir="${basedir}/src/main/resources/hibernate" includes="**/*"/>
</copy>
<copy file="${project.build.directory}/LICENSE" toFile="${basedir}/LICENSE"
overwrite="true"/>
<copy file="${project.build.directory}/version.info" toFile="${basedir}/../version.info"
overwrite="true"/>
<copy file="${project.build.directory}/README.md" toFile="${basedir}/../README.md"
overwrite="true"/>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project> </project>

250
engine/src/main/java/com/sothr/imagetools/engine/AppConfig.java

@ -16,130 +16,130 @@ import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
public class AppConfig {
// Logging defaults
private static final String LOGSETTINGSFILE = "./logback.xml";
// Properties defaults
private static final String DEFAULTPROPERTIESFILE = "application.conf";
private static final String USERPROPERTIESFILE = "user.conf";
// General Akka Actor System
private static final ActorSystem appSystem = ActorSystem.create("ITActorSystem");
public static CacheManager cacheManager;
public static FXMLLoader fxmlLoader = null;
private static Logger logger;
private static Boolean configuredLogging = false;
private static Boolean loadedProperties = false;
// Cache defaults
private static Boolean configuredCache = false;
// The Main App
private static Stage primaryStage = null;
public static Stage getPrimaryStage() {
return primaryStage;
}
public static void setPrimaryStage(Stage newPrimaryStage) {
primaryStage = newPrimaryStage;
}
public static FXMLLoader getFxmlLoader() {
return fxmlLoader;
}
public static void setFxmlLoader(FXMLLoader loader) {
fxmlLoader = loader;
}
public static ActorSystem getAppActorSystem() {
return appSystem;
}
public static void configureApp() {
logger = (Logger) LoggerFactory.getLogger(AppConfig.class);
loadProperties();
configLogging();
configCache();
}
private static void configLogging(String location) {
//Logging Config
//remove previous configuration if it exists
Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
LoggerContext context = rootLogger.getLoggerContext();
context.reset();
File file = new File(location);
Boolean fromFile = false;
if (file.exists()) {
fromFile = true;
try {
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
// Call context.reset() to clear any previous configuration, e.g. default
// configuration. For multi-step configuration, omit calling context.reset().
context.reset();
configurator.doConfigure(location);
} catch (JoranException je) {
// StatusPrinter will handle this
}
StatusPrinter.printInCaseOfErrorsOrWarnings(context);
} else {
try {
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
// Call context.reset() to clear any previous configuration, e.g. default
// configuration. For multi-step configuration, omit calling context.reset().
context.reset();
configurator.doConfigure(ResourceLoader.get().getResourceStream("logback-minimum-config.xml"));
} catch (JoranException je) {
// StatusPrinter will handle this
}
StatusPrinter.printInCaseOfErrorsOrWarnings(context);
}
String message = fromFile ? "From File" : "From Defaults";
logger.info(String.format("Configured Logger %s", message));
logger.info(String.format("Detected Version: %s of Image Tools", PropertiesService.getVersion().toString()));
logger.info(String.format("Running on %s, %s, %s", PropertiesService.OS(), PropertiesService.OS_VERSION(), PropertiesService.OS_ARCH()));
}
//Only configure logging from the default file once
private static void configLogging() {
if (!configuredLogging) {
configLogging(LOGSETTINGSFILE);
configuredLogging = true;
logger.info("Configured logging");
}
}
private static void loadProperties() {
if (!loadedProperties) {
File file = new File(USERPROPERTIESFILE);
if (file.exists()) {
PropertiesService.loadProperties(DEFAULTPROPERTIESFILE, USERPROPERTIESFILE);
} else {
PropertiesService.loadProperties(DEFAULTPROPERTIESFILE, null);
}
loadedProperties = true;
logger.info("Loaded Properties");
}
}
private static void configCache() {
if (!configuredCache) {
cacheManager = CacheManager.newInstance();
configuredCache = true;
logger.info("Configured EHCache");
}
}
public static void shutdown() {
saveProperties();
HibernateUtil.getSessionFactory().close();
}
private static void saveProperties() {
PropertiesService.saveConf(USERPROPERTIESFILE);
logger.debug("Saved properties");
}
public class AppConfig {
// Logging defaults
private static final String LOGSETTINGSFILE = "./logback.xml";
// Properties defaults
private static final String DEFAULTPROPERTIESFILE = "application.conf";
private static final String USERPROPERTIESFILE = "user.conf";
// General Akka Actor System
private static final ActorSystem appSystem = ActorSystem.create("ITActorSystem");
public static CacheManager cacheManager;
public static FXMLLoader fxmlLoader = null;
private static Logger logger;
private static Boolean configuredLogging = false;
private static Boolean loadedProperties = false;
// Cache defaults
private static Boolean configuredCache = false;
// The Main App
private static Stage primaryStage = null;
public static Stage getPrimaryStage() {
return primaryStage;
}
public static void setPrimaryStage(Stage newPrimaryStage) {
primaryStage = newPrimaryStage;
}
public static FXMLLoader getFxmlLoader() {
return fxmlLoader;
}
public static void setFxmlLoader(FXMLLoader loader) {
fxmlLoader = loader;
}
public static ActorSystem getAppActorSystem() {
return appSystem;
}
public static void configureApp() {
logger = (Logger) LoggerFactory.getLogger(AppConfig.class);
loadProperties();
configLogging();
configCache();
}
private static void configLogging(String location) {
//Logging Config
//remove previous configuration if it exists
Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
LoggerContext context = rootLogger.getLoggerContext();
context.reset();
File file = new File(location);
Boolean fromFile = false;
if (file.exists()) {
fromFile = true;
try {
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
// Call context.reset() to clear any previous configuration, e.g. default
// configuration. For multi-step configuration, omit calling context.reset().
context.reset();
configurator.doConfigure(location);
} catch (JoranException je) {
// StatusPrinter will handle this
}
StatusPrinter.printInCaseOfErrorsOrWarnings(context);
} else {
try {
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
// Call context.reset() to clear any previous configuration, e.g. default
// configuration. For multi-step configuration, omit calling context.reset().
context.reset();
configurator.doConfigure(ResourceLoader.get().getResourceStream("logback-minimum-config.xml"));
} catch (JoranException je) {
// StatusPrinter will handle this
}
StatusPrinter.printInCaseOfErrorsOrWarnings(context);
}
String message = fromFile ? "From File" : "From Defaults";
logger.info(String.format("Configured Logger %s", message));
logger.info(String.format("Detected Version: %s of Image Tools", PropertiesService.getVersion().toString()));
logger.info(String.format("Running on %s, %s, %s", PropertiesService.OS(), PropertiesService.OS_VERSION(), PropertiesService.OS_ARCH()));
}
//Only configure logging from the default file once
private static void configLogging() {
if (!configuredLogging) {
configLogging(LOGSETTINGSFILE);
configuredLogging = true;
logger.info("Configured logging");
}
}
private static void loadProperties() {
if (!loadedProperties) {
File file = new File(USERPROPERTIESFILE);
if (file.exists()) {
PropertiesService.loadProperties(DEFAULTPROPERTIESFILE, USERPROPERTIESFILE);
} else {
PropertiesService.loadProperties(DEFAULTPROPERTIESFILE, null);
}
loadedProperties = true;
logger.info("Loaded Properties");
}
}
private static void configCache() {
if (!configuredCache) {
cacheManager = CacheManager.newInstance();
configuredCache = true;
logger.info("Configured EHCache");
}
}
public static void shutdown() {
saveProperties();
HibernateUtil.getSessionFactory().close();
}
private static void saveProperties() {
PropertiesService.saveConf(USERPROPERTIESFILE);
logger.debug("Saved properties");
}
} }

24
engine/src/main/java/com/sothr/imagetools/engine/errors/ImageToolsException.java

@ -7,20 +7,20 @@ package com.sothr.imagetools.engine.errors;
*/ */
public class ImageToolsException extends Exception { public class ImageToolsException extends Exception {
public ImageToolsException() {
super();
}
public ImageToolsException() {
super();
}
public ImageToolsException(String message) {
super(message);
}
public ImageToolsException(String message) {
super(message);
}
public ImageToolsException(String message, Throwable cause) {
super(message, cause);
}
public ImageToolsException(String message, Throwable cause) {
super(message, cause);
}
public ImageToolsException(Throwable cause) {
super(cause);
}
public ImageToolsException(Throwable cause) {
super(cause);
}
} }

2
engine/src/main/java/com/sothr/imagetools/engine/image/ImageType.java

@ -1,5 +1,5 @@
package com.sothr.imagetools.engine.image; package com.sothr.imagetools.engine.image;
public enum ImageType { public enum ImageType {
SingleFrameImage, MultiFrameImage
SingleFrameImage, MultiFrameImage
} }

32
engine/src/main/java/com/sothr/imagetools/engine/util/ResourceLoader.java

@ -13,26 +13,26 @@ import java.net.URL;
*/ */
public class ResourceLoader { public class ResourceLoader {
private static final ResourceLoader instance = new ResourceLoader();
private static final ResourceLoader instance = new ResourceLoader();
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private ResourceLoader() {
logger.info("Created Resource Loader");
}
private ResourceLoader() {
logger.info("Created Resource Loader");
}
public static ResourceLoader get() {
return instance;
}
public static ResourceLoader get() {
return instance;
}
public URL getResource(String location) {
logger.debug(String.format("Attempting to load resource: %s", location));
return Thread.currentThread().getContextClassLoader().getResource(location);
}
public URL getResource(String location) {
logger.debug(String.format("Attempting to load resource: %s", location));
return Thread.currentThread().getContextClassLoader().getResource(location);
}
public InputStream getResourceStream(String location) {
logger.debug(String.format("Attempting to get stream for resource: %s", location));
return Thread.currentThread().getContextClassLoader().getResourceAsStream(location);
}
public InputStream getResourceStream(String location) {
logger.debug(String.format("Attempting to get stream for resource: %s", location));
return Thread.currentThread().getContextClassLoader().getResourceAsStream(location);
}
} }

14
engine/src/main/resources/ehcache.xml

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

80
engine/src/main/resources/hibernate.cfg.xml

@ -1,51 +1,51 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC <!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
"-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration> <hibernate-configuration>
<!-- a SessionFactory instance listed as /jndi/name -->
<session-factory>
<!-- a SessionFactory instance listed as /jndi/name -->
<session-factory>
<!-- properties -->
<property name="hibernate.connection.driver_class">org.h2.Driver</property>
<!--<property name="hibernate.connection.connection.url">jdbc:h2:imageTools.db</property>-->
<property name="hibernate.dialect">org.hibernate.dialect.H2Dialect</property>
<property name="hibernate.show_sql">false</property>
<property name="hibernate.generate_statistics"></property>
<property name="hibernate.use_sql_comments"></property>
<!--<property name="transaction.factory_class">
org.hibernate.transaction.JTATransactionFactory
</property>-->
<property name="jta.UserTransaction">java:comp/UserTransaction</property>
<!-- properties -->
<property name="hibernate.connection.driver_class">org.h2.Driver</property>
<!--<property name="hibernate.connection.connection.url">jdbc:h2:imageTools.db</property>-->
<property name="hibernate.dialect">org.hibernate.dialect.H2Dialect</property>
<property name="hibernate.show_sql">false</property>
<property name="hibernate.generate_statistics"></property>
<property name="hibernate.use_sql_comments"></property>
<!--<property name="transaction.factory_class">
org.hibernate.transaction.JTATransactionFactory
</property>-->
<property name="jta.UserTransaction">java:comp/UserTransaction</property>
<property name="hibernate.hbm2ddl.auto">update</property>
<property name="hibernate.hbm2ddl.auto">update</property>
<!-- Enable Hibernate's automatic session context management -->
<property name="hibernate.current_session_context_class">thread</property>
<!-- Enable Hibernate's automatic session context management -->
<property name="hibernate.current_session_context_class">thread</property>
<!-- Enable the second-level cache -->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory
</property>
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory
</property>
<property name="hibernate.cache.use_second_level_cache">true</property>
<!-- Enable the second-level cache -->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory
</property>
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory
</property>
<property name="hibernate.cache.use_second_level_cache">true</property>
<property name="c3p0.acquire_increment">1</property>
<property name="c3p0.idle_test_period">100</property>
<!-- seconds -->
<property name="c3p0.max_size">50</property>
<property name="c3p0.max_statements">0</property>
<property name="c3p0.min_size">5</property>
<property name="c3p0.timeout">100</property>
<!-- seconds -->
<property name="c3p0.acquire_increment">1</property>
<property name="c3p0.idle_test_period">100</property>
<!-- seconds -->
<property name="c3p0.max_size">50</property>
<property name="c3p0.max_statements">0</property>
<property name="c3p0.min_size">5</property>
<property name="c3p0.timeout">100</property>
<!-- seconds -->
<!-- mapping files -->
<mapping resource="hibernate/Image.hbm.xml"/>
<mapping resource="hibernate/ImageHash.hbm.xml"/>
<!-- mapping files -->
<mapping resource="hibernate/Image.hbm.xml"/>
<mapping resource="hibernate/ImageHash.hbm.xml"/>
<!-- cache settings -->
<!--<class-cache class="org.hibernate.auction.Item" usage="read-write"/>
<class-cache class="org.hibernate.auction.Bid" usage="read-only"/>
<collection-cache collection="org.hibernate.auction.Item.bids" usage="read-write"/>-->
</session-factory>
<!-- cache settings -->
<!--<class-cache class="org.hibernate.auction.Item" usage="read-write"/>
<class-cache class="org.hibernate.auction.Bid" usage="read-only"/>
<collection-cache collection="org.hibernate.auction.Item.bids" usage="read-write"/>-->
</session-factory>
</hibernate-configuration> </hibernate-configuration>

26
engine/src/main/resources/hibernate/Image.hbm.xml

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC <!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping> <hibernate-mapping>
<class name="com.sothr.imagetools.engine.image.Image" table="Image">
<meta attribute="class-description">
This class contains the image hashes and meta data
</meta>
<id name="imagePath" type="string" column="path"/>
<property name="thumbnailPath" column="thumbnail_path" type="string" not-null="true"/>
<property name="width" column="width" type="int" not-null="true"/>
<property name="height" column="height" type="int" not-null="true"/>
<many-to-one name="hashes" column="hashes" unique="true" class="com.sothr.imagetools.engine.vo.ImageHashVO"
cascade="save-update, delete" not-null="true" lazy="false"/>
</class>
<class name="com.sothr.imagetools.engine.image.Image" table="Image">
<meta attribute="class-description">
This class contains the image hashes and meta data
</meta>
<id name="imagePath" type="string" column="path"/>
<property name="thumbnailPath" column="thumbnail_path" type="string" not-null="true"/>
<property name="width" column="width" type="int" not-null="true"/>
<property name="height" column="height" type="int" not-null="true"/>
<many-to-one name="hashes" column="hashes" unique="true" class="com.sothr.imagetools.engine.vo.ImageHashVO"
cascade="save-update, delete" not-null="true" lazy="false"/>
</class>
</hibernate-mapping> </hibernate-mapping>

28
engine/src/main/resources/hibernate/ImageHash.hbm.xml

@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC <!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping> <hibernate-mapping>
<class name="com.sothr.imagetools.engine.vo.ImageHashVO" table="ImageHash">
<meta attribute="class-description">
This class contains the image hashes
</meta>
<id name="id" type="int" column="id">
<generator class="native"/>
</id>
<property name="ahash" column="ahash" type="long"/>
<property name="dhash" column="dhash" type="long"/>
<property name="phash" column="phash" type="long"/>
<property name="fileHash" column="fileHash" type="string"/>
</class>
<class name="com.sothr.imagetools.engine.vo.ImageHashVO" table="ImageHash">
<meta attribute="class-description">
This class contains the image hashes
</meta>
<id name="id" type="int" column="id">
<generator class="native"/>
</id>
<property name="ahash" column="ahash" type="long"/>
<property name="dhash" column="dhash" type="long"/>
<property name="phash" column="phash" type="long"/>
<property name="fileHash" column="fileHash" type="string"/>
</class>
</hibernate-mapping> </hibernate-mapping>

40
engine/src/main/resources/logback-minimum-config.xml

@ -1,23 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<configuration> <configuration>
<appender name="EL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>ImageTools.err</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%date{HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>5</maxIndex>
<FileNamePattern>ImageTools.err.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>500KB</MaxFileSize>
</triggeringPolicy>
</appender>
<root level="ERROR">
<appender-ref ref="EL"/>
</root>
<appender name="EL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>ImageTools.err</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%date{HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>5</maxIndex>
<FileNamePattern>ImageTools.err.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>500KB</MaxFileSize>
</triggeringPolicy>
</appender>
<root level="ERROR">
<appender-ref ref="EL"/>
</root>
</configuration> </configuration>

762
engine/src/main/scala/com/sothr/imagetools/engine/ConcurrentEngine.scala

@ -7,101 +7,101 @@ import akka.actor.{Actor, ActorLogging, ActorRef, PoisonPill, Props}
import akka.pattern.ask import akka.pattern.ask
import akka.routing.{Broadcast, RoundRobinPool, SmallestMailboxPool} import akka.routing.{Broadcast, RoundRobinPool, SmallestMailboxPool}
import akka.util.Timeout import akka.util.Timeout
import com.sothr.imagetools.hash.{HashService, ImageHash}
import com.sothr.imagetools.engine.image.{Image, ImageService, SimilarImages} import com.sothr.imagetools.engine.image.{Image, ImageService, SimilarImages}
import com.sothr.imagetools.engine.util.{PropertiesService, PropertyEnum} import com.sothr.imagetools.engine.util.{PropertiesService, PropertyEnum}
import com.sothr.imagetools.hash.HashService
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.Await 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 engineSimilarityController = system.actorOf(Props[ConcurrentEngineSimilarityController], name = "EngineSimilarityController")
implicit val timeout = Timeout(30, TimeUnit.SECONDS)
override def setSearchedListener(listenerRef: ActorRef) = {
this.searchedListener = listenerRef
}
override def setProcessedListener(listenerRef: ActorRef) = {
engineProcessingController ! SetNewListener(listenerRef)
}
override def setSimilarityListener(listenerRef: ActorRef) = {
engineSimilarityController ! SetNewListener(listenerRef)
}
//needs to be rebuilt
def getSimilarImagesForDirectory(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[SimilarImages] = {
debug(s"Looking for similar images in directory: $directoryPath")
val images = getImagesForDirectory(directoryPath, recursive, recursiveDepth)
info(s"Searching ${images.length} images for similarities")
// make sure the engine is listening
engineSimilarityController ! EngineStart
for (rootImage <- images) {
debug(s"Looking for images similar to: ${rootImage.imagePath}")
engineSimilarityController ! EngineCompareImages(rootImage, images)
}
//tell the comparison engine there's nothing left to compare
engineSimilarityController ! EngineNoMoreComparisons
var doneProcessing = false
while (!doneProcessing) {
val f = engineSimilarityController ? EngineIsSimilarityFinished
val result = Await.result(f, timeout.duration).asInstanceOf[Boolean]
result match {
case true =>
doneProcessing = true
debug("Processing Complete")
case false =>
debug("Still Processing")
//sleep thread
Thread.sleep(5000L)
//val future = Future { blocking(Thread.sleep(5000L)); "done" }
}
}
val f = engineSimilarityController ? EngineGetSimilarityResults
val result = Await.result(f, timeout.duration).asInstanceOf[List[SimilarImages]]
val cleanedSimilarImages = this.processSimilarities(result)
var similarCount = 0
for (similarImage <- cleanedSimilarImages) {
similarCount += 1 + similarImage.similarImages.size
}
info(s"Finished processing ${images.size} images. Found $similarCount similar images")
cleanedSimilarImages
}
def getImagesForDirectory(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[Image] = {
debug(s"Looking for images in directory: $directoryPath")
val imageFiles = getAllImageFiles(directoryPath, recursive, recursiveDepth)
val images: mutable.MutableList[Image] = new mutable.MutableList[Image]()
// make sure the engine is listening
engineProcessingController ! EngineStart
for (file <- imageFiles) {
engineProcessingController ! EngineProcessFile(file)
}
engineProcessingController ! EngineNoMoreFiles
var doneProcessing = false
while (!doneProcessing) {
val f = engineProcessingController ? EngineIsProcessingFinished
val result = Await.result(f, timeout.duration).asInstanceOf[Boolean]
result match {
case true =>
doneProcessing = true
debug("Processing Complete")
case false =>
debug("Still Processing")
//sleep thread
Thread.sleep(5000L)
//val future = Future { blocking(Thread.sleep(5000L)); "done" }
}
}
val f = engineProcessingController ? EngineGetProcessingResults
val result = Await.result(f, timeout.duration).asInstanceOf[List[Image]]
images ++= result
images.toList
}
val engineProcessingController = system.actorOf(Props[ConcurrentEngineProcessingController], name = "EngineProcessingController")
val engineSimilarityController = system.actorOf(Props[ConcurrentEngineSimilarityController], name = "EngineSimilarityController")
implicit val timeout = Timeout(30, TimeUnit.SECONDS)
override def setSearchedListener(listenerRef: ActorRef) = {
this.searchedListener = listenerRef
}
override def setProcessedListener(listenerRef: ActorRef) = {
engineProcessingController ! SetNewListener(listenerRef)
}
override def setSimilarityListener(listenerRef: ActorRef) = {
engineSimilarityController ! SetNewListener(listenerRef)
}
//needs to be rebuilt
def getSimilarImagesForDirectory(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[SimilarImages] = {
debug(s"Looking for similar images in directory: $directoryPath")
val images = getImagesForDirectory(directoryPath, recursive, recursiveDepth)
info(s"Searching ${images.length} images for similarities")
// make sure the engine is listening
engineSimilarityController ! EngineStart
for (rootImage <- images) {
debug(s"Looking for images similar to: ${rootImage.imagePath}")
engineSimilarityController ! EngineCompareImages(rootImage, images)
}
//tell the comparison engine there's nothing left to compare
engineSimilarityController ! EngineNoMoreComparisons
var doneProcessing = false
while (!doneProcessing) {
val f = engineSimilarityController ? EngineIsSimilarityFinished
val result = Await.result(f, timeout.duration).asInstanceOf[Boolean]
result match {
case true =>
doneProcessing = true
debug("Processing Complete")
case false =>
debug("Still Processing")
//sleep thread
Thread.sleep(5000L)
//val future = Future { blocking(Thread.sleep(5000L)); "done" }
}
}
val f = engineSimilarityController ? EngineGetSimilarityResults
val result = Await.result(f, timeout.duration).asInstanceOf[List[SimilarImages]]
val cleanedSimilarImages = this.processSimilarities(result)
var similarCount = 0
for (similarImage <- cleanedSimilarImages) {
similarCount += 1 + similarImage.similarImages.size
}
info(s"Finished processing ${images.size} images. Found $similarCount similar images")
cleanedSimilarImages
}
def getImagesForDirectory(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[Image] = {
debug(s"Looking for images in directory: $directoryPath")
val imageFiles = getAllImageFiles(directoryPath, recursive, recursiveDepth)
val images: mutable.MutableList[Image] = new mutable.MutableList[Image]()
// make sure the engine is listening
engineProcessingController ! EngineStart
for (file <- imageFiles) {
engineProcessingController ! EngineProcessFile(file)
}
engineProcessingController ! EngineNoMoreFiles
var doneProcessing = false
while (!doneProcessing) {
val f = engineProcessingController ? EngineIsProcessingFinished
val result = Await.result(f, timeout.duration).asInstanceOf[Boolean]
result match {
case true =>
doneProcessing = true
debug("Processing Complete")
case false =>
debug("Still Processing")
//sleep thread
Thread.sleep(5000L)
//val future = Future { blocking(Thread.sleep(5000L)); "done" }
}
}
val f = engineProcessingController ? EngineGetProcessingResults
val result = Await.result(f, timeout.duration).asInstanceOf[List[Image]]
images ++= result
images.toList
}
} }
@ -127,144 +127,144 @@ case object EngineActorProcessingFinished
case object EngineActorReactivate case object EngineActorReactivate
class ConcurrentEngineProcessingController extends Actor with ActorLogging { class ConcurrentEngineProcessingController extends Actor with ActorLogging {
val numOfRouters = {
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
threads
}
var router = context.actorOf(Props[ConcurrentEngineProcessingActor].withRouter(SmallestMailboxPool(nrOfInstances = numOfRouters)))
var images: mutable.MutableList[Image] = new mutable.MutableList[Image]()
var toProcess = 0
var processed = 0
var processorsFinished = 0
var listener = context.actorOf(Props[DefaultLoggingEngineListener],
name = "ProcessedEngineListener")
override def preStart() = {
log.info("Staring the controller for processing images")
log.info("Using {} actors to process images", numOfRouters)
}
override def receive = {
case command: SetNewListener => setListener(command.listenerType)
case command: EngineProcessFile => processFile(command)
case command: EngineFileProcessed => fileProcessed(command)
case EngineStart => startEngine()
case EngineNoMoreFiles => requestWrapup()
case EngineActorProcessingFinished => actorProcessingFinished()
case EngineIsProcessingFinished => checkIfProcessingIsFinished()
case EngineGetProcessingResults => checkForResults()
case _ => log.info("received unknown message")
}
def setListener(newListener: ActorRef) = {
//remove the old listener
this.listener ! PoisonPill
//setup the new listener
this.listener = newListener
}
def startEngine() = {
router ! Broadcast(EngineActorReactivate)
}
def processFile(command: EngineProcessFile) = {
log.debug(s"Started evaluating ${command.file.getAbsolutePath}")
toProcess += 1
router ! command
}
def fileProcessed(command: EngineFileProcessed) = {
processed += 1
if (processed % 25 == 0 || processed == toProcess) {
//log.info(s"Processed $processed/$toProcess")
listener ! ComparedFileCount(processed, toProcess)
}
if (command.image != null) {
log.debug(s"processed image: ${command.image.imagePath}")
images += command.image
}
}
def requestWrapup() = {
router ! Broadcast(EngineNoMoreFiles)
}
/*
* Record that a processor is done
*/
def actorProcessingFinished() = {
processorsFinished += 1
}
/*
* Check if processing is done
*/
def checkIfProcessingIsFinished() = {
try {
if (processorsFinished >= numOfRouters) sender ! true else sender ! false
} catch {
case e: Exception
sender ! akka.actor.Status.Failure(e)
throw e
}
}
/*
* Get the results of the processing
*/
def checkForResults() = {
try {
listener ! SubmitMessage(s"Finished processing $processed/$processed images")
processorsFinished = 0
toProcess = 0
processed = 0
sender ! images.toList
images.clear()
} catch {
case e: Exception
sender ! akka.actor.Status.Failure(e)
throw e
}
}
override def postStop() = {
super.postStop()
this.listener ! PoisonPill
}
val numOfRouters = {
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
threads
}
var router = context.actorOf(Props[ConcurrentEngineProcessingActor].withRouter(SmallestMailboxPool(nrOfInstances = numOfRouters)))
var images: mutable.MutableList[Image] = new mutable.MutableList[Image]()
var toProcess = 0
var processed = 0
var processorsFinished = 0
var listener = context.actorOf(Props[DefaultLoggingEngineListener],
name = "ProcessedEngineListener")
override def preStart() = {
log.info("Staring the controller for processing images")
log.info("Using {} actors to process images", numOfRouters)
}
override def receive = {
case command: SetNewListener => setListener(command.listenerType)
case command: EngineProcessFile => processFile(command)
case command: EngineFileProcessed => fileProcessed(command)
case EngineStart => startEngine()
case EngineNoMoreFiles => requestWrapup()
case EngineActorProcessingFinished => actorProcessingFinished()
case EngineIsProcessingFinished => checkIfProcessingIsFinished()
case EngineGetProcessingResults => checkForResults()
case _ => log.info("received unknown message")
}
def setListener(newListener: ActorRef) = {
//remove the old listener
this.listener ! PoisonPill
//setup the new listener
this.listener = newListener
}
def startEngine() = {
router ! Broadcast(EngineActorReactivate)
}
def processFile(command: EngineProcessFile) = {
log.debug(s"Started evaluating ${command.file.getAbsolutePath}")
toProcess += 1
router ! command
}
def fileProcessed(command: EngineFileProcessed) = {
processed += 1
if (processed % 25 == 0 || processed == toProcess) {
//log.info(s"Processed $processed/$toProcess")
listener ! ComparedFileCount(processed, toProcess)
}
if (command.image != null) {
log.debug(s"processed image: ${command.image.imagePath}")
images += command.image
}
}
def requestWrapup() = {
router ! Broadcast(EngineNoMoreFiles)
}
/*
* Record that a processor is done
*/
def actorProcessingFinished() = {
processorsFinished += 1
}
/*
* Check if processing is done
*/
def checkIfProcessingIsFinished() = {
try {
if (processorsFinished >= numOfRouters) sender ! true else sender ! false
} catch {
case e: Exception
sender ! akka.actor.Status.Failure(e)
throw e
}
}
/*
* Get the results of the processing
*/
def checkForResults() = {
try {
listener ! SubmitMessage(s"Finished processing $processed/$processed images")
processorsFinished = 0
toProcess = 0
processed = 0
sender ! images.toList
images.clear()
} catch {
case e: Exception
sender ! akka.actor.Status.Failure(e)
throw e
}
}
override def postStop() = {
super.postStop()
this.listener ! PoisonPill
}
} }
class ConcurrentEngineProcessingActor extends Actor with ActorLogging { class ConcurrentEngineProcessingActor extends Actor with ActorLogging {
var ignoreMessages = false
override def receive = {
case command: EngineProcessFile => processFile(command)
case EngineNoMoreFiles => finishedProcessingFiles()
case EngineActorReactivate => ignoreMessages = false
case _ => log.info("received unknown message")
}
def processFile(command: EngineProcessFile) = {
if (!ignoreMessages) {
val image = ImageService.getImage(command.file)
if (image != null) {
sender ! EngineFileProcessed(image)
} else {
log.debug(s"Failed to process image: ${command.file.getAbsolutePath}")
}
}
}
def finishedProcessingFiles() = {
if (!ignoreMessages) {
ignoreMessages = true
sender ! EngineActorProcessingFinished
}
}
var ignoreMessages = false
override def receive = {
case command: EngineProcessFile => processFile(command)
case EngineNoMoreFiles => finishedProcessingFiles()
case EngineActorReactivate => ignoreMessages = false
case _ => log.info("received unknown message")
}
def processFile(command: EngineProcessFile) = {
if (!ignoreMessages) {
val image = ImageService.getImage(command.file)
if (image != null) {
sender ! EngineFileProcessed(image)
} else {
log.debug(s"Failed to process image: ${command.file.getAbsolutePath}")
}
}
}
def finishedProcessingFiles() = {
if (!ignoreMessages) {
ignoreMessages = true
sender ! EngineActorProcessingFinished
}
}
} }
//finding similarities between images //finding similarities between images
@ -281,165 +281,165 @@ case object EngineGetSimilarityResults
case object EngineActorCompareImagesFinished case object EngineActorCompareImagesFinished
class ConcurrentEngineSimilarityController extends Actor with ActorLogging { class ConcurrentEngineSimilarityController extends Actor with ActorLogging {
val numOfRouters = {
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
threads
}
val router = context.actorOf(Props[ConcurrentEngineSimilarityActor].withRouter(RoundRobinPool(nrOfInstances = numOfRouters)))
val allSimilarImages = new mutable.MutableList[SimilarImages]
var toProcess = 0
var processed = 0
var processorsFinished = 0
var listener = context.actorOf(Props[DefaultLoggingEngineListener],
name = "SimilarityEngineListener")
override def preStart() = {
log.info("Staring the controller for processing similarities between images")
log.info("Using {} actors to process image similarities", numOfRouters)
}
override def receive = {
case command: SetNewListener => setListener(command.listenerType)
case command: EngineCompareImages => findSimilarities(command)
case command: EngineCompareImagesComplete => similarityProcessed(command)
case EngineStart => startEngine()
case EngineNoMoreComparisons => requestWrapup()
case EngineActorCompareImagesFinished => actorProcessingFinished()
case EngineIsSimilarityFinished => checkIfProcessingIsFinished()
case EngineGetSimilarityResults => checkForResults()
case _ => log.info("received unknown message")
}
def setListener(newListener: ActorRef) = {
//remove the old listener
this.listener ! PoisonPill
//setup the new listener
this.listener = newListener
}
def startEngine() = {
router ! Broadcast(EngineActorReactivate)
}
def findSimilarities(command: EngineCompareImages) = {
log.debug(s"Finding similarities between {} and {} images", command.image1.imagePath, command.images.length)
toProcess += 1
if (toProcess % 250 == 0) {
//log.info("Sent {} images to be processed for similarites", toProcess)
listener ! SubmitMessage(s"Sent $toProcess images to be processed for similarites")
}
//just relay the command to our workers
router ! command
}
def similarityProcessed(command: EngineCompareImagesComplete) = {
processed += 1
if (processed % 25 == 0 || processed == toProcess) {
//log.info(s"Processed $processed/$toProcess")
listener ! ScannedFileCount(processed, toProcess)
}
if (command.similarImages != null) {
log.debug(s"Found similar images: ${command.similarImages}")
allSimilarImages += command.similarImages
}
}
def requestWrapup() = {
router ! Broadcast(EngineNoMoreComparisons)
}
/*
* Record that a processor is done
*/
def actorProcessingFinished() = {
processorsFinished += 1
log.debug("Similarity Processor Reported Finished")
}
/*
* Check if processing is done
*/
def checkIfProcessingIsFinished() = {
try {
log.debug("Processors finished {}/{}", processorsFinished, numOfRouters)
if (processorsFinished >= numOfRouters) sender ! true else sender ! false
} catch {
case e: Exception
sender ! akka.actor.Status.Failure(e)
throw e
}
}
/*
* Get the results of the processing
*/
def checkForResults() = {
try {
listener ! SubmitMessage(s"Finished scanning $processed/$processed images")
processorsFinished = 0
toProcess = 0
processed = 0
sender ! allSimilarImages.toList
allSimilarImages.clear()
} catch {
case e: Exception
sender ! akka.actor.Status.Failure(e)
throw e
}
}
override def postStop() = {
super.postStop()
this.listener ! PoisonPill
}
val numOfRouters = {
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
threads
}
val router = context.actorOf(Props[ConcurrentEngineSimilarityActor].withRouter(RoundRobinPool(nrOfInstances = numOfRouters)))
val allSimilarImages = new mutable.MutableList[SimilarImages]
var toProcess = 0
var processed = 0
var processorsFinished = 0
var listener = context.actorOf(Props[DefaultLoggingEngineListener],
name = "SimilarityEngineListener")
override def preStart() = {
log.info("Staring the controller for processing similarities between images")
log.info("Using {} actors to process image similarities", numOfRouters)
}
override def receive = {
case command: SetNewListener => setListener(command.listenerType)
case command: EngineCompareImages => findSimilarities(command)
case command: EngineCompareImagesComplete => similarityProcessed(command)
case EngineStart => startEngine()
case EngineNoMoreComparisons => requestWrapup()
case EngineActorCompareImagesFinished => actorProcessingFinished()
case EngineIsSimilarityFinished => checkIfProcessingIsFinished()
case EngineGetSimilarityResults => checkForResults()
case _ => log.info("received unknown message")
}
def setListener(newListener: ActorRef) = {
//remove the old listener
this.listener ! PoisonPill
//setup the new listener
this.listener = newListener
}
def startEngine() = {
router ! Broadcast(EngineActorReactivate)
}
def findSimilarities(command: EngineCompareImages) = {
log.debug(s"Finding similarities between {} and {} images", command.image1.imagePath, command.images.length)
toProcess += 1
if (toProcess % 250 == 0) {
//log.info("Sent {} images to be processed for similarites", toProcess)
listener ! SubmitMessage(s"Sent $toProcess images to be processed for similarites")
}
//just relay the command to our workers
router ! command
}
def similarityProcessed(command: EngineCompareImagesComplete) = {
processed += 1
if (processed % 25 == 0 || processed == toProcess) {
//log.info(s"Processed $processed/$toProcess")
listener ! ScannedFileCount(processed, toProcess)
}
if (command.similarImages != null) {
log.debug(s"Found similar images: ${command.similarImages}")
allSimilarImages += command.similarImages
}
}
def requestWrapup() = {
router ! Broadcast(EngineNoMoreComparisons)
}
/*
* Record that a processor is done
*/
def actorProcessingFinished() = {
processorsFinished += 1
log.debug("Similarity Processor Reported Finished")
}
/*
* Check if processing is done
*/
def checkIfProcessingIsFinished() = {
try {
log.debug("Processors finished {}/{}", processorsFinished, numOfRouters)
if (processorsFinished >= numOfRouters) sender ! true else sender ! false
} catch {
case e: Exception
sender ! akka.actor.Status.Failure(e)
throw e
}
}
/*
* Get the results of the processing
*/
def checkForResults() = {
try {
listener ! SubmitMessage(s"Finished scanning $processed/$processed images")
processorsFinished = 0
toProcess = 0
processed = 0
sender ! allSimilarImages.toList
allSimilarImages.clear()
} catch {
case e: Exception
sender ! akka.actor.Status.Failure(e)
throw e
}
}
override def postStop() = {
super.postStop()
this.listener ! PoisonPill
}
} }
class ConcurrentEngineSimilarityActor extends Actor with ActorLogging { class ConcurrentEngineSimilarityActor extends Actor with ActorLogging {
var ignoreMessages = false
override def receive = {
case command: EngineCompareImages => compareImages(command)
case EngineNoMoreComparisons => finishedComparisons()
case EngineActorReactivate => ignoreMessages = false
case _ => log.info("received unknown message")
}
def compareImages(command: EngineCompareImages) = {
if (!ignoreMessages) {
val similarImages = new mutable.MutableList[Image]()
for (image <- command.images) {
if (!command.image1.equals(image)) {
val image1Hashes = ImageService.convertToImageHashDTO(command.image1.hashes)
val imageHashes = ImageService.convertToImageHashDTO(image.hashes)
if (HashService.areImageHashesSimilar(PropertiesService.aHashSettings, PropertiesService.dHashSettings, PropertiesService.pHashSettings, image1Hashes, imageHashes)) {
similarImages += image
}
}
}
//only send a message if we find similar images
if (similarImages.length >= 1) {
similarImages += command.image1
val similarImage = new SimilarImages(similarImages.toSet)
log.debug(s"Found ${similarImage.similarImages.size} similar images to ${command.image1}")
sender ! EngineCompareImagesComplete(similarImage)
} else {
log.debug(s"Found no similar images to ${command.image1}")
sender ! EngineCompareImagesComplete(null)
}
}
}
def finishedComparisons() = {
if (!ignoreMessages) {
ignoreMessages = true
log.debug("Finished processing comparisons")
sender ! EngineActorCompareImagesFinished
}
}
var ignoreMessages = false
override def receive = {
case command: EngineCompareImages => compareImages(command)
case EngineNoMoreComparisons => finishedComparisons()
case EngineActorReactivate => ignoreMessages = false
case _ => log.info("received unknown message")
}
def compareImages(command: EngineCompareImages) = {
if (!ignoreMessages) {
val similarImages = new mutable.MutableList[Image]()
for (image <- command.images) {
if (!command.image1.equals(image)) {
val image1Hashes = ImageService.convertToImageHashDTO(command.image1.hashes)
val imageHashes = ImageService.convertToImageHashDTO(image.hashes)
if (HashService.areImageHashesSimilar(PropertiesService.aHashSettings, PropertiesService.dHashSettings, PropertiesService.pHashSettings, image1Hashes, imageHashes)) {
similarImages += image
}
}
}
//only send a message if we find similar images
if (similarImages.length >= 1) {
similarImages += command.image1
val similarImage = new SimilarImages(similarImages.toSet)
log.debug(s"Found ${similarImage.similarImages.size} similar images to ${command.image1}")
sender ! EngineCompareImagesComplete(similarImage)
} else {
log.debug(s"Found no similar images to ${command.image1}")
sender ! EngineCompareImagesComplete(null)
}
}
}
def finishedComparisons() = {
if (!ignoreMessages) {
ignoreMessages = true
log.debug("Finished processing comparisons")
sender ! EngineActorCompareImagesFinished
}
}
} }

348
engine/src/main/scala/com/sothr/imagetools/engine/Engine.scala

@ -12,133 +12,135 @@ import scala.collection.mutable
import scala.util.control.Breaks._ import scala.util.control.Breaks._
/** /**
* Engine definition
*
* Created by drew on 1/26/14.
*/
* Engine definition
*
* Created by drew on 1/26/14.
*/
abstract class Engine extends Logging { abstract class Engine extends Logging {
val system = ActorSystem("EngineActorSystem")
val imageFilter: ImageFilter = new ImageFilter()
val imageCache = AppConfig.cacheManager.getCache("images")
//file search listener
var searchedListener = system.actorOf(Props[DefaultLoggingEngineListener],
name = "SearchedEngineListener")
def setSearchedListener(listenerRef: ActorRef) = {
this.searchedListener = listenerRef
}
def setProcessedListener(listenerType: ActorRef)
def setSimilarityListener(listenerType: ActorRef)
def getAllImageFiles(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[File] = {
val fileList = new mutable.MutableList[File]()
if (directoryPath != null && directoryPath != "") {
val directory: File = new File(directoryPath)
val imageFilter = new ImageFilter
if (directory.isDirectory) {
val files = directory.listFiles(imageFilter)
if (files != null) {
fileList ++= files
debug(s"Found ${files.length} files that are images in directory: $directoryPath")
if (recursive) {
val directoryFilter = new DirectoryFilter
val directories = directory.listFiles(directoryFilter)
for (directory <- directories) {
fileList ++= getAllImageFiles(directory.getAbsolutePath, recursive, recursiveDepth - 1)
this.searchedListener ! SubmitMessage(s"Found ${fileList.length} files to process")
}
} else {
this.searchedListener ! SubmitMessage(s"Found ${fileList.length} files to process")
}
}
}
}
fileList.toList
}
/**
* Get all images for a directory with hashes
*/
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 deleteImage(image: Image): Unit = {
ImageService.deleteImage(image)
}
def deleteImages(images: List[Image]): Unit = {
for (image <- images) {
deleteImage(image)
}
}
/**
* Go through the list of similarities and group them so that they represent actual similarities
*
* For example. A is similar to B and C is similar to B but A is was not considered similar to C. Therefore A B and C should be considered similar unless otherwise noted.
*
* @param similarList a list of detected similar images
* @return a grouped and combined list of similar images
*/
def processSimilarities(similarList: List[SimilarImages]): List[SimilarImages] = {
//process the result into a list we want in cleanedSimilarImages
/*
Go through all the images. If a similar image for the image doesn't exist, create it. if it does,
add it to that similar image. The end result is that all images should be grouped according to
their similar images and images that are similar to them.
*/
var count = 0
// similar image mapping to map known images back to their similar set
val similarImageMap = new mutable.HashMap[Image, SimilarImages]()
// List of the actual similar image sets
val cleanedSimilarImages = new mutable.MutableList[SimilarImages]()
// loop over all of the similar image sets
for (similarImages <- similarList) {
count += 1
if (count % 25 == 0 || count == similarList.length) {
debug(s"Cleaning similar images. $count/${similarList.length} ${similarList.length - count} left to clean")
this.searchedListener ! SubmitMessage(s"Cleaning similar images. $count/${similarList.length}")
}
var foundSimilarity = false
var similarity:SimilarImages = null
breakable { for (similarImage <- similarImages.similarImages) {
if (similarImageMap.contains(similarImage)) {
similarity = similarImageMap(similarImage)
foundSimilarity = true
break()
}
} }
//if no similarity was found, one should be created
if (!foundSimilarity) {
similarity = new SimilarImages(new HashSet[Image])
// the created similarity is added to the cleaned list
cleanedSimilarImages += similarity
}
// all images should be added to this new similarity
similarity.similarImages = similarity.similarImages ++ similarImages.similarImages
similarImages.similarImages.foreach(img => similarImageMap.put(img, similarity))
}
//get a count of similar images found
var totalCount = 0
cleanedSimilarImages.foreach(img => totalCount += img.similarImages.size)
debug(s"Found $totalCount images with similarities")
this.searchedListener ! SubmitMessage(s"Found $totalCount images with similarities")
// Sort the similarImages by ?!?!?!? and return
cleanedSimilarImages.toList.sorted(SimilarImagesOrdering)
}
val system = ActorSystem("EngineActorSystem")
val imageFilter: ImageFilter = new ImageFilter()
val imageCache = AppConfig.cacheManager.getCache("images")
//file search listener
var searchedListener = system.actorOf(Props[DefaultLoggingEngineListener],
name = "SearchedEngineListener")
def setSearchedListener(listenerRef: ActorRef) = {
this.searchedListener = listenerRef
}
def setProcessedListener(listenerType: ActorRef)
def setSimilarityListener(listenerType: ActorRef)
def getAllImageFiles(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[File] = {
val fileList = new mutable.MutableList[File]()
if (directoryPath != null && directoryPath != "") {
val directory: File = new File(directoryPath)
val imageFilter = new ImageFilter
if (directory.isDirectory) {
val files = directory.listFiles(imageFilter)
if (files != null) {
fileList ++= files
debug(s"Found ${files.length} files that are images in directory: $directoryPath")
if (recursive) {
val directoryFilter = new DirectoryFilter
val directories = directory.listFiles(directoryFilter)
for (directory <- directories) {
fileList ++= getAllImageFiles(directory.getAbsolutePath, recursive, recursiveDepth - 1)
this.searchedListener ! SubmitMessage(s"Found ${fileList.length} files to process")
}
} else {
this.searchedListener ! SubmitMessage(s"Found ${fileList.length} files to process")
}
}
}
}
fileList.toList
}
/**
* Get all images for a directory with hashes
*/
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 deleteImages(images: List[Image]): Unit = {
for (image <- images) {
deleteImage(image)
}
}
def deleteImage(image: Image): Unit = {
ImageService.deleteImage(image)
}
/**
* Go through the list of similarities and group them so that they represent actual similarities
*
* For example. A is similar to B and C is similar to B but A is was not considered similar to C. Therefore A B and C should be considered similar unless otherwise noted.
*
* @param similarList a list of detected similar images
* @return a grouped and combined list of similar images
*/
def processSimilarities(similarList: List[SimilarImages]): List[SimilarImages] = {
//process the result into a list we want in cleanedSimilarImages
/*
Go through all the images. If a similar image for the image doesn't exist, create it. if it does,
add it to that similar image. The end result is that all images should be grouped according to
their similar images and images that are similar to them.
*/
var count = 0
// similar image mapping to map known images back to their similar set
val similarImageMap = new mutable.HashMap[Image, SimilarImages]()
// List of the actual similar image sets
val cleanedSimilarImages = new mutable.MutableList[SimilarImages]()
// loop over all of the similar image sets
for (similarImages <- similarList) {
count += 1
if (count % 25 == 0 || count == similarList.length) {
debug(s"Cleaning similar images. $count/${similarList.length} ${similarList.length - count} left to clean")
this.searchedListener ! SubmitMessage(s"Cleaning similar images. $count/${similarList.length}")
}
var foundSimilarity = false
var similarity: SimilarImages = null
breakable {
for (similarImage <- similarImages.similarImages) {
if (similarImageMap.contains(similarImage)) {
similarity = similarImageMap(similarImage)
foundSimilarity = true
break()
}
}
}
//if no similarity was found, one should be created
if (!foundSimilarity) {
similarity = new SimilarImages(new HashSet[Image])
// the created similarity is added to the cleaned list
cleanedSimilarImages += similarity
}
// all images should be added to this new similarity
similarity.similarImages = similarity.similarImages ++ similarImages.similarImages
similarImages.similarImages.foreach(img => similarImageMap.put(img, similarity))
}
//get a count of similar images found
var totalCount = 0
cleanedSimilarImages.foreach(img => totalCount += img.similarImages.size)
debug(s"Found $totalCount images with similarities")
this.searchedListener ! SubmitMessage(s"Found $totalCount images with similarities")
// Sort the similarImages by ?!?!?!? and return
cleanedSimilarImages.toList.sorted(SimilarImagesOrdering)
}
} }
case class SubmitMessage(message: String) case class SubmitMessage(message: String)
@ -148,62 +150,62 @@ case class ScannedFileCount(count: Integer, total: Integer, message: String = nu
case class ComparedFileCount(count: Integer, total: Integer, message: String = null) case class ComparedFileCount(count: Integer, total: Integer, message: String = null)
abstract class EngineListener extends Actor with ActorLogging { abstract class EngineListener extends Actor with ActorLogging {
override def receive: Actor.Receive = {
case command: SubmitMessage => handleMessage(command)
case command: ScannedFileCount => handleScannedFileCount(command)
case command: ComparedFileCount => handleComparedFileCount(command)
case _ => log.info("received unknown message")
}
override def receive: Actor.Receive = {
case command: SubmitMessage => handleMessage(command)
case command: ScannedFileCount => handleScannedFileCount(command)
case command: ComparedFileCount => handleComparedFileCount(command)
case _ => log.info("received unknown message")
}
def handleMessage(command: SubmitMessage)
def handleMessage(command: SubmitMessage)
def handleScannedFileCount(command: ScannedFileCount)
def handleScannedFileCount(command: ScannedFileCount)
def handleComparedFileCount(command: ComparedFileCount)
def handleComparedFileCount(command: ComparedFileCount)
} }
/** /**
* Actor for logging output information
*/
* Actor for logging output information
*/
class DefaultLoggingEngineListener extends EngineListener with ActorLogging { class DefaultLoggingEngineListener extends EngineListener with ActorLogging {
override def handleComparedFileCount(command: ComparedFileCount): Unit = {
if (command.message != null) {
log.info(command.message)
}
log.info("Processed {}/{}", command.count, command.total)
}
override def handleScannedFileCount(command: ScannedFileCount): Unit = {
if (command.message != null) {
log.info(command.message)
}
log.info("Scanned {}/{} For Similarities", command.count, command.total)
}
override def handleMessage(command: SubmitMessage): Unit = {
log.info(command.message)
}
override def handleComparedFileCount(command: ComparedFileCount): Unit = {
if (command.message != null) {
log.info(command.message)
}
log.info("Processed {}/{}", command.count, command.total)
}
override def handleScannedFileCount(command: ScannedFileCount): Unit = {
if (command.message != null) {
log.info(command.message)
}
log.info("Scanned {}/{} For Similarities", command.count, command.total)
}
override def handleMessage(command: SubmitMessage): Unit = {
log.info(command.message)
}
} }
/** /**
* Actor for writing progress out to the commandline
*/
* Actor for writing progress out to the commandline
*/
class CLIEngineListener extends EngineListener with ActorLogging { class CLIEngineListener extends EngineListener with ActorLogging {
override def handleComparedFileCount(command: ComparedFileCount): Unit = {
if (command.message != null) {
System.out.println(command.message)
}
System.out.println(s"Processed ${command.count}/${command.total}")
}
override def handleScannedFileCount(command: ScannedFileCount): Unit = {
if (command.message != null) {
System.out.println(command.message)
}
System.out.println(s"Scanned ${command.count}/${command.total} For Similarities")
}
override def handleMessage(command: SubmitMessage): Unit = {
System.out.println(command.message)
}
override def handleComparedFileCount(command: ComparedFileCount): Unit = {
if (command.message != null) {
System.out.println(command.message)
}
System.out.println(s"Processed ${command.count}/${command.total}")
}
override def handleScannedFileCount(command: ScannedFileCount): Unit = {
if (command.message != null) {
System.out.println(command.message)
}
System.out.println(s"Scanned ${command.count}/${command.total} For Similarities")
}
override def handleMessage(command: SubmitMessage): Unit = {
System.out.println(command.message)
}
} }

148
engine/src/main/scala/com/sothr/imagetools/engine/SequentialEngine.scala

@ -9,84 +9,84 @@ import grizzled.slf4j.Logging
import scala.collection.mutable import scala.collection.mutable
/** /**
* Engine that works sequentially
* Very Slow, but consistent. Excellent for testing
*
* Created by drew on 1/26/14.
*/
* Engine that works sequentially
* Very Slow, but consistent. Excellent for testing
*
* Created by drew on 1/26/14.
*/
class SequentialEngine extends Engine with Logging { class SequentialEngine extends Engine with Logging {
var processedListener = system.actorOf(Props[DefaultLoggingEngineListener],
name = "ProcessedEngineListener")
var similarityListener = system.actorOf(Props[DefaultLoggingEngineListener],
name = "SimilarityEngineListener")
var processedListener = system.actorOf(Props[DefaultLoggingEngineListener],
name = "ProcessedEngineListener")
var similarityListener = system.actorOf(Props[DefaultLoggingEngineListener],
name = "SimilarityEngineListener")
override def setProcessedListener(listenerRef: ActorRef) = {
this.processedListener = listenerRef
}
override def setProcessedListener(listenerRef: ActorRef) = {
this.processedListener = listenerRef
}
override def setSimilarityListener(listenerRef: ActorRef) = {
this.similarityListener = listenerRef
}
override def setSimilarityListener(listenerRef: ActorRef) = {
this.similarityListener = listenerRef
}
def getSimilarImagesForDirectory(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[SimilarImages] = {
debug(s"Looking for similar images in directory: $directoryPath")
val images = getImagesForDirectory(directoryPath, recursive, recursiveDepth)
info(s"Searching ${images.length} images for similarities")
val ignoreSet = new mutable.HashSet[Image]()
val allSimilarImages = new mutable.MutableList[SimilarImages]()
var processedCount = 0
var similarCount = 0
for (rootImage <- images) {
if (!ignoreSet.contains(rootImage)) {
if (processedCount % 25 == 0) {
//info(s"Processed ${processedCount}/${images.length - ignoreSet.size} About ${images.length -
// processedCount} images to go")
similarityListener ! ScannedFileCount(processedCount, images.length - ignoreSet.size)
}
debug(s"Looking for images similar to: ${rootImage.imagePath}")
ignoreSet += rootImage
val similarImages = new mutable.MutableList[Image]()
for (image <- images) {
if (!ignoreSet.contains(image)) {
if (rootImage.isSimilarTo(image)) {
debug(s"Image: ${image.imagePath} is similar")
similarImages += image
ignoreSet += image
similarCount += 1
}
}
}
if (similarImages.length > 1) {
similarImages += rootImage
val similar = new SimilarImages(similarImages.toSet)
debug(s"Found similar images: ${similar.toString}")
allSimilarImages += similar
}
processedCount += 1
}
}
info(s"Finished processing ${images.size} images. Found $similarCount similar images")
this.processSimilarities(allSimilarImages.toList)
}
def getSimilarImagesForDirectory(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[SimilarImages] = {
debug(s"Looking for similar images in directory: $directoryPath")
val images = getImagesForDirectory(directoryPath, recursive, recursiveDepth)
info(s"Searching ${images.length} images for similarities")
val ignoreSet = new mutable.HashSet[Image]()
val allSimilarImages = new mutable.MutableList[SimilarImages]()
var processedCount = 0
var similarCount = 0
for (rootImage <- images) {
if (!ignoreSet.contains(rootImage)) {
if (processedCount % 25 == 0) {
//info(s"Processed ${processedCount}/${images.length - ignoreSet.size} About ${images.length -
// processedCount} images to go")
similarityListener ! ScannedFileCount(processedCount, images.length - ignoreSet.size)
}
debug(s"Looking for images similar to: ${rootImage.imagePath}")
ignoreSet += rootImage
val similarImages = new mutable.MutableList[Image]()
for (image <- images) {
if (!ignoreSet.contains(image)) {
if (rootImage.isSimilarTo(image)) {
debug(s"Image: ${image.imagePath} is similar")
similarImages += image
ignoreSet += image
similarCount += 1
}
}
}
if (similarImages.length > 1) {
similarImages += rootImage
val similar = new SimilarImages(similarImages.toSet)
debug(s"Found similar images: ${similar.toString}")
allSimilarImages += similar
}
processedCount += 1
}
}
info(s"Finished processing ${images.size} images. Found $similarCount similar images")
this.processSimilarities(allSimilarImages.toList)
}
def getImagesForDirectory(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[Image] = {
debug(s"Looking for images in directory: $directoryPath")
val images: mutable.MutableList[Image] = new mutable.MutableList[Image]()
val imageFiles = getAllImageFiles(directoryPath, recursive, recursiveDepth)
val directory: File = new File(directoryPath)
var count = 0
for (file <- imageFiles) {
count += 1
if (count % 25 == 0) {
//info(s"Processed ${count}/${imageFiles.size}")
processedListener ! ScannedFileCount(count, imageFiles.size)
}
val image = ImageService.getImage(file)
if (image != null) {
images += image
}
}
images.toList
}
def getImagesForDirectory(directoryPath: String, recursive: Boolean = false, recursiveDepth: Int = 500): List[Image] = {
debug(s"Looking for images in directory: $directoryPath")
val images: mutable.MutableList[Image] = new mutable.MutableList[Image]()
val imageFiles = getAllImageFiles(directoryPath, recursive, recursiveDepth)
val directory: File = new File(directoryPath)
var count = 0
for (file <- imageFiles) {
count += 1
if (count % 25 == 0) {
//info(s"Processed ${count}/${imageFiles.size}")
processedListener ! ScannedFileCount(count, imageFiles.size)
}
val image = ImageService.getImage(file)
if (image != null) {
images += image
}
}
images.toList
}
} }

52
engine/src/main/scala/com/sothr/imagetools/engine/dao/HibernateUtil.scala

@ -8,35 +8,33 @@ import org.hibernate.cfg.Configuration
import org.hibernate.service.ServiceRegistry import org.hibernate.service.ServiceRegistry
/** /**
* Utility class to interface with hibernate
*
* Created by drew on 2/8/14.
*/
* Utility class to interface with hibernate
*
* Created by drew on 2/8/14.
*/
object HibernateUtil extends Logging { object HibernateUtil extends Logging {
private val sessionFactory: SessionFactory = buildSessionFactory()
private var serviceRegistry: ServiceRegistry = null
def getSessionFactory: SessionFactory = {
sessionFactory
}
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(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
error("Initial SessionFactory creation failed.", ex)
throw new ExceptionInInitializerError(ex)
}
}
private val sessionFactory: SessionFactory = buildSessionFactory()
private var serviceRegistry: ServiceRegistry = null
def getSessionFactory: SessionFactory = {
sessionFactory
}
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(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
error("Initial SessionFactory creation failed.", ex)
throw new ExceptionInInitializerError(ex)
}
}
} }

69
engine/src/main/scala/com/sothr/imagetools/engine/dao/ImageDAO.scala

@ -4,41 +4,40 @@ import com.sothr.imagetools.engine.image.Image
import org.hibernate.{Session, SessionFactory} import org.hibernate.{Session, SessionFactory}
/** /**
* Interact with stored images
*
* Created by drew on 2/8/14.
*/
* Interact with stored images
*
* Created by drew on 2/8/14.
*/
class ImageDAO { class ImageDAO {
private val sessionFactory: SessionFactory = HibernateUtil.getSessionFactory
def find(path: String): Image = {
val session: Session = sessionFactory.getCurrentSession
session.getTransaction.begin()
val result = session.get(classOf[Image], path).asInstanceOf[Image]
session.getTransaction.commit()
result
}
def save(image: Image) = {
val session: Session = sessionFactory.getCurrentSession
session.getTransaction.begin()
session.saveOrUpdate(image)
session.getTransaction.commit()
}
def save(images: List[Image]) = {
val session: Session = sessionFactory.getCurrentSession
session.getTransaction.begin()
for (image <- images) session.saveOrUpdate(image)
session.getTransaction.commit()
}
def delete(image: Image) = {
val session: Session = sessionFactory.getCurrentSession
session.getTransaction.begin()
session.delete(image)
session.getTransaction.commit()
}
private val sessionFactory: SessionFactory = HibernateUtil.getSessionFactory
def find(path: String): Image = {
val session: Session = sessionFactory.getCurrentSession
session.getTransaction.begin()
val result = session.get(classOf[Image], path).asInstanceOf[Image]
session.getTransaction.commit()
result
}
def save(image: Image) = {
val session: Session = sessionFactory.getCurrentSession
session.getTransaction.begin()
session.saveOrUpdate(image)
session.getTransaction.commit()
}
def save(images: List[Image]) = {
val session: Session = sessionFactory.getCurrentSession
session.getTransaction.begin()
for (image <- images) session.saveOrUpdate(image)
session.getTransaction.commit()
}
def delete(image: Image) = {
val session: Session = sessionFactory.getCurrentSession
session.getTransaction.begin()
session.delete(image)
session.getTransaction.commit()
}
} }

193
engine/src/main/scala/com/sothr/imagetools/engine/image/Image.scala

@ -11,101 +11,100 @@ import grizzled.slf4j.Logging
@Table(name = "Image") @Table(name = "Image")
class Image(val image: String, val thumbnail: String, val size: (Int, Int), val imageHashes: ImageHashVO = null) extends Serializable with Logging { class Image(val image: String, val thumbnail: String, val size: (Int, Int), val imageHashes: ImageHashVO = null) extends Serializable with Logging {
@Id
var imagePath: String = image
var thumbnailPath: String = thumbnail
var width: Int = size._1
var height: Int = size._2
var hashes: ImageHashVO = imageHashes
@transient
var imageSize: (Int, Int) = {
new Tuple2(width, height)
}
@transient
var imageName: String = ""
var imageType: ImageType = ImageType.SingleFrameImage
def this() = this("", "", (0, 0), null)
def getThumbnailPath: String = thumbnailPath
def setThumbnailPath(path: String) = {
thumbnailPath = path
}
def getWidth: Int = width
def setWidth(size: Int) = {
width = size
}
def getHeight: Int = height
def setHeight(size: Int) = {
height = size
}
def getHashes: ImageHashVO = hashes
def setHashes(newHashes: ImageHashVO) = {
hashes = newHashes
}
def getName: String = {
if (this.imageName.length < 1) {
this.imageName = this.getImagePath.split('/').last
}
this.imageName
}
def getImagePath: String = imagePath
def setImagePath(path: String) = {
imagePath = path
}
def isSimilarTo(otherImage: Image): Boolean = {
//debug(s"Checking $imagePath for similarities with ${otherImage.imagePath}")
HashService.areImageHashesSimilar(PropertiesService.aHashSettings,
PropertiesService.dHashSettings,
PropertiesService.pHashSettings,
ImageService.convertToImageHashDTO(this.hashes),
ImageService.convertToImageHashDTO(otherImage.hashes))
}
def getSimilarity(otherImage: Image): Float = {
HashService.getWeightedHashSimilarity(PropertiesService.aHashSettings,
PropertiesService.dHashSettings,
PropertiesService.pHashSettings,
ImageService.convertToImageHashDTO(this.hashes),
ImageService.convertToImageHashDTO(otherImage.hashes))
}
/*def getSimilar(otherImages:Traversable[Image]):Traversable[Image] = {
}*/
def cloneImage: Image = {
new Image(imagePath, thumbnailPath, imageSize, hashes.cloneHashes)
}
override def toString: String = {
s"Image: $imagePath Thumbnail: $thumbnailPath Image Size: ${imageSize._1}x${imageSize._2} Hashes: $hashes"
}
override def equals(obj: Any) = {
obj match {
case that: Image =>
that.hashCode.equals(this.hashCode)
case _ => false
}
}
override def hashCode: Int = {
var result = 365
result = 37 * result + imagePath.hashCode
result = 41 * result + hashes.hashCode()
result
}
@Id
var imagePath: String = image
var thumbnailPath: String = thumbnail
var width: Int = size._1
var height: Int = size._2
var hashes: ImageHashVO = imageHashes
@transient
var imageSize: (Int, Int) = {
new Tuple2(width, height)
}
@transient
var imageName: String = ""
var imageType: ImageType = ImageType.SingleFrameImage
def this() = this("", "", (0, 0), null)
def getThumbnailPath: String = thumbnailPath
def setThumbnailPath(path: String) = {
thumbnailPath = path
}
def getWidth: Int = width
def setWidth(size: Int) = {
width = size
}
def getHeight: Int = height
def setHeight(size: Int) = {
height = size
}
def getHashes: ImageHashVO = hashes
def setHashes(newHashes: ImageHashVO) = {
hashes = newHashes
}
def getName: String = {
if (this.imageName.length < 1) {
this.imageName = this.getImagePath.split('/').last
}
this.imageName
}
def getImagePath: String = imagePath
def setImagePath(path: String) = {
imagePath = path
}
def isSimilarTo(otherImage: Image): Boolean = {
//debug(s"Checking $imagePath for similarities with ${otherImage.imagePath}")
HashService.areImageHashesSimilar(PropertiesService.aHashSettings,
PropertiesService.dHashSettings,
PropertiesService.pHashSettings,
ImageService.convertToImageHashDTO(this.hashes),
ImageService.convertToImageHashDTO(otherImage.hashes))
}
def getSimilarity(otherImage: Image): Float = {
HashService.getWeightedHashSimilarity(PropertiesService.aHashSettings,
PropertiesService.dHashSettings,
PropertiesService.pHashSettings,
ImageService.convertToImageHashDTO(this.hashes),
ImageService.convertToImageHashDTO(otherImage.hashes))
}
/*def getSimilar(otherImages:Traversable[Image]):Traversable[Image] = {
}*/
def cloneImage: Image = {
new Image(imagePath, thumbnailPath, imageSize, hashes.cloneHashes)
}
override def toString: String = {
s"Image: $imagePath Thumbnail: $thumbnailPath Image Size: ${imageSize._1}x${imageSize._2} Hashes: $hashes"
}
override def equals(obj: Any) = {
obj match {
case that: Image =>
that.hashCode.equals(this.hashCode)
case _ => false
}
}
override def hashCode: Int = {
var result = 365
result = 37 * result + imagePath.hashCode
result = 41 * result + hashes.hashCode()
result
}
} }

24
engine/src/main/scala/com/sothr/imagetools/engine/image/ImageFilter.scala

@ -5,19 +5,19 @@ 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.
*/
* Filter for file names
*
* Used to detect image files based on extension
*
* Created by drew on 1/26/14.
*/
class ImageFilter extends FilenameFilter { class ImageFilter extends FilenameFilter {
private val extensions: HashSet[String] = new HashSet[String]() ++ Array("png", "bmp", "gif", "jpg", "jpeg")
private val extensions: HashSet[String] = new HashSet[String]() ++ Array("png", "bmp", "gif", "jpg", "jpeg")
def accept(dir: File, name: String): Boolean = {
val splitName = name.split('.')
val extension = if (splitName.length > 1) splitName(splitName.length - 1) else ""
if (extensions.contains(extension)) true else false
}
def accept(dir: File, name: String): Boolean = {
val splitName = name.split('.')
val extension = if (splitName.length > 1) splitName(splitName.length - 1) else ""
if (extensions.contains(extension)) true else false
}
} }

254
engine/src/main/scala/com/sothr/imagetools/engine/image/ImageService.scala

@ -8,148 +8,148 @@ import com.sothr.imagetools.engine.AppConfig
import com.sothr.imagetools.engine.dao.ImageDAO import com.sothr.imagetools.engine.dao.ImageDAO
import com.sothr.imagetools.engine.util.{PropertiesService, PropertyEnum} import com.sothr.imagetools.engine.util.{PropertiesService, PropertyEnum}
import com.sothr.imagetools.engine.vo.ImageHashVO import com.sothr.imagetools.engine.vo.ImageHashVO
import com.sothr.imagetools.hash.{HashService, ImageHash}
import com.sothr.imagetools.hash.util.ImageUtil import com.sothr.imagetools.hash.util.ImageUtil
import com.sothr.imagetools.hash.{HashService, ImageHash}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import net.sf.ehcache.Element import net.sf.ehcache.Element
import org.hibernate.HibernateException import org.hibernate.HibernateException
object ImageService extends Logging { object ImageService extends Logging {
val imageCache = AppConfig.cacheManager.getCache("images")
private val imageDAO = new ImageDAO()
val imageCache = AppConfig.cacheManager.getCache("images")
private val imageDAO = new ImageDAO()
def getImage(file: File): Image = {
try {
val image = lookupImage(file)
if (image != null) {
debug(s"${file.getAbsolutePath} was already processed")
return image
} else {
debug(s"Processing image: ${file.getAbsolutePath}")
val bufferedImage = ImageIO.read(file)
val hashes = HashService.getImageHashes(
PropertiesService.aHashSettings,
PropertiesService.dHashSettings,
PropertiesService.pHashSettings,
bufferedImage,
file.getAbsolutePath)
def getImage(file: File): Image = {
try {
val image = lookupImage(file)
if (image != null) {
debug(s"${file.getAbsolutePath} was already processed")
return image
} else {
debug(s"Processing image: ${file.getAbsolutePath}")
val bufferedImage = ImageIO.read(file)
val hashes = HashService.getImageHashes(
PropertiesService.aHashSettings,
PropertiesService.dHashSettings,
PropertiesService.pHashSettings,
bufferedImage,
file.getAbsolutePath)
var thumbnailPath = lookupThumbnailPath(hashes.fileHash)
if (thumbnailPath == null) thumbnailPath = getThumbnail(bufferedImage, hashes.fileHash)
val imageSize = {
(bufferedImage.getWidth, bufferedImage.getHeight)
}
val image = new Image(file.getAbsolutePath, thumbnailPath, imageSize, ImageService.convertToImageHashVO(hashes))
debug(s"Created image: $image")
return saveImage(image)
}
} catch {
case ioe: IOException => error(s"Error processing ${file.getAbsolutePath}... ${ioe.getMessage}")
case ex: Exception => error(s"Error processing ${file.getAbsolutePath}... ${ex.getMessage}", ex)
}
null
}
var thumbnailPath = lookupThumbnailPath(hashes.fileHash)
if (thumbnailPath == null) thumbnailPath = getThumbnail(bufferedImage, hashes.fileHash)
val imageSize = {
(bufferedImage.getWidth, bufferedImage.getHeight)
}
val image = new Image(file.getAbsolutePath, thumbnailPath, imageSize, ImageService.convertToImageHashVO(hashes))
debug(s"Created image: $image")
return saveImage(image)
}
} catch {
case ioe: IOException => error(s"Error processing ${file.getAbsolutePath}... ${ioe.getMessage}")
case ex: Exception => error(s"Error processing ${file.getAbsolutePath}... ${ex.getMessage}", ex)
}
null
}
def convertToImageHashDTO(imageHashVO: ImageHashVO): ImageHash = {
new ImageHash(imageHashVO.ahash, imageHashVO.dhash, imageHashVO.phash, imageHashVO.fileHash)
}
def convertToImageHashVO(imageHashDTO: ImageHash): ImageHashVO = {
new ImageHashVO(imageHashDTO.ahash, imageHashDTO.dhash, imageHashDTO.phash, imageHashDTO.fileHash)
}
def convertToImageHashVO(imageHashDTO: ImageHash): ImageHashVO = {
new ImageHashVO(imageHashDTO.ahash, imageHashDTO.dhash, imageHashDTO.phash, imageHashDTO.fileHash)
}
private def lookupImage(file: File): Image = {
var image: Image = null
var found = false
//get from memory cache if possible
try {
if (imageCache.isKeyInCache(file.getAbsolutePath)) {
image = imageCache.get(file.getAbsolutePath).getObjectValue.asInstanceOf[Image]
found = true
}
} catch {
case npe: NullPointerException => debug(s"\'${file.getAbsolutePath}\' was supposed to be in the cache, but was not")
}
//get from datastore if possible
if (!found) {
try {
val tempImage = imageDAO.find(file.getAbsolutePath)
if (tempImage != null) image = tempImage
} catch {
case ex: Exception => error(s"Error looking up \'${file.getAbsolutePath}\' was supposed to be in the database, but was not", ex)
}
}
image
}
private def lookupImage(file: File): Image = {
var image: Image = null
var found = false
//get from memory cache if possible
try {
if (imageCache.isKeyInCache(file.getAbsolutePath)) {
image = imageCache.get(file.getAbsolutePath).getObjectValue.asInstanceOf[Image]
found = true
}
} catch {
case npe: NullPointerException => debug(s"\'${file.getAbsolutePath}\' was supposed to be in the cache, but was not")
}
//get from datastore if possible
if (!found) {
try {
val tempImage = imageDAO.find(file.getAbsolutePath)
if (tempImage != null) image = tempImage
} catch {
case ex: Exception => error(s"Error looking up \'${file.getAbsolutePath}\' was supposed to be in the database, but was not", ex)
}
}
image
}
private def saveImage(image: Image): Image = {
//save to cache
imageCache.put(new Element(image.imagePath, image))
//save to datastore
try {
imageDAO.save(image)
} catch {
case ex: Exception => error(s"Error saving \'${image.imagePath}\' to database", ex)
}
image
}
private def saveImage(image: Image): Image = {
//save to cache
imageCache.put(new Element(image.imagePath, image))
//save to datastore
try {
imageDAO.save(image)
} catch {
case ex: Exception => error(s"Error saving \'${image.imagePath}\' to database", ex)
}
image
}
def lookupThumbnailPath(md5: String): String = {
var thumbPath: String = null
if (md5 != null) {
//check for the actual file
val checkPath = calculateThumbPath(md5)
if (new File(checkPath).exists) thumbPath = checkPath
} else {
error("Null md5 passed in")
}
thumbPath
}
def lookupThumbnailPath(md5: String): String = {
var thumbPath: String = null
if (md5 != null) {
//check for the actual file
val checkPath = calculateThumbPath(md5)
if (new File(checkPath).exists) thumbPath = checkPath
} else {
error("Null md5 passed in")
}
thumbPath
}
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(PropertyEnum.ThumbnailDirectory.toString)}${PropertiesService.get(PropertyEnum.ThumbnailSize.toString)}/$subPath/"
try {
val dir = new File(path)
if (!dir.exists()) dir.mkdirs()
} catch {
case ioe: IOException => error(s"Unable to create dirs for path: \'$path\'", ioe)
}
path += md5 + ".jpg"
path
}
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(PropertyEnum.ThumbnailDirectory.toString)}${PropertiesService.get(PropertyEnum.ThumbnailSize.toString)}/$subPath/"
try {
val dir = new File(path)
if (!dir.exists()) dir.mkdirs()
} catch {
case ioe: IOException => error(s"Unable to create dirs for path: \'$path\'", ioe)
}
path += md5 + ".jpg"
path
}
def getThumbnail(image: BufferedImage, md5: String): String = {
//create thumbnail
val thumb = ImageUtil.resize(image, PropertiesService.get(PropertyEnum.ThumbnailSize.toString).toInt, forced = false)
//calculate path
val path = calculateThumbPath(md5)
// save thumbnail to path
try {
ImageIO.write(thumb, "png", new File(path))
debug(s"Wrote thumbnail to $path")
} catch {
case ioe: IOException => error(s"Unable to save thumbnail to $path", ioe)
}
// return path
path
}
def getThumbnail(image: BufferedImage, md5: String): String = {
//create thumbnail
val thumb = ImageUtil.resize(image, PropertiesService.get(PropertyEnum.ThumbnailSize.toString).toInt, forced = false)
//calculate path
val path = calculateThumbPath(md5)
// save thumbnail to path
try {
ImageIO.write(thumb, "png", new File(path))
debug(s"Wrote thumbnail to $path")
} catch {
case ioe: IOException => error(s"Unable to save thumbnail to $path", ioe)
}
// return path
path
}
def convertToImageHashDTO(imageHashVO: ImageHashVO): ImageHash = {
new ImageHash(imageHashVO.ahash, imageHashVO.dhash, imageHashVO.phash, imageHashVO.fileHash)
}
def deleteImage(image: Image) = {
debug(s"Attempting to delete all traces of image: ${image.getImagePath}")
try {
val imageFile = new File(image.imagePath)
//try to delete the file
imageFile.delete()
//purge the file from the database and cache
this.imageCache.remove(imageFile.getAbsolutePath)
this.imageDAO.delete(image)
} catch {
case se: SecurityException => error(s"Unable to delete file: ${image.getImagePath} due to a security exception", se)
case ise: IllegalStateException => error(s"Unable to delete file: ${image.getImagePath} due to an illegal state exception", ise)
case he: HibernateException => error(s"Unable to delete file: ${image.getImagePath} due to a hibernate exception", he)
}
}
def deleteImage(image: Image) = {
debug(s"Attempting to delete all traces of image: ${image.getImagePath}")
try {
val imageFile = new File(image.imagePath)
//try to delete the file
imageFile.delete()
//purge the file from the database and cache
this.imageCache.remove(imageFile.getAbsolutePath)
this.imageDAO.delete(image)
} catch {
case se: SecurityException => error(s"Unable to delete file: ${image.getImagePath} due to a security exception", se)
case ise: IllegalStateException => error(s"Unable to delete file: ${image.getImagePath} due to an illegal state exception", ise)
case he: HibernateException => error(s"Unable to delete file: ${image.getImagePath} due to a hibernate exception", he)
}
}
} }

56
engine/src/main/scala/com/sothr/imagetools/engine/image/SimilarImages.scala

@ -3,41 +3,41 @@ package com.sothr.imagetools.engine.image
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
/** /**
* Similar Image payload class
*
* Created by drew on 1/26/14.
*/
* Similar Image payload class
*
* Created by drew on 1/26/14.
*/
class SimilarImages(var similarImages: Set[Image]) extends Logging { class SimilarImages(var similarImages: Set[Image]) extends Logging {
override def hashCode: Int = {
val prime = 7
var result = prime * similarImages.size
for (similarImage <- similarImages) {
result = prime * result + similarImage.hashCode
}
result
}
override def toString: String = {
s"""Similar Images:
override def hashCode: Int = {
val prime = 7
var result = prime * similarImages.size
for (similarImage <- similarImages) {
result = prime * result + similarImage.hashCode
}
result
}
override def toString: String = {
s"""Similar Images:
$getPrettySimilarImagesList""".stripMargin $getPrettySimilarImagesList""".stripMargin
}
}
protected def getPrettySimilarImagesList: String = {
val sb = new StringBuilder()
for (image <- similarImages) {
sb.append(image.imagePath)
sb.append(System.lineSeparator())
}
sb.toString()
}
protected def getPrettySimilarImagesList: String = {
val sb = new StringBuilder()
for (image <- similarImages) {
sb.append(image.imagePath)
sb.append(System.lineSeparator())
}
sb.toString()
}
def ordering() = {
1 * similarImages.size
}
def ordering() = {
1 * similarImages.size
}
} }
object SimilarImagesOrdering extends Ordering[SimilarImages] { object SimilarImagesOrdering extends Ordering[SimilarImages] {
def compare(a:SimilarImages, b:SimilarImages) = a.ordering() compare b.ordering()
def compare(a: SimilarImages, b: SimilarImages) = a.ordering() compare b.ordering()
} }

14
engine/src/main/scala/com/sothr/imagetools/engine/util/DirectoryFilter.scala

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

24
engine/src/main/scala/com/sothr/imagetools/engine/util/Hamming.scala

@ -2,17 +2,17 @@ package com.sothr.imagetools.engine.util
object Hamming { object Hamming {
/**
* Calculate the hamming distance between two longs
*
* @param hash1 The first hash to compare
* @param hash2 The second hash to compare
* @return
*/
def getDistance(hash1: Long, hash2: Long): Int = {
//The XOR of hash1 and hash2 is converted to a binary string
//then the number of '1's is counted. This is the hamming distance
(hash1 ^ hash2).toBinaryString.count(_ == '1')
}
/**
* Calculate the hamming distance between two longs
*
* @param hash1 The first hash to compare
* @param hash2 The second hash to compare
* @return
*/
def getDistance(hash1: Long, hash2: Long): Int = {
//The XOR of hash1 and hash2 is converted to a binary string
//then the number of '1's is counted. This is the hamming distance
(hash1 ^ hash2).toBinaryString.count(_ == '1')
}
} }

226
engine/src/main/scala/com/sothr/imagetools/engine/util/PropertiesService.scala

@ -12,118 +12,118 @@ import grizzled.slf4j.Logging
*/ */
object PropertiesService extends Logging { object PropertiesService extends Logging {
//OS information
val OS: String = System.getProperty("os.name", "UNKNOWN")
val OS_VERSION: String = System.getProperty("os.version", "UNKNOWN")
val OS_ARCH: String = System.getProperty("os.arch", "UNKNOWN")
private val newUserConf: Properties = new Properties()
private val configRenderOptions = ConfigRenderOptions.concise().setFormatted(true)
//specific highly used properties
var TimingEnabled: Boolean = false
var aHashSettings = new HashSetting("AHash", false, 0, 0, 0.0f)
var dHashSettings = new HashSetting("DHash", false, 0, 0, 0.0f)
var pHashSettings = new HashSetting("PHash", false, 0, 0, 0.0f)
private var defaultConf: Config = _
private var userConf: Config = _
private var version: Version = _
def getVersion: Version = this.version
/*
* Load the properties file from the specified location
*/
def loadProperties(defaultLocation: String, userLocation: String = null) = {
info(s"Attempting to load properties from: $defaultLocation")
defaultConf = ConfigFactory.load(defaultLocation)
if (userLocation != null) {
userConf = ConfigFactory.parseFile(new File(userLocation))
} else {
userConf = ConfigFactory.empty
info("No user properties file exists to load from")
}
version = new Version(get(PropertyEnum.Version.toString))
info(s"Detected Version: $version")
//load special properties
TimingEnabled = get(PropertyEnum.Timed.toString).toBoolean
aHashSettings = new HashSetting(
"AHash",
get(PropertyEnum.UseAhash.toString).toBoolean,
get(PropertyEnum.AhashPrecision.toString).toInt,
get(PropertyEnum.AhashTolerance.toString).toInt,
get(PropertyEnum.AhashWeight.toString).toFloat
)
dHashSettings = new HashSetting(
"DHash",
get(PropertyEnum.UseDhash.toString).toBoolean,
get(PropertyEnum.DhashPrecision.toString).toInt,
get(PropertyEnum.DhashTolerance.toString).toInt,
get(PropertyEnum.DhashWeight.toString).toFloat
)
pHashSettings = new HashSetting(
"PHash",
get(PropertyEnum.UsePhash.toString).toBoolean,
get(PropertyEnum.PhashPrecision.toString).toInt,
get(PropertyEnum.PhashTolerance.toString).toInt,
get(PropertyEnum.PhashWeight.toString).toFloat
)
info("Loaded Special Properties")
}
def get(key: String, defaultValue: String = null): String = {
var result: String = defaultValue
//check the latest properties
if (newUserConf.containsKey(key)) {
result = newUserConf.getProperty(key)
}
//check the loaded user properties
else if (userConf.hasPath(key)) {
result = userConf.getString(key)
}
//check the default properties
else if (defaultConf.hasPath(key)) {
result = defaultConf.getString(key)
}
result
}
def saveConf(location: String) = {
info(s"Saving user properties to $location")
val out: PrintStream = new PrintStream(new FileOutputStream(location, false))
val userConfToSave = getCleanedMergedUserConf
//print to the output stream
out.print(userConfToSave.root().render(configRenderOptions))
out.flush()
out.close()
}
private def getCleanedMergedUserConf: Config = {
ConfigFactory.parseProperties(cleanAndPrepareNewUserProperties()) withFallback userConf
}
private def cleanAndPrepareNewUserProperties(): Properties = {
//insert special keys here
newUserConf.setProperty(PropertyEnum.PreviousVersion.toString, version.parsableToString())
//remove special keys here
newUserConf.remove(PropertyEnum.Version.toString)
newUserConf
}
def has(key: String): Boolean = {
var result = false
if (newUserConf.containsKey(key)
|| userConf.hasPath(key)
|| defaultConf.hasPath(key)) {
result = true
}
result
}
def set(key: String, value: String) = {
newUserConf.setProperty(key, value)
}
//OS information
val OS: String = System.getProperty("os.name", "UNKNOWN")
val OS_VERSION: String = System.getProperty("os.version", "UNKNOWN")
val OS_ARCH: String = System.getProperty("os.arch", "UNKNOWN")
private val newUserConf: Properties = new Properties()
private val configRenderOptions = ConfigRenderOptions.concise().setFormatted(true)
//specific highly used properties
var TimingEnabled: Boolean = false
var aHashSettings = new HashSetting("AHash", false, 0, 0, 0.0f)
var dHashSettings = new HashSetting("DHash", false, 0, 0, 0.0f)
var pHashSettings = new HashSetting("PHash", false, 0, 0, 0.0f)
private var defaultConf: Config = _
private var userConf: Config = _
private var version: Version = _
def getVersion: Version = this.version
/*
* Load the properties file from the specified location
*/
def loadProperties(defaultLocation: String, userLocation: String = null) = {
info(s"Attempting to load properties from: $defaultLocation")
defaultConf = ConfigFactory.load(defaultLocation)
if (userLocation != null) {
userConf = ConfigFactory.parseFile(new File(userLocation))
} else {
userConf = ConfigFactory.empty
info("No user properties file exists to load from")
}
version = new Version(get(PropertyEnum.Version.toString))
info(s"Detected Version: $version")
//load special properties
TimingEnabled = get(PropertyEnum.Timed.toString).toBoolean
aHashSettings = new HashSetting(
"AHash",
get(PropertyEnum.UseAhash.toString).toBoolean,
get(PropertyEnum.AhashPrecision.toString).toInt,
get(PropertyEnum.AhashTolerance.toString).toInt,
get(PropertyEnum.AhashWeight.toString).toFloat
)
dHashSettings = new HashSetting(
"DHash",
get(PropertyEnum.UseDhash.toString).toBoolean,
get(PropertyEnum.DhashPrecision.toString).toInt,
get(PropertyEnum.DhashTolerance.toString).toInt,
get(PropertyEnum.DhashWeight.toString).toFloat
)
pHashSettings = new HashSetting(
"PHash",
get(PropertyEnum.UsePhash.toString).toBoolean,
get(PropertyEnum.PhashPrecision.toString).toInt,
get(PropertyEnum.PhashTolerance.toString).toInt,
get(PropertyEnum.PhashWeight.toString).toFloat
)
info("Loaded Special Properties")
}
def get(key: String, defaultValue: String = null): String = {
var result: String = defaultValue
//check the latest properties
if (newUserConf.containsKey(key)) {
result = newUserConf.getProperty(key)
}
//check the loaded user properties
else if (userConf.hasPath(key)) {
result = userConf.getString(key)
}
//check the default properties
else if (defaultConf.hasPath(key)) {
result = defaultConf.getString(key)
}
result
}
def saveConf(location: String) = {
info(s"Saving user properties to $location")
val out: PrintStream = new PrintStream(new FileOutputStream(location, false))
val userConfToSave = getCleanedMergedUserConf
//print to the output stream
out.print(userConfToSave.root().render(configRenderOptions))
out.flush()
out.close()
}
private def getCleanedMergedUserConf: Config = {
ConfigFactory.parseProperties(cleanAndPrepareNewUserProperties()) withFallback userConf
}
private def cleanAndPrepareNewUserProperties(): Properties = {
//insert special keys here
newUserConf.setProperty(PropertyEnum.PreviousVersion.toString, version.parsableToString())
//remove special keys here
newUserConf.remove(PropertyEnum.Version.toString)
newUserConf
}
def has(key: String): Boolean = {
var result = false
if (newUserConf.containsKey(key)
|| userConf.hasPath(key)
|| defaultConf.hasPath(key)) {
result = true
}
result
}
def set(key: String, value: String) = {
newUserConf.setProperty(key, value)
}
} }

56
engine/src/main/scala/com/sothr/imagetools/engine/util/PropertyEnum.scala

@ -1,32 +1,32 @@
package com.sothr.imagetools.engine.util package com.sothr.imagetools.engine.util
object PropertyEnum 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 ConcurrentSimilarityLimit = Value("app.engine.concurrent.similarity.limit")
val ConcurrentProcessingLimit = Value("app.engine.concurrent.processing.limit")
//default image settings
val ImageDifferenceThreshold = Value("app.image.differenceThreshold")
val HashPrecision = Value("app.image.hash.precision")
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.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.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.tolerance")
//Default Thumbnail Settings
val ThumbnailDirectory = Value("app.thumbnail.directory")
val ThumbnailSize = Value("app.thumbnail.size")
//Default Database Settings
val DatabaseConnectionURL = Value("app.database.connectionURL")
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 ConcurrentSimilarityLimit = Value("app.engine.concurrent.similarity.limit")
val ConcurrentProcessingLimit = Value("app.engine.concurrent.processing.limit")
//default image settings
val ImageDifferenceThreshold = Value("app.image.differenceThreshold")
val HashPrecision = Value("app.image.hash.precision")
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.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.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.tolerance")
//Default Thumbnail Settings
val ThumbnailDirectory = Value("app.thumbnail.directory")
val ThumbnailSize = Value("app.thumbnail.size")
//Default Database Settings
val DatabaseConnectionURL = Value("app.database.connectionURL")
} }

49
engine/src/main/scala/com/sothr/imagetools/engine/util/Timing.scala

@ -4,32 +4,31 @@ import grizzled.slf4j.Logging
trait Timing extends Logging { trait Timing extends Logging {
def time[R](block: => R): R = {
val t0 = System.currentTimeMillis
val result = block // call-by-name
val t1 = System.currentTimeMillis
debug("Elapsed time: " + (t1 - t0) + "ms")
result
}
def time[R](block: => R): R = {
val t0 = System.currentTimeMillis
val result = block // call-by-name
val t1 = System.currentTimeMillis
debug("Elapsed time: " + (t1 - t0) + "ms")
result
}
def getTime[R](block: => R): Long = {
val t0 = System.currentTimeMillis
val result = block // call-by-name
val t1 = System.currentTimeMillis
debug("Elapsed time: " + (t1 - t0) + "ms")
t1 - t0
}
def getTime[R](block: => R): Long = {
val t0 = System.currentTimeMillis
val result = block // call-by-name
val t1 = System.currentTimeMillis
debug("Elapsed time: " + (t1 - t0) + "ms")
t1 - t0
}
def getMean(times: Long*): Long = {
getMean(times.toArray[Long])
}
def getMean(times: Array[Long]): Long = {
var ag = 0L
for (i <- times.indices) {
ag += times(i)
}
ag / times.length
}
def getMean(times: Long*): Long = {
getMean(times.toArray[Long])
}
def getMean(times: Array[Long]): Long = {
var ag = 0L
for (i <- times.indices) {
ag += times(i)
}
ag / times.length
}
} }

162
engine/src/main/scala/com/sothr/imagetools/engine/util/Version.scala

@ -3,90 +3,90 @@ package com.sothr.imagetools.engine.util
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
/** /**
* Class to handle version detection and evaluation
*
* Created by drew on 1/6/14.
*/
* Class to handle version detection and evaluation
*
* 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
//typical version string i.e. 0.1.0-DEV-27-060aec7
val (major, minor, patch, buildTag, buildNumber, buildHash) = {
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( """-""")
version = (splitVersion(0).toInt, splitVersion(1).toInt, splitType(0).toInt, splitType(1), splitType(2).toInt, splitType(3))
} catch {
case nfe: NumberFormatException => error(s"Error parsing number from version string '$versionString'", nfe)
case e: Exception => error(s"Unexpected error parsing version string '$versionString'", e)
}
version
}
//parse version into parts
//typical version string i.e. 0.1.0-DEV-27-060aec7
val (major, minor, patch, buildTag, buildNumber, buildHash) = {
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( """-""")
version = (splitVersion(0).toInt, splitVersion(1).toInt, splitType(0).toInt, splitType(1), splitType(2).toInt, splitType(3))
} catch {
case nfe: NumberFormatException => error(s"Error parsing number from version string '$versionString'", nfe)
case e: Exception => error(s"Unexpected error parsing version string '$versionString'", e)
}
version
}
/*
* -3 = this.patch < that.patch
* -2 = this.minor < that.minor
* -1 = this.major < that.major
* 0 = Identical Versions
* 1 = this.major > that.major
* 2 = this.minor > that.minor
* 3 = this.patch > that.patch
* 4 = this.buildTag != that.buildTag
*/
def compare(that: Version): Integer = {
//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) {
-1
// major is the same
} else {
// This is at least a minor version ahead
if (this.minor > that.minor) {
2
// This is at least a minor version behind
} else if (this.minor < that.minor) {
-2
// major.minor are the same
} else {
// This is at least a patch version ahead
if (this.patch > that.patch) {
3
// This is at least a patch version version
} else if (this.patch < that.patch) {
-3
//major.minor.patch are all the same
} else {
// This is a different build
if (this.buildTag != that.buildTag) {
4
}
//should be caught by the first if, but in case not
0
}
}
}
}
/*
* -3 = this.patch < that.patch
* -2 = this.minor < that.minor
* -1 = this.major < that.major
* 0 = Identical Versions
* 1 = this.major > that.major
* 2 = this.minor > that.minor
* 3 = this.patch > that.patch
* 4 = this.buildTag != that.buildTag
*/
def compare(that: Version): Integer = {
//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) {
-1
// major is the same
} else {
// This is at least a minor version ahead
if (this.minor > that.minor) {
2
// This is at least a minor version behind
} else if (this.minor < that.minor) {
-2
// major.minor are the same
} else {
// This is at least a patch version ahead
if (this.patch > that.patch) {
3
// This is at least a patch version version
} else if (this.patch < that.patch) {
-3
//major.minor.patch are all the same
} else {
// This is a different build
if (this.buildTag != that.buildTag) {
4
}
//should be caught by the first if, but in case not
0
}
}
}
}
override def hashCode(): Int = {
val prime: Int = 37
val result: Int = 255
var hash: Int = major
hash += minor
hash += patch
hash += buildTag.hashCode
prime * result + hash
}
override def hashCode(): Int = {
val prime: Int = 37
val result: Int = 255
var hash: Int = major
hash += minor
hash += patch
hash += buildTag.hashCode
prime * result + hash
}
def parsableToString(): String = {
s"$major.$minor.$patch-$buildTag-$buildNumber-$buildHash"
}
def parsableToString(): String = {
s"$major.$minor.$patch-$buildTag-$buildNumber-$buildHash"
}
override def toString: String = {
s"$major.$minor.$patch-$buildTag build:$buildNumber code:$buildHash"
}
override def toString: String = {
s"$major.$minor.$patch-$buildTag build:$buildNumber code:$buildHash"
}
} }

74
engine/src/main/scala/com/sothr/imagetools/engine/vo/ImageHashVO.scala

@ -8,55 +8,55 @@ import grizzled.slf4j.Logging
@Table(name = "ImageHash") @Table(name = "ImageHash")
class ImageHashVO(var ahash: Long, var dhash: Long, var phash: Long, var fileHash: String) extends Serializable with Logging { class ImageHashVO(var ahash: Long, var dhash: Long, var phash: Long, var fileHash: String) extends Serializable with Logging {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
var id: Int = _
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
var id: Int = _
def this() = this(0l, 0l, 0l, "")
def this() = this(0l, 0l, 0l, "")
def getId: Int = id
def getId: Int = id
def setId(newId: Int) = {
id = newId
}
def setId(newId: Int) = {
id = newId
}
def getAhash: Long = ahash
def getAhash: Long = ahash
def setAhash(hash: Long) = {
ahash = hash
}
def setAhash(hash: Long) = {
ahash = hash
}
def getDhash: Long = dhash
def getDhash: Long = dhash
def setDhash(hash: Long) = {
dhash = hash
}
def setDhash(hash: Long) = {
dhash = hash
}
def getPhash: Long = phash
def getPhash: Long = phash
def setPhash(hash: Long) = {
phash = hash
}
def setPhash(hash: Long) = {
phash = hash
}
def getFileHash: String = fileHash
def getFileHash: String = fileHash
def setFileHash(hash: String) = {
fileHash = hash
}
def setFileHash(hash: String) = {
fileHash = hash
}
def cloneHashes: ImageHashVO = {
new ImageHashVO(ahash, dhash, phash, fileHash)
}
def cloneHashes: ImageHashVO = {
new ImageHashVO(ahash, dhash, phash, fileHash)
}
override def hashCode(): Int = {
var result = 365
result = 41 * result + (this.ahash ^ (this.ahash >>> 32)).toInt
result = 37 * result + (this.dhash ^ (this.dhash >>> 32)).toInt
result = 2 * result + (this.phash ^ (this.phash >>> 32)).toInt
result
}
override def hashCode(): Int = {
var result = 365
result = 41 * result + (this.ahash ^ (this.ahash >>> 32)).toInt
result = 37 * result + (this.dhash ^ (this.dhash >>> 32)).toInt
result = 2 * result + (this.phash ^ (this.phash >>> 32)).toInt
result
}
override def toString: String = {
s"fileHash: $fileHash ahash: $ahash dhash: $dhash phash: $phash"
}
override def toString: String = {
s"fileHash: $fileHash ahash: $ahash dhash: $dhash phash: $phash"
}
} }

40
engine/src/test/java/com/sothr/imagetools/engine/AppTest.java

@ -8,26 +8,26 @@ import junit.framework.TestSuite;
* Unit test for simple App. * Unit test for simple App.
*/ */
public class AppTest extends TestCase { public class AppTest extends TestCase {
/**
* Create the test case
*
* @param testName name of the test case
*/
public AppTest(String testName) {
super(testName);
}
/**
* Create the test case
*
* @param testName name of the test case
*/
public AppTest(String testName) {
super(testName);
}
/**
* @return the suite of tests being tested
*/
public static Test suite() {
return new TestSuite(AppTest.class);
}
/**
* @return the suite of tests being tested
*/
public static Test suite() {
return new TestSuite(AppTest.class);
}
/**
* Rigourous Test :-)
*/
public void testApp() {
assertTrue(true);
}
/**
* Rigourous Test :-)
*/
public void testApp() {
assertTrue(true);
}
} }

12
engine/src/test/resources/ehcache.xml

@ -1,8 +1,8 @@
<ehcache maxBytesLocalHeap="32M"> <ehcache maxBytesLocalHeap="32M">
<cache name="images"
timeToLiveSeconds="100">
</cache>
<cache name="thumbnails"
timeToLiveSeconds="100">
</cache>
<cache name="images"
timeToLiveSeconds="100">
</cache>
<cache name="thumbnails"
timeToLiveSeconds="100">
</cache>
</ehcache> </ehcache>

74
engine/src/test/resources/hibernate.cfg.xml

@ -1,48 +1,48 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC <!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
"-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration> <hibernate-configuration>
<!-- a SessionFactory instance listed as /jndi/name -->
<session-factory>
<!-- a SessionFactory instance listed as /jndi/name -->
<session-factory>
<!-- properties -->
<property name="hibernate.connection.driver_class">org.h2.Driver</property>
<property name="hibernate.dialect">org.hibernate.dialect.H2Dialect</property>
<property name="hibernate.show_sql">true</property>
<!--<property name="transaction.factory_class">
org.hibernate.transaction.JTATransactionFactory
</property>-->
<property name="jta.UserTransaction">java:comp/UserTransaction</property>
<!-- properties -->
<property name="hibernate.connection.driver_class">org.h2.Driver</property>
<property name="hibernate.dialect">org.hibernate.dialect.H2Dialect</property>
<property name="hibernate.show_sql">true</property>
<!--<property name="transaction.factory_class">
org.hibernate.transaction.JTATransactionFactory
</property>-->
<property name="jta.UserTransaction">java:comp/UserTransaction</property>
<property name="hibernate.hbm2ddl.auto">create</property>
<property name="hibernate.hbm2ddl.auto">create</property>
<!-- Enable Hibernate's automatic session context management -->
<property name="current_session_context_class">thread</property>
<!-- Enable Hibernate's automatic session context management -->
<property name="current_session_context_class">thread</property>
<!-- Enable the second-level cache -->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory
</property>
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory
</property>
<property name="hibernate.cache.use_second_level_cache">true</property>
<!-- Enable the second-level cache -->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory
</property>
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory
</property>
<property name="hibernate.cache.use_second_level_cache">true</property>
<property name="c3p0.acquire_increment">1</property>
<property name="c3p0.idle_test_period">100</property>
<!-- seconds -->
<property name="c3p0.max_size">50</property>
<property name="c3p0.max_statements">0</property>
<property name="c3p0.min_size">5</property>
<property name="c3p0.timeout">100</property>
<!-- seconds -->
<property name="c3p0.acquire_increment">1</property>
<property name="c3p0.idle_test_period">100</property>
<!-- seconds -->
<property name="c3p0.max_size">50</property>
<property name="c3p0.max_statements">0</property>
<property name="c3p0.min_size">5</property>
<property name="c3p0.timeout">100</property>
<!-- seconds -->
<!-- mapping files -->
<mapping resource="hibernate/Image.hbm.xml"/>
<mapping resource="hibernate/ImageHash.hbm.xml"/>
<!-- mapping files -->
<mapping resource="hibernate/Image.hbm.xml"/>
<mapping resource="hibernate/ImageHash.hbm.xml"/>
<!-- cache settings -->
<!--<class-cache class="org.hibernate.auction.Item" usage="read-write"/>
<class-cache class="org.hibernate.auction.Bid" usage="read-only"/>
<collection-cache collection="org.hibernate.auction.Item.bids" usage="read-write"/>-->
</session-factory>
<!-- cache settings -->
<!--<class-cache class="org.hibernate.auction.Item" usage="read-write"/>
<class-cache class="org.hibernate.auction.Bid" usage="read-only"/>
<collection-cache collection="org.hibernate.auction.Item.bids" usage="read-write"/>-->
</session-factory>
</hibernate-configuration> </hibernate-configuration>

26
engine/src/test/resources/hibernate/Image.hbm.xml

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC <!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping> <hibernate-mapping>
<class name="com.sothr.imagetools.engine.image.Image" table="Image">
<meta attribute="class-description">
This class contains the image hashes and meta data
</meta>
<id name="imagePath" type="string" column="path"/>
<property name="thumbnailPath" column="thumbnail_path" type="string" not-null="true"/>
<property name="width" column="width" type="int" not-null="true"/>
<property name="height" column="height" type="int" not-null="true"/>
<many-to-one name="hashes" column="hashes" unique="true" class="com.sothr.imagetools.engine.vo.ImageHashVO"
cascade="save-update, delete" not-null="true" lazy="false"/>
</class>
<class name="com.sothr.imagetools.engine.image.Image" table="Image">
<meta attribute="class-description">
This class contains the image hashes and meta data
</meta>
<id name="imagePath" type="string" column="path"/>
<property name="thumbnailPath" column="thumbnail_path" type="string" not-null="true"/>
<property name="width" column="width" type="int" not-null="true"/>
<property name="height" column="height" type="int" not-null="true"/>
<many-to-one name="hashes" column="hashes" unique="true" class="com.sothr.imagetools.engine.vo.ImageHashVO"
cascade="save-update, delete" not-null="true" lazy="false"/>
</class>
</hibernate-mapping> </hibernate-mapping>

28
engine/src/test/resources/hibernate/ImageHash.hbm.xml

@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC <!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping> <hibernate-mapping>
<class name="com.sothr.imagetools.engine.vo.ImageHashVO" table="ImageHash">
<meta attribute="class-description">
This class contains the image hashes
</meta>
<id name="id" type="int" column="id">
<generator class="native"/>
</id>
<property name="ahash" column="ahash" type="long"/>
<property name="dhash" column="dhash" type="long"/>
<property name="phash" column="phash" type="long"/>
<property name="fileHash" column="fileHash" type="string"/>
</class>
<class name="com.sothr.imagetools.engine.vo.ImageHashVO" table="ImageHash">
<meta attribute="class-description">
This class contains the image hashes
</meta>
<id name="id" type="int" column="id">
<generator class="native"/>
</id>
<property name="ahash" column="ahash" type="long"/>
<property name="dhash" column="dhash" type="long"/>
<property name="phash" column="phash" type="long"/>
<property name="fileHash" column="fileHash" type="string"/>
</class>
</hibernate-mapping> </hibernate-mapping>

112
engine/src/test/resources/logback-minimum-config.xml

@ -1,59 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<configuration> <configuration>
<appender name="DL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>ImageTools.debug</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.debug.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>5MB</MaxFileSize>
</triggeringPolicy>
</appender>
<appender name="IL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>ImageTools.info</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.info.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>500KB</MaxFileSize>
</triggeringPolicy>
</appender>
<appender name="EL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>ImageTools.err</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%.16thread] [%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.err.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>500KB</MaxFileSize>
</triggeringPolicy>
</appender>
<root level="INFO">
<appender-ref ref="DL"/>
<appender-ref ref="IL"/>
<appender-ref ref="EL"/>
</root>
<appender name="DL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>ImageTools.debug</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.debug.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>5MB</MaxFileSize>
</triggeringPolicy>
</appender>
<appender name="IL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>ImageTools.info</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.info.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>500KB</MaxFileSize>
</triggeringPolicy>
</appender>
<appender name="EL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>ImageTools.err</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%.16thread] [%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.err.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>500KB</MaxFileSize>
</triggeringPolicy>
</appender>
<root level="INFO">
<appender-ref ref="DL"/>
<appender-ref ref="IL"/>
<appender-ref ref="EL"/>
</root>
</configuration> </configuration>

6
engine/src/test/scala/com/sothr/imagetools/engine/BaseTest.scala

@ -6,8 +6,8 @@ import org.scalatest.{BeforeAndAfter, FunSuite, Inside, Inspectors, Matchers, Op
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 {
before {
AppConfig.configureApp()
}
before {
AppConfig.configureApp()
}
} }

72
engine/src/test/scala/com/sothr/imagetools/engine/EngineTest.scala

@ -1,44 +1,44 @@
package com.sothr.imagetools.engine package com.sothr.imagetools.engine
/** /**
* Basic Test of the engines
*
* Created by drew on 1/26/14.
*/
* Basic Test of the engines
*
* Created by drew on 1/26/14.
*/
class EngineTest extends BaseTest { class EngineTest extends BaseTest {
test("SequentialEngine Test getImagesForDirectory for sample directory") {
val engine: Engine = new SequentialEngine()
assertResult(3) {
engine.getImagesForDirectory("sample").length
}
}
test("SequentialEngine Test getImagesForDirectory for sample directory") {
val engine: Engine = new SequentialEngine()
assertResult(3) {
engine.getImagesForDirectory("sample").length
}
}
test("SequentialEngine Test getSimilarImagesForDirectory for sample directory") {
val engine = new SequentialEngine()
val similarImages = engine.getSimilarImagesForDirectory("sample")
assertResult(1) {
similarImages.length
}
assertResult(3) {
similarImages(0).similarImages.size
}
}
test("SequentialEngine Test getSimilarImagesForDirectory for sample directory") {
val engine = new SequentialEngine()
val similarImages = engine.getSimilarImagesForDirectory("sample")
assertResult(1) {
similarImages.length
}
assertResult(3) {
similarImages(0).similarImages.size
}
}
test("ConcurrentEngine Test getImagesForDirectory for sample directory") {
val engine: Engine = new ConcurrentEngine()
assertResult(3) {
engine.getImagesForDirectory("sample").length
}
}
test("ConcurrentEngine Test getImagesForDirectory for sample directory") {
val engine: Engine = new ConcurrentEngine()
assertResult(3) {
engine.getImagesForDirectory("sample").length
}
}
test("ConcurrentEngine Test getSimilarImagesForDirectory for sample directory") {
val engine = new ConcurrentEngine()
val similarImages = engine.getSimilarImagesForDirectory("sample")
assertResult(1) {
similarImages.length
}
assertResult(3) {
similarImages(0).similarImages.size
}
}
test("ConcurrentEngine Test getSimilarImagesForDirectory for sample directory") {
val engine = new ConcurrentEngine()
val similarImages = engine.getSimilarImagesForDirectory("sample")
assertResult(1) {
similarImages.length
}
assertResult(3) {
similarImages(0).similarImages.size
}
}
} }

6
engine/src/test/scala/com/sothr/imagetools/engine/ScalaAppTest.scala

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

6
engine/src/test/scala/com/sothr/imagetools/engine/TestParams.scala

@ -1,7 +1,7 @@
package com.sothr.imagetools.engine package com.sothr.imagetools.engine
object TestParams { object TestParams {
val LargeSampleImage1 = "sample/sample_01_large.jpg"
val MediumSampleImage1 = "sample/sample_01_medium.jpg"
val SmallSampleImage1 = "sample/sample_01_small.jpg"
val LargeSampleImage1 = "sample/sample_01_large.jpg"
val MediumSampleImage1 = "sample/sample_01_medium.jpg"
val SmallSampleImage1 = "sample/sample_01_small.jpg"
} }

60
engine/src/test/scala/com/sothr/imagetools/engine/image/ImageFilterTest.scala

@ -5,38 +5,38 @@ import java.io.File
import com.sothr.imagetools.engine.BaseTest import com.sothr.imagetools.engine.BaseTest
/** /**
* Test to make sure that the image filters work
*
* Created by drew on 1/26/14.
*/
* Test to make sure that the image filters work
*
* Created by drew on 1/26/14.
*/
class ImageFilterTest extends BaseTest { class ImageFilterTest extends BaseTest {
test("Confirm ImageFilter Works") {
val filter: ImageFilter = new ImageFilter()
val bogusDirectory = new File(".")
assert(filter.accept(bogusDirectory, "test.png"))
assert(filter.accept(bogusDirectory, "test.bmp"))
assert(filter.accept(bogusDirectory, "test.gif"))
assert(filter.accept(bogusDirectory, "test.jpg"))
assert(filter.accept(bogusDirectory, "test.jpeg"))
assert(filter.accept(bogusDirectory, "test.jpeg.jpg"))
}
test("Confirm ImageFilter Works") {
val filter: ImageFilter = new ImageFilter()
val bogusDirectory = new File(".")
assert(filter.accept(bogusDirectory, "test.png"))
assert(filter.accept(bogusDirectory, "test.bmp"))
assert(filter.accept(bogusDirectory, "test.gif"))
assert(filter.accept(bogusDirectory, "test.jpg"))
assert(filter.accept(bogusDirectory, "test.jpeg"))
assert(filter.accept(bogusDirectory, "test.jpeg.jpg"))
}
test("Confirm ImageFiler Fails") {
val filter: ImageFilter = new ImageFilter()
val bogusDirectory = new File(".")
assertResult(false) {
filter.accept(bogusDirectory, "test")
}
assertResult(false) {
filter.accept(bogusDirectory, "test.mp4")
}
assertResult(false) {
filter.accept(bogusDirectory, "test.gif.mp4")
}
assertResult(false) {
filter.accept(bogusDirectory, "")
}
}
test("Confirm ImageFiler Fails") {
val filter: ImageFilter = new ImageFilter()
val bogusDirectory = new File(".")
assertResult(false) {
filter.accept(bogusDirectory, "test")
}
assertResult(false) {
filter.accept(bogusDirectory, "test.mp4")
}
assertResult(false) {
filter.accept(bogusDirectory, "test.gif.mp4")
}
assertResult(false) {
filter.accept(bogusDirectory, "")
}
}
} }

234
gui/pom.xml

@ -1,126 +1,126 @@
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.sothr.imagetools</groupId>
<artifactId>parent</artifactId>
<version>1.0.1</version>
<relativePath>../parent</relativePath>
</parent>
<parent>
<groupId>com.sothr.imagetools</groupId>
<artifactId>parent</artifactId>
<version>1.0.1</version>
<relativePath>../parent</relativePath>
</parent>
<artifactId>gui</artifactId>
<version>0.1.1</version>
<packaging>jar</packaging>
<artifactId>gui</artifactId>
<version>0.1.1</version>
<packaging>jar</packaging>
<name>ImageTools-GUI</name>
<description>The Graphical User Interface for Image-Tools</description>
<url>http://imagetools.sothr.com</url>
<organization>
<name>Sothr Software</name>
</organization>
<name>ImageTools-GUI</name>
<description>The Graphical User Interface for Image-Tools</description>
<url>http://imagetools.sothr.com</url>
<organization>
<name>Sothr Software</name>
</organization>
<dependencies>
<dependency>
<groupId>com.sothr.imagetools</groupId>
<artifactId>engine</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.clapper</groupId>
<artifactId>grizzled-slf4j_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
</dependency>
<dependency>
<groupId>org.commonjava.googlecode.markdown4j</groupId>
<artifactId>markdown4j</artifactId>
</dependency>
</dependencies>
<dependencies>
<dependency>
<groupId>com.sothr.imagetools</groupId>
<artifactId>engine</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.clapper</groupId>
<artifactId>grizzled-slf4j_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
</dependency>
<dependency>
<groupId>org.commonjava.googlecode.markdown4j</groupId>
<artifactId>markdown4j</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Packaging Configuration -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>com.sothr.imagetools.ui.App</mainClass>
</manifest>
</archive>
<outputDirectory>
${project.build.directory}/release
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<id>prepare</id>
<phase>process-resources</phase>
<configuration>
<tasks>
<copy file="${project.build.directory}/version.info" toFile="${basedir}/version.info"
overwrite="true"/>
<copy file="${project.build.directory}/name.info" toFile="${basedir}/name.info"
overwrite="true"/>
<copy file="${project.build.directory}/LICENSE" toFile="${basedir}/LICENSE"
overwrite="true"/>
<chmod file="${project.build.directory}/startGUI.sh" perm="755"/>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
<execution>
<id>package</id>
<phase>package</phase>
<configuration>
<tasks>
<!-- set permissions on run files -->
<chmod file="${project.build.directory}/release/startGUI.sh" perm="755"/>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<build>
<plugins>
<!-- Packaging Configuration -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>com.sothr.imagetools.ui.App</mainClass>
</manifest>
</archive>
<outputDirectory>
${project.build.directory}/release
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<id>prepare</id>
<phase>process-resources</phase>
<configuration>
<tasks>
<copy file="${project.build.directory}/version.info" toFile="${basedir}/version.info"
overwrite="true"/>
<copy file="${project.build.directory}/name.info" toFile="${basedir}/name.info"
overwrite="true"/>
<copy file="${project.build.directory}/LICENSE" toFile="${basedir}/LICENSE"
overwrite="true"/>
<chmod file="${project.build.directory}/startGUI.sh" perm="755"/>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
<execution>
<id>package</id>
<phase>package</phase>
<configuration>
<tasks>
<!-- set permissions on run files -->
<chmod file="${project.build.directory}/release/startGUI.sh" perm="755"/>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project> </project>

144
gui/src/includes/logback.xml

@ -1,75 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<configuration> <configuration>
<logger name="org.hibernate" level="WARN"/>
<logger name="net.sf.ehcache" level="WARN"/>
<appender name="C" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- Sorry Windows Users -->
<withJansi>false</withJansi>
<pattern>[%date{HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<appender name="DL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.debug</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.debug.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>5MB</MaxFileSize>
</triggeringPolicy>
</appender>
<appender name="IL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.info</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.info.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>500KB</MaxFileSize>
</triggeringPolicy>
</appender>
<appender name="EL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.err</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%.16thread] [%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.err.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>500KB</MaxFileSize>
</triggeringPolicy>
</appender>
<root level="DEBUG">
<appender-ref ref="C"/>
<appender-ref ref="DL"/>
<appender-ref ref="IL"/>
<appender-ref ref="EL"/>
</root>
<logger name="org.hibernate" level="WARN"/>
<logger name="net.sf.ehcache" level="WARN"/>
<appender name="C" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- Sorry Windows Users -->
<withJansi>false</withJansi>
<pattern>[%date{HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<appender name="DL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.debug</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.debug.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>5MB</MaxFileSize>
</triggeringPolicy>
</appender>
<appender name="IL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.info</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.info.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>500KB</MaxFileSize>
</triggeringPolicy>
</appender>
<appender name="EL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--See also http://logback.qos.ch/manual/appenders.html#RollingFileAppender-->
<File>ImageTools.err</File>
<encoder>
<withJansi>false</withJansi>
<pattern>[%.16thread] [%date{yy-MM-dd HH:mm:ss}] %-5level [%c{16}] - %message%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>1</maxIndex>
<FileNamePattern>ImageTools.err.%i</FileNamePattern>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>500KB</MaxFileSize>
</triggeringPolicy>
</appender>
<root level="DEBUG">
<appender-ref ref="C"/>
<appender-ref ref="DL"/>
<appender-ref ref="IL"/>
<appender-ref ref="EL"/>
</root>
</configuration> </configuration>

136
gui/src/main/java/com/sothr/imagetools/ui/App.java

@ -24,78 +24,78 @@ import java.util.List;
*/ */
public class App extends Application { public class App extends Application {
private static final String MAINGUI_FXML = "fxml/mainapp/MainApp.fxml";
private static Logger logger;
private static final String MAINGUI_FXML = "fxml/mainapp/MainApp.fxml";
private static Logger logger;
public static void main(String[] args) {
AppConfig.configureApp();
public static void main(String[] args) {
AppConfig.configureApp();
try {
//try to run the UI
launch(args);
} catch (Exception ex) {
logger.error("A fatal error has occurred: ", ex);
//show popup about the error to the user then exit
}
}
try {
//try to run the UI
launch(args);
} catch (Exception ex) {
logger.error("A fatal error has occurred: ", ex);
//show popup about the error to the user then exit
}
}
@Override
public void init() throws Exception {
AppConfig.configureApp();
logger = LoggerFactory.getLogger(this.getClass());
logger.info("Initializing Image Tools");
List<String> parameters = this.getParameters().getRaw();
logger.info(String.format("Application was called with '%s' parameters", parameters.toString()));
super.init();
}
@Override
public void init() throws Exception {
AppConfig.configureApp();
logger = LoggerFactory.getLogger(this.getClass());
logger.info("Initializing Image Tools");
List<String> parameters = this.getParameters().getRaw();
logger.info(String.format("Application was called with '%s' parameters", parameters.toString()));
super.init();
}
@Override
public void start(Stage primaryStage) throws Exception {
logger.info("Image-Tools is starting");
logger.info(String.format("Launching GUI with FXML file %s", MAINGUI_FXML));
//store the primary stage globally for reference in popups and the like
AppConfig.setPrimaryStage(primaryStage);
try {
//Confirm we have the plugin for JPEG color fixes
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("JPEG");
while (readers.hasNext()) {
logger.info("Image Reader Plugin: [{}] Available", new Object[]{readers.next()});
}
@Override
public void start(Stage primaryStage) throws Exception {
logger.info("Image-Tools is starting");
logger.info(String.format("Launching GUI with FXML file %s", MAINGUI_FXML));
//store the primary stage globally for reference in popups and the like
AppConfig.setPrimaryStage(primaryStage);
try {
//Confirm we have the plugin for JPEG color fixes
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("JPEG");
while (readers.hasNext()) {
logger.info("Image Reader Plugin: [{}] Available", new Object[]{readers.next()});
}
FXMLLoader loader = new FXMLLoader();
URL location = ResourceLoader.get().getResource(MAINGUI_FXML);
loader.setLocation(location);
loader.setBuilderFactory(new JavaFXBuilderFactory());
Parent root = loader.load(location.openStream());
//save the primary controller
AppConfig.setFxmlLoader(loader);
primaryStage.setScene(new Scene(root));
//config main scene
primaryStage.setTitle("Image Tools");
primaryStage.setMinHeight(600.0);
primaryStage.setMinWidth(800.0);
primaryStage.setResizable(true);
//show main scene
primaryStage.show();
} catch (IOException ioe) {
String message = String.format("Unable to load FXML file: %s", MAINGUI_FXML);
ImageToolsException ite = new ImageToolsException(message, ioe);
logger.error(message, ioe);
throw ite;
} catch (Exception ex) {
String message = "An unhandled exception was thrown by the GUI";
ImageToolsException ite = new ImageToolsException(message, ex);
logger.error(message, ex);
throw ite;
}
}
FXMLLoader loader = new FXMLLoader();
URL location = ResourceLoader.get().getResource(MAINGUI_FXML);
loader.setLocation(location);
loader.setBuilderFactory(new JavaFXBuilderFactory());
Parent root = loader.load(location.openStream());
//save the primary controller
AppConfig.setFxmlLoader(loader);
primaryStage.setScene(new Scene(root));
//config main scene
primaryStage.setTitle("Image Tools");
primaryStage.setMinHeight(600.0);
primaryStage.setMinWidth(800.0);
primaryStage.setResizable(true);
//show main scene
primaryStage.show();
} catch (IOException ioe) {
String message = String.format("Unable to load FXML file: %s", MAINGUI_FXML);
ImageToolsException ite = new ImageToolsException(message, ioe);
logger.error(message, ioe);
throw ite;
} catch (Exception ex) {
String message = "An unhandled exception was thrown by the GUI";
ImageToolsException ite = new ImageToolsException(message, ex);
logger.error(message, ex);
throw ite;
}
}
@Override
public void stop() throws Exception {
logger.info("Image-Tools is shutting down");
AppConfig.shutdown();
super.stop();
//force the JVM to close
System.exit(0);
}
@Override
public void stop() throws Exception {
logger.info("Image-Tools is shutting down");
AppConfig.shutdown();
super.stop();
//force the JVM to close
System.exit(0);
}
} }

395
gui/src/main/resources/fxml/mainapp/MainApp.fxml

@ -2,196 +2,215 @@
<!--suppress ALL --> <!--suppress ALL -->
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.Pagination?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.FlowPane?>
<?import javafx.scene.layout.TilePane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?> <?import javafx.scene.text.Font?>
<AnchorPane xmlns:fx="http://javafx.com/fxml/1" fx:id="rootPane" minHeight="600.0" minWidth="1024.0" prefHeight="600.0" <AnchorPane xmlns:fx="http://javafx.com/fxml/1" fx:id="rootPane" minHeight="600.0" minWidth="1024.0" prefHeight="600.0"
prefWidth="1024.0" xmlns="http://javafx.com/javafx/8" prefWidth="1024.0" xmlns="http://javafx.com/javafx/8"
fx:controller="com.sothr.imagetools.ui.controller.AppController"> 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">
<menus>
<Menu mnemonicParsing="false" text="File">
<items>
<MenuItem fx:id="" mnemonicParsing="false" onAction="#closeAction" text="Close"/>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Edit">
<items>
<MenuItem mnemonicParsing="false" text="Settings"/>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Help">
<items>
<MenuItem mnemonicParsing="false" onAction="#aboutAction" text="About"/>
<MenuItem mnemonicParsing="false" onAction="#helpAction" text="Help Site"/>
</items>
</Menu>
</menus>
</MenuBar>
<VBox id="VBox" alignment="CENTER" spacing="5.0" AnchorPane.bottomAnchor="30.0" AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="30.0">
<children>
<SplitPane dividerPositions="0.2181996086105675" focusTraversable="true" prefHeight="569.0"
prefWidth="1024.0" visible="true" VBox.vgrow="ALWAYS">
<items>
<TabPane maxWidth="220.0" minHeight="0.0" minWidth="220.0" prefHeight="567.0" prefWidth="220.0"
tabClosingPolicy="UNAVAILABLE">
<tabs>
<Tab closable="false" text="Folders">
<content>
<BorderPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308"
minWidth="200.0" prefWidth="200.0">
<top>
<Button maxWidth="1.7976931348623157E308" minWidth="200.0"
mnemonicParsing="false" onAction="#browseFolders" text="Browse"
BorderPane.alignment="CENTER"/>
</top>
<center>
<FlowPane prefHeight="200.0" prefWidth="200.0"
BorderPane.alignment="CENTER">
<children>
<Label maxHeight="1.7976931348623157E308"
maxWidth="1.7976931348623157E308" minWidth="210.0"
text="Selected Folder:">
<padding>
<Insets left="5.0" right="5.0"/>
</padding>
</Label>
<Label fx:id="selectedDirectoryLabel" alignment="TOP_LEFT"
lineSpacing="2.0" maxHeight="1.7976931348623157E308"
maxWidth="210.0" minWidth="210.0" prefWidth="210.0"
text="&lt;SELECTED&gt;" wrapText="true">
<font>
<Font name="System Bold" size="12.0"/>
</font>
<padding>
<Insets left="5.0" right="5.0"/>
</padding>
</Label>
</children>
</FlowPane>
</center>
<bottom>
<FlowPane maxHeight="1.7976931348623157E308"
maxWidth="1.7976931348623157E308" prefHeight="60.0"
prefWidth="220.0" BorderPane.alignment="CENTER">
<children>
<CheckBox fx:id="doRecursiveProcessing" mnemonicParsing="false"
text="Recursive Search">
<FlowPane.margin>
<Insets bottom="5.0"/>
</FlowPane.margin>
</CheckBox>
<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>
<padding>
<Insets bottom="5.0" top="5.0"/>
</padding>
</BorderPane>
</content>
</Tab>
<Tab text="Tags">
<content>
<AnchorPane id="Content" minHeight="0.0" minWidth="0.0" prefHeight="180.0"
prefWidth="200.0">
<children>
<AnchorPane id="AnchorPane" maxHeight="50.0" prefHeight="50.0"
AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="5.0">
<children>
<TextField layoutY="0.0" prefWidth="200.0" text=""
AnchorPane.leftAnchor="20.0"
AnchorPane.rightAnchor="20.0"/>
<Button layoutY="27.0" mnemonicParsing="false" prefWidth="192.0"
text="Filter" AnchorPane.leftAnchor="20.0"
AnchorPane.rightAnchor="20.0"/>
</children>
</AnchorPane>
<ListView fx:id="tagListView" prefHeight="385.0" prefWidth="198.0"
AnchorPane.bottomAnchor="60.0" AnchorPane.leftAnchor="10.0"
AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="60.0"/>
<AnchorPane id="AnchorPane" prefWidth="192.0"
AnchorPane.bottomAnchor="5.0" AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0">
<children>
<Button layoutY="2.0" mnemonicParsing="false" prefWidth="192.0"
text="View All Images In Tags"
AnchorPane.leftAnchor="20.0"
AnchorPane.rightAnchor="20.0"/>
<Button layoutY="28.0" mnemonicParsing="false" prefWidth="192.0"
text="Search For Similarities In Tags"
AnchorPane.leftAnchor="20.0"
AnchorPane.rightAnchor="20.0"/>
</children>
</AnchorPane>
</children>
</AnchorPane>
</content>
</Tab>
</tabs>
</TabPane>
<VBox id="VBox" alignment="CENTER" minWidth="600.0" prefWidth="600.0" spacing="5.0">
<children>
<ToolBar maxHeight="30.0" minHeight="30.0" prefHeight="30.0" VBox.vgrow="ALWAYS">
<items>
<Label text="Current Directory:"/>
<Separator orientation="VERTICAL" prefHeight="200.0"/>
<Label fx:id="currentDirectoryLabel" text="&lt;CURRENT DIRECTORY&gt;"/>
</items>
<VBox.margin>
<Insets bottom="-5.0"/>
</VBox.margin>
</ToolBar>
<ScrollPane id="ScrollPane" fx: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="160.0" prefTileWidth="160.0"
prefWidth="-1.0" tileAlignment="TOP_LEFT" vgap="5.0"/>
</content>
<VBox.margin>
<Insets/>
</VBox.margin>
</ScrollPane>
<Pagination fx:id="paginator" disable="true" maxHeight="40.0" maxPageIndicatorCount="20"
maxWidth="1.7976931348623157E308" minHeight="40.0" pageCount="1"
prefHeight="40.0">
<opaqueInsets>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
</opaqueInsets>
</Pagination>
</children>
</VBox>
</items>
</SplitPane>
</children>
</VBox>
<ToolBar maxHeight="30.0" maxWidth="1.7976931348623157E308" minHeight="30.0" orientation="HORIZONTAL"
prefHeight="30.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0">
<items>
<Label text="Progress:"/>
<Separator orientation="VERTICAL" prefHeight="200.0"/>
<ProgressBar fx:id="progressBar" prefWidth="200.0" progress="0.0"/>
<Separator orientation="VERTICAL" prefHeight="200.0"/>
<Label fx:id="progressLabel" text="&lt;PROGRESS INFORMATION&gt;"/>
</items>
</ToolBar>
</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">
<menus>
<Menu mnemonicParsing="false" text="File">
<items>
<MenuItem fx:id="" mnemonicParsing="false" onAction="#closeAction" text="Close"/>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Edit">
<items>
<MenuItem mnemonicParsing="false" text="Settings"/>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Help">
<items>
<MenuItem mnemonicParsing="false" onAction="#aboutAction" text="About"/>
<MenuItem mnemonicParsing="false" onAction="#helpAction" text="Help Site"/>
</items>
</Menu>
</menus>
</MenuBar>
<VBox id="VBox" alignment="CENTER" spacing="5.0" AnchorPane.bottomAnchor="30.0" AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="30.0">
<children>
<SplitPane dividerPositions="0.2181996086105675" focusTraversable="true" prefHeight="569.0"
prefWidth="1024.0" visible="true" VBox.vgrow="ALWAYS">
<items>
<TabPane maxWidth="220.0" minHeight="0.0" minWidth="220.0" prefHeight="567.0" prefWidth="220.0"
tabClosingPolicy="UNAVAILABLE">
<tabs>
<Tab closable="false" text="Folders">
<content>
<BorderPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308"
minWidth="200.0" prefWidth="200.0">
<top>
<Button maxWidth="1.7976931348623157E308" minWidth="200.0"
mnemonicParsing="false" onAction="#browseFolders" text="Browse"
BorderPane.alignment="CENTER"/>
</top>
<center>
<FlowPane prefHeight="200.0" prefWidth="200.0"
BorderPane.alignment="CENTER">
<children>
<Label maxHeight="1.7976931348623157E308"
maxWidth="1.7976931348623157E308" minWidth="210.0"
text="Selected Folder:">
<padding>
<Insets left="5.0" right="5.0"/>
</padding>
</Label>
<Label fx:id="selectedDirectoryLabel" alignment="TOP_LEFT"
lineSpacing="2.0" maxHeight="1.7976931348623157E308"
maxWidth="210.0" minWidth="210.0" prefWidth="210.0"
text="&lt;SELECTED&gt;" wrapText="true">
<font>
<Font name="System Bold" size="12.0"/>
</font>
<padding>
<Insets left="5.0" right="5.0"/>
</padding>
</Label>
</children>
</FlowPane>
</center>
<bottom>
<FlowPane maxHeight="1.7976931348623157E308"
maxWidth="1.7976931348623157E308" prefHeight="60.0"
prefWidth="220.0" BorderPane.alignment="CENTER">
<children>
<CheckBox fx:id="doRecursiveProcessing" mnemonicParsing="false"
text="Recursive Search">
<FlowPane.margin>
<Insets bottom="5.0"/>
</FlowPane.margin>
</CheckBox>
<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>
<padding>
<Insets bottom="5.0" top="5.0"/>
</padding>
</BorderPane>
</content>
</Tab>
<Tab text="Tags">
<content>
<AnchorPane id="Content" minHeight="0.0" minWidth="0.0" prefHeight="180.0"
prefWidth="200.0">
<children>
<AnchorPane id="AnchorPane" maxHeight="50.0" prefHeight="50.0"
AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="5.0">
<children>
<TextField layoutY="0.0" prefWidth="200.0" text=""
AnchorPane.leftAnchor="20.0"
AnchorPane.rightAnchor="20.0"/>
<Button layoutY="27.0" mnemonicParsing="false" prefWidth="192.0"
text="Filter" AnchorPane.leftAnchor="20.0"
AnchorPane.rightAnchor="20.0"/>
</children>
</AnchorPane>
<ListView fx:id="tagListView" prefHeight="385.0" prefWidth="198.0"
AnchorPane.bottomAnchor="60.0" AnchorPane.leftAnchor="10.0"
AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="60.0"/>
<AnchorPane id="AnchorPane" prefWidth="192.0"
AnchorPane.bottomAnchor="5.0" AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0">
<children>
<Button layoutY="2.0" mnemonicParsing="false" prefWidth="192.0"
text="View All Images In Tags"
AnchorPane.leftAnchor="20.0"
AnchorPane.rightAnchor="20.0"/>
<Button layoutY="28.0" mnemonicParsing="false" prefWidth="192.0"
text="Search For Similarities In Tags"
AnchorPane.leftAnchor="20.0"
AnchorPane.rightAnchor="20.0"/>
</children>
</AnchorPane>
</children>
</AnchorPane>
</content>
</Tab>
</tabs>
</TabPane>
<VBox id="VBox" alignment="CENTER" minWidth="600.0" prefWidth="600.0" spacing="5.0">
<children>
<ToolBar maxHeight="30.0" minHeight="30.0" prefHeight="30.0" VBox.vgrow="ALWAYS">
<items>
<Label text="Current Directory:"/>
<Separator orientation="VERTICAL" prefHeight="200.0"/>
<Label fx:id="currentDirectoryLabel" text="&lt;CURRENT DIRECTORY&gt;"/>
</items>
<VBox.margin>
<Insets bottom="-5.0"/>
</VBox.margin>
</ToolBar>
<ScrollPane id="ScrollPane" fx: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="160.0" prefTileWidth="160.0"
prefWidth="-1.0" tileAlignment="TOP_LEFT" vgap="5.0"/>
</content>
<VBox.margin>
<Insets/>
</VBox.margin>
</ScrollPane>
<Pagination fx:id="paginator" disable="true" maxHeight="40.0" maxPageIndicatorCount="20"
maxWidth="1.7976931348623157E308" minHeight="40.0" pageCount="1"
prefHeight="40.0">
<opaqueInsets>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
</opaqueInsets>
</Pagination>
</children>
</VBox>
</items>
</SplitPane>
</children>
</VBox>
<ToolBar maxHeight="30.0" maxWidth="1.7976931348623157E308" minHeight="30.0" orientation="HORIZONTAL"
prefHeight="30.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0">
<items>
<Label text="Progress:"/>
<Separator orientation="VERTICAL" prefHeight="200.0"/>
<ProgressBar fx:id="progressBar" prefWidth="200.0" progress="0.0"/>
<Separator orientation="VERTICAL" prefHeight="200.0"/>
<Label fx:id="progressLabel" text="&lt;PROGRESS INFORMATION&gt;"/>
</items>
</ToolBar>
</children>
</AnchorPane> </AnchorPane>

184
gui/src/main/scala/com/sothr/imagetools/ui/component/ImageTile.scala

@ -13,109 +13,109 @@ import grizzled.slf4j.Logging
import resource._ import resource._
/** /**
* ImageTile class that is a special VBox
*
* Created by drew on 8/22/14.
*/
* ImageTile class that is a special VBox
*
* Created by drew on 8/22/14.
*/
class ImageTile(thumbnailWidth: Integer, class ImageTile(thumbnailWidth: Integer,
image: com.sothr.imagetools.engine.image.Image, image: com.sothr.imagetools.engine.image.Image,
imageTilePane: ImageTilePane) extends VBox with Logging { imageTilePane: ImageTilePane) extends VBox with Logging {
val thisTile = this
val imageData = image
val preferedTileWidth = (thumbnailWidth + 8).toDouble
val preferedTileHeight = (thumbnailWidth + 32).toDouble
//set tile size
this.setPrefSize(preferedTileWidth, preferedTileHeight)
this.setMinSize(preferedTileWidth, preferedTileHeight)
this.setMaxSize(preferedTileWidth, preferedTileHeight)
val thisTile = this
val imageData = image
val preferedTileWidth = (thumbnailWidth + 8).toDouble
val preferedTileHeight = (thumbnailWidth + 32).toDouble
//set tile size
this.setPrefSize(preferedTileWidth, preferedTileHeight)
this.setMinSize(preferedTileWidth, preferedTileHeight)
this.setMaxSize(preferedTileWidth, preferedTileHeight)
//set padding on the tiles
//this.setPadding(new Insets(10.0d,0.0d,10.0d,0.0d))
//set padding on the tiles
//this.setPadding(new Insets(10.0d,0.0d,10.0d,0.0d))
this.setAlignment(Pos.CENTER)
this.addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler[MouseEvent] {
override def handle(event: MouseEvent): Unit = {
if (event.isShiftDown) {
//multiple selection
imageTilePane.addImageSelected(thisTile)
//remove individual images with control
} else if (event.isControlDown) {
imageTilePane.removeImageSelected(thisTile)
}
else {
if (event.isPrimaryButtonDown) {
imageTilePane.imageSelected(thisTile)
//double click
if (event.getClickCount == 2) {
// Look into http://stackoverflow.com/questions/228477/how-do-i-programmatically-determine-operating-system-in-java
// for proper multi-platform opening support
FileUtil.openInEditor(new File(image.getImagePath))
} else {
this.setAlignment(Pos.CENTER)
this.addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler[MouseEvent] {
override def handle(event: MouseEvent): Unit = {
if (event.isShiftDown) {
//multiple selection
imageTilePane.addImageSelected(thisTile)
//remove individual images with control
} else if (event.isControlDown) {
imageTilePane.removeImageSelected(thisTile)
}
else {
if (event.isPrimaryButtonDown) {
imageTilePane.imageSelected(thisTile)
//double click
if (event.getClickCount == 2) {
// Look into http://stackoverflow.com/questions/228477/how-do-i-programmatically-determine-operating-system-in-java
// for proper multi-platform opening support
FileUtil.openInEditor(new File(image.getImagePath))
} else {
}
} else if (event.isSecondaryButtonDown) {
//right click context menu
debug("Requesting Context Menu")
imageTilePane.addImageSelected(thisTile)
val contextMenuEvent = new ContextMenuEvent(
thisTile,
thisTile,
ContextMenuEvent.CONTEXT_MENU_REQUESTED,
event.getX, event.getY,
event.getScreenX, event.getScreenY,
false,
new PickResult(thisTile, event.getSceneX, event.getSceneY))
imageTilePane.handleContextMenu(contextMenuEvent)
}
}
}
})
}
} else if (event.isSecondaryButtonDown) {
//right click context menu
debug("Requesting Context Menu")
imageTilePane.addImageSelected(thisTile)
val contextMenuEvent = new ContextMenuEvent(
thisTile,
thisTile,
ContextMenuEvent.CONTEXT_MENU_REQUESTED,
event.getX, event.getY,
event.getScreenX, event.getScreenY,
false,
new PickResult(thisTile, event.getSceneX, event.getSceneY))
imageTilePane.handleContextMenu(contextMenuEvent)
}
}
}
})
//Separator
val separator = new Separator()
separator.setOrientation(Orientation.HORIZONTAL)
separator.setMaxHeight(5.0d)
separator.setVisible(false)
this.getChildren.add(separator)
//Separator
val separator = new Separator()
separator.setOrientation(Orientation.HORIZONTAL)
separator.setMaxHeight(5.0d)
separator.setVisible(false)
this.getChildren.add(separator)
// Image
val genImageView = new ImageView()
debug(s"Getting thumbnail from: ${image.getThumbnailPath}")
managed(new FileInputStream(image.getThumbnailPath)) acquireAndGet {
thumbSource =>
val thumbnail = new javafx.scene.image.Image(thumbSource)
genImageView.setImage(thumbnail)
if (thumbnail.getHeight > thumbnail.getWidth) {
genImageView.setFitHeight(128.0)
} else {
genImageView.setFitWidth(128.0)
}
}
genImageView.setPreserveRatio(true)
// Image
val genImageView = new ImageView()
debug(s"Getting thumbnail from: ${image.getThumbnailPath}")
managed(new FileInputStream(image.getThumbnailPath)) acquireAndGet {
thumbSource =>
val thumbnail = new javafx.scene.image.Image(thumbSource)
genImageView.setImage(thumbnail)
if (thumbnail.getHeight > thumbnail.getWidth) {
genImageView.setFitHeight(128.0)
} else {
genImageView.setFitWidth(128.0)
}
}
genImageView.setPreserveRatio(true)
this.getChildren.add(genImageView)
this.getChildren.add(genImageView)
//Label
val imageLabel = new Label()
imageLabel.setText(s"${image.getHeight}x${image.getWidth}")
imageLabel.setWrapText(true)
imageLabel.setMaxHeight(32d)
imageLabel.setMaxWidth(preferedTileWidth - 2)
imageLabel.setAlignment(Pos.BOTTOM_CENTER)
//Label
val imageLabel = new Label()
imageLabel.setText(s"${image.getHeight}x${image.getWidth}")
imageLabel.setWrapText(true)
imageLabel.setMaxHeight(32d)
imageLabel.setMaxWidth(preferedTileWidth - 2)
imageLabel.setAlignment(Pos.BOTTOM_CENTER)
//Tooltip
val tooltip = new Tooltip()
tooltip.setText(s"${image.getName}")
imageLabel.setTooltip(tooltip)
this.getChildren.add(imageLabel)
//Tooltip
val tooltip = new Tooltip()
tooltip.setText(s"${image.getName}")
imageLabel.setTooltip(tooltip)
this.getChildren.add(imageLabel)
//this.setOnContextMenuRequested(new EventHandler[ContextMenuEvent] {
// override def handle(event: ContextMenuEvent): Unit = {
// imageTilePane.handleContextMenu(event)
// }
//})
//this.setOnContextMenuRequested(new EventHandler[ContextMenuEvent] {
// override def handle(event: ContextMenuEvent): Unit = {
// imageTilePane.handleContextMenu(event)
// }
//})
def getImageData: com.sothr.imagetools.engine.image.Image = {
imageData
}
def getImageData: com.sothr.imagetools.engine.image.Image = {
imageData
}
} }

23
gui/src/main/scala/com/sothr/imagetools/ui/component/ImageTileFactory.scala

@ -7,19 +7,18 @@ import com.sothr.imagetools.engine.util.PropertiesService
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
/** /**
* Created by drew on 8/6/14.
*
* Creates pre-generated image tiles that can be rendered to a scene
*/
* Created by drew on 8/6/14.
*
* Creates pre-generated image tiles that can be rendered to a scene
*/
object ImageTileFactory extends Logging { object ImageTileFactory extends Logging {
def get(image: com.sothr.imagetools.engine.image.Image, pane: TilePane): ImageTile = {
val thumbnailWidth = PropertiesService.get("app.thumbnail.size", "128").toInt
val imageTile = new ImageTile(thumbnailWidth, image, pane.asInstanceOf[ImageTilePane])
//set padding
imageTile.setPadding(new Insets(2, 2, 2, 2))
imageTile
}
def get(image: com.sothr.imagetools.engine.image.Image, pane: TilePane): ImageTile = {
val thumbnailWidth = PropertiesService.get("app.thumbnail.size", "128").toInt
val imageTile = new ImageTile(thumbnailWidth, image, pane.asInstanceOf[ImageTilePane])
//set padding
imageTile.setPadding(new Insets(2, 2, 2, 2))
imageTile
}
} }

491
gui/src/main/scala/com/sothr/imagetools/ui/component/ImageTilePane.scala

@ -20,266 +20,265 @@ import scala.concurrent._
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
/** /**
* Custom Tile Pane with a multi selection model
*
* Created by drew on 8/29/14.
*/
* Custom Tile Pane with a multi selection model
*
* Created by drew on 8/29/14.
*/
class ImageTilePane extends TilePane with Logging { class ImageTilePane extends TilePane with Logging {
val selectionModel = new ImageTilePaneSelectionModel[ImageTile](this)
def handleContextMenu(event: ContextMenuEvent) = {
//Build and show a context menu
debug("Context Menu Request Received")
val numSelected = this.selectionModel.getSelectedIndices.size()
if (numSelected > 0) {
if (numSelected == 1) {
val contextMenu = getSingleSelectionContextMenu
debug("Showing context menu")
contextMenu.show(event.getTarget.asInstanceOf[Node], Side.RIGHT, 0d, 0d)
} else {
val contextMenu = getMulipleSelectionContextMenu
debug("Showing context menu")
contextMenu.show(event.getTarget.asInstanceOf[Node], Side.RIGHT, 0d, 0d)
}
}
}
def getSingleSelectionContextMenu: ContextMenu = {
debug("Building single-selection context menu")
val contextMenu = new ContextMenu()
val delete = new MenuItem("Delete")
delete.setOnAction(new EventHandler[ActionEvent]() {
def handle(e: ActionEvent) = {
debug("Requesting Single Delete")
deleteSelected()
}
})
contextMenu.getItems.addAll(delete)
contextMenu
}
def getMulipleSelectionContextMenu: ContextMenu = {
debug("Building multi-selection context menu")
val contextMenu = new ContextMenu()
val delete = new MenuItem("Delete")
delete.setOnAction(new EventHandler[ActionEvent]() {
def handle(e: ActionEvent) = {
debug("Requesting Multiple Delete")
deleteSelected()
}
})
contextMenu.getItems.addAll(delete)
contextMenu
}
def imageSelected(imageTile: ImageTile) = {
this.selectionModel.clearAndSelect(this.getChildren.indexOf(imageTile))
}
def addImageSelected(imageTile: ImageTile) = {
this.selectionModel.select(this.getChildren.indexOf(imageTile))
}
def removeImageSelected(imageTile: ImageTile) = {
this.selectionModel.clearSelection(this.getChildren.indexOf(imageTile))
}
def clearSelection() = {
this.selectionModel.clearSelection()
}
//request deletion of selected images
def deleteSelected() = {
val f: Future[Unit] = Future {
val selected = this.selectionModel.getSelectedItems
val iterator = selected.iterator()
while (iterator.hasNext) {
val item = iterator.next()
val imageTile = item.asInstanceOf[ImageTile]
val data = imageTile.imageData
val controller = AppConfig.getFxmlLoader.getController[AppController]()
controller.engine.deleteImage(data)
}
val pane = this
Platform.runLater(new Runnable() {
override def run(): Unit = {
//remove from the current panel
pane.getChildren.removeAll(selected)
clearSelection()
}
})
}
f onComplete {
case Success(a) => info("Successfully deleted files")
case Failure(f) => error("Failed to delete files", f)
}
}
val selectionModel = new ImageTilePaneSelectionModel[ImageTile](this)
def handleContextMenu(event: ContextMenuEvent) = {
//Build and show a context menu
debug("Context Menu Request Received")
val numSelected = this.selectionModel.getSelectedIndices.size()
if (numSelected > 0) {
if (numSelected == 1) {
val contextMenu = getSingleSelectionContextMenu
debug("Showing context menu")
contextMenu.show(event.getTarget.asInstanceOf[Node], Side.RIGHT, 0d, 0d)
} else {
val contextMenu = getMulipleSelectionContextMenu
debug("Showing context menu")
contextMenu.show(event.getTarget.asInstanceOf[Node], Side.RIGHT, 0d, 0d)
}
}
}
def getSingleSelectionContextMenu: ContextMenu = {
debug("Building single-selection context menu")
val contextMenu = new ContextMenu()
val delete = new MenuItem("Delete")
delete.setOnAction(new EventHandler[ActionEvent]() {
def handle(e: ActionEvent) = {
debug("Requesting Single Delete")
deleteSelected()
}
})
contextMenu.getItems.addAll(delete)
contextMenu
}
def getMulipleSelectionContextMenu: ContextMenu = {
debug("Building multi-selection context menu")
val contextMenu = new ContextMenu()
val delete = new MenuItem("Delete")
delete.setOnAction(new EventHandler[ActionEvent]() {
def handle(e: ActionEvent) = {
debug("Requesting Multiple Delete")
deleteSelected()
}
})
contextMenu.getItems.addAll(delete)
contextMenu
}
def imageSelected(imageTile: ImageTile) = {
this.selectionModel.clearAndSelect(this.getChildren.indexOf(imageTile))
}
def addImageSelected(imageTile: ImageTile) = {
this.selectionModel.select(this.getChildren.indexOf(imageTile))
}
def removeImageSelected(imageTile: ImageTile) = {
this.selectionModel.clearSelection(this.getChildren.indexOf(imageTile))
}
def clearSelection() = {
this.selectionModel.clearSelection()
}
//request deletion of selected images
def deleteSelected() = {
val f: Future[Unit] = Future {
val selected = this.selectionModel.getSelectedItems
val iterator = selected.iterator()
while (iterator.hasNext) {
val item = iterator.next()
val imageTile = item.asInstanceOf[ImageTile]
val data = imageTile.imageData
val controller = AppConfig.getFxmlLoader.getController[AppController]()
controller.engine.deleteImage(data)
}
val pane = this
Platform.runLater(new Runnable() {
override def run(): Unit = {
//remove from the current panel
pane.getChildren.removeAll(selected)
clearSelection()
}
})
}
f onComplete {
case Success(a) => info("Successfully deleted files")
case Failure(f) => error("Failed to delete files", f)
}
}
} }
/** /**
* Multiple selection model for ImageTilePane
*
*/
* Multiple selection model for ImageTilePane
*
*/
class ImageTilePaneSelectionModel[ImageTile](parentTilePane: ImageTilePane) extends MultipleSelectionModel[ImageTile] { class ImageTilePaneSelectionModel[ImageTile](parentTilePane: ImageTilePane) extends MultipleSelectionModel[ImageTile] {
val selectedIndexes: ObservableList[Integer] = new ArrayObservableList[Integer]()
override def getSelectedIndices: ObservableList[Integer] = {
this.selectedIndexes
}
override def getSelectedItems: ObservableList[ImageTile] = {
val selected = new ArrayObservableList[ImageTile]()
val iterator = selectedIndexes.iterator()
while (iterator.hasNext) {
selected.add(this.parentTilePane.getChildren.get(iterator.next()).asInstanceOf[ImageTile])
}
selected
}
override def selectIndices(index: Int, indices: Int*): Unit = {
clearSelectionFormatting
this.selectedIndexes.clear()
setSelectionFormatting(index)
this.selectedIndexes.add(index)
for (i <- indices) {
setSelectionFormatting(i)
this.selectedIndexes.add(i)
}
}
override def selectAll(): Unit = {
clearSelectionFormatting
this.selectedIndexes.clear()
for (index <- 0 until this.parentTilePane.getChildren.size()) {
setSelectionFormatting(index)
this.selectedIndexes.add(index)
}
}
override def selectFirst(): Unit = {
clearSelectionFormatting
this.selectedIndexes.clear()
setSelectionFormatting(0)
this.selectedIndexes.add(0)
}
override def selectLast(): Unit = {
clearSelectionFormatting
this.selectedIndexes.clear()
setSelectionFormatting(this.parentTilePane.getChildren.size() - 1)
this.selectedIndexes.add(this.parentTilePane.getChildren.size() - 1)
}
override def clearAndSelect(index: Int): Unit = {
clearSelectionFormatting
this.selectedIndexes.clear()
setSelectionFormatting(index)
this.selectedIndexes.add(index)
}
override def clearSelection(index: Int): Unit = {
val i = this.selectedIndexes.indexOf(index)
if (i >= 0) {
clearSelectionFormatting(index)
this.selectedIndexes.remove(i)
}
}
private def clearSelectionFormatting(index: Int) = {
val tile = this.parentTilePane.getChildren.get(index).asInstanceOf[VBox]
tile.setBorder(Border.EMPTY)
}
override def clearSelection(): Unit = {
clearSelectionFormatting
this.selectedIndexes.clear()
}
override def selectPrevious(): Unit = {
if (this.selectedIndexes.size == 1) {
val currentIndex = this.selectedIndexes.get(0)
val nextIndex = if (currentIndex < 1) 0 else currentIndex - 1
this.selectedIndexes.set(0, nextIndex)
}
}
override def selectNext(): Unit = {
if (this.selectedIndexes.size == 1) {
val currentIndex = this.selectedIndexes.get(0)
val nextIndex = if (currentIndex >= this.parentTilePane.getChildren.size - 1) this.parentTilePane.getChildren.size - 1 else currentIndex + 1
this.selectedIndexes.set(0, nextIndex)
}
}
override def select(index: Int): Unit = {
//can only select once
if (!this.selectedIndexes.contains(index)) {
setSelectionFormatting(index)
this.selectedIndexes.add(index)
}
}
private def setSelectionFormatting(index: Int): Unit = {
setSelectionFormatting(this.parentTilePane.getChildren.get(index).asInstanceOf[ImageTile])
}
private def setSelectionFormatting(imageTile: ImageTile) = {
val borderStroke = new BorderStroke(Color.BLUE, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderStroke.THIN)
imageTile.asInstanceOf[VBox].setBorder(new Border(borderStroke))
}
override def select(obj: ImageTile): Unit = {
if (this.parentTilePane.getChildren.contains(obj)) {
clearSelectionFormatting
this.selectedIndexes.clear()
setSelectionFormatting(obj)
this.selectedIndexes.add(this.parentTilePane.getChildren.indexOf(obj))
}
}
private def clearSelectionFormatting() = {
val iterator = this.parentTilePane.getChildren.iterator()
while (iterator.hasNext) {
//remove the selection styling
val imageTile: VBox = iterator.next().asInstanceOf[VBox]
imageTile.setBorder(Border.EMPTY)
}
}
override def isEmpty: Boolean = {
this.parentTilePane.getChildren.isEmpty
}
override def isSelected(index: Int): Boolean = {
this.selectedIndexes.contains(index)
}
val selectedIndexes: ObservableList[Integer] = new ArrayObservableList[Integer]()
override def getSelectedIndices: ObservableList[Integer] = {
this.selectedIndexes
}
override def getSelectedItems: ObservableList[ImageTile] = {
val selected = new ArrayObservableList[ImageTile]()
val iterator = selectedIndexes.iterator()
while (iterator.hasNext) {
selected.add(this.parentTilePane.getChildren.get(iterator.next()).asInstanceOf[ImageTile])
}
selected
}
override def selectIndices(index: Int, indices: Int*): Unit = {
clearSelectionFormatting
this.selectedIndexes.clear()
setSelectionFormatting(index)
this.selectedIndexes.add(index)
for (i <- indices) {
setSelectionFormatting(i)
this.selectedIndexes.add(i)
}
}
override def selectAll(): Unit = {
clearSelectionFormatting
this.selectedIndexes.clear()
for (index <- 0 until this.parentTilePane.getChildren.size()) {
setSelectionFormatting(index)
this.selectedIndexes.add(index)
}
}
override def selectFirst(): Unit = {
clearSelectionFormatting
this.selectedIndexes.clear()
setSelectionFormatting(0)
this.selectedIndexes.add(0)
}
override def selectLast(): Unit = {
clearSelectionFormatting
this.selectedIndexes.clear()
setSelectionFormatting(this.parentTilePane.getChildren.size() - 1)
this.selectedIndexes.add(this.parentTilePane.getChildren.size() - 1)
}
override def clearAndSelect(index: Int): Unit = {
clearSelectionFormatting
this.selectedIndexes.clear()
setSelectionFormatting(index)
this.selectedIndexes.add(index)
}
private def setSelectionFormatting(index: Int): Unit = {
setSelectionFormatting(this.parentTilePane.getChildren.get(index).asInstanceOf[ImageTile])
}
private def setSelectionFormatting(imageTile: ImageTile) = {
val borderStroke = new BorderStroke(Color.BLUE, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderStroke.THIN)
imageTile.asInstanceOf[VBox].setBorder(new Border(borderStroke))
}
private def clearSelectionFormatting() = {
val iterator = this.parentTilePane.getChildren.iterator()
while (iterator.hasNext) {
//remove the selection styling
val imageTile: VBox = iterator.next().asInstanceOf[VBox]
imageTile.setBorder(Border.EMPTY)
}
}
override def clearSelection(index: Int): Unit = {
val i = this.selectedIndexes.indexOf(index)
if (i >= 0) {
clearSelectionFormatting(index)
this.selectedIndexes.remove(i)
}
}
private def clearSelectionFormatting(index: Int) = {
val tile = this.parentTilePane.getChildren.get(index).asInstanceOf[VBox]
tile.setBorder(Border.EMPTY)
}
override def clearSelection(): Unit = {
clearSelectionFormatting
this.selectedIndexes.clear()
}
override def selectPrevious(): Unit = {
if (this.selectedIndexes.size == 1) {
val currentIndex = this.selectedIndexes.get(0)
val nextIndex = if (currentIndex < 1) 0 else currentIndex - 1
this.selectedIndexes.set(0, nextIndex)
}
}
override def selectNext(): Unit = {
if (this.selectedIndexes.size == 1) {
val currentIndex = this.selectedIndexes.get(0)
val nextIndex = if (currentIndex >= this.parentTilePane.getChildren.size - 1) this.parentTilePane.getChildren.size - 1 else currentIndex + 1
this.selectedIndexes.set(0, nextIndex)
}
}
override def select(index: Int): Unit = {
//can only select once
if (!this.selectedIndexes.contains(index)) {
setSelectionFormatting(index)
this.selectedIndexes.add(index)
}
}
override def select(obj: ImageTile): Unit = {
if (this.parentTilePane.getChildren.contains(obj)) {
clearSelectionFormatting
this.selectedIndexes.clear()
setSelectionFormatting(obj)
this.selectedIndexes.add(this.parentTilePane.getChildren.indexOf(obj))
}
}
override def isEmpty: Boolean = {
this.parentTilePane.getChildren.isEmpty
}
override def isSelected(index: Int): Boolean = {
this.selectedIndexes.contains(index)
}
} }
class ArrayObservableList[E] extends ModifiableObservableListBase[E] { class ArrayObservableList[E] extends ModifiableObservableListBase[E] {
val delegate: util.ArrayList[E] = new util.ArrayList[E]()
val delegate: util.ArrayList[E] = new util.ArrayList[E]()
def get(index: Int): E = {
delegate.get(index)
}
def get(index: Int): E = {
delegate.get(index)
}
def size = {
delegate.size
}
def size = {
delegate.size
}
def doAdd(index: Int, element: E) = {
delegate.add(index, element)
}
def doAdd(index: Int, element: E) = {
delegate.add(index, element)
}
def doSet(index: Int, element: E): E = {
delegate.set(index, element)
}
def doRemove(index: Int): E = {
delegate.remove(index)
}
def doSet(index: Int, element: E): E = {
delegate.set(index, element)
}
def doRemove(index: Int): E = {
delegate.remove(index)
}
} }

841
gui/src/main/scala/com/sothr/imagetools/ui/controller/AppController.scala

@ -27,435 +27,434 @@ import scala.concurrent._
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
/** /**
* Main Application controller
*
* Created by drew on 12/31/13.
*/
* Main Application controller
*
* Created by drew on 12/31/13.
*/
class AppController extends Logging { class AppController extends Logging {
// Engine
val engine: Engine = new ConcurrentEngine()
//Define controls
@FXML var rootPane: AnchorPane = null
@FXML var rootMenuBar: MenuBar = null
@FXML var scrollPane: ScrollPane = null
@FXML var imageTilePane: TilePane = null
@FXML var tagListView: ListView[String] = null
// Labels
@FXML var selectedDirectoryLabel: Label = null
@FXML var currentDirectoryLabel: Label = null
@FXML var progressLabel: Label = null
// Others
@FXML var progressBar: ProgressBar = null
@FXML var paginator: Pagination = null
@FXML var doRecursiveProcessing: CheckBox = null
// Current State
var currentDirectory: String = "."
var currentImages: List[Image] = List[Image]()
@FXML def initialize() = {
if (PropertiesService.has("app.ui.lastPath")) {
currentDirectory = PropertiesService.get("app.ui.lastPath", ".")
selectedDirectoryLabel.setText(PropertiesService.get("app.ui.lastPath", ""))
}
//setup the engine listener
val system: ActorSystem = AppConfig.getAppActorSystem
val guiListenerProps: Props = Props.create(classOf[GUIEngineListener])
val guiListener: ActorRef = system.actorOf(guiListenerProps)
// configure the listener
guiListener ! SetupListener(progressBar, progressLabel)
// tell the engine to use our listener
this.engine.setSearchedListener(guiListener)
this.engine.setProcessedListener(guiListener)
this.engine.setSimilarityListener(guiListener)
// Initialize the progress label
guiListener ! SubmitMessage("Initialized System... Ready!")
// set the default images per page if it doesn't exist yet
if (!PropertiesService.has("app.ui.thumbsPerPage")) {
PropertiesService.set("app.ui.thumbsPerPage", "100")
}
//setup the paginator
//font size doesn't increase the size of the buttons
//paginator.setStyle("-fx-font-size:13;")
// configure the page factory
paginator.setPageFactory(new Callback[Integer, Node]() {
override def call(pageIndex: Integer): Node = {
// do all of our display logic
showPage(pageIndex)
// override behavior to display anything
new VBox()
}
})
//override the imageTilePane
debug("Replacing the default TilePane with a custom ImageTilePane")
val newImageTilePane = new ImageTilePane()
newImageTilePane.setHgap(this.imageTilePane.getHgap)
newImageTilePane.setVgap(this.imageTilePane.getVgap)
newImageTilePane.setMinHeight(this.imageTilePane.getMinHeight)
newImageTilePane.setMinWidth(this.imageTilePane.getMinWidth)
newImageTilePane.setMaxHeight(this.imageTilePane.getMaxHeight)
newImageTilePane.setMaxWidth(this.imageTilePane.getMaxWidth)
newImageTilePane.setPrefColumns(this.imageTilePane.getPrefColumns)
newImageTilePane.setPrefRows(this.imageTilePane.getPrefRows)
//newImageTilePane.setPrefTileHeight(this.imageTilePane.getPrefTileHeight)
//newImageTilePane.setPrefTileWidth(this.imageTilePane.getPrefTileWidth)
newImageTilePane.setTileAlignment(this.imageTilePane.getTileAlignment)
debug("Assigning the the new ImageTilePane to the ScrollPane")
this.scrollPane.setContent(newImageTilePane)
this.imageTilePane = newImageTilePane
//test
//val testImage = new Image()
//testImage.setThumbnailPath("test.jpg")
//testImage.setImagePath("test.jpg")
//val testImageList = new mutable.MutableList[Image]
//for (i <- 1 to 100) {
// testImageList += testImage
//}
//setPagesContent(testImageList.toList)
//showPage(0)
//val list = FXCollections.observableArrayList[String]()
//for (i <- 1 to 100) {
// list.add(s"test-item ${i}")
//}
//tagListView.setItems(list)
}
//region MenuItem Actions
@FXML
def helpAction(event: ActionEvent) = {
showExternalHTMLUtilityDialog("http://www.sothr.com")
}
def showExternalHTMLUtilityDialog(url: String) = {
val dialog: Stage = new Stage()
dialog.initStyle(StageStyle.UTILITY)
val parent: Group = new Group()
//setup the HTML view
val htmlView = new WebView
htmlView.getEngine.load(url)
//htmlView.setMinWidth(width)
//htmlView.setMinHeight(height)
//htmlView.setPrefWidth(width)
//htmlView.setPrefHeight(height)
parent.getChildren.add(htmlView)
val scene: Scene = new Scene(parent)
dialog.setScene(scene)
dialog.setResizable(false)
dialog.setTitle(htmlView.getEngine.getTitle)
dialog.show()
}
@FXML
def aboutAction(event: ActionEvent) = {
debug("Displaying about screen")
var aboutMessage = "Simple About Message"
try {
val scanner = new Scanner(ResourceLoader.get().getResourceStream("documents/about.md"))
aboutMessage = ""
while (scanner.hasNextLine) {
aboutMessage += scanner.nextLine().trim() + "\n"
}
debug(s"Parsed About Message: '$aboutMessage'")
} catch {
case ioe: IOException =>
error("Unable to read about file")
}
showMarkdownUtilityDialog("About", aboutMessage, 400.0, 300.0)
debug("Showing About Dialog")
}
//endregion
//region buttons
//todo: show a dialog that is rendered from markdown content
def showMarkdownUtilityDialog(title: String, markdown: String, width: Double = 800.0, height: Double = 600.0) = {
val htmlBody = new Markdown4jProcessor().process(markdown)
showHTMLUtilityDialog(title, htmlBody, width, height)
}
/**
* Render HTML content to a utility dialog. No input or output, just raw rendered content through a webkit engine.
*
* @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()
dialog.initStyle(StageStyle.UTILITY)
val parent: Group = new Group()
//setup the HTML view
val htmlView = new WebView
htmlView.getEngine.loadContent(htmlBody)
htmlView.setMinWidth(width)
htmlView.setMinHeight(height)
htmlView.setPrefWidth(width)
htmlView.setPrefHeight(height)
parent.getChildren.add(htmlView)
val scene: Scene = new Scene(parent)
dialog.setScene(scene)
dialog.setResizable(false)
dialog.setTitle(title)
dialog.show()
}
@FXML
def closeAction(event: ActionEvent) = {
debug("Closing application from the menu bar")
val stage: Stage = this.rootMenuBar.getScene.getWindow.asInstanceOf[Stage]
stage.close()
}
//endregion
//region pagination
@FXML
def browseFolders(event: ActionEvent) = {
val chooser = new DirectoryChooser()
chooser.setTitle("ImageTools Browser")
try {
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("app.ui.lastPath", selectedDirectory.getAbsolutePath)
this.currentDirectoryLabel.setText(selectedDirectory.getAbsolutePath)
} catch {
// fall back on the default because the directory we tried probably didn't exist
case iae: IllegalArgumentException =>
logger.error("The old directory no longer exists", iae)
chooser.setInitialDirectory(null)
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("app.ui.lastPath", selectedDirectory.getAbsolutePath)
this.currentDirectoryLabel.setText(selectedDirectory.getAbsolutePath)
}
}
@FXML
def showAllImages(event: ActionEvent) = {
resetPaginator()
getImageTilePane.getChildren.setAll(new java.util.ArrayList[Node]())
val f: Future[List[Image]] = Future {
val images = engine.getImagesForDirectory(currentDirectory, recursive = doRecursiveProcessing.isSelected)
images.sortWith((x, y) => x.imagePath < y.imagePath)
}
f onComplete {
case Success(images) =>
info(s"Displaying ${images.length} images")
// This is used so that JavaFX updates on the proper thread
// This is important since UI updates can only happen on that thread
Platform.runLater(new Runnable() {
override def run() {
setPagesContent(images)
showPage(0)
}
})
case Failure(t) =>
error("An Error Occurred", t)
}
}
def resetPaginator() = {
this.paginator.setDisable(true)
this.paginator.setPageCount(1)
}
//endregion
def setPagesContent(images: List[Image]) = {
this.currentImages = images
//set the appropriate size for the pagination
val itemsPerPage = PropertiesService.get("app.ui.thumbsPerPage", "100").toInt
val pageNum = Math.ceil(this.currentImages.size.toFloat / itemsPerPage).toInt
this.paginator.setPageCount(pageNum)
this.paginator.setDisable(false)
}
//todo: include a templating engine for rendering information
def showPage(pageIndex: Integer) = {
val itemsPerPage = PropertiesService.get("app.ui.thumbsPerPage", "100").toInt
val startIndex = pageIndex * itemsPerPage
val endIndex = if ((startIndex + itemsPerPage) > this.currentImages.size) this.currentImages.length else startIndex + itemsPerPage
//clear any selections
getImageTilePane.asInstanceOf[ImageTilePane].clearSelection()
//clear and populate the scrollpane
getImageTilePane.getChildren.setAll(new java.util.ArrayList[Node]())
val images = this.currentImages.slice(startIndex, endIndex)
Platform.runLater(new Runnable() {
override def run() {
for (image <- images) {
debug(s"Adding image ${image.toString} to app")
getImageTilePane.getChildren.add(ImageTileFactory.get(image, getImageTilePane))
}
}
})
}
def getImageTilePane: TilePane = {
this.imageTilePane
}
@FXML
def showSimilarImages(event: ActionEvent) = {
resetPaginator()
imageTilePane.getChildren.setAll(new java.util.ArrayList[Node]())
val f: Future[List[Image]] = Future {
val similarImages = engine.getSimilarImagesForDirectory(currentDirectory, recursive = doRecursiveProcessing.isSelected)
val tempImages = new mutable.MutableList[Image]()
for (similarImage <- similarImages) {
debug(s"Adding similar images ${similarImage.toString} to app")
similarImage.similarImages.foreach(image => tempImages += image)
}
tempImages.toList
}
f onComplete {
case Success(images) =>
info(s"Displaying ${images.length} similar images")
Platform.runLater(new Runnable() {
override def run() {
setPagesContent(images)
showPage(0)
}
})
case Failure(t) =>
error("An Error Occurred", t)
}
}
/**
* Show a plain text utility dialog
*
* @param message Message to display
* @param wrapWidth When to wrap
* @param alignment How it should be aligned
*/
def showUtilityDialog(title: String,
message: String,
wrapWidth: Double = 300.0,
xOffset: Double = 25.0,
yOffset: Double = 25.0,
alignment: TextAlignment = TextAlignment.JUSTIFY) = {
val dialog: Stage = new Stage()
dialog.initStyle(StageStyle.UTILITY)
val parent: Group = new Group()
// fill the text box
val messageText = new Text()
messageText.setText(message)
messageText.setWrappingWidth(wrapWidth)
messageText.setX(xOffset)
messageText.setY(yOffset)
messageText.setTextAlignment(TextAlignment.JUSTIFY)
parent.getChildren.add(messageText)
val scene: Scene = new Scene(parent)
dialog.setScene(scene)
dialog.setResizable(false)
dialog.setMinWidth(wrapWidth + xOffset * 2)
dialog.setTitle(title)
dialog.show()
}
def print(): String = {
"This method works"
}
// Engine
val engine: Engine = new ConcurrentEngine()
//Define controls
@FXML var rootPane: AnchorPane = null
@FXML var rootMenuBar: MenuBar = null
@FXML var scrollPane: ScrollPane = null
@FXML var imageTilePane: TilePane = null
@FXML var tagListView: ListView[String] = null
// Labels
@FXML var selectedDirectoryLabel: Label = null
@FXML var currentDirectoryLabel: Label = null
@FXML var progressLabel: Label = null
// Others
@FXML var progressBar: ProgressBar = null
@FXML var paginator: Pagination = null
@FXML var doRecursiveProcessing: CheckBox = null
// Current State
var currentDirectory: String = "."
var currentImages: List[Image] = List[Image]()
@FXML def initialize() = {
if (PropertiesService.has("app.ui.lastPath")) {
currentDirectory = PropertiesService.get("app.ui.lastPath", ".")
selectedDirectoryLabel.setText(PropertiesService.get("app.ui.lastPath", ""))
}
//setup the engine listener
val system: ActorSystem = AppConfig.getAppActorSystem
val guiListenerProps: Props = Props.create(classOf[GUIEngineListener])
val guiListener: ActorRef = system.actorOf(guiListenerProps)
// configure the listener
guiListener ! SetupListener(progressBar, progressLabel)
// tell the engine to use our listener
this.engine.setSearchedListener(guiListener)
this.engine.setProcessedListener(guiListener)
this.engine.setSimilarityListener(guiListener)
// Initialize the progress label
guiListener ! SubmitMessage("Initialized System... Ready!")
// set the default images per page if it doesn't exist yet
if (!PropertiesService.has("app.ui.thumbsPerPage")) {
PropertiesService.set("app.ui.thumbsPerPage", "100")
}
//setup the paginator
//font size doesn't increase the size of the buttons
//paginator.setStyle("-fx-font-size:13;")
// configure the page factory
paginator.setPageFactory(new Callback[Integer, Node]() {
override def call(pageIndex: Integer): Node = {
// do all of our display logic
showPage(pageIndex)
// override behavior to display anything
new VBox()
}
})
//override the imageTilePane
debug("Replacing the default TilePane with a custom ImageTilePane")
val newImageTilePane = new ImageTilePane()
newImageTilePane.setHgap(this.imageTilePane.getHgap)
newImageTilePane.setVgap(this.imageTilePane.getVgap)
newImageTilePane.setMinHeight(this.imageTilePane.getMinHeight)
newImageTilePane.setMinWidth(this.imageTilePane.getMinWidth)
newImageTilePane.setMaxHeight(this.imageTilePane.getMaxHeight)
newImageTilePane.setMaxWidth(this.imageTilePane.getMaxWidth)
newImageTilePane.setPrefColumns(this.imageTilePane.getPrefColumns)
newImageTilePane.setPrefRows(this.imageTilePane.getPrefRows)
//newImageTilePane.setPrefTileHeight(this.imageTilePane.getPrefTileHeight)
//newImageTilePane.setPrefTileWidth(this.imageTilePane.getPrefTileWidth)
newImageTilePane.setTileAlignment(this.imageTilePane.getTileAlignment)
debug("Assigning the the new ImageTilePane to the ScrollPane")
this.scrollPane.setContent(newImageTilePane)
this.imageTilePane = newImageTilePane
//test
//val testImage = new Image()
//testImage.setThumbnailPath("test.jpg")
//testImage.setImagePath("test.jpg")
//val testImageList = new mutable.MutableList[Image]
//for (i <- 1 to 100) {
// testImageList += testImage
//}
//setPagesContent(testImageList.toList)
//showPage(0)
//val list = FXCollections.observableArrayList[String]()
//for (i <- 1 to 100) {
// list.add(s"test-item ${i}")
//}
//tagListView.setItems(list)
}
//region MenuItem Actions
@FXML
def helpAction(event: ActionEvent) = {
showExternalHTMLUtilityDialog("http://www.sothr.com")
}
def showExternalHTMLUtilityDialog(url: String) = {
val dialog: Stage = new Stage()
dialog.initStyle(StageStyle.UTILITY)
val parent: Group = new Group()
//setup the HTML view
val htmlView = new WebView
htmlView.getEngine.load(url)
//htmlView.setMinWidth(width)
//htmlView.setMinHeight(height)
//htmlView.setPrefWidth(width)
//htmlView.setPrefHeight(height)
parent.getChildren.add(htmlView)
val scene: Scene = new Scene(parent)
dialog.setScene(scene)
dialog.setResizable(false)
dialog.setTitle(htmlView.getEngine.getTitle)
dialog.show()
}
@FXML
def aboutAction(event: ActionEvent) = {
debug("Displaying about screen")
var aboutMessage = "Simple About Message"
try {
val scanner = new Scanner(ResourceLoader.get().getResourceStream("documents/about.md"))
aboutMessage = ""
while (scanner.hasNextLine) {
aboutMessage += scanner.nextLine().trim() + "\n"
}
debug(s"Parsed About Message: '$aboutMessage'")
} catch {
case ioe: IOException =>
error("Unable to read about file")
}
showMarkdownUtilityDialog("About", aboutMessage, 400.0, 300.0)
debug("Showing About Dialog")
}
//endregion
//region buttons
//todo: show a dialog that is rendered from markdown content
def showMarkdownUtilityDialog(title: String, markdown: String, width: Double = 800.0, height: Double = 600.0) = {
val htmlBody = new Markdown4jProcessor().process(markdown)
showHTMLUtilityDialog(title, htmlBody, width, height)
}
/**
* Render HTML content to a utility dialog. No input or output, just raw rendered content through a webkit engine.
*
* @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()
dialog.initStyle(StageStyle.UTILITY)
val parent: Group = new Group()
//setup the HTML view
val htmlView = new WebView
htmlView.getEngine.loadContent(htmlBody)
htmlView.setMinWidth(width)
htmlView.setMinHeight(height)
htmlView.setPrefWidth(width)
htmlView.setPrefHeight(height)
parent.getChildren.add(htmlView)
val scene: Scene = new Scene(parent)
dialog.setScene(scene)
dialog.setResizable(false)
dialog.setTitle(title)
dialog.show()
}
@FXML
def closeAction(event: ActionEvent) = {
debug("Closing application from the menu bar")
val stage: Stage = this.rootMenuBar.getScene.getWindow.asInstanceOf[Stage]
stage.close()
}
//endregion
//region pagination
@FXML
def browseFolders(event: ActionEvent) = {
val chooser = new DirectoryChooser()
chooser.setTitle("ImageTools Browser")
try {
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("app.ui.lastPath", selectedDirectory.getAbsolutePath)
this.currentDirectoryLabel.setText(selectedDirectory.getAbsolutePath)
} catch {
// fall back on the default because the directory we tried probably didn't exist
case iae: IllegalArgumentException =>
logger.error("The old directory no longer exists", iae)
chooser.setInitialDirectory(null)
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("app.ui.lastPath", selectedDirectory.getAbsolutePath)
this.currentDirectoryLabel.setText(selectedDirectory.getAbsolutePath)
}
}
@FXML
def showAllImages(event: ActionEvent) = {
resetPaginator()
getImageTilePane.getChildren.setAll(new java.util.ArrayList[Node]())
val f: Future[List[Image]] = Future {
val images = engine.getImagesForDirectory(currentDirectory, recursive = doRecursiveProcessing.isSelected)
images.sortWith((x, y) => x.imagePath < y.imagePath)
}
f onComplete {
case Success(images) =>
info(s"Displaying ${images.length} images")
// This is used so that JavaFX updates on the proper thread
// This is important since UI updates can only happen on that thread
Platform.runLater(new Runnable() {
override def run() {
setPagesContent(images)
showPage(0)
}
})
case Failure(t) =>
error("An Error Occurred", t)
}
}
def resetPaginator() = {
this.paginator.setDisable(true)
this.paginator.setPageCount(1)
}
//endregion
def setPagesContent(images: List[Image]) = {
this.currentImages = images
//set the appropriate size for the pagination
val itemsPerPage = PropertiesService.get("app.ui.thumbsPerPage", "100").toInt
val pageNum = Math.ceil(this.currentImages.size.toFloat / itemsPerPage).toInt
this.paginator.setPageCount(pageNum)
this.paginator.setDisable(false)
}
//todo: include a templating engine for rendering information
def showPage(pageIndex: Integer) = {
val itemsPerPage = PropertiesService.get("app.ui.thumbsPerPage", "100").toInt
val startIndex = pageIndex * itemsPerPage
val endIndex = if ((startIndex + itemsPerPage) > this.currentImages.size) this.currentImages.length else startIndex + itemsPerPage
//clear any selections
getImageTilePane.asInstanceOf[ImageTilePane].clearSelection()
//clear and populate the scrollpane
getImageTilePane.getChildren.setAll(new java.util.ArrayList[Node]())
val images = this.currentImages.slice(startIndex, endIndex)
Platform.runLater(new Runnable() {
override def run() {
for (image <- images) {
debug(s"Adding image ${image.toString} to app")
getImageTilePane.getChildren.add(ImageTileFactory.get(image, getImageTilePane))
}
}
})
}
def getImageTilePane: TilePane = {
this.imageTilePane
}
@FXML
def showSimilarImages(event: ActionEvent) = {
resetPaginator()
imageTilePane.getChildren.setAll(new java.util.ArrayList[Node]())
val f: Future[List[Image]] = Future {
val similarImages = engine.getSimilarImagesForDirectory(currentDirectory, recursive = doRecursiveProcessing.isSelected)
val tempImages = new mutable.MutableList[Image]()
for (similarImage <- similarImages) {
debug(s"Adding similar images ${similarImage.toString} to app")
similarImage.similarImages.foreach(image => tempImages += image)
}
tempImages.toList
}
f onComplete {
case Success(images) =>
info(s"Displaying ${images.length} similar images")
Platform.runLater(new Runnable() {
override def run() {
setPagesContent(images)
showPage(0)
}
})
case Failure(t) =>
error("An Error Occurred", t)
}
}
/**
* Show a plain text utility dialog
*
* @param message Message to display
* @param wrapWidth When to wrap
* @param alignment How it should be aligned
*/
def showUtilityDialog(title: String,
message: String,
wrapWidth: Double = 300.0,
xOffset: Double = 25.0,
yOffset: Double = 25.0,
alignment: TextAlignment = TextAlignment.JUSTIFY) = {
val dialog: Stage = new Stage()
dialog.initStyle(StageStyle.UTILITY)
val parent: Group = new Group()
// fill the text box
val messageText = new Text()
messageText.setText(message)
messageText.setWrappingWidth(wrapWidth)
messageText.setX(xOffset)
messageText.setY(yOffset)
messageText.setTextAlignment(TextAlignment.JUSTIFY)
parent.getChildren.add(messageText)
val scene: Scene = new Scene(parent)
dialog.setScene(scene)
dialog.setResizable(false)
dialog.setMinWidth(wrapWidth + xOffset * 2)
dialog.setTitle(title)
dialog.show()
}
def print(): String = {
"This method works"
}
} }
//region EngineListener //region EngineListener
case class SetupListener(progressBar: ProgressBar, progressLabel: Label) case class SetupListener(progressBar: ProgressBar, progressLabel: Label)
/** /**
* Actor for logging output information
*/
* Actor for logging output information
*/
class GUIEngineListener extends EngineListener with ActorLogging { class GUIEngineListener extends EngineListener with ActorLogging {
var progressBar: javafx.scene.control.ProgressBar = null
var progressLabel: javafx.scene.control.Label = null
var isStarted = false
var isFinished = false
override def receive: Actor.Receive = {
case command: SetupListener => setupListener(command)
case command: SubmitMessage => handleMessage(command)
case command: ScannedFileCount => handleScannedFileCount(command)
case command: ComparedFileCount => handleComparedFileCount(command)
case _ => log.info("received unknown message")
}
def setupListener(command: SetupListener) = {
this.progressBar = command.progressBar
this.progressLabel = command.progressLabel
}
override def handleComparedFileCount(command: ComparedFileCount): Unit = {
Platform.runLater(new Runnable() {
override def run(): Unit = {
if (command.message != null) {
log.debug(command.message)
progressLabel.setText(command.message)
} else {
progressLabel.setText(s"Processed ${command.count}/${command.total}")
}
log.debug("Processed {}/{}", command.count, command.total)
progressBar.setProgress(command.count.toFloat / command.total)
}
})
}
override def handleScannedFileCount(command: ScannedFileCount): Unit = {
Platform.runLater(new Runnable() {
override def run(): Unit = {
if (command.message != null) {
log.debug(command.message)
progressLabel.setText(command.message)
} else {
progressLabel.setText(s"Scanned ${command.count}/${command.total} For Similarities")
}
log.debug("Scanned {}/{} For Similarities", command.count, command.total)
progressBar.setProgress(command.count.toFloat / command.total)
}
})
}
override def handleMessage(command: SubmitMessage): Unit = {
Platform.runLater(new Runnable() {
override def run(): Unit = {
log.debug(command.message)
progressLabel.setText(command.message)
}
})
}
var progressBar: javafx.scene.control.ProgressBar = null
var progressLabel: javafx.scene.control.Label = null
var isStarted = false
var isFinished = false
override def receive: Actor.Receive = {
case command: SetupListener => setupListener(command)
case command: SubmitMessage => handleMessage(command)
case command: ScannedFileCount => handleScannedFileCount(command)
case command: ComparedFileCount => handleComparedFileCount(command)
case _ => log.info("received unknown message")
}
def setupListener(command: SetupListener) = {
this.progressBar = command.progressBar
this.progressLabel = command.progressLabel
}
override def handleComparedFileCount(command: ComparedFileCount): Unit = {
Platform.runLater(new Runnable() {
override def run(): Unit = {
if (command.message != null) {
log.debug(command.message)
progressLabel.setText(command.message)
} else {
progressLabel.setText(s"Processed ${command.count}/${command.total}")
}
log.debug("Processed {}/{}", command.count, command.total)
progressBar.setProgress(command.count.toFloat / command.total)
}
})
}
override def handleScannedFileCount(command: ScannedFileCount): Unit = {
Platform.runLater(new Runnable() {
override def run(): Unit = {
if (command.message != null) {
log.debug(command.message)
progressLabel.setText(command.message)
} else {
progressLabel.setText(s"Scanned ${command.count}/${command.total} For Similarities")
}
log.debug("Scanned {}/{} For Similarities", command.count, command.total)
progressBar.setProgress(command.count.toFloat / command.total)
}
})
}
override def handleMessage(command: SubmitMessage): Unit = {
Platform.runLater(new Runnable() {
override def run(): Unit = {
log.debug(command.message)
progressLabel.setText(command.message)
}
})
}
} }
//endregion //endregion

32
gui/src/main/scala/com/sothr/imagetools/ui/util/FileUtil.scala

@ -7,24 +7,24 @@ import com.sothr.imagetools.engine.util.PropertiesService
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
/** /**
* Created by Drew Short on 8/31/2014.
*/
* Created by Drew Short on 8/31/2014.
*/
object FileUtil extends Logging { object FileUtil extends Logging {
def openInEditor(file: File) = {
PropertiesService.OS.toLowerCase match {
// Open file on windows
case os if os.startsWith("windows") => openFileWindows(file)
case os if os.startsWith("linux") => openFileLinux(file)
case default => error(s"Do not know how to open editor for OS: ${PropertiesService.OS}, ${PropertiesService.OS_VERSION}, ${PropertiesService.OS_ARCH}")
}
}
def openInEditor(file: File) = {
PropertiesService.OS.toLowerCase match {
// Open file on windows
case os if os.startsWith("windows") => openFileWindows(file)
case os if os.startsWith("linux") => openFileLinux(file)
case default => error(s"Do not know how to open editor for OS: ${PropertiesService.OS}, ${PropertiesService.OS_VERSION}, ${PropertiesService.OS_ARCH}")
}
}
private def openFileWindows(file: File) = {
Desktop.getDesktop.open(file)
}
private def openFileWindows(file: File) = {
Desktop.getDesktop.open(file)
}
private def openFileLinux(file: File) = {
Runtime.getRuntime.exec(s"xdg-open ${file.getAbsolutePath}")
}
private def openFileLinux(file: File) = {
Runtime.getRuntime.exec(s"xdg-open ${file.getAbsolutePath}")
}
} }

4
hash/pom.xml

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent> <parent>
<groupId>com.sothr.imagetools</groupId> <groupId>com.sothr.imagetools</groupId>

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

@ -37,10 +37,6 @@ object HashService extends Logging {
imagePath) imagePath)
} }
def getPrecisionMap(precisionSet: Set[Int], grayImage: BufferedImage): Map[Int, Array[Array[Int]]] = {
precisionSet.map(p => p -> getImageData(p, grayImage, alreadyGray = true))(collection.breakOut)
}
/** /**
* Given hash settings, a buffered image and an image path, calculate the perceptual hashes * Given hash settings, a buffered image and an image path, calculate the perceptual hashes
* *
@ -86,18 +82,8 @@ object HashService extends Logging {
hashes hashes
} }
def getSHA1(filePath: String): String = {
managed(new FileInputStream(filePath)) acquireAndGet {
input => DigestUtils.sha1Hex(input)
}
}
def getPrecisionSet(ahashSettings: HashSetting, dhashSettings: HashSetting, phashSettings: HashSetting): Set[Int] = {
Set(
if (ahashSettings.use) Option(ahashSettings.precision) else None,
if (dhashSettings.use) Option(dhashSettings.precision) else None,
if (phashSettings.use) Option(phashSettings.precision) else None
).flatten
def getPrecisionMap(precisionSet: Set[Int], grayImage: BufferedImage): Map[Int, Array[Array[Int]]] = {
precisionSet.map(p => p -> getImageData(p, grayImage, alreadyGray = true))(collection.breakOut)
} }
def getImageData(precision: Int, image: BufferedImage, alreadyGray: Boolean): Array[Array[Int]] = { def getImageData(precision: Int, image: BufferedImage, alreadyGray: Boolean): Array[Array[Int]] = {
@ -113,6 +99,20 @@ object HashService extends Logging {
ImageUtil.getImageData(resizedImage) ImageUtil.getImageData(resizedImage)
} }
def getSHA1(filePath: String): String = {
managed(new FileInputStream(filePath)) acquireAndGet {
input => DigestUtils.sha1Hex(input)
}
}
def getPrecisionSet(ahashSettings: HashSetting, dhashSettings: HashSetting, phashSettings: HashSetting): Set[Int] = {
Set(
if (ahashSettings.use) Option(ahashSettings.precision) else None,
if (dhashSettings.use) Option(dhashSettings.precision) else None,
if (phashSettings.use) Option(phashSettings.precision) else None
).flatten
}
/** /**
* Simpler function that only works with the imageData and the hashFunction * Simpler function that only works with the imageData and the hashFunction
* *

892
parent/pom.xml

@ -1,460 +1,460 @@
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modelVersion>4.0.0</modelVersion>
<groupId>com.sothr.imagetools</groupId>
<artifactId>parent</artifactId>
<version>1.0.1</version>
<groupId>com.sothr.imagetools</groupId>
<artifactId>parent</artifactId>
<version>1.0.1</version>
<packaging>pom</packaging>
<name>Image-Tools-Parent</name>
<packaging>pom</packaging>
<name>Image-Tools-Parent</name>
<repositories>
<repository>
<name>Sothr Maven Public Proxy</name>
<id>mvn-public-sothr-proxy</id>
<url>https://nexus.sothr.com/repository/maven-public/</url>
</repository>
<repository>
<name>Sothr Releases</name>
<id>sothr-nexus-releases</id>
<url>https://nexus.sothr.com/repository/maven-releases/</url>
<releases>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
<checksumPolicy>fail</checksumPolicy>
</releases>
<snapshots>
<enabled>false</enabled>
<updatePolicy>never</updatePolicy>
<checksumPolicy>warn</checksumPolicy>
</snapshots>
</repository>
<repository>
<name>Sothr Snapshots</name>
<id>sothr-nexus-snapshots</id>
<url>https://nexus.sothr.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
<updatePolicy>never</updatePolicy>
<checksumPolicy>warn</checksumPolicy>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
<checksumPolicy>fail</checksumPolicy>
</snapshots>
</repository>
</repositories>
<repositories>
<repository>
<name>Sothr Maven Public Proxy</name>
<id>mvn-public-sothr-proxy</id>
<url>https://nexus.sothr.com/repository/maven-public/</url>
</repository>
<repository>
<name>Sothr Releases</name>
<id>sothr-nexus-releases</id>
<url>https://nexus.sothr.com/repository/maven-releases/</url>
<releases>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
<checksumPolicy>fail</checksumPolicy>
</releases>
<snapshots>
<enabled>false</enabled>
<updatePolicy>never</updatePolicy>
<checksumPolicy>warn</checksumPolicy>
</snapshots>
</repository>
<repository>
<name>Sothr Snapshots</name>
<id>sothr-nexus-snapshots</id>
<url>https://nexus.sothr.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
<updatePolicy>never</updatePolicy>
<checksumPolicy>warn</checksumPolicy>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
<checksumPolicy>fail</checksumPolicy>
</snapshots>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>sothr-nexus-releases</id>
<url>https://nexus.sothr.com/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>sothr-nexus-snapshots</id>
<url>https://nexus.sothr.com/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
<distributionManagement>
<repository>
<id>sothr-nexus-releases</id>
<url>https://nexus.sothr.com/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>sothr-nexus-snapshots</id>
<url>https://nexus.sothr.com/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
<pluginRepositories>
<pluginRepository>
<id>sonatype-releases</id>
<url>http://oss.sonatype.org/content/repositories/releases</url>
</pluginRepository>
<pluginRepository>
<id>clojars.org</id>
<url>http://clojars.org/repo</url>
</pluginRepository>
</pluginRepositories>
<pluginRepositories>
<pluginRepository>
<id>sonatype-releases</id>
<url>http://oss.sonatype.org/content/repositories/releases</url>
</pluginRepository>
<pluginRepository>
<id>clojars.org</id>
<url>http://clojars.org/repo</url>
</pluginRepository>
</pluginRepositories>
<properties>
<!-- Image Tools Library Versions -->
<imagetools.hash.version>0.2.0</imagetools.hash.version>
<imagetools.engine.version>0.1.3</imagetools.engine.version>
<!-- Other Dependency Versions -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jdk.version>1.8</jdk.version>
<scala.binary.version>2.12</scala.binary.version>
<lib.scala-library.version>2.12.4</lib.scala-library.version>
<lib.junit.version>4.12</lib.junit.version>
<lib.scalatest.version>3.0.4</lib.scalatest.version>
<lib.logback.version>1.2.3</lib.logback.version>
<lib.slf4j.version>1.7.25</lib.slf4j.version>
<lib.grizzled-slf4j.version>1.3.2</lib.grizzled-slf4j.version>
<lib.akka.version>2.5.8</lib.akka.version>
<lib.jta.version>1.1</lib.jta.version>
<lib.ehcache.version>2.10.4</lib.ehcache.version>
<lib.commons-cli.version>1.4</lib.commons-cli.version>
<lib.commons-codec.version>1.11</lib.commons-codec.version>
<lib.jtransforms.version>2.4.0</lib.jtransforms.version>
<lib.typesafe-config.version>1.3.2</lib.typesafe-config.version>
<lib.thumbnailator.version>[0.4, 0.5)</lib.thumbnailator.version>
<lib.h2database.version>1.4.196</lib.h2database.version>
<lib.hibernate.version>4.3.11.Final</lib.hibernate.version><!-- Upgrade to 5.2.12.Final -->
<lib.hibernate.ehcache.version>2.6.11</lib.hibernate.ehcache.version>
<lib.markdown4j.version>2.2-cj-1.1</lib.markdown4j.version>
<lib.scala-arm.version>2.0</lib.scala-arm.version>
<lib.twelvemonkeys.imageio.version>3.3.2</lib.twelvemonkeys.imageio.version>
</properties>
<properties>
<!-- Image Tools Library Versions -->
<imagetools.hash.version>0.2.0</imagetools.hash.version>
<imagetools.engine.version>0.1.3</imagetools.engine.version>
<!-- Other Dependency Versions -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jdk.version>1.8</jdk.version>
<scala.binary.version>2.12</scala.binary.version>
<lib.scala-library.version>2.12.4</lib.scala-library.version>
<lib.junit.version>4.12</lib.junit.version>
<lib.scalatest.version>3.0.4</lib.scalatest.version>
<lib.logback.version>1.2.3</lib.logback.version>
<lib.slf4j.version>1.7.25</lib.slf4j.version>
<lib.grizzled-slf4j.version>1.3.2</lib.grizzled-slf4j.version>
<lib.akka.version>2.5.8</lib.akka.version>
<lib.jta.version>1.1</lib.jta.version>
<lib.ehcache.version>2.10.4</lib.ehcache.version>
<lib.commons-cli.version>1.4</lib.commons-cli.version>
<lib.commons-codec.version>1.11</lib.commons-codec.version>
<lib.jtransforms.version>2.4.0</lib.jtransforms.version>
<lib.typesafe-config.version>1.3.2</lib.typesafe-config.version>
<lib.thumbnailator.version>[0.4, 0.5)</lib.thumbnailator.version>
<lib.h2database.version>1.4.196</lib.h2database.version>
<lib.hibernate.version>4.3.11.Final</lib.hibernate.version><!-- Upgrade to 5.2.12.Final -->
<lib.hibernate.ehcache.version>2.6.11</lib.hibernate.ehcache.version>
<lib.markdown4j.version>2.2-cj-1.1</lib.markdown4j.version>
<lib.scala-arm.version>2.0</lib.scala-arm.version>
<lib.twelvemonkeys.imageio.version>3.3.2</lib.twelvemonkeys.imageio.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.sothr.imagetools</groupId>
<artifactId>hash</artifactId>
<version>${imagetools.hash.version}</version>
</dependency>
<dependency>
<groupId>com.sothr.imagetools</groupId>
<artifactId>engine</artifactId>
<version>${imagetools.engine.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${lib.junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_${scala.binary.version}</artifactId>
<version>${lib.scalatest.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${lib.logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${lib.logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>${lib.logback.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${lib.slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.clapper</groupId>
<artifactId>grizzled-slf4j_${scala.binary.version}</artifactId>
<version>${lib.grizzled-slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${lib.scala-library.version}</version>
</dependency>
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>${lib.thumbnailator.version}</version>
</dependency>
<dependency>
<groupId>com.typesafe</groupId>
<artifactId>config</artifactId>
<version>${lib.typesafe-config.version}</version>
</dependency>
<dependency>
<groupId>net.sourceforge.jtransforms</groupId>
<artifactId>jtransforms</artifactId>
<version>${lib.jtransforms.version}</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>${lib.commons-cli.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${lib.commons-codec.version}</version>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>${lib.jta.version}</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>${lib.ehcache.version}</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor_${scala.binary.version}</artifactId>
<version>${lib.akka.version}</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-slf4j_${scala.binary.version}</artifactId>
<version>${lib.akka.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${lib.h2database.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${lib.hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
<version>${lib.hibernate.version}</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>${lib.hibernate.ehcache.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-c3p0</artifactId>
<version>${lib.hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.commonjava.googlecode.markdown4j</groupId>
<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>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-jpeg</artifactId>
<version>${lib.twelvemonkeys.imageio.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.sothr.imagetools</groupId>
<artifactId>hash</artifactId>
<version>${imagetools.hash.version}</version>
</dependency>
<dependency>
<groupId>com.sothr.imagetools</groupId>
<artifactId>engine</artifactId>
<version>${imagetools.engine.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${lib.junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_${scala.binary.version}</artifactId>
<version>${lib.scalatest.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${lib.logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${lib.logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>${lib.logback.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${lib.slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.clapper</groupId>
<artifactId>grizzled-slf4j_${scala.binary.version}</artifactId>
<version>${lib.grizzled-slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${lib.scala-library.version}</version>
</dependency>
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>${lib.thumbnailator.version}</version>
</dependency>
<dependency>
<groupId>com.typesafe</groupId>
<artifactId>config</artifactId>
<version>${lib.typesafe-config.version}</version>
</dependency>
<dependency>
<groupId>net.sourceforge.jtransforms</groupId>
<artifactId>jtransforms</artifactId>
<version>${lib.jtransforms.version}</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>${lib.commons-cli.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${lib.commons-codec.version}</version>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>${lib.jta.version}</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>${lib.ehcache.version}</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor_${scala.binary.version}</artifactId>
<version>${lib.akka.version}</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-slf4j_${scala.binary.version}</artifactId>
<version>${lib.akka.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${lib.h2database.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${lib.hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
<version>${lib.hibernate.version}</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>${lib.hibernate.ehcache.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-c3p0</artifactId>
<version>${lib.hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.commonjava.googlecode.markdown4j</groupId>
<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>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-jpeg</artifactId>
<version>${lib.twelvemonkeys.imageio.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*.conf</include>
<include>**/*.properties</include>
<include>**/*.info</include>
<include>**/*.md</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>**/*.conf</exclude>
<exclude>**/*.properties</exclude>
<exclude>**/*.info</exclude>
<exclude>**/*.md</exclude>
</excludes>
</resource>
</resources>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*.conf</include>
<include>**/*.properties</include>
<include>**/*.info</include>
<include>**/*.md</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>**/*.conf</exclude>
<exclude>**/*.properties</exclude>
<exclude>**/*.info</exclude>
<exclude>**/*.md</exclude>
</excludes>
</resource>
</resources>
<testResources>
<testResource>
<directory>src/test/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*.conf</include>
<include>**/*.properties</include>
<include>**/*.info</include>
<include>**/*.md</include>
</includes>
</testResource>
<testResource>
<directory>src/test/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>**/*.conf</exclude>
<exclude>**/*.properties</exclude>
<exclude>**/*.info</exclude>
<exclude>**/*.md</exclude>
</excludes>
</testResource>
</testResources>
<testResources>
<testResource>
<directory>src/test/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*.conf</include>
<include>**/*.properties</include>
<include>**/*.info</include>
<include>**/*.md</include>
</includes>
</testResource>
<testResource>
<directory>src/test/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>**/*.conf</exclude>
<exclude>**/*.properties</exclude>
<exclude>**/*.info</exclude>
<exclude>**/*.md</exclude>
</excludes>
</testResource>
</testResources>
<pluginManagement>
<plugins>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.1.6</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
</plugin>
</plugins>
</pluginManagement>
<pluginManagement>
<plugins>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.1.6</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<!-- disable surefire for java tests-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.7</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<!-- Version Management -->
<plugin>
<groupId>com.code54.mojo</groupId>
<artifactId>buildversion-plugin</artifactId>
<version>1.0.3</version>
<executions>
<execution>
<goals>
<goal>set-properties</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Override Compilation Settings -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${jdk.version}</source>
<target>${jdk.version}</target>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Handle Polygot Scala -->
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<executions>
<execution>
<id>scala-compile-first</id>
<phase>process-resources</phase>
<goals>
<goal>add-source</goal>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>scala-test-compile</id>
<phase>process-test-resources</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
<!--<configuration>
<jvmArgs>
<jvmArg>-Xms64m</jvmArg>
<jvmArg>-Xmx1024m</jvmArg>
</jvmArgs>
</configuration>-->
</plugin>
<!-- Build Management -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<outputDirectory>
${project.build.directory}/release/lib
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- Source Management -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Resource Management -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>initialize</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}</outputDirectory>
<resources>
<resource>
<directory>src/includes</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>copy-resources-package</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/release</outputDirectory>
<resources>
<resource>
<directory>src/includes</directory>
<filtering>true</filtering>
<excludes>
<exclude>version.info</exclude>
<exclude>name.info</exclude>
</excludes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<plugins>
<!-- disable surefire for java tests-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.7</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<!-- Version Management -->
<plugin>
<groupId>com.code54.mojo</groupId>
<artifactId>buildversion-plugin</artifactId>
<version>1.0.3</version>
<executions>
<execution>
<goals>
<goal>set-properties</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Override Compilation Settings -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${jdk.version}</source>
<target>${jdk.version}</target>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Handle Polygot Scala -->
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<executions>
<execution>
<id>scala-compile-first</id>
<phase>process-resources</phase>
<goals>
<goal>add-source</goal>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>scala-test-compile</id>
<phase>process-test-resources</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
<!--<configuration>
<jvmArgs>
<jvmArg>-Xms64m</jvmArg>
<jvmArg>-Xmx1024m</jvmArg>
</jvmArgs>
</configuration>-->
</plugin>
<!-- Build Management -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<outputDirectory>
${project.build.directory}/release/lib
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- Source Management -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Resource Management -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>initialize</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}</outputDirectory>
<resources>
<resource>
<directory>src/includes</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>copy-resources-package</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/release</outputDirectory>
<resources>
<resource>
<directory>src/includes</directory>
<filtering>true</filtering>
<excludes>
<exclude>version.info</exclude>
<exclude>name.info</exclude>
</excludes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project> </project>

42
pom.xml

@ -1,29 +1,29 @@
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modelVersion>4.0.0</modelVersion>
<groupId>com.sothr.imagetools</groupId>
<artifactId>build</artifactId>
<version>0.1.3-DEV</version>
<packaging>pom</packaging>
<groupId>com.sothr.imagetools</groupId>
<artifactId>build</artifactId>
<version>0.1.3-DEV</version>
<packaging>pom</packaging>
<name>Image-Tools</name>
<description>An image collection management utility</description>
<url>http://imagetools.sothr.com</url>
<organization>
<name>Sothr Software</name>
</organization>
<name>Image-Tools</name>
<description>An image collection management utility</description>
<url>http://imagetools.sothr.com</url>
<organization>
<name>Sothr Software</name>
</organization>
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<modules>
<module>parent</module>
<module>hash</module>
<module>engine</module>
<module>cli</module>
<module>gui</module>
</modules>
<modules>
<module>parent</module>
<module>hash</module>
<module>engine</module>
<module>cli</module>
<module>gui</module>
</modules>
</project> </project>
Loading…
Cancel
Save