diff --git a/docs/README.md b/docs/README.md
index ac3464fbc6315c31e3439488f650238bc2b0b081..a19d52025ac5dc598d6e2a867587c439238e00aa 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,118 +1,148 @@
-# Mode opératoire
-## Présentation
-**Cet outil vous accompagne**  - pas à pas - dans **l'analyse des photos de blobs**, afin de fournir les données nécessaires à l'apprentissage du _Machine Learning_. 
-
-L'analyse se déroule en **5 étapes** :
-
-[1. Charger une photo](#Étape-1-charger-une-photo) <br>
-[2. Positionner la règle](#Étape-2-positioner-la-règle) <br>
-[3. Positionner la boîte de Petri](#Étape-3-positionner-la-boîte-de-petri) <br>
-[4. Détourer le blob à main levée](#Étape-4-détourer-le-blob) <br>
-[5. Télécharger les fichiers de mesures](#Étape-5-télécharger-les-résultats) <br>
-
-Chaque étape est décrite dans la partie droite du _lab_. Il est possible de revenir à tout moment
-en arrière en cliquant sur le titre de l'étape.
-
-C'est parti...
-
----
-
-## Étape 1 : charger une photo
-
-**Charger une photo** à analyser - située sur l'ordinateur - avec le **bouton "Parcourir..."**.
-
-![](images/file_panel.png)
-
-Une fois la photo chargée, **l'étape 2 est automatiquement activée**.
-
-Mais avant...
-
-> ### Comment zoomer et déplacer la photo
-> Pour prendre les mesures avec le plus de précision, il est possible de :
-> * **zoomer/dézoomer (agrandir/rétrécir) la photo** avec la molette de la souris
-> * **déplacer la photo** : tout en appuyant sur la touche contrôle (le curseur devient des flèches), cliquer sur la photo et la déplacer avec la souris
-> 
-> La barre de boutons de zoom sur le haut de la page permet d'ajuster l'affichage :
-> * ![](images/zoom_in.png) : agrandir la photo
-> * ![](images/zoom_out.png) : rétrécir la photo
-> * ![](images/zoom_fit.png) : ajuster la photo à l'écran et la repositionner au centre (:sparkles: bien utile si l'on perd la photo de vue)
-> * ![](images/zoom_1-1.png) : afficher la photo en taille réelle
-
-Maintenant au travail !
-
----
-
-## Étape 2 : positioner la règle
-Cette étape permet au logiciel de déterminer l'échelle de la photo.
-
-**Déplacer la ligne jaune à l'aide des 2 "poignées"** (petits carrés blancs) à chacune de ses extrémités afin de 
-**couvrir 10cm de la règle**. 
-
-![](images/ruler_handles.png)
-
-Pour s'assurer que 10 centimètres sont bien couverts, **les petits points "détrompeurs" doivent tomber sur chaque centimètre**.
-
-![](images/ruler_pokayoke.png)
-
-Si il y a moins de 10cm règle sur la photo, il est possible de modifier la taille à couvrir avec les boutons +/- (en bleu ci-dessous).
-Le nombre de détrompeurs sera ajusté.
-
-Pour placer la règle avec plus de précision, utiliser le bouton ![Zoom règle](images/zoom_object.png) (en jaune ci-dessous).
-
-![](images/ruler_panel.png)
-
-Une fois la règle placée, **passer à l'étape suivante en appuyant sur le bouton "Terminé !"** (en vert ci-dessus).
-
----
-
-## Étape 3 : positionner la boîte de Petri
-
-Tout comme la règle, **placer la boîte de Petri à l'aide des poignées**, et utiliser le bouton ![Zoom boîte de Petri](images/zoom_object.png)
-pour la placer avec précision.
-
-![](images/petri_handles.png)
-
->
->
-> Au début on y va à tâtons, mais on prend vite le coup de main.
->
-> 
-
-![](images/petri_panel.png)
-
-Une fois la boîte de Petri correctement positionnée, **passer à l'étape suivante en appuyant sur "C'est fini !"**
-
----
-
-## Étape 4 : détourer le blob
-
-Lors de cette étape, **entourer d'une ligne jaune le blob** (dessiner en maintenant le bouton de la souris pressé).
-
-![](images/blob_draw.png)
-
->
-> **Oh non !** Si près de la fin
->
-> ![](https://i.giphy.com/media/JuFwy0zPzd6jC/giphy.webp)
-> ![](images/blob_scrambled.png)
-> 
-> Il est possible de revenir en arrière sur les derniers points du tracé avec le bouton  ![](blob_back_button.png)
-> 
-> ![](images/blob_back_panel.png)
->
-
-Lorsque l'on a rejoint le point de départ marqué par un carré blanc, **le contour est terminé et colorié en jaune**.
-
-![](images/blob_closed.png)
-
-Le bouton  ![](images/blob_done.png) s'active et permet de **passer à l'étape suivante**.
-
----
-
-## Étape 5 : télécharger les résultats
-
-![](images/download_panel.png)
-
-**Télécharger les fichiers un à un** avec ![](images/download_button.png).
-
-Les fichiers sont stockés dans le répertoire "Téléchargements" du navigateur, **les transmettre par [la plateforme de dépôt de données](https://blob.cnrs.fr/)**.  
+# Mode opératoire
+## Présentation
+**Cet outil vous accompagne**  - pas à pas - dans **l'analyse des photos de blobs**, afin de fournir les données nécessaires à l'apprentissage du _Machine Learning_. 
+
+L'analyse se déroule en **5 étapes** :
+
+[1. Charger une photo](#Étape-1-charger-une-photo) <br>
+[2. Positionner la règle](#Étape-2-positioner-la-règle) <br>
+[3. Positionner la boîte de Petri](#Étape-3-positionner-la-boîte-de-petri) <br>
+[4. Détourer le blob à main levée](#Étape-4-détourer-le-blob) <br>
+[5. Télécharger les fichiers de mesures](#Étape-5-télécharger-les-résultats) <br>
+[5'. Fusionner les .csv ](#Étape-539-fusionner-les-csv)
+
+Chaque étape est décrite dans la partie droite du _lab_. Il est possible de revenir à tout moment
+en arrière en cliquant sur le titre de l'étape.
+
+C'est parti...
+
+---
+
+## Étape 1 : charger une photo
+
+**Charger une photo** à analyser - située sur l'ordinateur - avec le **bouton "Parcourir..."**.
+
+![](images/file_panel.png)
+
+Une fois la photo chargée, **l'étape 2 est automatiquement activée**.
+
+Mais avant...
+
+> ### Comment zoomer et déplacer la photo
+> Pour prendre les mesures avec le plus de précision, il est possible de :
+> * **zoomer/dézoomer (agrandir/rétrécir) la photo** avec la molette de la souris
+> * **déplacer la photo** : tout en appuyant sur la touche contrôle (le curseur devient des flèches), cliquer sur la photo et la déplacer avec la souris
+> 
+> La barre de boutons de zoom sur le haut de la page permet d'ajuster l'affichage :
+> * ![](images/zoom_in.png) : agrandir la photo
+> * ![](images/zoom_out.png) : rétrécir la photo
+> * ![](images/zoom_fit.png) : ajuster la photo à l'écran et la repositionner au centre (:sparkles: bien utile si l'on perd la photo de vue)
+> * ![](images/zoom_1-1.png) : afficher la photo en taille réelle
+
+Maintenant au travail !
+
+---
+
+## Étape 2 : positioner la règle
+Cette étape permet au logiciel de déterminer l'échelle de la photo.
+
+**Déplacer la ligne jaune à l'aide des 2 "poignées"** (petits carrés blancs) à chacune de ses extrémités afin de 
+**couvrir 10cm de la règle**. 
+
+![](images/ruler_handles.png)
+
+Pour s'assurer que 10 centimètres sont bien couverts, **les petits points "détrompeurs" doivent tomber sur chaque centimètre**.
+
+![](images/ruler_pokayoke.png)
+
+Si il y a moins de 10cm règle sur la photo, il est possible de modifier la taille à couvrir avec les boutons +/- (en bleu ci-dessous).
+Le nombre de détrompeurs sera ajusté.
+
+Pour placer la règle avec plus de précision, utiliser le bouton ![Zoom règle](images/zoom_object.png) (en jaune ci-dessous).
+
+![](images/ruler_panel.png)
+
+Une fois la règle placée, **passer à l'étape suivante en appuyant sur le bouton "Terminé !"** (en vert ci-dessus).
+
+---
+
+## Étape 3 : positionner la boîte de Petri
+
+Tout comme la règle, **placer la boîte de Petri à l'aide des poignées**, et utiliser le bouton ![Zoom boîte de Petri](images/zoom_object.png)
+pour la placer avec précision.
+
+![](images/petri_handles.png)
+
+>
+>
+> Au début on y va à tâtons, mais on prend vite le coup de main.
+>
+> 
+
+![](images/petri_panel.png)
+
+Une fois la boîte de Petri correctement positionnée, **passer à l'étape suivante en appuyant sur "C'est fini !"**
+
+---
+
+## Étape 4 : détourer le blob
+
+Lors de cette étape, **entourer d'une ligne jaune le blob** (dessiner en maintenant le bouton de la souris pressé).
+
+![](images/blob_draw.png)
+
+>
+> **Oh non !** Si près de la fin
+>
+> ![](https://i.giphy.com/media/JuFwy0zPzd6jC/giphy.webp)
+> ![](images/blob_scrambled.png)
+> 
+> Il est possible de revenir en arrière sur les derniers points du tracé avec le bouton  ![](blob_back_button.png)
+> 
+> ![](images/blob_back_panel.png)
+>
+
+Lorsque l'on a rejoint le point de départ marqué par un carré blanc, **le contour est terminé et colorié en jaune**.
+
+![](images/blob_closed.png)
+
+Le bouton  ![](images/blob_done.png) s'active et permet de **passer à l'étape suivante**.
+
+---
+
+## Étape 5 : télécharger les résultats
+
+![](images/download_panel.png)
+
+**Télécharger les fichiers un à un** avec ![](images/download_button.png).
+
+Les fichiers sont stockés dans le répertoire "Téléchargements" du navigateur.  
+
+> [!NOTE|label:Attention]
+> Ces fichiers ne peuvent pas être tout de suite déposés sur l'espace de dépôt des fichiers d'analyse (https://blob.cnrs.fr/).
+> 
+> En effet, le fichier _.csv_ généré ne contient que les mesures (aire, périmètre, circualirité, etc.) que pour la photo qui vient d'être analysée. 
+> Le nom du fichier contient le numéro du blob (par exemple: Results_ConJ1Ex*B3*.csv).
+> 
+> L'espace de dépôt des analyses n'attend qu'**un seul fichier** _.csv_ **pour toutes** les photos d'une séquence groupe/jour/expérience (par ex.: Results_ConJ1Ex.csv).
+> 
+> Une étape supplémentaire permet de regrouper les fichiers.
+
+## Étape 5' : fusionner les .csv
+
+> :gift: Pour 5 étapes effectuées une étape offerte !
+
+Un bouton dans l'étape 5 permet d'ouvrir l'outil de fusion des fichiers.
+
+![](images/fusion_button.png)
+
+Une fenêtre comme celle-ci s'affiche :
+
+![](images/fusion_dialog.png)
+
+Pas à pas : 
+1. \[encadré en vert] Sélectionner des fichiers à fusionner. Plusieurs fichiers peuvent être sélectionnés à la fois. Ils peuvent être sélectionnés en vrac mêmes s'ils font partie de séquences (groupe/jour/expérience) d'analyse différentes.
+2. \[marqué en rouge] Les fichiers sont automatiquement triés et regroupés par séquence (groupe/jour/expérience)
+3. \[entouré en jaune] Les fichiers résultats sont téléchargeables avec le bouton ![](images/download_button.png)
+
+Maintenant **les fichiers peuvent être transmis par [la plateforme de dépôt de données](https://blob.cnrs.fr/)**.
diff --git a/docs/images/fusion_button.png b/docs/images/fusion_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..73790a4a96c3fc6c9ec4fcb496a8b5e7dc36991a
Binary files /dev/null and b/docs/images/fusion_button.png differ
diff --git a/docs/images/fusion_dialog.png b/docs/images/fusion_dialog.png
new file mode 100644
index 0000000000000000000000000000000000000000..7e2094b73fb5553a9d9cce052818f00e7e7aa923
Binary files /dev/null and b/docs/images/fusion_dialog.png differ
diff --git a/docs/index.html b/docs/index.html
index 7c443aae2a2e303c0dd9a4abad81c41b725d5c1a..ee42b579198eb7861f5822f32f240f0842e76d93 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1,20 +1,24 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <title>Blob Analysis Lab - Documentation</title>
-    <link rel="icon" href="favicon.ico" />
-    <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/themes/vue.css" />
-</head>
-<body>
-<div id="doc">Chargement...</div>
-<script>
-    window.$docsify = {
-        el: '#doc'
-    }
-</script>
-<script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
-</body>
-</html>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>Blob Analysis Lab - Documentation</title>
+    <link rel="icon" href="favicon.ico" />
+    <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/themes/vue.css" />
+</head>
+<body>
+<div id="doc">Chargement...</div>
+<script>
+    window.$docsify = {
+        el: '#doc',
+        'flexible-alerts': {
+            style: 'callout'
+        }
+    }
+</script>
+<script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
+<script src="https://unpkg.com/docsify-plugin-flexible-alerts"></script>
+</body>
+</html>
diff --git a/package-lock.json b/package-lock.json
index f7777b13592621c0a9ac3e7bb478770f678eedb1..65462d67e70c29034be4c61b83138b91adc252e3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,8 +14,10 @@
         "@fortawesome/free-solid-svg-icons": "^6.2.0",
         "@popperjs/core": "^2.11.6",
         "@svgr/webpack": "^6.5.0",
+        "@types/d3-dsv": "^3.0.0",
         "bootstrap": "^5.2.1",
         "bootswatch": "^5.2.0",
+        "d3-dsv": "^3.0.1",
         "jquery": "3.6.0",
         "paper": "0.12.15",
         "react": "^18.2.0",
@@ -2498,6 +2500,11 @@
       "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
       "dev": true
     },
+    "node_modules/@types/d3-dsv": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.0.tgz",
+      "integrity": "sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A=="
+    },
     "node_modules/@types/eslint": {
       "version": "8.4.6",
       "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz",
@@ -3903,6 +3910,49 @@
       "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==",
       "dev": true
     },
+    "node_modules/d3-dsv": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+      "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+      "dependencies": {
+        "commander": "7",
+        "iconv-lite": "0.6",
+        "rw": "1"
+      },
+      "bin": {
+        "csv2json": "bin/dsv2json.js",
+        "csv2tsv": "bin/dsv2dsv.js",
+        "dsv2dsv": "bin/dsv2dsv.js",
+        "dsv2json": "bin/dsv2json.js",
+        "json2csv": "bin/json2dsv.js",
+        "json2dsv": "bin/json2dsv.js",
+        "json2tsv": "bin/json2dsv.js",
+        "tsv2csv": "bin/dsv2dsv.js",
+        "tsv2json": "bin/dsv2json.js"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dsv/node_modules/commander": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+      "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/d3-dsv/node_modules/iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/date-format": {
       "version": "4.0.14",
       "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
@@ -7170,6 +7220,11 @@
         "queue-microtask": "^1.2.2"
       }
     },
+    "node_modules/rw": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+      "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
+    },
     "node_modules/safe-buffer": {
       "version": "5.2.1",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -7193,8 +7248,7 @@
     "node_modules/safer-buffer": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
-      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
-      "dev": true
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
     },
     "node_modules/sass": {
       "version": "1.55.0",
@@ -10523,6 +10577,11 @@
       "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
       "dev": true
     },
+    "@types/d3-dsv": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.0.tgz",
+      "integrity": "sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A=="
+    },
     "@types/eslint": {
       "version": "8.4.6",
       "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz",
@@ -11659,6 +11718,31 @@
       "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==",
       "dev": true
     },
+    "d3-dsv": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+      "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+      "requires": {
+        "commander": "7",
+        "iconv-lite": "0.6",
+        "rw": "1"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+          "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="
+        },
+        "iconv-lite": {
+          "version": "0.6.3",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+          "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+          "requires": {
+            "safer-buffer": ">= 2.1.2 < 3.0.0"
+          }
+        }
+      }
+    },
     "date-format": {
       "version": "4.0.14",
       "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
@@ -14109,6 +14193,11 @@
         "queue-microtask": "^1.2.2"
       }
     },
+    "rw": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+      "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
+    },
     "safe-buffer": {
       "version": "5.2.1",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -14118,8 +14207,7 @@
     "safer-buffer": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
-      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
-      "dev": true
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
     },
     "sass": {
       "version": "1.55.0",
diff --git a/package.json b/package.json
index 5dd0203d41e8d7352006a4af413164064367b142..0caa83558355a59b76662e17a5a45904d21ef8b8 100644
--- a/package.json
+++ b/package.json
@@ -17,8 +17,10 @@
     "@fortawesome/free-solid-svg-icons": "^6.2.0",
     "@popperjs/core": "^2.11.6",
     "@svgr/webpack": "^6.5.0",
+    "@types/d3-dsv": "^3.0.0",
     "bootstrap": "^5.2.1",
     "bootswatch": "^5.2.0",
+    "d3-dsv": "^3.0.1",
     "jquery": "3.6.0",
     "paper": "0.12.15",
     "react": "^18.2.0",
diff --git a/src/data/coords/process/coordsMeasurement.ts b/src/data/coords/process/coordsMeasurement.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8e323f5c5d5b4c50eac67f3985561177e4010362
--- /dev/null
+++ b/src/data/coords/process/coordsMeasurement.ts
@@ -0,0 +1,45 @@
+import {PathCoords} from "../pathCoords";
+import {Measurement} from "../../measurement/measurement";
+import {ToEllipseFitter} from "../transform/toEllipseFitter";
+import {ToConvexHull} from "../transform/toConvexHull";
+import {VectorCoords} from "../vectorCoords";
+
+/**
+ * Transforme des coordonnées en Measures
+ */
+export class CoordsMeasurement {
+
+    /**
+     *  Effectue des mesures sur le coordonnées
+     */
+    public measure(coords : PathCoords, scaleCoords : VectorCoords, scaleUnitCount: number, label: string) : Measurement {
+        let path = coords.toRemovedPath();
+        path.closePath();
+
+        let linearScale = scaleCoords.distance() / scaleUnitCount; // pixels/cm
+        let areaScale = Math.pow(linearScale, 2);
+        let fittingEllipse = new ToEllipseFitter().transform(coords);
+        let convexHull = new ToConvexHull().transform(coords);
+
+        let area = Math.abs(path.area) / areaScale;
+        let convexArea = Math.abs(convexHull.toRemovedPath().area) / areaScale;
+        let perimeter = path.length / linearScale;
+        let major = fittingEllipse.getMajorAxis() / linearScale;
+        let minor = fittingEllipse.getMinorAxis() / linearScale;
+        let circularity = 4 * Math.PI * area / Math.pow(perimeter, 2);
+        let ar = major / minor;
+        let round = (4 * area) / (Math.PI * major * major);
+        let solid = area / convexArea;
+
+        return  {
+            label: label,
+            area: area,
+            perimeter: perimeter,
+            circularity: circularity,
+            ar: ar,
+            round: round, solid: solid
+        }
+    }
+
+
+}
\ No newline at end of file
diff --git a/src/data/coords/process/pointMerger.ts b/src/data/coords/process/pointMerger.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d748578743ca80a7f0113ef8101b51415643e25b
--- /dev/null
+++ b/src/data/coords/process/pointMerger.ts
@@ -0,0 +1,67 @@
+/**
+ * Merge des point alignés horizontalement ou verticalement (4-connected)
+ */
+class PointMerger {
+
+    private startPoint: paper.Point | null;
+    private endPoint: paper.Point | null;
+
+    next(point : paper.Point | null) : paper.Point[] {
+        if(point == null) { // Null lorsque l'on a passé le dernier point => lus de point à merger, on retourne ce qu'il reste
+            if(this.endPoint != null) {
+                return [this.startPoint, this.endPoint];
+            } else if(this.startPoint != null) {
+                return [this.startPoint];
+            } else {
+                return [];
+            }
+        } else {
+            if (this.startPoint == null) { // On est sur le premier point
+                this.startPoint = point;
+                return [];
+            } else {
+                if(this.endPoint == null) { // On a déjà un point de départ
+                    if(point.y == this.startPoint.y && point.x == this.startPoint.x) {
+                        return []
+                    } else if(point.y == this.startPoint.y
+                        || point.x == this.startPoint.x) {
+                        this.endPoint = point;
+                        return [];
+                    } else {
+                        let result = [this.startPoint];
+                        this.startPoint = point;
+                        return result
+                    }
+                } else {
+                    if(point.y == this.endPoint.y && point.x == this.endPoint.x) {
+                        return [];
+
+                    } else if(point.y == this.endPoint.y && Math.sign(this.endPoint.x - this.startPoint.x) == Math.sign(this.endPoint.x - this.startPoint.x)) {
+                        // Dans l'alignement horizontal
+                        this.endPoint = point;
+                        return [];
+
+                    } else  if(point.x == this.endPoint.x && Math.sign(this.endPoint.y - this.startPoint.y) == Math.sign(this.endPoint.y - this.startPoint.y)) {
+                        // Dans l'alignement vertical
+                        this.endPoint = point;
+                        return [];
+
+                    } else {
+                        if(point.y == this.endPoint.y || point.x == this.endPoint.x) {
+                            let result = [this.startPoint];
+                            this.startPoint = this.endPoint;
+                            this.endPoint = point;
+                            return  result;
+
+                        } else {
+                            let result = [this.startPoint, this.endPoint];
+                            this.startPoint = point;
+                            this.endPoint = null;
+                            return  result;
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/data/dataExporter.ts b/src/data/dataExporter.ts
index 732e6e74e980785e8757e2a8ec661af893b539ed..ac63b69ba50b4e752e485792b6e4226a72509018 100644
--- a/src/data/dataExporter.ts
+++ b/src/data/dataExporter.ts
@@ -1,160 +1,68 @@
-/**
- * Utilitaire d'export des données
- */
-import {LabData} from "../lab";
-import {ToEllipseFitter} from "./coords/transform/toEllipseFitter";
-import {Coords} from "./coords/coords";
-import {MathUtils} from "../utils/mathUtils";
-import {ToConvexHull} from "./coords/transform/toConvexHull";
-import {PathCoords} from "./coords/pathCoords";
-
-const ROUNDING_DECIMALS = 3;
-
-/**
- * Merge des point alignés horizontalement ou verticalement
- */
-class PointMerger {
-
-    private startPoint: paper.Point | null;
-    private endPoint: paper.Point | null;
-
-    next(point : paper.Point | null) : paper.Point[] {
-        if(point == null) { // Null lorsque l'on a passé le dernier point => lus de point à merger, on retourne ce qu'il reste
-            if(this.endPoint != null) {
-                return [this.startPoint, this.endPoint];
-            } else if(this.startPoint != null) {
-                return [this.startPoint];
-            } else {
-                return [];
-            }
-        } else {
-            if (this.startPoint == null) { // On est sur le premier point
-                this.startPoint = point;
-                return [];
-            } else {
-                if(this.endPoint == null) { // On a déjà un point de départ
-                    if(point.y == this.startPoint.y && point.x == this.startPoint.x) {
-                        return []
-                    } else if(point.y == this.startPoint.y
-                        || point.x == this.startPoint.x) {
-                        this.endPoint = point;
-                        return [];
-                    } else {
-                        let result = [this.startPoint];
-                        this.startPoint = point;
-                        return result
-                    }
-                } else {
-                    if(point.y == this.endPoint.y && point.x == this.endPoint.x) {
-                        return [];
-
-                    } else if(point.y == this.endPoint.y && Math.sign(this.endPoint.x - this.startPoint.x) == Math.sign(this.endPoint.x - this.startPoint.x)) {
-                        // Dans l'alignement horizontal
-                        this.endPoint = point;
-                        return [];
-
-                    } else  if(point.x == this.endPoint.x && Math.sign(this.endPoint.y - this.startPoint.y) == Math.sign(this.endPoint.y - this.startPoint.y)) {
-                        // Dans l'alignement vertical
-                        this.endPoint = point;
-                        return [];
-
-                    } else {
-                        if(point.y == this.endPoint.y || point.x == this.endPoint.x) {
-                            let result = [this.startPoint];
-                            this.startPoint = this.endPoint;
-                            this.endPoint = point;
-                            return  result;
-
-                        } else {
-                            let result = [this.startPoint, this.endPoint];
-                            this.startPoint = point;
-                            this.endPoint = null;
-                            return  result;
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
-
-
-export class DataExporter {
-
-    /**
-     * Transforme un Path en CSV
-     */
-    public exportPathPointsAsXYCsv(coords : Coords, close : boolean) : string {
-        const path = coords.toRemovedPath();
-        let data = "";
-        let pointMerger: PointMerger = new PointMerger();
-        for (let i = 0; i < path.length; i++) {
-            let point = path.getPointAt(i).round();
-            pointMerger.next(point).forEach(
-                (p) => data += this.toCsv(p)
-            );
-        }
-        pointMerger.next(null).forEach(
-            (p) => data += this.toCsv(p)
-        );
-        return data;
-    }
-
-    /**
-     * Transforme un Path en CSV par segments
-     */
-    public exportPathSegmentsAsXYCsv(coords : Coords, close : boolean) : string {
-        const path = coords.toRemovedPath();
-        let data = "";
-        let pointMerger: PointMerger = new PointMerger();
-        for (let i = 0; i < path.segments.length; i++) {
-            let segment = path.segments[i];
-            data += this.toCsv(segment.point.round());
-        }
-        return data;
-    }
-
-    /**
-     * Transforme un Path en CSV
-     */
-    public exportPathDescriptorsAsCsv(labData : LabData, coords : PathCoords) : string {
-        let path = coords.toRemovedPath();
-        path.closePath();
-
-        let linearScale = labData.rulerCoords.distance() / labData.rulerTickCount; // pixels/cm
-        let areaScale = Math.pow(linearScale, 2);
-        let fittingEllipse = new ToEllipseFitter().transform(coords);
-        let convexHull = new ToConvexHull().transform(coords);
-
-        let line = 1;
-        let label = labData.filename;
-        let area = Math.abs(path.area) / areaScale;
-        let convexArea = Math.abs(convexHull.toRemovedPath().area) / areaScale;
-        let perimeter = path.length / linearScale;
-        let major = fittingEllipse.getMajorAxis() / linearScale;
-        let minor = fittingEllipse.getMinorAxis() / linearScale;
-        let circularity = 4 * Math.PI * area / Math.pow(perimeter, 2);
-        let ar = major / minor;
-        let round = (4 * area) / (Math.PI * major * major);
-        let solid = area / convexArea;
-
-        let headers : string[] = [ " ", "Label", "Area", "Perim.", "Circ.","AR","Round","Solidity"];
-        let data = [ line,
-            label,
-            MathUtils.round(area, ROUNDING_DECIMALS),
-            MathUtils.round(perimeter, ROUNDING_DECIMALS),
-            MathUtils.round(circularity, ROUNDING_DECIMALS),
-            MathUtils.round(ar, ROUNDING_DECIMALS),
-            MathUtils.round(round, ROUNDING_DECIMALS),
-            MathUtils.round(solid, ROUNDING_DECIMALS),
-        ]
-
-        return headers.join(",") + "\n" + data.join(",");
-    }
-
-
-    private toCsv(point : paper.Point) : string {
-        return point.x + "\t" + point.y + "\n";
-    }
-
+/**
+ * Utilitaire d'export des données
+ */
+import {LabData} from "../lab";
+import {Coords} from "./coords/coords";
+import {MeasurementExport} from "./measurement/process/measurementExport";
+import {CoordsMeasurement} from "./coords/process/coordsMeasurement";
+
+export class DataExporter {
+
+    /**
+     * Pour effectuer les mesures
+     */
+    private coordsMeasure = new CoordsMeasurement();
+
+    /**
+     * Pour l'export des données de mesures
+     */
+    private measureExport: MeasurementExport = new MeasurementExport();
+
+    /**
+     * Transforme un Path en CSV
+     */
+    public exportPathPointsAsXYCsv(coords : Coords, close : boolean) : string {
+        const path = coords.toRemovedPath();
+        let data = "";
+        let pointMerger = new PointMerger();
+        for (let i = 0; i < path.length; i++) {
+            let point = path.getPointAt(i).round();
+            pointMerger.next(point).forEach(
+                (p) => data += this.toCsv(p)
+            );
+        }
+        pointMerger.next(null).forEach(
+            (p) => data += this.toCsv(p)
+        );
+        return data;
+    }
+
+    /**
+     * Transforme un Path en CSV par segments
+     */
+    public exportPathSegmentsAsXYCsv(coords : Coords, close : boolean) : string {
+        const path = coords.toRemovedPath();
+        let data = "";
+        let pointMerger: PointMerger = new PointMerger();
+        for (let i = 0; i < path.segments.length; i++) {
+            let segment = path.segments[i];
+            data += this.toCsv(segment.point.round());
+        }
+        return data;
+    }
+
+    /**
+     * Transforme un Path en CSV
+     */
+    public exportPathDescriptorsAsCsv(labData : LabData) : string {
+        let measurement = this.coordsMeasure.measure(labData.blobMaskCoords, labData.rulerCoords, labData.rulerTickCount, labData.filename);
+        return this.measureExport.exportMeasurements(labData.workInfo,[measurement]).csv;
+    }
+
+
+    private toCsv(point : paper.Point) : string {
+        return point.x + "\t" + point.y + "\n";
+    }
+
+
 }
\ No newline at end of file
diff --git a/src/data/dataImporter.ts b/src/data/dataImporter.ts
index a1e475f01c124462efaf0ce0b132167736645e97..b1521476376a0b262e8c6244727cfd00af65ffdc 100644
--- a/src/data/dataImporter.ts
+++ b/src/data/dataImporter.ts
@@ -1,25 +1,47 @@
-import {Coords} from "./coords/coords";
-import {PathCoords} from "./coords/pathCoords";
-import {StringUtils} from "../utils/stringUtils";
-import * as paper from "paper";
-
-/**
- * Import de données, sert plutôt ôu rle debug
- */
-export class DataImporter {
-
-    /**
-     * Charge un PathCoords depuis des coordonnées text
-     */
-    public import(text: string): PathCoords {
-        let pathCoords = new PathCoords();
-        let lines = StringUtils.splitLines(text);
-        lines.filter(line => line.trim().length > 0).forEach(
-            (line: string) => {
-                let [x, y] = line.split("\t");
-                pathCoords.points.push(new paper.Point(Number(x), Number(y)));
-            }
-        );
-        return pathCoords;
-    }
-}
+import {PathCoords} from "./coords/pathCoords";
+import {StringUtils} from "../utils/stringUtils";
+import * as paper from "paper";
+import {ExperienceEnum, GroupeEnum, WorkInfo} from "./work/workInfo";
+import {Measurement} from "./measurement/measurement";
+import {csvParse} from "d3-dsv";
+
+
+
+/**
+ * Le contenu d'un fichier CSV de mesure (pour une photo)
+ */
+export interface CsvResultFileData {
+    filename: string,
+    workInfo: WorkInfo,
+    measures: Measurement[]
+}
+
+
+/**
+ * Import de données, sert plutôt pour le debug
+ */
+export class DataImporter {
+
+    /**
+     * Fichier de résultats
+     */
+    public readCsvResultFileData(filename: string, csv: string) : CsvResultFileData {
+        console.log(csvParse(csv));
+        return null;
+    }
+
+    /**
+     * Charge un PathCoords depuis des coordonnées text
+     */
+    public readPathCoords(text: string): PathCoords {
+        let pathCoords = new PathCoords();
+        let lines = StringUtils.splitLines(text);
+        lines.filter(line => line.trim().length > 0).forEach(
+            (line: string) => {
+                let [x, y] = line.split("\t");
+                pathCoords.points.push(new paper.Point(Number(x), Number(y)));
+            }
+        );
+        return pathCoords;
+    }
+}
diff --git a/src/data/measurement/measurement.ts b/src/data/measurement/measurement.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7980351622126d2ae1d5d2c4975bc361b341155e
--- /dev/null
+++ b/src/data/measurement/measurement.ts
@@ -0,0 +1,12 @@
+/**
+ * Contient les mesures métriques
+ */
+export interface Measurement {
+    label: string, // le nom du fichier
+    area: number,
+    perimeter: number,
+    circularity: number,
+    ar: number,
+    round: number,
+    solid: number
+}
\ No newline at end of file
diff --git a/src/data/measurement/measurementWorks.ts b/src/data/measurement/measurementWorks.ts
new file mode 100644
index 0000000000000000000000000000000000000000..13fcd7ea812e1724bf04f0407f083bed098ea01d
--- /dev/null
+++ b/src/data/measurement/measurementWorks.ts
@@ -0,0 +1,14 @@
+import {WorkInfo} from "../work/workInfo";
+import {Measurement} from "./measurement";
+
+/**
+ * Regroupe des mesures par sessions de travail
+ */
+export interface MeasurementWorks {
+
+    [work: string]: {
+        work: WorkInfo,
+        files: { [filename: string] : Measurement[] }
+    }
+
+}
\ No newline at end of file
diff --git a/src/data/measurement/process/measurementExport.ts b/src/data/measurement/process/measurementExport.ts
new file mode 100644
index 0000000000000000000000000000000000000000..78a3d6082fc9b97f7c77e0351fbfdee6bd0dab2e
--- /dev/null
+++ b/src/data/measurement/process/measurementExport.ts
@@ -0,0 +1,44 @@
+import {Measurement} from "../measurement";
+import {MathUtils} from "../../../utils/mathUtils";
+import {WorkInfo} from "../../work/workInfo";
+import {csvFormatRows} from "d3-dsv";
+import {WorkInfoFormatter} from "../../work/process/workInfoFormatter";
+
+const ROUNDING_DECIMALS = 3;
+
+/**
+ * Export de mesures
+ */
+export class MeasurementExport {
+
+    private workInfoFormatter = new WorkInfoFormatter();
+
+
+    /**
+     * Export les mesures au format CSV
+     */
+    public exportMeasurements(work: WorkInfo, measurements: Measurement[]) : { filename: string, csv: string }  {
+        return {
+            filename: "Results_" + this.workInfoFormatter.format(work) + ".csv",
+            csv: csvFormatRows([[" ", "Label", "Area", "Perim.", "Circ.","AR","Round","Solidity"]]
+                .concat(measurements.map((measurement, i) =>
+                    [ (i + 1).toString(),
+                        measurement.label,
+                        MathUtils.round(measurement.area, ROUNDING_DECIMALS).toString(),
+                        MathUtils.round(measurement.perimeter, ROUNDING_DECIMALS).toString(),
+                        MathUtils.round(measurement.circularity, ROUNDING_DECIMALS).toString(),
+                        MathUtils.round(measurement.ar, ROUNDING_DECIMALS).toString(),
+                        MathUtils.round(measurement.round, ROUNDING_DECIMALS).toString(),
+                        MathUtils.round(measurement.solid, ROUNDING_DECIMALS).toString(),
+                    ]
+                )))};
+    }
+    /**
+     * Export les mesures au format CSV
+     */
+    public exportDryMeasures(work: WorkInfo) : { filename: string, csv: string }  {
+        return this.exportMeasurements(work, []);
+    }
+
+
+}
\ No newline at end of file
diff --git a/src/data/measurement/process/measurementImport.ts b/src/data/measurement/process/measurementImport.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2d318d585236d22ff660762d73c018632d065ba1
--- /dev/null
+++ b/src/data/measurement/process/measurementImport.ts
@@ -0,0 +1,56 @@
+/**
+ * Import de mesures
+ */
+import {Measurement} from "../measurement";
+import {WorkInfo} from "../../work/workInfo";
+import {WorkInfoParser} from "../../work/process/workInfoParser";
+import {csvParseRows} from "d3-dsv";
+
+const MEASURE_FILENAME_REGEX = /^Results_(?<work>[a-zA-Z0-9]+).csv$/i;
+
+/**
+ * EN charge de l'import de mesures (lecture de fichiers CSV)
+ */
+export class MeasurementImport {
+
+    /**
+     * Poru parser la session de travail
+     */
+    private workInfoParser = new WorkInfoParser();
+
+    /**
+     * Lit des mesures depuis des données csv
+     * @param String
+     */
+    public readMeasures(filename: string, csv: string) : { work: WorkInfo, measurements: Measurement[] } {
+        let match = filename.match(MEASURE_FILENAME_REGEX);
+        let work = null;
+        if(match != null) {
+            work = this.workInfoParser.parse(match.groups.work);
+        }
+        if(work == null) {
+            throw new Error("Le nom du fichier " + filename + " ne respecte pas la nomenclature (par ex.: Results_ExpJ2ConB3.csv)");
+        }
+
+        let measurements : Measurement[] = csvParseRows(csv, (d, i) : Measurement => {
+            if(i == 0) {
+                if(d.length != 8 || d[0] != " " || d[1] != "Label" || d[2] != "Area" || d[3] != "Perim." || d[4] != "Circ." || d[5] != "AR" || d[6] != "Round" || d[7] != "Solidity" ) {
+                    throw new Error("Fichier " + filename + " : la première ligne du fichier ne contient pas les en-têtes attendus (\" ,Label,Area,Perim.,Circ.,AR,Round,Solidity\")")
+                }
+            } else {
+                return {
+                    label: d[1], // le nom du fichier
+                    area:  +d[2],
+                    perimeter: +d[3],
+                    circularity: +d[4],
+                    ar: +d[5],
+                    round: +d[6],
+                    solid: +d[7]
+                };
+            }
+        });
+        return { work: work, measurements: measurements };
+    }
+
+
+}
\ No newline at end of file
diff --git a/src/data/measurement/process/measurementMerge.ts b/src/data/measurement/process/measurementMerge.ts
new file mode 100644
index 0000000000000000000000000000000000000000..77a040d4df88b40766bf7f3b8d4d4f82c97f85b8
--- /dev/null
+++ b/src/data/measurement/process/measurementMerge.ts
@@ -0,0 +1,45 @@
+/**
+ * Merge des mesures
+ */
+import {MeasurementWorks} from "../measurementWorks";
+import {WorkInfo} from "../../work/workInfo";
+import {Measurement} from "../measurement";
+import {ObjectUtils} from "../../../utils/objectUtils";
+import {MeasurementImport} from "./measurementImport";
+import {WorkInfoFormatter} from "../../work/process/workInfoFormatter";
+
+/**
+ * En charge du merge des mesures
+ */
+export class MeasurementMerge {
+
+    /**
+     * Pour importer les données CSV
+     */
+    private measurementImport = new MeasurementImport();
+
+    /**
+     * Pour encoder le WorkInfo
+     */
+    private workInfoFormatter = new WorkInfoFormatter();
+
+    /**
+     * Ajoute un fichier de mesures dans
+     */
+    public mergeInto(measurementWorks: MeasurementWorks, filename: string, work: WorkInfo, measurements: Measurement[]) : MeasurementWorks {
+        // La session de travail (ensemble de blobs)
+        let groupWork = ObjectUtils.deepClone(work);
+        groupWork.blob = null;
+
+        measurementWorks = ObjectUtils.deepClone(measurementWorks);
+
+        let workKey = this.workInfoFormatter.format(groupWork);
+        if (measurementWorks[workKey] == null) {
+            measurementWorks[workKey] = { work: groupWork, files: {}};
+        }
+        measurementWorks[workKey].files[filename] = measurements;
+        return measurementWorks;
+    }
+
+
+}
\ No newline at end of file
diff --git a/src/data/work/process/workInfoFormatter.ts b/src/data/work/process/workInfoFormatter.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8c41520437c4f4986b30a226263a694677b22e13
--- /dev/null
+++ b/src/data/work/process/workInfoFormatter.ts
@@ -0,0 +1,22 @@
+/**
+ * Import et export des infos de travail en cours
+ */
+import {WorkInfo} from "../workInfo";
+
+/**
+ * Formattage des infos du travail en cours
+ */
+export class WorkInfoFormatter {
+
+    /**
+     * Transforme le work info en code
+     */
+    public format(workInfo : WorkInfo) : string {
+        return workInfo.groupe
+        + "J" + workInfo.jour
+        + workInfo.experience
+        +  (workInfo.blob != null  ? "B" + workInfo.blob : "");
+    }
+
+
+}
\ No newline at end of file
diff --git a/src/data/work/process/workInfoParser.ts b/src/data/work/process/workInfoParser.ts
new file mode 100644
index 0000000000000000000000000000000000000000..76e9b5f1799399a9db7a9e8890a735bee01a6249
--- /dev/null
+++ b/src/data/work/process/workInfoParser.ts
@@ -0,0 +1,28 @@
+import {ExperienceEnum, GroupeEnum, WorkInfo} from "../workInfo";
+
+
+const WORK_REGEXP = /^(?<groupe>Con|Exp)J(?<jour>[0-9]+)(?<experience>Cr|Ex)(B(?<blob>[0-9]+))?$/i;
+
+/**
+ * Parsing de code de travail en cours
+ */
+export class WorkInfoParser {
+
+    /**
+     * Valide le bom de fichier
+     */
+    public parse(name : string) : WorkInfo | null {
+        let match = name.match(WORK_REGEXP);
+        if(match == null) {
+            return null;
+        } else {
+            return {
+                groupe:  match.groups.groupe.toLowerCase() == 'Con'.toLowerCase() ?  GroupeEnum.Controle : GroupeEnum.Experimental,
+                jour: parseInt(match.groups.jour),
+                experience: match.groups.experience.toLowerCase() == 'Cr'.toLowerCase() ?  ExperienceEnum.Croissance : ExperienceEnum.Exploration,
+                blob:  match.groups.blob != null ? parseInt(match.groups.blob) : null,
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/data/work/workInfo.ts b/src/data/work/workInfo.ts
new file mode 100644
index 0000000000000000000000000000000000000000..88d6aebdf0c67fc44da6456372769c4e8cd413fa
--- /dev/null
+++ b/src/data/work/workInfo.ts
@@ -0,0 +1,25 @@
+/**
+ * Le boîte de blob
+ */
+export enum GroupeEnum {
+    Experimental = 'Exp',
+    Controle = 'Con'
+}
+
+/**
+ * La phase du blob
+ */
+export enum ExperienceEnum {
+    Croissance = 'Cr',
+    Exploration = 'Ex'
+}
+
+/**
+ * Informations sur la photo
+ */
+export interface WorkInfo {
+    groupe : GroupeEnum,
+    jour : number,
+    experience : ExperienceEnum,
+    blob: number;
+}
\ No newline at end of file
diff --git a/src/lab.tsx b/src/lab.tsx
index 654753b806b4aa5084045597608b1e2c07deea56..97d17585a262cf41a0e32df9c32453a94e80c7f4 100644
--- a/src/lab.tsx
+++ b/src/lab.tsx
@@ -1,348 +1,372 @@
-import * as React from 'react';
-import * as paper from "paper";
-import {StepManager} from "./ui/steps/stepManager";
-import {LoadPictureStep} from "./ui/steps/loadPictureStep";
-import {RulerStep} from "./ui/steps/rulerStep";
-import {Badge, Button, ButtonGroup, Col, Container, Form, Navbar, Row} from "react-bootstrap";
-import {Ruler} from "./instruments/ruler";
-import {PlacePetriDishStep} from "./ui/steps/placePetriDishStep";
-import {PetriDish} from "./instruments/petriDish";
-import {DrawBlobMaskStep} from "./ui/steps/drawBlobMaskStep";
-import {BlobMask} from "./instruments/blobMask";
-import {VectorCoords} from "./data/coords/vectorCoords";
-import {PathCoords} from "./data/coords/pathCoords";
-import {DownloadStep} from "./ui/steps/downloadStep";
-import {PaperUtils} from "./utils/paperUtils";
-import {EllipseCoords} from "./data/coords/ellipseCoords";
-import {Welcome} from "./ui/welcome";
-
-/**
- * Debug mode (ou pas)
- */
-export const DEBUG_MODE = false;
-
-/**
- * La taille par défaut de la règle
- */
-export const DEFAULT_RULER_TICK_COUNT = 10;
-
-/**
- * La taille minimum de la règle
- */
-export const MIN_RULER_TICK_COUNT = 8;
-
-/**
- * La taille maximum de la règle
- */
-export const MAX_RULER_TICK_COUNT = 15;
-
-
-export interface LabData {
-    pictureSize : paper.Size,
-
-    filename : string,
-
-    rulerTickCount : number,
-
-    rulerCoords : VectorCoords,
-
-    petriDishCoords : EllipseCoords,
-
-    blobMaskCoords : PathCoords
-}
-
-
-/**
- * Le lab
- */
-export class Lab extends React.Component<{}> {
-
-    /**
-     * Récolte des données
-     */
-    public data : LabData | null;
-
-    /**
-     * Le canvas sur lequel est dessiné la photo
-     */
-    private canvas : HTMLCanvasElement | null = null;
-
-    /**
-     * Le canvas sur lequel est dessiné la photo
-     */
-    private welcome = React.createRef<Welcome>();
-
-    /**
-     * La photo du blob
-     */
-    public raster : null | paper.Raster = null;
-
-    /**
-     * La règle
-     */
-    public ruler: Ruler;
-
-    /**
-     * La boîte de petri
-     */
-    public petriDish: PetriDish;
-
-    /**
-     * Le contour du blob
-     */
-    public blobMask: BlobMask;
-
-    public constructor(props : {}) {
-        super(props);
-    }
-
-    /**
-     * Post-init lorsque le lab est affiché
-     */
-    public componentDidMount() {
-        if(this.canvas == null) {
-            throw new Error("Canvas should have been initiated");
-        }
-
-        paper.setup(this.canvas);
-        new paper.Tool();
-        paper.tool.activate();
-
-        // Ajout du zoom avec la molette
-        this.canvas.addEventListener('wheel', (event) => {
-            let target = paper.view.viewToProject(new paper.Point(event.offsetX, event.offsetY));
-            if (event.deltaY > 0) {
-                this.zoomOut(target);
-            } else {
-                this.zoomIn(target);
-            }
-        });
-
-        // Ajout des déplacements
-        paper.tool.onKeyDown = (event : paper.KeyEvent) => {
-            if(event.key == "control") {
-                PaperUtils.changeCursor("move");
-            }
-        }
-        paper.tool.onKeyUp = (event : paper.KeyEvent) => {
-            if(event.key == "control") {
-                PaperUtils.changeCursor(null);
-            }
-        }
-        paper.tool.onMouseDrag = function(event) {
-            if(event.modifiers.control) {
-                let delta = event.point.subtract(event.downPoint);
-                paper.view.center = paper.view.center.subtract(delta);
-                event.preventDefault();
-            }
-        }
-
-        // Prise en compte du redimensionnement navigateur (ou agrandissement/retrécissement de la vue)
-        paper.view.onResize = (event) => {
-            if(this.raster != null) {
-                paper.view.center = this.raster.position;
-            }
-        }
-    }
-
-    /**
-     * Lance une nouvelle session de travail
-     */
-    public new(image : HTMLImageElement, filename : string) : boolean {
-        console.info("Nouvelle session de travail")
-        if(this.raster != null) {
-            if(window.confirm("Écraser le travail en cours ?")) {
-                this.reset();
-            } else {
-                return false;
-            }
-        }
-
-        // Masque le composant d'accueil
-        this.welcome.current.setState({visible: false });
-
-        // Init du Raster de l'image
-        this.raster = new paper.Raster();
-        this.raster.image = image;
-        this.raster.bounds.point = new paper.Point(0, 0);
-        this.raster.smoothing = 'off';
-
-        // Init des données
-        let width = image.naturalWidth;
-        let height = image.naturalHeight;
-        this.data = {
-            pictureSize: new paper.Size(width, height),
-
-            filename: filename,
-
-            rulerTickCount : DEFAULT_RULER_TICK_COUNT,
-
-            rulerCoords: new VectorCoords(new paper.Point(width * 0.25, height / 2), new paper.Point(width * 0.75, height / 2)),
-
-            petriDishCoords: new EllipseCoords(new paper.Point(width / 2, height / 2), width * 0.75 / 2, width * 0.75 / 2, 0),
-
-            blobMaskCoords: new PathCoords(),
-        };
-
-        // Le plus en dessous en premier
-        this.blobMask = new BlobMask(this, this.data.blobMaskCoords);
-        this.petriDish = new PetriDish(this, this.data.petriDishCoords);
-        this.ruler = new Ruler(this, this.data.rulerCoords);
-
-        // Zoom global
-        this.zoomFit();
-
-        // Petit  refresh
-        paper.view.update();
-
-        return true;
-    }
-
-    /**
-     * Remet à zéro l'espace de travail
-     */
-    private reset() : void {
-        if(this.raster != null) {
-            this.raster.remove();
-        }
-        this.raster = null;
-
-        // Vidage des instruments
-        this.ruler.clear();
-        this.petriDish.clear();
-        this.blobMask.clear();
-
-    }
-
-    /**
-     * Zoom au plus près de la photo
-     */
-    public zoomFit() : void {
-        if(this.raster != null && this.canvas != null && this.data.pictureSize != null) {
-            this.zoomOn(new paper.Rectangle(0, 0, this.data.pictureSize.width, this.data.pictureSize.height))
-        }
-    }
-
-    /**
-     * Zoom sur une zone
-     */
-    public zoomOn(rectangle : paper.Rectangle, marginPercent : number = 0) {
-        paper.view.center = rectangle.center;
-        let xZoomFactor = Math.min(1, this.canvas.width / rectangle.width);
-        let yZoomFactor = Math.min(1, this.canvas.height / rectangle.height);
-        paper.view.zoom = Math.min(xZoomFactor, yZoomFactor) * (1 - marginPercent) / paper.view.pixelRatio;
-        paper.view.update();
-        this.onZoomChanged();
-    }
-
-    /**
-     * Plus de zoom
-     */
-    public zoomIn(target? : paper.Point) : void {
-        this.zoom(1.10, target);
-    }
-
-    // Zooms par delta
-
-    /**
-     * Moins de zoom
-     */
-    public zoomOut(target? : paper.Point) : void {
-        this.zoom(0.90, target);
-    }
-
-    /**
-     * Pas de zoom 1:1
-     */
-    public zoomNone() : void {
-        paper.view.zoom = 1;
-        this.onZoomChanged();
-    }
-
-    /**
-     * Centralise le zoom
-     */
-    private zoom(zoomFactor : number, target? : paper.Point) {
-        let oldZoom = paper.view.zoom;
-        let newZoom = oldZoom * zoomFactor;
-
-        if(target != undefined) {
-            // let viewPosition = paper.view.viewToProject(target);
-            let viewPosition = target;
-            let mpos = viewPosition;
-            let ctr = paper.view.center;
-
-            let pc = mpos.subtract(ctr);
-            let offset = mpos.subtract(pc.multiply(0.95 / zoomFactor)).subtract(ctr);
-
-            paper.view.center = paper.view.center.add(offset);
-        }
-
-        // On borne les zooms pour ne pas que ça parte partout
-        // Zoom minimum = zoom fit
-        let minZoom =  0;
-        if(this.data != null) {
-            let xZoomFactor = Math.min(1, this.canvas.width / this.data.pictureSize.width)
-            let yZoomFactor = Math.min(1, this.canvas.height / this.data.pictureSize.height);
-            minZoom = Math.min(xZoomFactor, yZoomFactor)  / paper.view.pixelRatio;
-        }
-        let maxZoom =  10;
-        paper.view.zoom = Math.min(maxZoom, Math.max(newZoom, minZoom));
-        this.onZoomChanged();
-    }
-
-    /**
-     * Appelé lorsque le zoom a changé
-     */
-    private onZoomChanged() {
-        this.ruler?.refresh();
-        this.petriDish?.refresh();
-        this.blobMask?.refresh();
-    }
-
-    render(): React.ReactNode {
-        return <Container fluid={true} className={"vh-100 d-flex flex-column"}>
-            <Navbar bg="light" expand="lg" className={"p-0"}>
-                <Container fluid={true}>
-                    <Row className={"col-md-12"}>
-                        <div className="d-flex d-flex justify-content-between">
-                            <Navbar.Brand className={"p-0"}><i className={"fa-solid fa-flask me-2"}></i>Blob Analysis Lab <sup><Badge pill bg="secondary" text="primary">demo</Badge></sup></Navbar.Brand>
-                            <Form className={"inline-form"}>
-                                <Form.Group controlId="zoomGroup">
-                                    <Form.Label>Zoom :</Form.Label>
-                                    <ButtonGroup aria-label="Zoom" className={"ms-2"}>
-                                        <Button onClick={() => this.zoomIn()} variant={"primary"} size={"sm"}><i className="fa-solid fa-magnifying-glass-plus"></i></Button>
-                                        <Button onClick={() => this.zoomOut()} variant={"primary"} size={"sm"}><i className="fa-solid fa-magnifying-glass-minus"></i></Button>
-                                        <Button onClick={() => this.zoomFit()} variant={"primary"} size={"sm"}><i className="fa-regular fa-image"></i></Button>
-                                        <Button onClick={() => this.zoomNone()} variant={"primary"} size={"sm"}>1:1</Button>
-                                    </ButtonGroup>
-                                    <Form.Label className={"ms-3"}>[CTRL] + <i className="fa-solid fa-computer-mouse"></i> = <i className="fa-solid fa-arrows-up-down-left-right"></i></Form.Label>
-                                </Form.Group>
-                            </Form>
-                            <div>
-                                <Button href={"docs/index.html"} target="_blank" variant={"primary"} size={"sm"}>Tutoriel<i className="ms-2 fa-solid fa-book"></i></Button>
-                            </div>
-                        </div>
-                    </Row>
-                </Container>
-            </Navbar>
-            <Row className="flex-grow-1">
-                <div className="col position-relative">
-                   <Welcome ref={this.welcome}/>
-                    <canvas ref={(canvas : HTMLCanvasElement)=> this.canvas = canvas} data-paper-resize="false"  className="h-100 w-100 d-block" onContextMenu={() => false}></canvas>
-                </div>
-                <Col md={4} className={"border-2"}  >
-                    <div className="mb-3">
-                        <StepManager>
-                            <LoadPictureStep lab={this} code="loadPicture" title="Charger une photo"/>
-                            <RulerStep lab={this} code="placeRuler" title="Positionner la règle"/>
-                            <PlacePetriDishStep  lab={this} code="placePetriDish" title="Positionner la boîte de petri"/>
-                            <DrawBlobMaskStep  lab={this} code="drawBlobMask" title="Détourer le blob"/>
-                            <DownloadStep  lab={this} code="download" title="Télécharger les résultats"/>
-                        </StepManager>
-                    </div>
-                </Col>
-            </Row>
-        </Container>
-    }
+import * as React from 'react';
+import * as paper from "paper";
+import {StepManager} from "./ui/steps/stepManager";
+import {LoadPictureStep} from "./ui/steps/loadPictureStep";
+import {RulerStep} from "./ui/steps/rulerStep";
+import {Badge, Button, ButtonGroup, Col, Container, Form, Navbar, Row} from "react-bootstrap";
+import {Ruler} from "./instruments/ruler";
+import {PlacePetriDishStep} from "./ui/steps/placePetriDishStep";
+import {PetriDish} from "./instruments/petriDish";
+import {DrawBlobMaskStep} from "./ui/steps/drawBlobMaskStep";
+import {BlobMask} from "./instruments/blobMask";
+import {VectorCoords} from "./data/coords/vectorCoords";
+import {PathCoords} from "./data/coords/pathCoords";
+import {DownloadStep} from "./ui/steps/downloadStep";
+import {PaperUtils} from "./utils/paperUtils";
+import {EllipseCoords} from "./data/coords/ellipseCoords";
+import {Welcome} from "./ui/welcome";
+import {DataImporter} from "./data/dataImporter";
+import {WorkInfo} from "./data/work/workInfo";
+import {IoUtils} from "./utils/ioUtils";
+import {WorkInfoParser} from "./data/work/process/workInfoParser";
+
+/**
+ * Debug mode (ou pas)
+ */
+export const DEBUG_MODE = false;
+
+/**
+ * La taille par défaut de la règle
+ */
+export const DEFAULT_RULER_TICK_COUNT = 10;
+
+/**
+ * La taille minimum de la règle
+ */
+export const MIN_RULER_TICK_COUNT = 8;
+
+/**
+ * La taille maximum de la règle
+ */
+export const MAX_RULER_TICK_COUNT = 15;
+
+
+export interface LabData {
+
+    pictureSize : paper.Size,
+
+    filename : string,
+
+    workInfo : WorkInfo,
+
+    rulerTickCount : number,
+
+    rulerCoords : VectorCoords,
+
+    petriDishCoords : EllipseCoords,
+
+    blobMaskCoords : PathCoords
+}
+
+
+/**
+ * Le lab
+ */
+export class Lab extends React.Component<{}> {
+
+    /**
+     * Récolte des données
+     */
+    public data : LabData | null;
+
+    /**
+     * Le canvas sur lequel est dessiné la photo
+     */
+    private canvas : HTMLCanvasElement | null = null;
+
+    /**
+     * Le canvas sur lequel est dessiné la photo
+     */
+    private welcome = React.createRef<Welcome>();
+
+    /**
+     * La photo du blob
+     */
+    public raster : null | paper.Raster = null;
+
+    /**
+     * La règle
+     */
+    public ruler: Ruler;
+
+    /**
+     * La boîte de petri
+     */
+    public petriDish: PetriDish;
+
+    /**
+     * Le contour du blob
+     */
+    public blobMask: BlobMask;
+
+    /**
+     * Outil d'import de données
+     */
+    public dataImporter = new DataImporter();
+
+    /**
+     * Parsing du work in progress
+     */
+    public workInfoParser = new WorkInfoParser();
+
+    public constructor(props : {}) {
+        super(props);
+    }
+
+    /**
+     * Post-init lorsque le lab est affiché
+     */
+    public componentDidMount() {
+        if(this.canvas == null) {
+            throw new Error("Canvas should have been initiated");
+        }
+
+        paper.setup(this.canvas);
+        new paper.Tool();
+        paper.tool.activate();
+
+        // Ajout du zoom avec la molette
+        this.canvas.addEventListener('wheel', (event) => {
+            let target = paper.view.viewToProject(new paper.Point(event.offsetX, event.offsetY));
+            if (event.deltaY > 0) {
+                this.zoomOut(target);
+            } else {
+                this.zoomIn(target);
+            }
+        });
+
+        // Ajout des déplacements
+        paper.tool.onKeyDown = (event : paper.KeyEvent) => {
+            if(event.key == "control") {
+                PaperUtils.changeCursor("move");
+            }
+        }
+        paper.tool.onKeyUp = (event : paper.KeyEvent) => {
+            if(event.key == "control") {
+                PaperUtils.changeCursor(null);
+            }
+        }
+        paper.tool.onMouseDrag = function(event) {
+            if(event.modifiers.control) {
+                let delta = event.point.subtract(event.downPoint);
+                paper.view.center = paper.view.center.subtract(delta);
+                event.preventDefault();
+            }
+        }
+
+        // Prise en compte du redimensionnement navigateur (ou agrandissement/retrécissement de la vue)
+        paper.view.onResize = (event) => {
+            if(this.raster != null) {
+                paper.view.center = this.raster.position;
+            }
+        }
+    }
+
+    /**
+     * Lance une nouvelle session de travail
+     */
+    public new(image : HTMLImageElement, filename : string) : boolean {
+        console.info("Nouvelle session de travail")
+        let workInfo = this.workInfoParser.parse(IoUtils.basename(filename));
+        if(workInfo == null) {
+            window.alert("Le nom du fichier ne respecte pas nomenclature")
+            return false;
+        }
+        if(this.raster != null) {
+            if(window.confirm("Écraser le travail en cours ?")) {
+                this.reset();
+            } else {
+                return false;
+            }
+        }
+
+        // Masque le composant d'accueil
+        this.welcome.current.setState({visible: false });
+
+        // Init du Raster de l'image
+        this.raster = new paper.Raster();
+        this.raster.image = image;
+        this.raster.bounds.point = new paper.Point(0, 0);
+        this.raster.smoothing = 'off';
+
+        // Init des données
+        let width = image.naturalWidth;
+        let height = image.naturalHeight;
+        this.data = {
+            pictureSize: new paper.Size(width, height),
+
+            filename: filename,
+
+            workInfo: workInfo,
+
+            rulerTickCount : DEFAULT_RULER_TICK_COUNT,
+
+            rulerCoords: new VectorCoords(new paper.Point(width * 0.25, height / 2), new paper.Point(width * 0.75, height / 2)),
+
+            petriDishCoords: new EllipseCoords(new paper.Point(width / 2, height / 2), width * 0.75 / 2, width * 0.75 / 2, 0),
+
+            blobMaskCoords: new PathCoords(),
+        };
+
+        // Le plus en dessous en premier
+        this.blobMask = new BlobMask(this, this.data.blobMaskCoords);
+        this.petriDish = new PetriDish(this, this.data.petriDishCoords);
+        this.ruler = new Ruler(this, this.data.rulerCoords);
+
+        // Zoom global
+        this.zoomFit();
+
+        // Petit  refresh
+        paper.view.update();
+
+        return true;
+    }
+
+    /**
+     * Remet à zéro l'espace de travail
+     */
+    private reset() : void {
+        if(this.raster != null) {
+            this.raster.remove();
+        }
+        this.raster = null;
+
+        // Vidage des instruments
+        this.ruler.clear();
+        this.petriDish.clear();
+        this.blobMask.clear();
+
+    }
+
+    /**
+     * Zoom au plus près de la photo
+     */
+    public zoomFit() : void {
+        if(this.raster != null && this.canvas != null && this.data.pictureSize != null) {
+            this.zoomOn(new paper.Rectangle(0, 0, this.data.pictureSize.width, this.data.pictureSize.height))
+        }
+    }
+
+    /**
+     * Zoom sur une zone
+     */
+    public zoomOn(rectangle : paper.Rectangle, marginPercent : number = 0) {
+        paper.view.center = rectangle.center;
+        let xZoomFactor = Math.min(1, this.canvas.width / rectangle.width);
+        let yZoomFactor = Math.min(1, this.canvas.height / rectangle.height);
+        paper.view.zoom = Math.min(xZoomFactor, yZoomFactor) * (1 - marginPercent) / paper.view.pixelRatio;
+        paper.view.update();
+        this.onZoomChanged();
+    }
+
+    /**
+     * Plus de zoom
+     */
+    public zoomIn(target? : paper.Point) : void {
+        this.zoom(1.10, target);
+    }
+
+    // Zooms par delta
+
+    /**
+     * Moins de zoom
+     */
+    public zoomOut(target? : paper.Point) : void {
+        this.zoom(0.90, target);
+    }
+
+    /**
+     * Pas de zoom 1:1
+     */
+    public zoomNone() : void {
+        paper.view.zoom = 1;
+        this.onZoomChanged();
+    }
+
+    /**
+     * Centralise le zoom
+     */
+    private zoom(zoomFactor : number, target? : paper.Point) {
+        let oldZoom = paper.view.zoom;
+        let newZoom = oldZoom * zoomFactor;
+
+        if(target != undefined) {
+            // let viewPosition = paper.view.viewToProject(target);
+            let viewPosition = target;
+            let mpos = viewPosition;
+            let ctr = paper.view.center;
+
+            let pc = mpos.subtract(ctr);
+            let offset = mpos.subtract(pc.multiply(0.95 / zoomFactor)).subtract(ctr);
+
+            paper.view.center = paper.view.center.add(offset);
+        }
+
+        // On borne les zooms pour ne pas que ça parte partout
+        // Zoom minimum = zoom fit
+        let minZoom =  0;
+        if(this.data != null) {
+            let xZoomFactor = Math.min(1, this.canvas.width / this.data.pictureSize.width)
+            let yZoomFactor = Math.min(1, this.canvas.height / this.data.pictureSize.height);
+            minZoom = Math.min(xZoomFactor, yZoomFactor)  / paper.view.pixelRatio;
+        }
+        let maxZoom =  10;
+        paper.view.zoom = Math.min(maxZoom, Math.max(newZoom, minZoom));
+        this.onZoomChanged();
+    }
+
+    /**
+     * Appelé lorsque le zoom a changé
+     */
+    private onZoomChanged() {
+        this.ruler?.refresh();
+        this.petriDish?.refresh();
+        this.blobMask?.refresh();
+    }
+
+    render(): React.ReactNode {
+        return <Container fluid={true} className={"vh-100 d-flex flex-column"}>
+            <Navbar bg="light" expand="lg" className={"p-0"}>
+                <Container fluid={true}>
+                    <Row className={"col-md-12"}>
+                        <div className="d-flex d-flex justify-content-between">
+                            <Navbar.Brand className={"p-0"}><i className={"fa-solid fa-flask me-2"}></i>Blob Analysis Lab <sup><Badge pill bg="secondary" text="primary">demo</Badge></sup></Navbar.Brand>
+                            <Form className={"inline-form"}>
+                                <Form.Group controlId="zoomGroup">
+                                    <Form.Label>Zoom :</Form.Label>
+                                    <ButtonGroup aria-label="Zoom" className={"ms-2"}>
+                                        <Button onClick={() => this.zoomIn()} variant={"primary"} size={"sm"}><i className="fa-solid fa-magnifying-glass-plus"></i></Button>
+                                        <Button onClick={() => this.zoomOut()} variant={"primary"} size={"sm"}><i className="fa-solid fa-magnifying-glass-minus"></i></Button>
+                                        <Button onClick={() => this.zoomFit()} variant={"primary"} size={"sm"}><i className="fa-regular fa-image"></i></Button>
+                                        <Button onClick={() => this.zoomNone()} variant={"primary"} size={"sm"}>1:1</Button>
+                                    </ButtonGroup>
+                                    <Form.Label className={"ms-3"}>[CTRL] + <i className="fa-solid fa-computer-mouse"></i> = <i className="fa-solid fa-arrows-up-down-left-right"></i></Form.Label>
+                                </Form.Group>
+                            </Form>
+                            <div>
+                                <Button href={"docs/index.html"} target="_blank" variant={"primary"} size={"sm"}>Tutoriel<i className="ms-2 fa-solid fa-book"></i></Button>
+                            </div>
+                        </div>
+                    </Row>
+                </Container>
+            </Navbar>
+            <Row className="flex-grow-1">
+                <div className="col position-relative">
+                   <Welcome ref={this.welcome}/>
+                    <canvas ref={(canvas : HTMLCanvasElement)=> this.canvas = canvas} data-paper-resize="false"  className="h-100 w-100 d-block" onContextMenu={() => false}></canvas>
+                </div>
+                <Col md={4} className={"border-2"}  >
+                    <div className="mb-3">
+                        <StepManager>
+                            <LoadPictureStep lab={this} code="loadPicture" title="Charger une photo"/>
+                            <RulerStep lab={this} code="placeRuler" title="Positionner la règle"/>
+                            <PlacePetriDishStep  lab={this} code="placePetriDish" title="Positionner la boîte de petri"/>
+                            <DrawBlobMaskStep  lab={this} code="drawBlobMask" title="Détourer le blob"/>
+                            <DownloadStep  lab={this} code="download" title="Télécharger les résultats"/>
+                        </StepManager>
+                    </div>
+                </Col>
+            </Row>
+        </Container>
+    }
 }
\ No newline at end of file
diff --git a/src/ui/merger.tsx b/src/ui/merger.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4733e817021b6ff763f2a4a7cce104140f4ac6cf
--- /dev/null
+++ b/src/ui/merger.tsx
@@ -0,0 +1,148 @@
+import * as React from "react";
+import {ChangeEvent} from "react";
+import {Alert, Button, Col, InputGroup, ListGroup, Modal, Row} from "react-bootstrap";
+import {IoUtils} from "../utils/ioUtils";
+import {MeasurementImport} from "../data/measurement/process/measurementImport";
+import {ExperienceEnum, GroupeEnum} from "../data/work/workInfo";
+import {DownloadButton} from "./steps/downloadStep";
+import {MeasurementWorks} from "../data/measurement/measurementWorks";
+import {MeasurementMerge} from "../data/measurement/process/measurementMerge";
+import {MeasurementExport} from "../data/measurement/process/measurementExport";
+import {Measurement} from "../data/measurement/measurement";
+
+/**
+ * L'état de l'affichage de l'outil de merge
+ */
+export interface MergerState {
+    visible : boolean,
+    measurementWorks: MeasurementWorks
+}
+
+/**
+ * Outil de merge des fichiers de mesures
+ */
+export class Merger extends React.Component<{}, MergerState> {
+
+    /**
+     * Pour importer des données
+     */
+    private measurementImport = new MeasurementImport();
+
+    /**
+     * Pour exporter des données
+     */
+    private measurementExport = new MeasurementExport();
+
+    /**
+     * Pour merger les données
+     */
+    private measurementMerge = new MeasurementMerge();
+
+    public constructor(props: {}) {
+        super(props);
+        this.state = {visible: false, measurementWorks: {} }
+    }
+
+    /**
+     * Affiche l'outil de merge
+     */
+    public show() {
+        this.setState({visible: true});
+    }
+
+    /**
+     * Masque l'outil de merge
+     */
+    public hide() {
+        this.setState({visible: false});
+    }
+
+    /**
+     *
+     * @private
+     */
+    private addFiles(event : ChangeEvent<HTMLInputElement>) {
+        if(event.target.files != null) {
+            for (let file of event.target.files) {
+                IoUtils.readTextFile(file, text => {
+                    let filename = file.name;
+                    try {
+                        let {work, measurements} = this.measurementImport.readMeasures(filename, text);
+                        let measurementWorks = this.measurementMerge.mergeInto(this.state.measurementWorks, filename, work, measurements);
+                        this.setState({measurementWorks: measurementWorks});
+                    } catch (error) {
+                        alert(error.message);
+                    }
+                });
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Téléchargement de la description du blob
+     */
+    private download(workCode: string) : void {
+        let measurements: Measurement[] = Object.keys(this.state.measurementWorks[workCode].files)
+            .flatMap(filename => this.state.measurementWorks[workCode].files[filename]);
+
+        let { filename, csv } = this.measurementExport.exportMeasurements(this.state.measurementWorks[workCode].work, measurements);
+        IoUtils.downloadData(filename, "text/plain;charset=UTF-8", csv);
+    }
+
+
+    public render() {
+        let that = this;
+        return <Modal
+            show={this.state.visible}
+            backdrop="static"
+            keyboard={false}
+            size={"lg"}
+            onHide={this.hide.bind(this)}
+            scrollable={true}
+        >
+            <Modal.Header closeButton={true} closeVariant={"white"}>
+                <Modal.Title as={"h6"}>Fusionner les CSV</Modal.Title>
+            </Modal.Header>
+            <Modal.Body>
+                <Alert className={"p-2"} variant={"success"}><b>A la fin de l'analyse d'une séquence de photos </b>, regroupez ici les fichiers .csv générés par photo (par ex.: <em>Results_ExpJ1CrB10.csv</em>) pour générer le fichier attendu dans l'espace blob (par ex.: <em>Results_ExpJ1Cr.csv</em>).</Alert>
+                <Row>
+                    <Col>
+                        <b>Sélectionner les fichiers en vrac à regrouper :</b>
+                        <div className="mb-3 mt-1">
+                            <input className="form-control" type="file" accept={".csv"} multiple={true} onChange={(e) => this.addFiles(e)}></input>
+                        </div>
+                    </Col>
+                </Row>
+                <Row>
+                    <Col>
+                        <b>Fichiers fusionnés :</b>
+                        <ListGroup>
+                            {Object.keys(this.state.measurementWorks).map((workCode) => {
+                                return <ListGroup.Item key={workCode}>
+                                    <div className={"mb-2"}>
+                                        <span className={"ms-0"}><span className={"small text-dark"}>Groupe : </span><b>{this.state.measurementWorks[workCode].work.groupe == GroupeEnum.Experimental ? "Expérimental" : "Contrôle"}</b></span>
+                                        <span className={"ms-2"}><span className={"small text-dark"}>Jour : </span><b>{this.state.measurementWorks[workCode].work.jour}</b></span>
+                                        <span className={"ms-2"}><span className={"small text-dark"}>Expérience : </span><b>{this.state.measurementWorks[workCode].work.experience == ExperienceEnum.Exploration ? "Exploration" : "Croissance"}</b></span>
+                                    </div>
+                                    <div className={"mb-2"}>
+                                        { Object.keys(this.state.measurementWorks[workCode].files).map(value =><span key={value} className={"me-1 p-1 rounded border bg-light font-monospace"}>{value}</span>)}
+                                    </div>
+                                    <div className={"col-5"}>
+                                        <InputGroup size={"sm"}>
+                                            <DownloadButton downloading={false} disabled={false} onClick={() => {this.download(workCode)}}/>
+                                            <InputGroup.Text className={"col"}>{ that.measurementExport.exportDryMeasures(this.state.measurementWorks[workCode].work).filename }</InputGroup.Text>
+                                        </InputGroup>
+                                    </div>
+                                </ListGroup.Item>
+                            })}
+                        </ListGroup>
+                    </Col>
+                </Row>
+            </Modal.Body>
+            <Modal.Footer>
+                <Button variant="secondary" onClick={this.hide.bind(this)}>Fermer</Button>
+            </Modal.Footer>
+        </Modal>
+    }
+}
\ No newline at end of file
diff --git a/src/ui/steps/downloadStep.tsx b/src/ui/steps/downloadStep.tsx
index 8effb924298103b047abe2c06ed60e86de8e60bc..804b73319ddc8c71abb32a86ec23d0eb850800d4 100644
--- a/src/ui/steps/downloadStep.tsx
+++ b/src/ui/steps/downloadStep.tsx
@@ -1,175 +1,206 @@
-import {Step, StepProps, StepState} from "./step";
-import * as React from "react";
-import {Alert, Button, InputGroup} from "react-bootstrap";
-import {IoUtils} from "../../utils/ioUtils";
-import {DataExporter} from "../../data/dataExporter";
-import * as paper from "paper";
-import {ImageDataWrapper, Rgba} from "../../render/ImageDataWrapper";
-
-export interface DownloadButtonProps {
-    downloading: boolean,
-    onClick : () => void,
-    disabled: boolean,
-}
-
-export class DownloadButton extends React.Component<DownloadButtonProps, any> {
-
-    constructor(props : DownloadButtonProps) {
-        super(props);
-    }
-
-    render() : React.ReactNode {
-        let faIcon = this.props.downloading ? "fa-solid fas fa-cog fa-spin" : "fa-solid fa-download";
-        let key = this.props.downloading ? "downloading" : "notDownloading";
-        return <Button key={key} onClick={this.props.onClick} disabled={this.props.disabled || this.props.downloading} variant={"primary"} size={"sm"}>
-            <i className={faIcon}></i>
-        </Button>
-    }
-
-}
-
-export interface DownloadStepState extends  StepState {
-    petriDishDataFilename: string | null,
-    blobMaskDataFilename: string | null,
-    blobMaskFilename: string | null,
-    resultsFilename: string | null,
-    downloading: boolean
-}
-
-
-
-/**
- * Etape de téléchargement des fichiers
- */
-export class DownloadStep extends Step<DownloadStepState> {
-
-    /**
-     * Outil d'export de données
-     */
-    private dataExporter: DataExporter = new DataExporter();
-
-    public constructor(props: StepProps) {
-        super(props, { active: false, activable: false, petriDishDataFilename : null,  blobMaskDataFilename: null,  blobMaskFilename: null, resultsFilename: null, downloading: false });
-    }
-
-    canBeActivated(): boolean {
-        return this.props.lab.data != null
-            && this.props.lab.ruler.hasBeenStarted()
-            && this.props.lab.petriDish.hasBeenStarted()
-            && this.props.lab.blobMask.hasBeenStarted()
-            && this.props.lab.data.blobMaskCoords.isClosed();
-    }
-
-    onActivation(): void {
-        this.setState({
-            petriDishDataFilename: IoUtils.basename(this.props.lab.data.filename) + "_Coord_Boite.txt",
-            blobMaskDataFilename: IoUtils.basename(this.props.lab.data.filename) + "_Coord_Blob.txt",
-            blobMaskFilename: IoUtils.basename(this.props.lab.data.filename) + "_Mask.png",
-            resultsFilename: "Results_" + IoUtils.basename(this.props.lab.data.filename) + ".csv",
-        });
-    }
-
-    onDeactivation(): void {
-    }
-
-    /**
-     * Téléchargement des données de la boîte de Petri
-     */
-    private downloadPetriDishData() : void {
-        let data = this.dataExporter.exportPathPointsAsXYCsv(this.props.lab.data.petriDishCoords, true);
-        IoUtils.downloadData(this.state.petriDishDataFilename, "text/plain;charset=UTF-8", data);
-    }
-
-    /**
-     * Téléchargement des données du mask
-     */
-    private downloadBlobMaskData() : void {
-        let data = this.dataExporter.exportPathSegmentsAsXYCsv(this.props.lab.data.blobMaskCoords, true);
-        IoUtils.downloadData(this.state.blobMaskDataFilename, "text/plain;charset=UTF-8", data);
-    }
-
-    /**
-     * Téléchargement des données du mask
-     */
-    private downloadBlobMask() : void {
-        let path = this.props.lab.data.blobMaskCoords.toRemovedPath();
-        path.closed = true;
-        path.fillColor = new paper.Color("black");
-        path.strokeColor = null;
-        path.scale(1 / paper.view.pixelRatio, path.bounds.point);
-
-        let raster = path.rasterize({ insert: false});
-        raster.smoothing = "off";
-
-        let newCanvas = document.createElement('canvas');
-        let w = this.props.lab.data.pictureSize.width;
-        newCanvas.width = w;
-        let h = this.props.lab.data.pictureSize.height;
-        newCanvas.height = h;
-
-        var newContext = newCanvas.getContext('2d');
-        newContext.fillStyle = "white";
-        newContext.fillRect(0, 0, newCanvas.width, newCanvas.height);
-        newContext.drawImage(raster.canvas, path.bounds.x - 0.5, path.bounds.y - 0.5);
-
-        // Re-aliasing
-        const imageDataWrapper = new ImageDataWrapper(newContext.getImageData(0, 0, w, h));
-
-        // Supprime les nuances de gris en se basant sur la valeur rouge (on est sur d'avoir du gris)
-        imageDataWrapper.apply((rgba: Rgba) => {
-            let l = rgba.r >= 128 ? 255 : 0;
-            // let l = rgba.r == 255 ? 255 : 0;
-            return {r: l, g: l, b: l, a: 255};
-        }).andStore(newContext);
-
-        IoUtils.downloadDataUrl(this.state.blobMaskFilename, newCanvas.toDataURL("image/png"));
-
-        newCanvas.remove();
-    }
-
-    /**
-     * Téléchargement de la description du blob
-     */
-    private downloadResults() : void {
-        this.downloading( () => {
-            let data = this.dataExporter.exportPathDescriptorsAsCsv(this.props.lab.data, this.props.lab.data.blobMaskCoords);
-            IoUtils.downloadData(this.state.resultsFilename, "text/plain;charset=UTF-8", data);
-        });
-    }
-
-    /**
-     * Execute une tâche en indiquant un chargement en cours
-     */
-    private downloading(task : () => void) {
-        this.setState({downloading : true}, () => {
-            setTimeout(() => {
-                task();
-                this.setState({downloading: false});
-            },500);
-        });
-    }
-
-    render() : React.ReactNode {
-        return <div>
-            <Alert show={!this.state.activable} variant="warning" className={"p-1"}>Veuillez terminer les étapes précédentes.</Alert>
-            <p>Téléchargez les fichiers d'analyse de la photo.</p>
-            <InputGroup className="mb-1">
-                <DownloadButton key="downloadPetriDishDataButtonKey" downloading={false} onClick={this.downloadPetriDishData.bind(this)} disabled={!this.state.active}/>
-                <InputGroup.Text className={"col"}>{ this.state.petriDishDataFilename }</InputGroup.Text>
-            </InputGroup>
-            <InputGroup className="mb-1">
-                <DownloadButton key="downloadBlobMaskDataButtonKey" downloading={false} onClick={this.downloadBlobMaskData.bind(this)} disabled={!this.state.active}/>
-                <InputGroup.Text className={"col"}>{ this.state.blobMaskDataFilename }</InputGroup.Text>
-            </InputGroup>
-            <InputGroup className="mb-1">
-                <DownloadButton key="downloadBlobMaskButtonKey" downloading={false} onClick={this.downloadBlobMask.bind(this)} disabled={!this.state.active}/>
-                <InputGroup.Text className={"col"}>{ this.state.blobMaskFilename }</InputGroup.Text>
-            </InputGroup>
-            <InputGroup className="mb-1">
-                <DownloadButton key="downloadResultsButtonKey" downloading={this.state.downloading} onClick={this.downloadResults.bind(this)} disabled={!this.state.active}></DownloadButton>
-                <InputGroup.Text className  ={"col"}>{ this.state.resultsFilename }</InputGroup.Text>
-            </InputGroup>
-        </div>
-    }
-
-}
+import {Step, StepProps, StepState} from "./step";
+import * as React from "react";
+import {Alert, Anchor, Button, Col, InputGroup, Row} from "react-bootstrap";
+import {IoUtils} from "../../utils/ioUtils";
+import {DataExporter} from "../../data/dataExporter";
+import * as paper from "paper";
+import {ImageDataWrapper, Rgba} from "../../render/ImageDataWrapper";
+import {Merger} from "../merger";
+import {WorkInfoFormatter} from "../../data/work/process/workInfoFormatter";
+import {MeasurementExport} from "../../data/measurement/process/measurementExport";
+
+export interface DownloadButtonProps {
+    downloading: boolean,
+    onClick : () => void,
+    disabled: boolean,
+}
+
+export class DownloadButton extends React.Component<DownloadButtonProps, any> {
+
+    constructor(props : DownloadButtonProps) {
+        super(props);
+    }
+
+    render() : React.ReactNode {
+        let faIcon = this.props.downloading ? "fa-solid fas fa-cog fa-spin" : "fa-solid fa-download";
+        let key = this.props.downloading ? "downloading" : "notDownloading";
+        return <Button key={key} onClick={this.props.onClick} disabled={this.props.disabled || this.props.downloading} variant={"primary"} size={"sm"}>
+            <i className={faIcon}></i>
+        </Button>
+    }
+
+}
+
+export interface DownloadStepState extends  StepState {
+    petriDishDataFilename: string | null,
+    blobMaskDataFilename: string | null,
+    blobMaskFilename: string | null,
+    resultsFilename: string | null,
+    downloading: boolean
+}
+
+
+
+/**
+ * Etape de téléchargement des fichiers
+ */
+export class DownloadStep extends Step<DownloadStepState> {
+
+    /**
+     * Accès au dialog d'outil de merge
+     * @private
+     */
+    private mergerRef = React.createRef<Merger>();
+
+    /**
+     * Outil d'export de données
+     */
+    private dataExporter = new DataExporter();
+
+    /**
+     * Outil d'export de données
+     */
+    private measureExport = new MeasurementExport();
+
+    public constructor(props: StepProps) {
+        super(props, { active: false, activable: false, petriDishDataFilename : null,  blobMaskDataFilename: null,  blobMaskFilename: null, resultsFilename: null, downloading: false });
+    }
+
+    canBeActivated(): boolean {
+        return this.props.lab.data != null
+            && this.props.lab.ruler.hasBeenStarted()
+            && this.props.lab.petriDish.hasBeenStarted()
+            && this.props.lab.blobMask.hasBeenStarted()
+            && this.props.lab.data.blobMaskCoords.isClosed();
+    }
+
+    onActivation(): void {
+        this.setState({
+            petriDishDataFilename: IoUtils.basename(this.props.lab.data.filename) + "_Coord_Boite.txt",
+            blobMaskDataFilename: IoUtils.basename(this.props.lab.data.filename) + "_Coord_Blob.txt",
+            blobMaskFilename: IoUtils.basename(this.props.lab.data.filename) + "_Mask.png",
+            resultsFilename: this.measureExport.exportDryMeasures(this.props.lab.data.workInfo).filename, // dry run juste pour le nom
+        });
+    }
+
+    onDeactivation(): void {
+    }
+
+    /**
+     * Téléchargement des données de la boîte de Petri
+     */
+    private downloadPetriDishData() : void {
+        let data = this.dataExporter.exportPathPointsAsXYCsv(this.props.lab.data.petriDishCoords, true);
+        IoUtils.downloadData(this.state.petriDishDataFilename, "text/plain;charset=UTF-8", data);
+    }
+
+    /**
+     * Téléchargement des données du mask
+     */
+    private downloadBlobMaskData() : void {
+        let data = this.dataExporter.exportPathSegmentsAsXYCsv(this.props.lab.data.blobMaskCoords, true);
+        IoUtils.downloadData(this.state.blobMaskDataFilename, "text/plain;charset=UTF-8", data);
+    }
+
+    /**
+     * Téléchargement des données du mask
+     */
+    private downloadBlobMask() : void {
+        let path = this.props.lab.data.blobMaskCoords.toRemovedPath();
+        path.closed = true;
+        path.fillColor = new paper.Color("black");
+        path.strokeColor = null;
+        path.scale(1 / paper.view.pixelRatio, path.bounds.point);
+
+        let raster = path.rasterize({ insert: false});
+        raster.smoothing = "off";
+
+        let newCanvas = document.createElement('canvas');
+        let w = this.props.lab.data.pictureSize.width;
+        newCanvas.width = w;
+        let h = this.props.lab.data.pictureSize.height;
+        newCanvas.height = h;
+
+        var newContext = newCanvas.getContext('2d');
+        newContext.fillStyle = "white";
+        newContext.fillRect(0, 0, newCanvas.width, newCanvas.height);
+        newContext.drawImage(raster.canvas, path.bounds.x - 0.5, path.bounds.y - 0.5);
+
+        // Re-aliasing
+        const imageDataWrapper = new ImageDataWrapper(newContext.getImageData(0, 0, w, h));
+
+        // Supprime les nuances de gris en se basant sur la valeur rouge (on est sur d'avoir du gris)
+        imageDataWrapper.apply((rgba: Rgba) => {
+            let l = rgba.r >= 128 ? 255 : 0;
+            // let l = rgba.r == 255 ? 255 : 0;
+            return {r: l, g: l, b: l, a: 255};
+        }).andStore(newContext);
+
+        IoUtils.downloadDataUrl(this.state.blobMaskFilename, newCanvas.toDataURL("image/png"));
+
+        newCanvas.remove();
+    }
+
+    /**
+     * Téléchargement de la description du blob
+     */
+    private downloadResults() : void {
+        this.downloading( () => {
+            let data = this.dataExporter.exportPathDescriptorsAsCsv(this.props.lab.data);
+            IoUtils.downloadData(this.state.resultsFilename, "text/plain;charset=UTF-8", data);
+        });
+    }
+
+    /**
+     * Ouvre l'outil de merge des fichiers CSV
+     * @private
+     */
+    private openMerger() {
+        this.mergerRef.current.show();
+    }
+
+    /**
+     * Execute une tâche en indiquant un chargement en cours
+     */
+    private downloading(task : () => void) {
+        this.setState({downloading : true}, () => {
+            setTimeout(() => {
+                task();
+                this.setState({downloading: false});
+            },500);
+        });
+    }
+
+
+
+    render() : React.ReactNode {
+        return <div>
+            <Alert show={!this.state.activable} variant="warning" className={"p-1"}>Veuillez terminer les étapes précédentes.</Alert>
+            <p>Téléchargez les fichiers d'analyse de la photo.</p>
+            <InputGroup className="mb-1">
+                <DownloadButton key="downloadPetriDishDataButtonKey" downloading={false} onClick={this.downloadPetriDishData.bind(this)} disabled={!this.state.active}/>
+                <InputGroup.Text className={"col"}>{ this.state.petriDishDataFilename }</InputGroup.Text>
+            </InputGroup>
+            <InputGroup className="mb-1">
+                <DownloadButton key="downloadBlobMaskDataButtonKey" downloading={false} onClick={this.downloadBlobMaskData.bind(this)} disabled={!this.state.active}/>
+                <InputGroup.Text className={"col"}>{ this.state.blobMaskDataFilename }</InputGroup.Text>
+            </InputGroup>
+            <InputGroup className="mb-1">
+                <DownloadButton key="downloadBlobMaskButtonKey" downloading={false} onClick={this.downloadBlobMask.bind(this)} disabled={!this.state.active}/>
+                <InputGroup.Text className={"col"}>{ this.state.blobMaskFilename }</InputGroup.Text>
+            </InputGroup>
+            <InputGroup className="mb-1">
+                <DownloadButton key="downloadResultsButtonKey" downloading={this.state.downloading} onClick={this.downloadResults.bind(this)} disabled={!this.state.active}></DownloadButton>
+                <InputGroup.Text className  ={"col"}>{ this.state.resultsFilename }</InputGroup.Text>
+            </InputGroup>
+            <Row className="mb-1">
+                <Col>
+                    <Button key="mergerButtonKey"  onClick={this.openMerger.bind(this)}><i className="fa-solid fa-object-group me-2"></i>Etape 5' : fusionner les CSV</Button>
+                    <a className={"ms-1"}  href={"http://localhost:8081/docs/index.html#/"} target={"_blank"}>en savoir plus</a>
+                </Col>
+            </Row>
+            <Merger ref={this.mergerRef}></Merger>
+        </div>
+    }
+
+}
diff --git a/src/ui/steps/drawBlobMaskStep.tsx b/src/ui/steps/drawBlobMaskStep.tsx
index b8d654f41a356dc679cb96108d7ddf7fbafc5500..64e514f7541bbb3f5fd640b829e329c41f6ad572 100644
--- a/src/ui/steps/drawBlobMaskStep.tsx
+++ b/src/ui/steps/drawBlobMaskStep.tsx
@@ -1,79 +1,79 @@
-import {Step, StepProps, StepState} from "./step";
-import * as React from "react";
-import * as paper from "paper";
-import {Alert, Button} from "react-bootstrap";
-import {IoUtils} from "../../utils/ioUtils";
-import {DEBUG_MODE} from "../../lab";
-import {StringUtils} from "../../utils/stringUtils";
-import {DataImporter} from "../../data/dataImporter";
-
-
-interface DrawBlobMaskStepState extends StepState {
-
-    closed: boolean;
-
-}
-
-/**
- * Étape de placement de la boîte de petri
- */
-export class DrawBlobMaskStep extends Step<DrawBlobMaskStepState> {
-
-    public constructor(props : StepProps) {
-        super(props, { active: false, activable : false, closed : false });
-    }
-
-    canBeActivated(): boolean {
-        return this.props.lab.data != null;
-    }
-
-    onActivation(): void {
-        this.props.lab.blobMask.activate();
-        this.setState({closed: this.props.lab.blobMask.isClosed() });
-        this.props.lab.blobMask.onClose = () => {
-            this.setState({closed: true });
-        };
-        this.props.lab.blobMask.onOpen = () => {
-            this.setState({closed: false });
-        };
-        this.props.lab.zoomOn( this.props.lab.data.petriDishCoords.bounds(), 0.05);
-    }
-
-    onDeactivation(): void {
-        this.props.lab.blobMask.deactivate();
-    }
-
-    loadData(): void {
-        let dataImporter = new DataImporter();
-        IoUtils.openTextFile(
-            (text : string) => {
-                this.props.lab.data.blobMaskCoords = dataImporter.import(text);
-                this.props.lab.blobMask.refresh();
-            });
-    }
-
-    render() : React.ReactNode {
-        return <div>
-            <div>
-                <Alert  show={!this.state.activable} variant="warning" className={"p-1"}><i className="ms-1 me-1 fa-solid fa-triangle-exclamation"></i>Veuillez charger une photo.</Alert>
-                <p>Entourez le blob <span className={"fw-bold"}>jusqu'à rejoindre le point de départ</span>.</p>
-                <Alert variant={"light"} className={"p-2"}><i className="ms-1 me-1 fa-solid fa-circle-info"></i>Un faux mouvement ? <br/>
-                    Revenir en arrière  : <Button className={"me-2"} size={"sm"} disabled={!this.state.active} onClick={() => this.props.lab.blobMask.undo()}><i className="fa-solid fa-delete-left"></i></Button>
-                    Tout effacer : <Button  className={"me-2"} size={"sm"} disabled={!this.state.active} onClick={() => this.props.lab.blobMask.undoAll()}><i className="fa-solid fa-trash-can"></i></Button>
-                </Alert>
-                <Button className={"col-3"} variant={"success"} disabled={!this.state.active || !this.state.closed} onClick={this.terminate.bind(this)}>
-                    <span hidden={!this.state.closed}><i className="fa-solid fa-hands-clapping fa-beat-fade me-2"></i></span>Fini !
-                </Button>
-                {DEBUG_MODE ?
-                    <Button className={"ms-2 col-5"} variant={"danger"} disabled={!this.state.active}
-                            onClick={this.loadData.bind(this)}>
-                        <i className="fa-solid fa-bug"></i> Charger XY
-                    </Button>
-                    : <></>
-                }
-            </div>
-        </div>
-    }
-
-}
-
+import {Step, StepProps, StepState} from "./step";
+import * as React from "react";
+import * as paper from "paper";
+import {Alert, Button} from "react-bootstrap";
+import {IoUtils} from "../../utils/ioUtils";
+import {DEBUG_MODE} from "../../lab";
+import {StringUtils} from "../../utils/stringUtils";
+import {DataImporter} from "../../data/dataImporter";
+
+
+interface DrawBlobMaskStepState extends StepState {
+
+    closed: boolean;
+
+}
+
+/**
+ * Étape de placement de la boîte de petri
+ */
+export class DrawBlobMaskStep extends Step<DrawBlobMaskStepState> {
+
+    public constructor(props : StepProps) {
+        super(props, { active: false, activable : false, closed : false });
+    }
+
+    canBeActivated(): boolean {
+        return this.props.lab.data != null;
+    }
+
+    onActivation(): void {
+        this.props.lab.blobMask.activate();
+        this.setState({closed: this.props.lab.blobMask.isClosed() });
+        this.props.lab.blobMask.onClose = () => {
+            this.setState({closed: true });
+        };
+        this.props.lab.blobMask.onOpen = () => {
+            this.setState({closed: false });
+        };
+        this.props.lab.zoomOn( this.props.lab.data.petriDishCoords.bounds(), 0.05);
+    }
+
+    onDeactivation(): void {
+        this.props.lab.blobMask.deactivate();
+    }
+
+    loadData(): void {
+        let dataImporter = new DataImporter();
+        IoUtils.openTextFile(
+            (text : string) => {
+                this.props.lab.data.blobMaskCoords = dataImporter.readPathCoords(text);
+                this.props.lab.blobMask.refresh();
+            });
+    }
+
+    render() : React.ReactNode {
+        return <div>
+            <div>
+                <Alert  show={!this.state.activable} variant="warning" className={"p-1"}><i className="ms-1 me-1 fa-solid fa-triangle-exclamation"></i>Veuillez charger une photo.</Alert>
+                <p>Entourez le blob <span className={"fw-bold"}>jusqu'à rejoindre le point de départ</span>.</p>
+                <Alert variant={"light"} className={"p-2"}><i className="ms-1 me-1 fa-solid fa-circle-info"></i>Un faux mouvement ? <br/>
+                    Revenir en arrière  : <Button className={"me-2"} size={"sm"} disabled={!this.state.active} onClick={() => this.props.lab.blobMask.undo()}><i className="fa-solid fa-delete-left"></i></Button>
+                    Tout effacer : <Button  className={"me-2"} size={"sm"} disabled={!this.state.active} onClick={() => this.props.lab.blobMask.undoAll()}><i className="fa-solid fa-trash-can"></i></Button>
+                </Alert>
+                <Button className={"col-3"} variant={"success"} disabled={!this.state.active || !this.state.closed} onClick={this.terminate.bind(this)}>
+                    <span hidden={!this.state.closed}><i className="fa-solid fa-hands-clapping fa-beat-fade me-2"></i></span>Fini !
+                </Button>
+                {DEBUG_MODE ?
+                    <Button className={"ms-2 col-5"} variant={"danger"} disabled={!this.state.active}
+                            onClick={this.loadData.bind(this)}>
+                        <i className="fa-solid fa-bug"></i> Charger XY
+                    </Button>
+                    : <></>
+                }
+            </div>
+        </div>
+    }
+
+}
+
diff --git a/src/ui/steps/loadPictureStep.tsx b/src/ui/steps/loadPictureStep.tsx
index f614a4ac32ddac039dcb3319e2925847a42597ce..ff7d9ff3d8a4d39dd807677c0c98b683e0114c15 100644
--- a/src/ui/steps/loadPictureStep.tsx
+++ b/src/ui/steps/loadPictureStep.tsx
@@ -1,49 +1,49 @@
-import {Step, StepProps, StepState} from "./step";
-import * as React from "react";
-import {ChangeEvent} from "react";
-
-/**
- * Etape de chargement du fichier
- */
-export class LoadPictureStep extends Step<StepState> {
-
-    public constructor(props : StepProps) {
-        super(props, { active: true, activable : false });
-    }
-
-    private fileChanged(event : ChangeEvent<HTMLInputElement>) : boolean {
-        if(event.target.files != null) {
-            const img = new Image();
-            const file = event.target.files[0];
-            img.src = URL.createObjectURL(file);
-            let filename = file.name;
-            let that = this;
-            img.onload = function () {
-                if(that.props.lab.new(img, filename)) {
-                    that.terminate();
-                }
-            }
-        }
-        return true;
-    }
-
-    canBeActivated(): boolean {
-        return true;
-    }
-
-    onActivation(): void {
-    }
-
-    onDeactivation(): void {
-    }
-
-    render() : React.ReactNode {
-        return <div>
-            <div className="mb-3">
-                <input className="form-control" type="file" disabled={!this.state.active} aria-disabled={!this.state.active} onChange={(e) => this.fileChanged(e)}></input>
-            </div>
-        </div>
-    }
-
-
+import {Step, StepProps, StepState} from "./step";
+import * as React from "react";
+import {ChangeEvent} from "react";
+
+/**
+ * Etape de chargement du fichier
+ */
+export class LoadPictureStep extends Step<StepState> {
+
+    public constructor(props : StepProps) {
+        super(props, { active: true, activable : false });
+    }
+
+    private fileChanged(event : ChangeEvent<HTMLInputElement>) : boolean {
+        if(event.target.files != null) {
+            const img = new Image();
+            const file = event.target.files[0];
+            img.src = URL.createObjectURL(file);
+            let filename = file.name;
+            let that = this;
+            img.onload = function () {
+                if(that.props.lab.new(img, filename)) {
+                    that.terminate();
+                }
+            }
+        }
+        return true;
+    }
+
+    canBeActivated(): boolean {
+        return true;
+    }
+
+    onActivation(): void {
+    }
+
+    onDeactivation(): void {
+    }
+
+    render() : React.ReactNode {
+        return <div>
+            <div className="mb-3">
+                <input className="form-control" type="file" accept={"image/*"} disabled={!this.state.active} aria-disabled={!this.state.active} onChange={(e) => this.fileChanged(e)}></input>
+            </div>
+        </div>
+    }
+
+
 }
\ No newline at end of file
diff --git a/src/utils/ioUtils.ts b/src/utils/ioUtils.ts
index a955a0a6225eab1074f46a9b98358552e683d606..e944ad4717c71d01d32293a92d879c38ef24f765 100644
--- a/src/utils/ioUtils.ts
+++ b/src/utils/ioUtils.ts
@@ -1,55 +1,61 @@
-/**
- * Une classe utilitaire pour les entrées/sorties
- */
-export class IoUtils {
-
-    /**
-     * Ouvre un chargement
-     */
-    public static openTextFile(onTextLoad : (text: string) => void) : void {
-        let input : HTMLInputElement = document.createElement('input');
-        input.type = 'file';
-        input.accept = '.txt';
-        input.onchange = (e: InputEvent)  => {
-            // getting a hold of the file reference
-            var file = (e.target as HTMLInputElement).files[0];
-
-            // setting up the reader
-            let reader = new FileReader();
-            reader.readAsText(file,'UTF-8');
-            // here we tell the reader what to do when it's done reading...
-            reader.onload = readerEvent => {
-                onTextLoad(readerEvent.target.result as string);
-            }
-
-        }
-        input.click();
-    }
-
-    /**
-     * Donne le nom de fichier sans extension
-     */
-    public static basename(filename : string) : string {
-        return filename.replace(/\.[^/.]+$/, "");
-    }
-
-    /**
-     * Déclenche un téléchargement
-     */
-    public static downloadData(filename : string, mimeType : string, data : string) {
-        this.downloadDataUrl(filename, "data:" + mimeType + "," + encodeURIComponent(data));
-    }
-
-    /**
-     * Déclenche un téléchargement
-     */
-    public static downloadDataUrl(filename : string, dataUrl : string) {
-        let anchor = document.createElement("a");
-        anchor.href = dataUrl;
-        anchor.download = filename;
-        anchor.style.display = 'none';
-        document.body.appendChild(anchor);
-        anchor.click();
-        document.body.removeChild(anchor);
-    }
+/**
+ * Une classe utilitaire pour les entrées/sorties
+ */
+export class IoUtils {
+
+    /**
+     * Ouvre un chargement
+     */
+    public static openTextFile(onTextLoad : (text: string) => void) : void {
+        let input : HTMLInputElement = document.createElement('input');
+        input.type = 'file';
+        input.accept = '.txt';
+        input.onchange = (e: InputEvent)  => {
+            // getting a hold of the file reference
+            var file = (e.target as HTMLInputElement).files[0];
+
+            // setting up the reader
+            this.readTextFile(file, onTextLoad)
+
+        }
+        input.click();
+    }
+
+    /**
+     * Lit le texte dans un fichier
+     */
+    public static readTextFile(file: Blob, onTextLoad : (text: string) => void) : void {
+        let reader = new FileReader();
+        reader.readAsText(file,'UTF-8');
+        // here we tell the reader what to do when it's done reading...
+        reader.onload = readerEvent => {
+            onTextLoad(readerEvent.target.result as string);
+        }
+    }
+    /**
+     * Donne le nom de fichier sans extension
+     */
+    public static basename(filename : string) : string {
+        return filename.replace(/\.[^/.]+$/, "");
+    }
+
+    /**
+     * Déclenche un téléchargement
+     */
+    public static downloadData(filename : string, mimeType : string, data : string) {
+        this.downloadDataUrl(filename, "data:" + mimeType + "," + encodeURIComponent(data));
+    }
+
+    /**
+     * Déclenche un téléchargement
+     */
+    public static downloadDataUrl(filename : string, dataUrl : string) {
+        let anchor = document.createElement("a");
+        anchor.href = dataUrl;
+        anchor.download = filename;
+        anchor.style.display = 'none';
+        document.body.appendChild(anchor);
+        anchor.click();
+        document.body.removeChild(anchor);
+    }
 }
\ No newline at end of file
diff --git a/src/utils/objectUtils.ts b/src/utils/objectUtils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1560063722b1a9de9964d25705e739dd1f8d8376
--- /dev/null
+++ b/src/utils/objectUtils.ts
@@ -0,0 +1,13 @@
+/**
+ * Utilitaire autour des objets
+ */
+export class ObjectUtils {
+
+    /**
+     * Deep clonin
+     */
+    public static deepClone<T>(obj:  T) : T{
+        return JSON.parse(JSON.stringify(obj)) as T;
+    }
+
+}
\ No newline at end of file
diff --git a/tests/data/dataExporter.test.ts b/tests/data/dataExporter.test.ts
index 6df1b56df30a8ba923eb89ccf0187c30735280aa..03306796693153cb2a83be50cf0bfd8b7a71d4e4 100644
--- a/tests/data/dataExporter.test.ts
+++ b/tests/data/dataExporter.test.ts
@@ -1,29 +1,31 @@
-import {DataExporter} from "../../src/data/dataExporter";
-import {Fixtures} from "../fixtures/fixtures";
-import expectedResultsCsv from "../fixtures/Results_ExpJ1CrB9.csv";
-
-describe('Testing DataExporter...', () => {
-
-    /**
-     * Class under test
-     */
-    let dataExporter = new DataExporter();
-
-    it('CSV metrics are correct', () => {
-        let labData = Fixtures.labData();
-        let descriptorsCsv = dataExporter.exportPathDescriptorsAsCsv(labData, labData.blobMaskCoords);
-        expect(descriptorsCsv).toEqual(expectedResultsCsv);
-    });
-
-    it('Tick count may vary along with the ruler length', () => {
-        let labData = Fixtures.labData();
-
-        labData.rulerTickCount = 8;
-        labData.rulerCoords.end = labData.rulerCoords.start.add(
-            labData.rulerCoords.end.subtract(labData.rulerCoords.start).multiply(8).divide(10)
-        )
-
-        let descriptorsCsv = dataExporter.exportPathDescriptorsAsCsv(labData, labData.blobMaskCoords);
-        expect(descriptorsCsv).toEqual(expectedResultsCsv);
-    });
+import {DataExporter} from "../../src/data/dataExporter";
+import {Fixtures} from "../fixtures/fixtures";
+import expectedResultsCsv from "../fixtures/Results_ExpJ1CrB9.csv";
+
+describe('Testing DataExporter...', () => {
+
+    /**
+     * Class under test
+     */
+    let dataExporter = new DataExporter();
+
+    it('CSV metrics are correct', () => {
+        let labData = Fixtures.labData();
+        let descriptorsCsv = dataExporter.exportPathDescriptorsAsCsv(labData);
+        expect(descriptorsCsv).toEqual(expectedResultsCsv);
+    });
+
+    it('Tick count may vary along with the ruler length', () => {
+        let labData = Fixtures.labData();
+
+        labData.rulerTickCount = 8;
+        labData.rulerCoords.end = labData.rulerCoords.start.add(
+            labData.rulerCoords.end.subtract(labData.rulerCoords.start).multiply(8).divide(10)
+        )
+
+        let descriptorsCsv = dataExporter.exportPathDescriptorsAsCsv(labData);
+        expect(descriptorsCsv).toEqual(expectedResultsCsv);
+    });
+
+
 });
\ No newline at end of file
diff --git a/tests/data/measurement/process/measurementImport.test.ts b/tests/data/measurement/process/measurementImport.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..60d1eb00d182ce025e18f97d0ac76f39520a9e8c
--- /dev/null
+++ b/tests/data/measurement/process/measurementImport.test.ts
@@ -0,0 +1,72 @@
+import {WorkInfoFormatter} from "../../../../src/data/work/process/workInfoFormatter";
+import {ExperienceEnum, GroupeEnum} from "../../../../src/data/work/workInfo";
+import {ObjectUtils} from "../../../../src/utils/objectUtils";
+import {MeasurementImport} from "../../../../src/data/measurement/process/measurementImport";
+import resultsExpJ1CrB9Csv from "../../../fixtures/Results_ExpJ1CrB9.csv";
+import resultsConJ11ExCsv from "../../../fixtures/Results_ConJ11Ex.csv";
+
+describe('Testing Measurement import...', () => {
+
+    /**
+     * Class under test
+     */
+    let measurementImport = new MeasurementImport();
+
+    it('Mono blob is well imported ', () => {
+        let data = measurementImport.readMeasures("Results_ExpJ1CrB9.csv", resultsExpJ1CrB9Csv);
+        expect(data.work.groupe).toEqual(GroupeEnum.Experimental);
+        expect(data.work.jour).toEqual(1);
+        expect(data.work.experience).toEqual(ExperienceEnum.Croissance);
+        expect(data.work.blob).toEqual(9);
+
+        expect(data.measurements).toHaveSize(1);
+        expect(data.measurements[0]).toEqual({
+            label: "ExpJ1CrB9.jpg" ,
+            area: 33.4,
+            perimeter: 33.93,
+            circularity: 0.365,
+            ar: 1.625,
+            round: 0.615,
+            solid: 0.871
+        });
+    });
+
+    it('Multi blob is well imported ', () => {
+        let data = measurementImport.readMeasures("Results_ConJ11Ex.csv", resultsConJ11ExCsv);
+        expect(data.work.groupe).toEqual(GroupeEnum.Controle);
+        expect(data.work.jour).toEqual(11);
+        expect(data.work.experience).toEqual(ExperienceEnum.Exploration);
+        expect(data.work.blob).toBeNull();
+
+        expect(data.measurements).toHaveSize(3);
+        expect(data.measurements[0]).toEqual({
+            label: "ConJ11ExB1.jpg" ,
+            area: 11.22,
+            perimeter: 54.93,
+            circularity: 1.212,
+            ar: 0.4,
+            round: 0.4,
+            solid: 0.271
+        });
+        expect(data.measurements[1]).toEqual({
+            label: "ConJ11ExB2.jpg" ,
+            area: 33.4,
+            perimeter: 33.93,
+            circularity: 0.365,
+            ar: 1.625,
+            round: 0.615,
+            solid: 0.871
+        });
+        expect(data.measurements[2]).toEqual({
+            label: "ConJ11ExB9.jpg" ,
+            area: 15.9,
+            perimeter: 55.87,
+            circularity: 0.6,
+            ar: 1.98,
+            round: 0.1,
+            solid: 0.45
+        });
+
+    });
+
+});
\ No newline at end of file
diff --git a/tests/data/measurement/process/measurementMerge.test.ts b/tests/data/measurement/process/measurementMerge.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0db5eccd86cf958a3b01afd498c87ab9e4c4504e
--- /dev/null
+++ b/tests/data/measurement/process/measurementMerge.test.ts
@@ -0,0 +1,88 @@
+import {ExperienceEnum, GroupeEnum} from "../../../../src/data/work/workInfo";
+import {MeasurementImport} from "../../../../src/data/measurement/process/measurementImport";
+import resultsExpJ1CrB9Csv from "../../../fixtures/Results_ExpJ1CrB9.csv";
+import resultsConJ11ExB1Csv from "../../../fixtures/Results_ConJ11ExB1.csv";
+import resultsConJ11ExB2Csv from "../../../fixtures/Results_ConJ11ExB2.csv";
+import resultsConJ11ExB9Csv from "../../../fixtures/Results_ConJ11ExB9.csv";
+import {MeasurementMerge} from "../../../../src/data/measurement/process/measurementMerge";
+import {MeasurementWorks} from "../../../../src/data/measurement/measurementWorks";
+
+describe('Testing Measurement merge...', () => {
+
+    /**
+     * Class under test
+     */
+    let measurementMerge = new MeasurementMerge();
+
+    /**
+     * Pour d'abord importer les données
+     */
+    let measurementImport = new MeasurementImport();
+
+    it('Merge works well', () => {
+        let measurementWorks: MeasurementWorks = {};
+        {
+            let {work, measurements} = measurementImport.readMeasures("Results_ConJ11ExB9.csv", resultsConJ11ExB9Csv);
+            measurementWorks = measurementMerge.mergeInto(measurementWorks, "Results_ConJ11ExB9.csv", work, measurements);
+        }
+        {
+            let {work, measurements} = measurementImport.readMeasures("Results_ConJ11ExB1.csv", resultsConJ11ExB1Csv);
+            measurementWorks = measurementMerge.mergeInto(measurementWorks, "Results_ConJ11ExB1.csv", work, measurements);
+        }
+        {
+            let {work, measurements} = measurementImport.readMeasures("Results_ExpJ1CrB9.csv", resultsExpJ1CrB9Csv);
+            measurementWorks = measurementMerge.mergeInto(measurementWorks, "Results_ExpJ1CrB9.csv", work, measurements);
+        }
+        {
+            let {work, measurements} = measurementImport.readMeasures("Results_ConJ11ExB2.csv", resultsConJ11ExB2Csv);
+            measurementWorks = measurementMerge.mergeInto(measurementWorks, "Results_ConJ11ExB2.csv", work, measurements);
+        }
+        expect(measurementWorks).toHaveSize(2);
+        expect(measurementWorks["ExpJ1Cr"].work).toEqual({groupe: GroupeEnum.Experimental, jour: 1, experience: ExperienceEnum.Croissance, blob: null});
+        expect(measurementWorks["ExpJ1Cr"].files).toHaveSize(1);
+        expect(measurementWorks["ExpJ1Cr"].files["Results_ExpJ1CrB9.csv"]).toHaveSize(1);
+        expect(measurementWorks["ExpJ1Cr"].files["Results_ExpJ1CrB9.csv"][0]).toEqual({
+            label: "ExpJ1CrB9.jpg" ,
+            area: 33.4,
+            perimeter: 33.93,
+            circularity: 0.365,
+            ar: 1.625,
+            round: 0.615,
+            solid: 0.871
+        });
+        expect(measurementWorks["ConJ11Ex"].work).toEqual({groupe: GroupeEnum.Controle, jour: 11, experience: ExperienceEnum.Exploration, blob: null});
+        expect(measurementWorks["ConJ11Ex"].files).toHaveSize(3);
+        expect(measurementWorks["ConJ11Ex"].files["Results_ConJ11ExB1.csv"]).toHaveSize(1);
+        expect(measurementWorks["ConJ11Ex"].files["Results_ConJ11ExB1.csv"][0]).toEqual({
+            label: "ConJ11ExB1.jpg" ,
+            area: 11.22,
+            perimeter: 54.93,
+            circularity: 1.212,
+            ar: 0.4,
+            round: 0.4,
+            solid: 0.271
+        });
+        expect(measurementWorks["ConJ11Ex"].files["Results_ConJ11ExB2.csv"]).toHaveSize(1);
+        expect(measurementWorks["ConJ11Ex"].files["Results_ConJ11ExB2.csv"][0]).toEqual({
+            label: "ConJ11ExB2.jpg" ,
+            area: 33.4,
+            perimeter: 33.93,
+            circularity: 0.365,
+            ar: 1.625,
+            round: 0.615,
+            solid: 0.871
+        });
+        expect(measurementWorks["ConJ11Ex"].files["Results_ConJ11ExB9.csv"]).toHaveSize(1);
+        expect(measurementWorks["ConJ11Ex"].files["Results_ConJ11ExB9.csv"][0]).toEqual({
+            label: "ConJ11ExB9.jpg" ,
+            area: 15.9,
+            perimeter: 55.87,
+            circularity: 0.6,
+            ar: 1.98,
+            round: 0.1,
+            solid: 0.45
+        });
+    });
+
+
+});
\ No newline at end of file
diff --git a/tests/data/work/process/workInfoFormatter.test.ts b/tests/data/work/process/workInfoFormatter.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a375111e5e1f0e60ae5362b030dfc513a23df028
--- /dev/null
+++ b/tests/data/work/process/workInfoFormatter.test.ts
@@ -0,0 +1,22 @@
+import {ExperienceEnum, GroupeEnum} from "../../../../src/data/work/workInfo";
+import {WorkInfoParser} from "../../../../src/data/work/process/workInfoParser";
+import {WorkInfoFormatter} from "../../../../src/data/work/process/workInfoFormatter";
+import {ObjectUtils} from "../../../../src/utils/objectUtils";
+
+describe('Testing WorkInfoFormatter...', () => {
+
+    /**
+     * Class under test
+     */
+    let workInfoFormatter = new WorkInfoFormatter();
+
+    it('Work session formatting is correct', () => {
+        let workInfo1 = {groupe: GroupeEnum.Controle, jour: 5, experience: ExperienceEnum.Croissance, blob: 22};
+        expect(workInfoFormatter.format(workInfo1)).toEqual("ConJ5CrB22");
+
+        let workInfo2 = ObjectUtils.deepClone(workInfo1);
+        workInfo2.blob = null;
+        expect(workInfoFormatter.format(workInfo2)).toEqual("ConJ5Cr");
+    });
+
+});
\ No newline at end of file
diff --git a/tests/data/work/process/workInfoParser.test.ts b/tests/data/work/process/workInfoParser.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..98c572e3a7d78e99bbf7a84fa6fa74a54c35c69a
--- /dev/null
+++ b/tests/data/work/process/workInfoParser.test.ts
@@ -0,0 +1,41 @@
+import {ExperienceEnum, GroupeEnum} from "../../../../src/data/work/workInfo";
+import {WorkInfoParser} from "../../../../src/data/work/process/workInfoParser";
+
+describe('Testing WorkInfoParser...', () => {
+
+    /**
+     * Class under test
+     */
+    let workInfoParser = new WorkInfoParser();
+
+    it('Parsing works well', () => {
+        let workInfo1 = workInfoParser.parse("ConJ1ExB2");
+        expect(workInfo1.groupe).toEqual(GroupeEnum.Controle);
+        expect(workInfo1.jour).toEqual(1);
+        expect(workInfo1.experience).toEqual(ExperienceEnum.Exploration);
+        expect(workInfo1.blob).toEqual(2);
+
+        let workInfo2 = workInfoParser.parse("ExpJ25CrB99");
+        expect(workInfo2.groupe).toEqual(GroupeEnum.Experimental);
+        expect(workInfo2.jour).toEqual(25);
+        expect(workInfo2.experience).toEqual(ExperienceEnum.Croissance);
+        expect(workInfo2.blob).toEqual(99);
+
+        let workInfo3 = workInfoParser.parse("conj2crb3");
+        expect(workInfo3.groupe).toEqual(GroupeEnum.Controle);
+        expect(workInfo3.jour).toEqual(2);
+        expect(workInfo3.experience).toEqual(ExperienceEnum.Croissance);
+        expect(workInfo3.blob).toEqual(3);
+
+        let workInfo4 = workInfoParser.parse("ExpJ25Cr");
+        expect(workInfo4.groupe).toEqual(GroupeEnum.Experimental);
+        expect(workInfo4.jour).toEqual(25);
+        expect(workInfo4.experience).toEqual(ExperienceEnum.Croissance);
+        expect(workInfo4.blob).toBeNull();
+
+        expect(workInfoParser.parse("ConJ1ExB2.jpg")).toBeNull();
+        expect(workInfoParser.parse("ConJExB")).toBeNull();
+        expect(workInfoParser.parse("J1ExB2")).toBeNull();
+        expect(workInfoParser.parse("DumJ25CrB99")).toBeNull();
+    });
+});
\ No newline at end of file
diff --git a/tests/fixtures/Results_ConJ11Ex.csv b/tests/fixtures/Results_ConJ11Ex.csv
new file mode 100644
index 0000000000000000000000000000000000000000..33ba8689f4826e833b30e09b3f90c590fa0c2eb1
--- /dev/null
+++ b/tests/fixtures/Results_ConJ11Ex.csv
@@ -0,0 +1,4 @@
+ ,Label,Area,Perim.,Circ.,AR,Round,Solidity
+1,ConJ11ExB1.jpg,11.22,54.93,1.212,0.4,0.4,0.271
+2,ConJ11ExB2.jpg,33.4,33.93,0.365,1.625,0.615,0.871
+3,ConJ11ExB9.jpg,15.9,55.87,0.6,1.98,0.1,0.45
diff --git a/tests/fixtures/Results_ConJ11ExB1.csv b/tests/fixtures/Results_ConJ11ExB1.csv
new file mode 100644
index 0000000000000000000000000000000000000000..c89804d59d19876f30ca568176e8072299858e78
--- /dev/null
+++ b/tests/fixtures/Results_ConJ11ExB1.csv
@@ -0,0 +1,2 @@
+ ,Label,Area,Perim.,Circ.,AR,Round,Solidity
+1,ConJ11ExB1.jpg,11.22,54.93,1.212,0.4,0.4,0.271
\ No newline at end of file
diff --git a/tests/fixtures/Results_ConJ11ExB2.csv b/tests/fixtures/Results_ConJ11ExB2.csv
new file mode 100644
index 0000000000000000000000000000000000000000..31b385d81b2cef124d6daad76f886a378e6334ea
--- /dev/null
+++ b/tests/fixtures/Results_ConJ11ExB2.csv
@@ -0,0 +1,2 @@
+ ,Label,Area,Perim.,Circ.,AR,Round,Solidity
+1,ConJ11ExB2.jpg,33.4,33.93,0.365,1.625,0.615,0.871
\ No newline at end of file
diff --git a/tests/fixtures/Results_ConJ11ExB9.csv b/tests/fixtures/Results_ConJ11ExB9.csv
new file mode 100644
index 0000000000000000000000000000000000000000..6f75c18c2acf07d06b80ddb28cfd40dffcf18feb
--- /dev/null
+++ b/tests/fixtures/Results_ConJ11ExB9.csv
@@ -0,0 +1,2 @@
+ ,Label,Area,Perim.,Circ.,AR,Round,Solidity
+1,ConJ11ExB9.jpg,15.9,55.87,0.6,1.98,0.1,0.45
\ No newline at end of file
diff --git a/tests/fixtures/Results_ExpJ1Cr.csv b/tests/fixtures/Results_ExpJ1Cr.csv
new file mode 100644
index 0000000000000000000000000000000000000000..6d2caa0fc90d8ea188ec25d5b634648028981f5e
--- /dev/null
+++ b/tests/fixtures/Results_ExpJ1Cr.csv
@@ -0,0 +1,2 @@
+ ,Label,Area,Perim.,Circ.,AR,Round,Solidity
+1,ExpJ1CrB9.jpg,33.4,33.93,0.365,1.625,0.615,0.871
\ No newline at end of file
diff --git a/tests/fixtures/fixtures.ts b/tests/fixtures/fixtures.ts
index 4c85fd40c2c6cb2e1535f4fb4ee2ff32684673da..9b201a288c8be41ad0a301e985cbd183eb5d6d53 100644
--- a/tests/fixtures/fixtures.ts
+++ b/tests/fixtures/fixtures.ts
@@ -1,37 +1,45 @@
-/**
- * Une classe utilitaire pour données mockées
- */
-import expJ1CrB9CoordBlobTxt from './ExpJ1CrB9_Coord_Blob.txt';
-import {DataImporter} from "../../src/data/dataImporter";
-import {LabData} from "../../src/lab";
-import {VectorCoords} from "../../src/data/coords/vectorCoords";
-import {EllipseCoords} from "../../src/data/coords/ellipseCoords";
-
-/**
- * Charge des données de tes
- */
-export class Fixtures {
-
-    /**
-     * Pour charger les données brutes
-     */
-    private static dataImporter = new DataImporter();
-
-    /**
-     * Contenu de ExpJ1CrB9_Coord_Blob.txt
-     */
-    public static labData() : LabData {
-        return  {
-            filename: "ExpJ1CrB9.jpg",
-            pictureSize: new paper.Size(2250, 4000),
-            rulerCoords: new VectorCoords( new paper.Point(542,438), new paper.Point(1929, 398)),
-            rulerTickCount : 10,
-            petriDishCoords: new EllipseCoords(new paper.Point(1172, 1305), 631, 625),
-            blobMaskCoords : this.dataImporter.import(expJ1CrB9CoordBlobTxt),
-        }
-    }
-
-
-
-
+/**
+ * Une classe utilitaire pour données mockées
+ */
+import expJ1CrB9CoordBlobTxt from './ExpJ1CrB9_Coord_Blob.txt';
+import {DataImporter} from "../../src/data/dataImporter";
+import {LabData} from "../../src/lab";
+import {VectorCoords} from "../../src/data/coords/vectorCoords";
+import {EllipseCoords} from "../../src/data/coords/ellipseCoords";
+import {WorkInfoFormatter} from "../../src/data/work/process/workInfoFormatter";
+import {WorkInfoParser} from "../../src/data/work/process/workInfoParser";
+
+/**
+ * Charge des données de tes
+ */
+export class Fixtures {
+
+    /**
+     * Pour charger les données brutes
+     */
+    private static dataImporter = new DataImporter();
+
+    /**
+     * Pour écrire les
+     */
+    private static workInfoParser = new WorkInfoParser();
+
+    /**
+     * Contenu de ExpJ1CrB9_Coord_Blob.txt
+     */
+    public static labData() : LabData {
+        return  {
+            filename: "ExpJ1CrB9.jpg",
+            pictureSize: new paper.Size(2250, 4000),
+            workInfo: this.workInfoParser.parse("ExpJ1CrB9"),
+            rulerCoords: new VectorCoords( new paper.Point(542,438), new paper.Point(1929, 398)),
+            rulerTickCount : 10,
+            petriDishCoords: new EllipseCoords(new paper.Point(1172, 1305), 631, 625),
+            blobMaskCoords : this.dataImporter.readPathCoords(expJ1CrB9CoordBlobTxt),
+        }
+    }
+
+
+
+
 }
\ No newline at end of file