Browse Source

Cleanup, Reorganize code, fix formatting, change thumbnail format to PNG to avoid a weird transparency issue.

master
Drew Short 10 years ago
parent
commit
fd43e7db8e
  1. 11
      cli/pom.xml
  2. 3
      cli/src/main/java/com/sothr/imagetools/cli/AppCLI.java
  3. 12
      engine/pom.xml
  4. 28
      engine/src/main/java/com/sothr/imagetools/engine/AppConfig.java
  5. 3
      engine/src/main/resources/hibernate.cfg.xml
  6. 98
      engine/src/main/scala/com/sothr/imagetools/engine/ConcurrentEngine.scala
  7. 40
      engine/src/main/scala/com/sothr/imagetools/engine/SequentialEngine.scala
  8. 8
      engine/src/main/scala/com/sothr/imagetools/engine/dao/HibernateUtil.scala
  9. 4
      engine/src/main/scala/com/sothr/imagetools/engine/dto/ImageHashDTO.scala
  10. 12
      engine/src/main/scala/com/sothr/imagetools/engine/hash/HashService.scala
  11. 41
      engine/src/main/scala/com/sothr/imagetools/engine/image/Image.scala
  12. 166
      engine/src/main/scala/com/sothr/imagetools/engine/image/ImageService.scala
  13. 18
      engine/src/main/scala/com/sothr/imagetools/engine/image/SimilarImages.scala
  14. 73
      engine/src/main/scala/com/sothr/imagetools/engine/util/PropertiesService.scala
  15. 16
      engine/src/main/scala/com/sothr/imagetools/engine/util/Version.scala
  16. 3
      engine/src/test/resources/hibernate.cfg.xml
  17. 11
      gui/pom.xml
  18. 12
      gui/src/main/java/com/sothr/imagetools/ui/App.java
  19. 153
      gui/src/main/resources/fxml/mainapp/MainApp.fxml
  20. 2
      gui/src/main/scala/com/sothr/imagetools/ui/component/ImageTile.scala
  21. 2
      gui/src/main/scala/com/sothr/imagetools/ui/component/ImageTileFactory.scala
  22. 68
      gui/src/main/scala/com/sothr/imagetools/ui/component/ImageTilePane.scala
  23. 142
      gui/src/main/scala/com/sothr/imagetools/ui/controller/AppController.scala
  24. 8
      parent/pom.xml
  25. 2
      pom.xml

11
cli/pom.xml

@ -1,4 +1,4 @@
<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">
<modelVersion>4.0.0</modelVersion>
@ -88,9 +88,12 @@
<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"/>
<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>

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

