Save imported image in the same format (WIP) master
authorGreg Burri <greg.burri@gmail.com>
Tue, 20 Apr 2021 20:47:20 +0000 (22:47 +0200)
committerGreg Burri <greg.burri@gmail.com>
Tue, 20 Apr 2021 20:47:20 +0000 (22:47 +0200)
#313

Parasitemia/ParasitemiaUI/Constants.fs
Parasitemia/ParasitemiaUI/GUI.fs
Parasitemia/ParasitemiaUI/PiaZ.fs
Parasitemia/ParasitemiaUI/SourceImage.fs
Parasitemia/ParasitemiaUI/State.fs
Parasitemia/ParasitemiaUI/Utils.fs
Parasitemia/ParasitemiaUIControls/MainWindow.xaml

index bef3910..255c27b 100644 (file)
@@ -3,10 +3,12 @@
 open System
 open System.IO
 
+let APPLICATION_NAME = "Parasitemia"
+
 let USER_DIRECTORY =
     Path.Combine (
         Environment.GetFolderPath Environment.SpecialFolder.ApplicationData,
-        "Parasitemia"
+        APPLICATION_NAME
     )
 
 let USER_DIRECTORY_LOG =
index 3b67894..0832a41 100644 (file)
@@ -18,6 +18,7 @@ open Types
 let run (defaultConfig : Config) (fileToOpen : string option) =
     let app = new Application ()
     let win = MainWindow ()
+    win.Title <- Constants.APPLICATION_NAME
 
     let state = State.State defaultConfig
     let mutable currentScale = 1.
@@ -401,6 +402,9 @@ let run (defaultConfig : Config) (fileToOpen : string option) =
         updateDocumentStatus ()
 
     let loadFile (filepath : string) =