@ -18,10 +18,9 @@ import scala.collection.immutable.List;
*/
class AppCLI {
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 {

12
engine/pom.xml

@ -1,4 +1,4 @@
<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">
<modelVersion>4.0.0</modelVersion>
@ -119,6 +119,10 @@
<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>
@ -192,10 +196,12 @@
<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}/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"/>
<copy file="${project.build.directory}/README.md" toFile="${basedir}/../README.md"
overwrite="true"/>
</tasks>
</configuration>
<goals>

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

@ -18,24 +18,20 @@ import java.io.File;
public class AppConfig {
private static Logger logger;
public static CacheManager cacheManager;
// Logging defaults
private static final String LOGSETTINGSFILE = "./logback.xml";
private static Boolean configuredLogging = false;
// 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;
// General Akka Actor System
private static final ActorSystem appSystem = ActorSystem.create("ITActorSystem");
// The Main App
private static Stage primaryStage = null;
@ -47,11 +43,13 @@ public class AppConfig {
primaryStage = newPrimaryStage;
}
public static FXMLLoader fxmlLoader = null;
public static FXMLLoader getFxmlLoader() { return fxmlLoader; }
public static FXMLLoader getFxmlLoader() {
return fxmlLoader;
}
public static void setFxmlLoader(FXMLLoader loader) { fxmlLoader = loader; }
public static void setFxmlLoader(FXMLLoader loader) {
fxmlLoader = loader;
}
public static ActorSystem getAppActorSystem() {
return appSystem;
@ -101,7 +99,7 @@ public class AppConfig {
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()));
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

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

@ -24,7 +24,8 @@
<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.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>

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

@ -31,37 +31,6 @@ class ConcurrentEngine extends Engine with grizzled.slf4j.Logging {
engineSimilarityController ! SetNewListener(listenerRef)
}
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
}
//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")
@ -116,6 +85,37 @@ class ConcurrentEngine extends Engine with grizzled.slf4j.Logging {
info(s"Finished processing ${images.size} images. Found $similarCount similar images")
cleanedSimilarImages.toList
}
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
}
}
@ -158,13 +158,6 @@ class ConcurrentEngineProcessingController extends Actor with ActorLogging {
var listener = context.actorOf(Props[DefaultLoggingEngineListener],
name = "ProcessedEngineListener")
def setListener(newListener: ActorRef) = {
//remove the old listener
this.listener ! PoisonPill
//setup the new listener
this.listener = newListener
}
override def preStart() = {
log.info("Staring the controller for processing images")
log.info("Using {} actors to process images", numOfRouters)
@ -182,9 +175,11 @@ class ConcurrentEngineProcessingController extends Actor with ActorLogging {
case _ => log.info("received unknown message")
}
override def postStop() = {
super.postStop()
def setListener(newListener: ActorRef) = {
//remove the old listener
this.listener ! PoisonPill
//setup the new listener
this.listener = newListener
}
def startEngine() = {
@ -250,6 +245,11 @@ class ConcurrentEngineProcessingController extends Actor with ActorLogging {
throw e
}
}
override def postStop() = {
super.postStop()
this.listener ! PoisonPill
}
}
class ConcurrentEngineProcessingActor extends Actor with ActorLogging {
@ -313,13 +313,6 @@ class ConcurrentEngineSimilarityController extends Actor with ActorLogging {
var listener = context.actorOf(Props[DefaultLoggingEngineListener],
name = "SimilarityEngineListener")
def setListener(newListener: ActorRef) = {
//remove the old listener
this.listener ! PoisonPill
//setup the new listener
this.listener = newListener
}
override def preStart() = {
log.info("Staring the controller for processing similarities between images")
log.info("Using {} actors to process image similarities", numOfRouters)
@ -337,9 +330,11 @@ class ConcurrentEngineSimilarityController extends Actor with ActorLogging {
case _ => log.info("received unknown message")
}
override def postStop() = {
super.postStop()
def setListener(newListener: ActorRef) = {
//remove the old listener
this.listener ! PoisonPill
//setup the new listener
this.listener = newListener
}
def startEngine() = {
@ -412,6 +407,11 @@ class ConcurrentEngineSimilarityController extends Actor with ActorLogging {
throw e
}
}
override def postStop() = {
super.postStop()
this.listener ! PoisonPill
}
}
class ConcurrentEngineSimilarityActor extends Actor with ActorLogging {

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

@ -29,26 +29,6 @@ class SequentialEngine extends Engine with Logging {
this.similarityListener = listenerRef
}
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 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)
@ -88,4 +68,24 @@ class SequentialEngine extends Engine with Logging {
info(s"Finished processing ${images.size} images. Found $similarCount similar images")
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
}
}

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

@ -17,6 +17,10 @@ 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
@ -34,9 +38,5 @@ object HibernateUtil extends Logging {
}
}
def getSessionFactory: SessionFactory = {
sessionFactory
}
}

4
engine/src/main/scala/com/sothr/imagetools/engine/dto/ImageHashDTO.scala

@ -8,12 +8,12 @@ import grizzled.slf4j.Logging
@Table(name = "ImageHash")
class ImageHashDTO(var ahash: Long, var dhash: Long, var phash: Long, var md5: String) extends Serializable with Logging {
def this() = this(0l, 0l, 0l, "")
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
var id: Int = _
def this() = this(0l, 0l, 0l, "")
def getId: Int = id
def setId(newId: Int) = {

12
engine/src/main/scala/com/sothr/imagetools/engine/hash/HashService.scala

@ -117,6 +117,12 @@ object HashService extends Logging {
if (distance <= tolerence) true else false
}
def areImageHashesSimilar(imageHash1: ImageHashDTO, imageHash2: ImageHashDTO): Boolean = {
val weightedHammingMean = getWeightedHashSimilarity(imageHash1, imageHash2)
val weightedToleranceMean = getWeightedHashTolerence
if (weightedHammingMean <= weightedToleranceMean) true else false
}
def getWeightedHashSimilarity(imageHash1: ImageHashDTO, imageHash2: ImageHashDTO): Float = {
//ahash
val aHashTolerance = PropertiesService.aHashTolerance
@ -196,10 +202,4 @@ object HashService extends Logging {
weightedTolerance
}
def areImageHashesSimilar(imageHash1: ImageHashDTO, imageHash2: ImageHashDTO): Boolean = {
val weightedHammingMean = getWeightedHashSimilarity(imageHash1, imageHash2)
val weightedToleranceMean = getWeightedHashTolerence
if (weightedHammingMean <= weightedToleranceMean) true else false
}
}

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

@ -10,18 +10,21 @@ import grizzled.slf4j.Logging
@Table(name = "Image")
class Image(val image: String, val thumbnail: String, val size: (Int, Int), val imageHashes: ImageHashDTO = null) extends Serializable with Logging {
def this() = this("", "", (0, 0), null)
@Id
var imagePath: String = image
def getImagePath: String = imagePath
def setImagePath(path: String) = {
imagePath = path
var thumbnailPath: String = thumbnail
var width: Int = size._1
var height: Int = size._2
var hashes: ImageHashDTO = imageHashes
@transient
var imageSize: (Int, Int) = {
new Tuple2(width, height)
}
@transient
var imageName: String = ""
var imageType: ImageType = ImageType.SingleFrameImage
var thumbnailPath: String = thumbnail
def this() = this("", "", (0, 0), null)
def getThumbnailPath: String = thumbnailPath
@ -29,40 +32,24 @@ class Image(val image: String, val thumbnail: String, val size: (Int, Int), val
thumbnailPath = path
}
var width: Int = size._1
def getWidth: Int = width
def setWidth(size: Int) = {
width = size
}
var height: Int = size._2
def getHeight: Int = height
def setHeight(size: Int) = {
height = size
}
var hashes: ImageHashDTO = imageHashes
def getHashes: ImageHashDTO = hashes
def setHashes(newHashes: ImageHashDTO) = {
hashes = newHashes
}
@transient
var imageSize: (Int, Int) = {
new Tuple2(width, height)
}
@transient
var imageName: String = ""
var imageType: ImageType = ImageType.SingleFrameImage
def getName: String = {
if (this.imageName.length < 1) {
this.imageName = this.getImagePath.split('/').last
@ -70,6 +57,12 @@ class Image(val image: String, val thumbnail: String, val size: (Int, Int), val
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(this.hashes, otherImage.hashes)

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

@ -18,6 +18,32 @@ object ImageService extends Logging {
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(bufferedImage, file.getAbsolutePath)
var thumbnailPath = lookupThumbnailPath(hashes.md5)
if (thumbnailPath == null) thumbnailPath = getThumbnail(bufferedImage, hashes.md5)
val imageSize = {
(bufferedImage.getWidth, bufferedImage.getHeight)
}
val image = new Image(file.getAbsolutePath, thumbnailPath, imageSize, 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
}
private def lookupImage(file: File): Image = {
var image: Image = null
var found = false
@ -54,46 +80,16 @@ object ImageService extends Logging {
image
}
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(bufferedImage, file.getAbsolutePath)
var thumbnailPath = lookupThumbnailPath(hashes.md5)
if (thumbnailPath == null) thumbnailPath = getThumbnail(bufferedImage, hashes.md5)
val imageSize = {
(bufferedImage.getWidth, bufferedImage.getHeight)
}
val image = new Image(file.getAbsolutePath, thumbnailPath, imageSize, 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 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 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 = {
@ -110,18 +106,6 @@ object ImageService extends Logging {
path
}
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 getThumbnail(image: BufferedImage, md5: String): String = {
//create thumbnail
val thumb = resize(image, PropertiesService.get(PropertyEnum.ThumbnailSize.toString).toInt, forced = false)
@ -129,7 +113,7 @@ object ImageService extends Logging {
val path = calculateThumbPath(md5)
// save thumbnail to path
try {
ImageIO.write(thumb, "jpg", new File(path))
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)
@ -138,37 +122,6 @@ object ImageService extends Logging {
path
}
/**
* Get the raw data for an image
*/
def getImageData(image: BufferedImage): Array[Array[Int]] = {
convertTo2DWithoutUsingGetRGB(image)
}
/**
* Quickly convert an image to grayscale
*
* @param image image to convert to greyscale
* @return
*/
def convertToGray(image: BufferedImage): BufferedImage = {
//debug("Converting an image to grayscale")
val grayImage = new BufferedImage(image.getWidth, image.getHeight, BufferedImage.TYPE_BYTE_GRAY)
//create a color conversion operation
val op = new ColorConvertOp(
image.getColorModel.getColorSpace,
grayImage.getColorModel.getColorSpace, null)
//convert the image to grey
val result = op.filter(image, grayImage)
//val g = image.getGraphics
//g.drawImage(image,0,0,null)
//g.dispose()
result
}
def resize(image: BufferedImage, size: Int, forced: Boolean = false): BufferedImage = {
//debug(s"Resizing an image to size: ${size}x${size} forced: $forced")
if (forced) {
@ -178,6 +131,29 @@ object ImageService extends Logging {
}
}
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)
}
}
/**
* Get the raw data for an image
*/
def getImageData(image: BufferedImage): Array[Array[Int]] = {
convertTo2DWithoutUsingGetRGB(image)
}
/**
* Convert a buffered image into a 2d pixel data array
*
@ -256,4 +232,28 @@ object ImageService extends Logging {
}
result
}
/**
* Quickly convert an image to grayscale
*
* @param image image to convert to greyscale
* @return
*/
def convertToGray(image: BufferedImage): BufferedImage = {
//debug("Converting an image to grayscale")
val grayImage = new BufferedImage(image.getWidth, image.getHeight, BufferedImage.TYPE_BYTE_GRAY)
//create a color conversion operation
val op = new ColorConvertOp(
image.getColorModel.getColorSpace,
grayImage.getColorModel.getColorSpace, null)
//convert the image to grey
val result = op.filter(image, grayImage)
//val g = image.getGraphics
//g.drawImage(image,0,0,null)
//g.dispose()
result
}
}

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

@ -9,15 +9,6 @@ import grizzled.slf4j.Logging
*/
class SimilarImages(val rootImage: Image, val similarImages: List[Image]) extends Logging {
protected def getPrettySimilarImagesList: String = {
val sb = new StringBuilder()
for (image <- similarImages) {
sb.append(image.imagePath)
sb.append(System.lineSeparator())
}
sb.toString()
}
override def hashCode: Int = {
val prime = 7
var result = prime * 1 + rootImage.hashCode
@ -33,4 +24,13 @@ class SimilarImages(val rootImage: Image, val similarImages: List[Image]) extend
$getPrettySimilarImagesList""".stripMargin
}
protected def getPrettySimilarImagesList: String = {
val sb = new StringBuilder()
for (image <- similarImages) {
sb.append(image.imagePath)
sb.append(System.lineSeparator())
}
sb.toString()
}
}

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

@ -11,17 +11,14 @@ import grizzled.slf4j.Logging
*/
object PropertiesService extends Logging {
private var defaultConf: Config = null
private var userConf: Config = null
//OS information
val OS = System.getProperty("os.name", "UNKNOWN")
val OS_VERSION = System.getProperty("os.version", "UNKNOWN")
val OS_ARCH = System.getProperty("os.arch", "UNKNOWN")
private val newUserConf: Properties = new Properties()
private var version: Version = null
private val configRenderOptions = ConfigRenderOptions.concise().setFormatted(true)
def getVersion: Version = this.version
//specific highly used properties
var TimingEnabled: Boolean = false
//ahash
var aHashPrecision = 0
var aHashTolerance = 0
@ -37,11 +34,11 @@ object PropertiesService extends Logging {
var pHashTolerance = 0
var pHashWeight = 0.0f
var usePhash = false
private var defaultConf: Config = null
private var userConf: Config = null
private var version: Version = null
//OS information
val OS = System.getProperty("os.name", "UNKNOWN")
val OS_VERSION = System.getProperty("os.version", "UNKNOWN")
val OS_ARCH = System.getProperty("os.arch", "UNKNOWN")
def getVersion: Version = this.version
/*
* Load the properties file from the specified location
@ -79,16 +76,21 @@ object PropertiesService extends Logging {
info("Loaded Special Properties")
}
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
}
private def getCleanedMergedUserConf: Config = {
ConfigFactory.parseProperties(cleanAndPrepareNewUserProperties()) withFallback userConf
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) = {
@ -101,6 +103,18 @@ object PropertiesService extends Logging {
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)
@ -111,23 +125,6 @@ object PropertiesService extends Logging {
result
}
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 set(key: String, value: String) = {
newUserConf.setProperty(key, value)
}

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

@ -72,14 +72,6 @@ class Version(val versionString: String) extends Logging {
}
}
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 hashCode(): Int = {
val prime: Int = 37
val result: Int = 255
@ -89,4 +81,12 @@ class Version(val versionString: String) extends Logging {
hash += buildTag.hashCode
prime * result + hash
}
def parsableToString(): String = {
s"$major.$minor.$patch-$buildTag-$buildNumber-$buildHash"
}
override def toString: String = {
s"$major.$minor.$patch-$buildTag build:$buildNumber code:$buildHash"
}
}

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

@ -21,7 +21,8 @@
<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.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>

11
gui/pom.xml

@ -1,4 +1,4 @@
<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">
<modelVersion>4.0.0</modelVersion>
@ -92,9 +92,12 @@
<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"/>
<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>

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

@ -12,8 +12,11 @@ import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import java.io.IOException;
import java.net.URL;
import java.util.Iterator;
import java.util.List;
/**
@ -21,9 +24,8 @@ import java.util.List;
*/
public class App extends Application {
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();
@ -54,6 +56,12 @@ public class App extends Application {
//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);

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

@ -6,97 +6,136 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.Font?>
<AnchorPane fx:id="rootPane" minHeight="600.0" minWidth="1024.0" prefHeight="600.0" prefWidth="1024.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sothr.imagetools.ui.controller.AppController">
<AnchorPane 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"
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">
<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" />
<MenuItem fx:id="" mnemonicParsing="false" onAction="#closeAction" text="Close"/>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Edit">
<items>
<MenuItem mnemonicParsing="false" text="Settings" />
<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" />
<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">
<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">
<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">
<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">
<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" />
<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">
<FlowPane prefHeight="200.0" prefWidth="200.0"
BorderPane.alignment="CENTER">
<children>
<Label maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minWidth="210.0" text="Selected Folder:">
<Label maxHeight="1.7976931348623157E308"
maxWidth="1.7976931348623157E308" minWidth="210.0"
text="Selected Folder:">
<padding>
<Insets left="5.0" right="5.0" />
<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">
<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 name="System Bold" size="12.0"/>
</font>
<padding>
<Insets left="5.0" right="5.0" />
<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">
<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">
<CheckBox fx:id="doRecursiveProcessing" mnemonicParsing="false"
text="Recursive Search">
<FlowPane.margin>
<Insets bottom="5.0" />
<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" />
<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" />
<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">
<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">
<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" />
<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">
<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" />
<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>
@ -109,39 +148,49 @@
<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;" />
<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" />
<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">
<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" />
<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 />
<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>
<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">
<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;" />
<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>

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

@ -100,7 +100,7 @@ class ImageTile(thumbnailWidth: Integer,
imageLabel.setText(s"${image.getHeight}x${image.getWidth}")
imageLabel.setWrapText(true)
imageLabel.setMaxHeight(32d)
imageLabel.setMaxWidth(preferedTileWidth-2)
imageLabel.setMaxWidth(preferedTileWidth - 2)
imageLabel.setAlignment(Pos.BOTTOM_CENTER)
//Tooltip

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

@ -14,7 +14,7 @@ import grizzled.slf4j.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 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))

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

@ -35,16 +35,16 @@ class ImageTilePane extends TilePane with Logging {
if (numSelected == 1) {
val contextMenu = getSingleSelectionContextMenu
debug("Showing context menu")
contextMenu.show(event.getTarget.asInstanceOf[Node],Side.RIGHT,0d,0d)
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)
contextMenu.show(event.getTarget.asInstanceOf[Node], Side.RIGHT, 0d, 0d)
}
}
}
def getSingleSelectionContextMenu : ContextMenu = {
def getSingleSelectionContextMenu: ContextMenu = {
debug("Building single-selection context menu")
val contextMenu = new ContextMenu()
val delete = new MenuItem("Delete")
@ -58,7 +58,7 @@ class ImageTilePane extends TilePane with Logging {
contextMenu
}
def getMulipleSelectionContextMenu : ContextMenu = {
def getMulipleSelectionContextMenu: ContextMenu = {
debug("Building multi-selection context menu")
val contextMenu = new ContextMenu()
val delete = new MenuItem("Delete")
@ -165,11 +165,29 @@ class ImageTilePaneSelectionModel[ImageTile](parentTilePane: ImageTilePane) exte
this.selectedIndexes.add(0)
}
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)
}
}
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 selectLast(): Unit = {
clearSelectionFormatting
this.selectedIndexes.clear()
setSelectionFormatting(this.parentTilePane.getChildren.size()-1)
this.selectedIndexes.add(this.parentTilePane.getChildren.size()-1)
setSelectionFormatting(this.parentTilePane.getChildren.size() - 1)
this.selectedIndexes.add(this.parentTilePane.getChildren.size() - 1)
}
override def clearAndSelect(index: Int): Unit = {
@ -187,6 +205,11 @@ class ImageTilePaneSelectionModel[ImageTile](parentTilePane: ImageTilePane) exte
}
}
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()
@ -203,14 +226,14 @@ class ImageTilePaneSelectionModel[ImageTile](parentTilePane: ImageTilePane) exte
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
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)) {
if (!this.selectedIndexes.contains(index)) {
setSelectionFormatting(index)
this.selectedIndexes.add(index)
}
@ -233,29 +256,6 @@ class ImageTilePaneSelectionModel[ImageTile](parentTilePane: ImageTilePane) exte
this.selectedIndexes.contains(index)
}
private def clearSelectionFormatting(index: Int) = {
val tile = this.parentTilePane.getChildren.get(index).asInstanceOf[VBox]
tile.setBorder(Border.EMPTY)
}
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)
}
}
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))
}
}
class ArrayObservableList[E] extends ModifiableObservableListBase[E] {
@ -270,15 +270,15 @@ class ArrayObservableList[E] extends ModifiableObservableListBase[E] {
delegate.size
}
def doAdd (index: Int, element: E) = {
def doAdd(index: Int, element: E) = {
delegate.add(index, element)
}
def doSet (index: Int, element: E): E = {
def doSet(index: Int, element: E): E = {
delegate.set(index, element)
}
def doRemove (index: Int): E = {
def doRemove(index: Int): E = {
delegate.remove(index)
}

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

@ -33,26 +33,22 @@ import scala.util.{Failure, Success}
*/
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
// Engine
val engine: Engine = new ConcurrentEngine()
// Current State
var currentDirectory: String = "."
var currentImages: List[Image] = List[Image]()
@ -135,6 +131,27 @@ class AppController extends Logging {
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")
@ -158,6 +175,45 @@ class AppController extends Logging {
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")
@ -167,7 +223,7 @@ class AppController extends Logging {
//endregion
//region buttons
//region pagination
@FXML
def browseFolders(event: ActionEvent) = {
@ -207,7 +263,7 @@ class AppController extends Logging {
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)
images.sortWith((x, y) => x.imagePath < y.imagePath)
}
f onComplete {
@ -258,13 +314,13 @@ class AppController extends Logging {
//endregion
//region pagination
def resetPaginator() = {
this.paginator.setDisable(true)
this.paginator.setPageCount(1)
}
//todo: include a templating engine for rendering information
def setPagesContent(images: List[Image]) = {
this.currentImages = images
//set the appropriate size for the pagination
@ -293,70 +349,10 @@ class AppController extends Logging {
})
}
//endregion
def getImageTilePane :TilePane = {
def getImageTilePane: TilePane = {
this.imageTilePane
}
//todo: include a templating engine for rendering information
//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()
}
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()
}
/**
* Show a plain text utility dialog
*
@ -432,7 +428,7 @@ class GUIEngineListener extends EngineListener with ActorLogging {
progressLabel.setText(s"Processed ${command.count}/${command.total}")
}
log.debug("Processed {}/{}", command.count, command.total)
progressBar.setProgress(command.count.toFloat/command.total)
progressBar.setProgress(command.count.toFloat / command.total)
}
})
}
@ -447,7 +443,7 @@ class GUIEngineListener extends EngineListener with ActorLogging {
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)
progressBar.setProgress(command.count.toFloat / command.total)
}
})
}

8
parent/pom.xml

@ -1,4 +1,4 @@
<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">
<modelVersion>4.0.0</modelVersion>
@ -44,6 +44,7 @@
<lib.hibernate.ehcache.version>2.6.6</lib.hibernate.ehcache.version>
<lib.markdown4j.version>2.2-cj-1.0</lib.markdown4j.version>
<lib.scala-arm.version>1.4</lib.scala-arm.version>
<lib.twelvemonkeys.imageio.version>3.0</lib.twelvemonkeys.imageio.version>
</properties>
<dependencyManagement>
@ -175,6 +176,11 @@
<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>

2
pom.xml

@ -1,4 +1,4 @@
<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">
<modelVersion>4.0.0</modelVersion>

Loading…
Cancel
Save