+        let displayLoadingErrorMessage (message : string) =
+            MessageBox.Show (sprintf "The document cannot be loaded from \"%s\": %s" filepath message, "Error loading the document", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore
+
         askSaveCurrent ()
         let previousFilePath = state.FilePath
         try
@@ -408,10 +412,14 @@ let run (defaultConfig : Config) (fileToOpen : string option) =
             state.Load ()
             updateGUI ()
         with
+        | PiaZ.VersionFileNewerException fileVersion ->
+            state.FilePath <- previousFilePath
+            displayLoadingErrorMessage $"File version ({fileVersion}) is newer than supported one ({PiaZ.CURRENT_FILE_VERSION}), you may update Parasitemia to the latest version"
+
         | :? IOException as ex ->
             Log.Error "%O" ex
             state.FilePath <- previousFilePath
-            MessageBox.Show (sprintf "The document cannot be loaded from \"%s\"" filepath, "Error loading the document", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore
+            displayLoadingErrorMessage "IO Error"
 
     let askLoadFile () =
         let dialog = OpenFileDialog (Filter = PiaZ.filter)
@@ -443,7 +451,7 @@ let run (defaultConfig : Config) (fileToOpen : string option) =
                 MessageBox.Show (sprintf "The results cannot be exported in \"%s\"" state.FilePath, "Error exporting the files", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore
 
     let importImage () =
-        let dialog = OpenFileDialog (Filter = "Image Files|*.png;*.jpg;*.tif;*.tiff", Multiselect = true)
+        let dialog = OpenFileDialog (Filter = "Image Files|*.png;*.jpg;.jpeg;.bmp;*.tif;*.tiff", Multiselect = true)
         let res = dialog.ShowDialog ()
         if res.HasValue && res.Value then
             let noSourceImage = state.SourceImages.Count () = 0
index 18e9d6a..8498c64 100644 (file)
@@ -43,9 +43,10 @@ type DocumentData =
         images : SourceImage list
     }
 
-let mainEntryName = "info.json"
-let imageExtension = ".tiff"
-let currentFileVersion = 2
+let MAIN_ENTRY_NAME = "info.json"
+let DEFAULT_IMAGE_EXTENSION = ".tiff"
+let JSON_EXTENSION = ".json"
+let CURRENT_FILE_VERSION = 3
 
 /// <summary>
 /// Save a document in a give file path. The file may already exist.
@@ -56,21 +57,31 @@ let currentFileVersion = 2
 let save (filePath : string) (data : DocumentData) =
     use file = ZipFile.Open (filePath, ZipArchiveMode.Update)
 
+    // We only delete JSON files and removed images.
     for e in List.ofSeq file.Entries do // 'ofSeq' to not iterate a collection currently modified.
-        e.Delete ()
+        if Path.GetExtension e.Name = JSON_EXTENSION || data.images |> List.exists (fun img -> img.OriginalName = e.Name) |> not then
+            e.Delete ()
 
     // Main JSON file.
-    let mainEntry = file.CreateEntry (mainEntryName, CompressionLevel.Fastest)
+    let mainEntry = file.CreateEntry (MAIN_ENTRY_NAME, CompressionLevel.Fastest)
     use mainEntryWriter = new StreamWriter (mainEntry.Open ())
-    mainEntryWriter.Write (JsonConvert.SerializeObject ({ patientID = data.patientID; fileVersion = currentFileVersion }))
+    mainEntryWriter.Write (JsonConvert.SerializeObject ({ patientID = data.patientID; fileVersion = CURRENT_FILE_VERSION }))
 
-    // Write each images and the associated information.
+    // Write each images and the associated information as a JSON file.
     for srcImg in data.images do
-        let imgFilename = (string srcImg.Num) + imageExtension
-        let imgEntry = file.CreateEntry (imgFilename, CompressionLevel.NoCompression) // FIXME: It seems a compression is applied to this file despite of the 'NoCompression' flag.
-        srcImg.Img.ToBitmap().Save (imgEntry.Open (), System.Drawing.Imaging.ImageFormat.Tiff)
+        match srcImg.TempFile with
+        | Some imgTempFile ->
+            let imgEntry = file.CreateEntry (srcImg.OriginalName, CompressionLevel.NoCompression)
+            (File.Open (imgTempFile, FileMode.Open, FileAccess.Read)).CopyTo (imgEntry.Open ())
+            srcImg.TempFile <- None
 
-        let imgJSONEntry = file.CreateEntry (imgFilename + ".json", CompressionLevel.Fastest)
+        | None -> ()
+
+        //let imgFilename = (string srcImg.Num) + DEFAULT_IMAGE_EXTENSION
+        //let imgEntry = file.CreateEntry (imgFilename, CompressionLevel.NoCompression)
+        //srcImg.Img.ToBitmap().Save (imgEntry.Open (), System.Drawing.Imaging.ImageFormat.Tiff)
+
+        let imgJSONEntry = file.CreateEntry (srcImg.OriginalName + JSON_EXTENSION, CompressionLevel.Fastest)
         use imgJSONFileWriter = new StreamWriter (imgJSONEntry.Open ())
         imgJSONFileWriter.Write (
             JsonConvert.SerializeObject (
@@ -95,33 +106,38 @@ let updateDocumentData (fromVersion : int) (toVersion : int) (data : DocumentDat
         | _ -> ()
     data
 
+exception VersionFileNewerException of int
+
 /// <summary>
 /// Load document from a give file path.
 /// </summary>
 /// <param name="filePath">Path to the PiaZ file</param>
 /// <param name="defaultConfig"></param>
 /// <exception cref="System.IOException">If the file cannot be read</exception>
+/// <exception cref="VersionFileNewerException">If the file version is newer than the current supported version</exception>
 let load (filePath : string) (defaultConfig : ParasitemiaCore.Config.Config) : DocumentData =
     use file = ZipFile.Open (filePath, ZipArchiveMode.Read)
 
-    let mainEntry = file.GetEntry (mainEntryName)
+    let mainEntry = file.GetEntry (MAIN_ENTRY_NAME)
     use mainEntryReader = new StreamReader (mainEntry.Open ())
     let info = JsonConvert.DeserializeObject<JSONInformation> (mainEntryReader.ReadToEnd ())
 
-    updateDocumentData info.fileVersion currentFileVersion
+    if info.fileVersion > CURRENT_FILE_VERSION then
+        raise <| VersionFileNewerException info.fileVersion
+
+    updateDocumentData info.fileVersion CURRENT_FILE_VERSION
         {
             patientID = info.patientID
             images =
                 [
-                    let mutable imgNum = 0
                     for imgEntry in file.Entries do
-                        if imgEntry.Name.EndsWith (imageExtension) then
+                        if imgEntry.Name.EndsWith JSON_EXTENSION |> not then
                             use bitmap = new System.Drawing.Bitmap (imgEntry.Open (), false)
                             let img = bitmap.ToImage<Bgr, byte> ()
-                            imgNum <- imgNum + 1
-                            let imgJSONEntry = file.GetEntry (imgEntry.Name + ".json")
+                            let imgJSONEntry = file.GetEntry (imgEntry.Name + JSON_EXTENSION)
                             use imgJSONFileReader = new StreamReader (imgJSONEntry.Open ())
                             let imgInfo = JsonConvert.DeserializeObject<JSONSourceImage> (imgJSONFileReader.ReadToEnd ())
+                            let imgNum = imgInfo.num
 
                             let config = defaultConfig.Copy ()
                             config.Parameters <-
@@ -132,6 +148,7 @@ let load (filePath : string) (defaultConfig : ParasitemiaCore.Config.Config) : D
 
                             config.SetRBCRadius imgInfo.RBCRadius
 
-                            SourceImage (imgNum, imgInfo.name, config, imgInfo.dateLastAnalysis, img, imgInfo.rbcs, HealthyRBCBrightness = imgInfo.healthyRBCBrightness, InfectedRBCBrightness = imgInfo.infectedRBCBrightness)
+                            SourceImage (imgNum, imgInfo.name, imgEntry.Name, config, imgInfo.dateLastAnalysis, FromMemory img, imgInfo.rbcs, HealthyRBCBrightness = imgInfo.healthyRBCBrightness, InfectedRBCBrightness = imgInfo.infectedRBCBrightness)
                 ]
+                |> List.sortBy (fun image -> image.Num)
         }
\ No newline at end of file
index 7f5c245..8dd20df 100644 (file)
@@ -1,6 +1,7 @@
 namespace ParasitemiaUI
 
 open System
+open System.IO
 open System.Windows.Media
 
 open Emgu.CV
@@ -8,12 +9,36 @@ open Emgu.CV.Structure
 
 open Types
 
-type SourceImage (num : int, name : string, config : ParasitemiaCore.Config.Config, dateLastAnalysis : DateTime, img : Image<Bgr, byte>, rbcs : RBC list) =
+type ImageData =
+    | FromMemory of Image<Bgr, byte>
+    | FromFile of string // This file will be stored in a temporary directory until the image is saved in a piaz file.
+
+type SourceImage (num : int, name : string, originalName : string, config : ParasitemiaCore.Config.Config, dateLastAnalysis : DateTime, imgData : ImageData, rbcs : RBC list) =
     let mutable num = num
     let mutable name = name
     let mutable config = config
     let mutable dateLastAnalysis = dateLastAnalysis // UTC.
-    let img = img
+    let img =
+        match imgData with
+        | FromMemory i -> i
+        | FromFile path -> new Image<Bgr, byte> (path)
+
+    let mutable tempFile : string option =
+        match imgData with
+        | FromMemory _ -> None
+        | FromFile path ->
+            let tmpDirname = Guid.NewGuid ()
+            let filename = Path.GetFileName path
+
+            // Copy it to a temporary directory.
+            let tmpDir = Path.Combine (Path.GetTempPath (), string tmpDirname)
+            Directory.CreateDirectory tmpDir |> ignore
+            let tmpPath = Path.Combine (tmpDir, filename)
+
+            File.Copy (path, tmpPath)
+
+            Some tmpPath
+
     let mutable rbcs = rbcs
     let mutable healthyRBCBrightness = 1.f
     let mutable infectedRBCBrightness = 1.f
@@ -39,12 +64,18 @@ type SourceImage (num : int, name : string, config : ParasitemiaCore.Config.Conf
 
     member this.Name with get () = name and set value = name <- value
 
+    member this.OriginalName = originalName
+
     member this.Config = config
 
     member this.DateLastAnalysis with get () = dateLastAnalysis and set value = dateLastAnalysis <- value
 
     member this.Img = img
 
+    member this.TempFile
+        with get () = tempFile
+        and set value = tempFile <- value // TODO: remove temp file if set to None
+
     member this.RBCs
         with get () = rbcs
         and set value =
index befbdec..37da042 100644 (file)
@@ -64,7 +64,8 @@ type State (defaultConfig : ParasitemiaCore.Config.Config) =
     /// </summary>
     /// <exception cref="System.IOException">If the image cannot be read</exception>
     member this.AddSourceImage (filePath : string) (defaultConfig : ParasitemiaCore.Config.Config) : SourceImage =
-        let srcImg = SourceImage (sourceImages.Count + 1, System.IO.FileInfo(filePath).Name, defaultConfig.Copy (), DateTime (0L), new Image<Bgr, byte> (filePath), [])
+        let filename = System.IO.FileInfo(filePath).Name
+        let srcImg = SourceImage (sourceImages.Count + 1, filename, filename, defaultConfig.Copy (), DateTime (0L), FromFile filePath, [])
         sourceImages.Add srcImg
         if sourceImages.Count = 1 then
             this.CurrentImage <- Some sourceImages.[0]
index 927e441..a536853 100644 (file)
@@ -108,6 +108,7 @@ let argsHelp =
 open System
 open System.Windows
 
+// Inspired by https://github.com/fsprojects/FSharp.ViewModule/blob/master/src/FSharp.ViewModule/FunCommand.fs
 type FunCommand (execute : obj -> unit, canExecute : obj -> bool) =
     let canExecuteChanged = Event<EventHandler, EventArgs> ()
 
index 142cc58..c6367af 100644 (file)
@@ -6,7 +6,7 @@
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
     mc:Ignorable="d"
-    Height="700" Width="1000" MinHeight="300" MinWidth="400" Title="Parasitemia" Icon="/ParasitemiaUIControls;component/Resources/icon.ico" ResizeMode="CanResizeWithGrip"
+    Height="700" Width="1000" MinHeight="300" MinWidth="400" Icon="/ParasitemiaUIControls;component/Resources/icon.ico" ResizeMode="CanResizeWithGrip"
 >
     <DockPanel x:Name="dockPanelMain" LastChildFill="True">
         <Menu DockPanel.Dock="Top">