diff --git a/README.md b/README.md
index 8c26808..57dc3dc 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,28 @@
-[](https://github.com/fiji/Time_Stamper/actions/workflows/build-main.yml)
+# A customized version of the Time_Stamper (v2.1.1) plugin for ImageJ/Fiji
+This plugin allows you to add timestamps to your image stacks.
+## New Features
+- **Interactive Positioning**: Users can specify where the timestamp is placed by simply clicking on the image.
+- **UI customization**: Font settings (family, style, size) and colors can be customized directly from the dialog.
+- **Custom Time Formatting**: You can define a custom time format using placeholders:
+ - Example: `36min20sec` using `${mm}min${ss}sec`
+ - Example: `1d14hr36min20sec` using `${d}d${hh}hr${mm}min${ss}sec`
+ - Example: `1:14:36:20` using `${d}:${hh}:${mm}:${ss}`
+ * *Note: Selecting the correct `Time Unit` (Seconds, Minutes, Hours) is required to parse the data precisely.*
+ * *Priority of time formats: `Custom Format` > `'00:00' format` > `Suffix` If a custom format pattern is specified, it takes the highest priority, overriding the `'00:00' format` checkbox and any defined `Suffix`.*
+- **Non-destructive Processing**: (Recommended) Checking the `Create New Window` box will generate timestamps on a duplicate window, leaving the original stack unmodified.
+
+## Requirements
+ImageJ 1.54g or later (Fiji is recommended).
+
+## License
+GPLv3+ according to the [original](https://imagej.net/plugins/time-stamper) version.
+
+## Installation
+Place the `Time_Stamper-2.1.1.jar` file in the `plugins` folder of Fiji or ImageJ (replacing the original version if it exists). Alternatively, you can compile the source code yourself.
+
+## Release Note
+2026.06.18 beta released.
+2026.06.19 v2.1.1 released: added the function "Create New Window" when timestamps are generated, which keeps the original images.
+
+
diff --git a/Time_Stamper-2.1.1.jar b/Time_Stamper-2.1.1.jar
new file mode 100644
index 0000000..2c73b18
Binary files /dev/null and b/Time_Stamper-2.1.1.jar differ
diff --git a/media/.gitkeep b/media/.gitkeep
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/media/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/media/UI4.jpg b/media/UI4.jpg
new file mode 100644
index 0000000..2bcc49b
Binary files /dev/null and b/media/UI4.jpg differ
diff --git a/pom.xml b/pom.xml
index ac085bf..7e6f31c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
sc.fiji
Time_Stamper
- 2.1.1-SNAPSHOT
+ 2.1.1
Time Stamper
Time Stamper plugin for Fiji.
@@ -129,6 +129,7 @@
net.imagej
ij
+ 1.54g
diff --git a/src/main/java/Time_Stamper.java b/src/main/java/Time_Stamper.java
index 354e8e7..9d7bc7f 100644
--- a/src/main/java/Time_Stamper.java
+++ b/src/main/java/Time_Stamper.java
@@ -1,6 +1,10 @@
import ij.IJ;
import ij.ImagePlus;
import ij.gui.GenericDialog;
+import ij.gui.NonBlockingGenericDialog;
+import ij.gui.DialogListener;
+import ij.gui.RoiListener;
+import ij.gui.Roi;
import ij.gui.Overlay;
import ij.gui.TextRoi;
import ij.gui.Toolbar;
@@ -8,15 +12,18 @@
import ij.plugin.filter.PlugInFilterRunner;
import ij.process.ImageProcessor;
+import java.awt.AWTEvent;
import java.awt.Font;
import java.awt.Rectangle;
+import java.awt.Color;
+import java.awt.TextField;
+import java.util.Vector;
-public class Time_Stamper implements ExtendedPlugInFilter {
+public class Time_Stamper implements ExtendedPlugInFilter, DialogListener, RoiListener {
ImagePlus imp;
- static int x = 2;
- static int y = 15;
+ static int x = 10;
+ static int y = 30;
static int size = 12;
- int maxWidth;
Font font;
static double start = 0;
static double interval = 1;
@@ -24,8 +31,20 @@ public class Time_Stamper implements ExtendedPlugInFilter {
static int decimalPlaces = 0;
int idx = 1;
static boolean digital = false;
- boolean AAtext=true;
- boolean addToOverlay;
+ boolean AAtext = true;
+ boolean addToOverlay = true;
+ static String fontName = "SansSerif";
+ static String fontStyleStr = "Plain";
+ static String colorStr = "White";
+ static String justificationStr = "Left";
+ static String customFormat = "";
+ static String timeUnitStr = "Seconds";
+ static boolean createNewWindow = false;
+
+ // For preview and listener synchronization
+ private TextRoi previewRoi;
+ private GenericDialog gdInstance;
+ private boolean isUpdatingRoi = false;
final int flags = DOES_ALL + DOES_STACKS + STACK_REQUIRED;
@@ -40,8 +59,10 @@ public void setNPasses(int nPasses) {
}
public void run(ImageProcessor ip) {
+ Color selectedColor = getSelectedColor();
ip.setFont(font);
- ip.setColor(Toolbar.getForegroundColor());
+ ip.setColor(selectedColor);
+ ip.setJustification(0); // Always draw left-justified (0) and control position using rx
ip.setAntialiasedText(AAtext);
// use frames, if possible
@@ -50,30 +71,148 @@ public void run(ImageProcessor ip) {
int[] pos = imp.convertIndexToPosition(idx);
int frame = useFrames ? pos[2] : pos[1];
double time = start + (frame - 1) * interval;
+
// create output string
- String s = "";
- if (digital)
- s = getString2(time);
- else
- s = getString(time);
+ String s = getFormattedText(time);
if (addToOverlay) {
- Overlay overlay = imp.getOverlay();
- if (overlay == null) {
- overlay = new Overlay();
- imp.setOverlay(overlay);
- }
- TextRoi textRoi = new TextRoi(x+maxWidth-ip.getStringWidth(s), y, s);
- textRoi.setPosition(pos[0], pos[1], pos[2]);
- overlay.add(textRoi);
+ Overlay overlay = imp.getOverlay();
+ if (overlay == null) {
+ overlay = new Overlay();
+ imp.setOverlay(overlay);
+ }
+ // Create temporary TextRoi to measure actual bounds width, then calculate alignment
+ TextRoi textRoi = new TextRoi(x, y, s, font);
+ int w = textRoi.getBounds().width;
+ int rx = x;
+ if (justificationStr.equals("Center")) {
+ rx = x - (w / 2);
+ } else if (justificationStr.equals("Right")) {
+ rx = x - w;
+ }
+ textRoi.setLocation(rx, y);
+ textRoi.setStrokeColor(selectedColor);
+ // Do not call setJustification, control alignment solely by left coordinate rx
+ textRoi.setAntiAlias(AAtext);
+
+ // Determine position setting method depending on whether it is a HyperStack or a standard Stack
+ if (imp.isHyperStack()) {
+ textRoi.setPosition(pos[0], pos[1], pos[2]);
+ } else {
+ textRoi.setPosition(idx);
+ }
+ overlay.add(textRoi);
} else {
- ip.moveTo(x+maxWidth-ip.getStringWidth(s), y);
- ip.drawString(s);
+ // Calculate drawing start position (rx) based on alignment settings
+ int w = ip.getStringWidth(s);
+ int rx = x;
+ if (justificationStr.equals("Center")) {
+ rx = x - (w / 2);
+ } else if (justificationStr.equals("Right")) {
+ rx = x - w;
+ }
+ ip.moveTo(rx, y + size);
+ ip.drawString(s);
}
// increment frame number
idx++;
}
+ String getFormattedText(double time) {
+ if (customFormat != null && !customFormat.trim().isEmpty()) {
+ return formatCustomTime(time, customFormat);
+ } else if (digital) {
+ return getString2(time);
+ } else {
+ return getString(time);
+ }
+ }
+
+ String formatCustomTime(double time, String format) {
+ if (time < 0) time = 0;
+
+ // Normalize time value to Hours
+ double timeInHours = time;
+ if (timeUnitStr.equals("Seconds")) {
+ timeInHours = time / 3600.0;
+ } else if (timeUnitStr.equals("Minutes")) {
+ timeInHours = time / 60.0;
+ }
+
+ boolean containsD = format.contains("${d}") || format.contains("${dd}");
+ boolean containsH = format.contains("${h}") || format.contains("${hh}");
+ boolean containsM = format.contains("${m}") || format.contains("${mm}");
+ boolean containsS = format.contains("${s}") || format.contains("${ss}");
+
+ double rem = timeInHours; // Converted time value
+
+ int days = 0;
+ if (containsD) {
+ if (!containsH && !containsM && !containsS) {
+ days = (int)Math.round(rem / 24.0);
+ } else {
+ days = (int)(rem / 24.0);
+ }
+ rem = rem % 24.0;
+ }
+
+ int hours = 0;
+ if (containsH) {
+ if (!containsM && !containsS) {
+ hours = (int)Math.round(rem);
+ } else {
+ hours = (int)rem;
+ }
+ rem = (rem - hours) * 60.0;
+ } else {
+ rem = rem * 60.0;
+ }
+
+ int minutes = 0;
+ if (containsM) {
+ if (!containsS) {
+ minutes = (int)Math.round(rem);
+ } else {
+ minutes = (int)rem;
+ }
+ rem = (rem - minutes) * 60.0;
+ } else {
+ rem = rem * 60.0;
+ }
+
+ double seconds = rem;
+
+ // Format seconds (apply Decimal Places)
+ String sSec;
+ if (decimalPlaces <= 0) {
+ sSec = String.valueOf((int)Math.round(seconds));
+ } else {
+ sSec = IJ.d2s(seconds, decimalPlaces);
+ }
+
+ // Create 2-digit zero-padded seconds for ${ss}
+ String ssSec = sSec;
+ int dotIndex = sSec.indexOf('.');
+ String intPart = dotIndex >= 0 ? sSec.substring(0, dotIndex) : sSec;
+ String fracPart = dotIndex >= 0 ? sSec.substring(dotIndex) : "";
+ if (intPart.length() < 2) {
+ ssSec = "0" + intPart + fracPart;
+ }
+
+ // Replace placeholders
+ String result = format;
+ result = result.replace("${dd}", String.format("%02d", days));
+ result = result.replace("${d}", String.valueOf(days));
+ result = result.replace("${hh}", String.format("%02d", hours));
+ result = result.replace("${h}", String.valueOf(hours));
+ result = result.replace("${mm}", String.format("%02d", minutes));
+ result = result.replace("${m}", String.valueOf(minutes));
+ result = result.replace("${ss}", ssSec);
+ result = result.replace("${s}", sSec);
+
+ return result;
+ }
+
String getString(double time) {
if (Math.abs(interval) < 0.00001)
return suffix;
@@ -102,59 +241,353 @@ String getString2(double time) {
return IJ.d2s(time,decimalPlaces);
}
+
+
public int showDialog(ImagePlus imp, String command, PlugInFilterRunner pfr) {
+ this.imp = imp;
ImageProcessor ip = imp.getProcessor();
Rectangle roi = ip.getRoi();
- if (roi.width80) size = 80;
+ if (size < 7) size = 7;
+ if (size > 80) size = 80;
}
- String[] timeunitsoptions = { "hours","minutes", "seconds"};
- GenericDialog gd = new GenericDialog("Time Stamper");
+ // Font setup for preview
+ font = getSelectedFont(size);
+
+ // Temporarily save original ROI
+ Roi originalRoi = imp.getRoi();
+
+ // Create preview ROI
+ String previewText = getFormattedText(start);
+ previewRoi = new TextRoi(x, y, previewText, font);
+ int w = previewRoi.getBounds().width;
+ int rx = x;
+ if (justificationStr.equals("Center")) {
+ rx = x - (w / 2);
+ } else if (justificationStr.equals("Right")) {
+ rx = x - w;
+ }
+ previewRoi.setLocation(rx, y);
+ previewRoi.setStrokeColor(getSelectedColor());
+ // Do not call setJustification
+ imp.setRoi(previewRoi);
+
+ // Create non-blocking generic dialog
+ NonBlockingGenericDialog gd = new NonBlockingGenericDialog("Time Stamper (Customized)");
+ gdInstance = gd;
+ String[] fonts = {"SansSerif", "Serif", "Monospaced", "Arial"};
+ String[] styles = {"Plain", "Bold", "Italic", "Bold+Italic"};
+ String[] colors = {"White", "Black", "Red", "Green", "Blue", "Yellow", "Magenta", "Cyan", "Orange"};
+ String[] alignments = {"Left", "Center", "Right"};
+ String[] timeUnits = {"Seconds", "Minutes", "Hours"};
+
gd.addNumericField("Starting Time:", start, 2);
gd.addNumericField("Interval:", interval, 2);
- // gd.addChoice("Time units:", timeunitsoptions, timeunitsoptions[1]);
-
- gd.addNumericField("X Location:", x, 0);
- gd.addNumericField("Y Location:", y, 0);
+ gd.addNumericField("Decimal Places:", decimalPlaces, 0);
+ gd.addNumericField("X Location (left):", x, 0);
+ gd.addNumericField("Y Location (top):", y, 0);
gd.addNumericField("Font Size:", size, 0);
+ gd.addChoice("Font Name:", fonts, fontName);
+ gd.addChoice("Font Style:", styles, fontStyleStr);
+ gd.addChoice("Color:", colors, colorStr);
+ gd.addChoice("Justification:", alignments, justificationStr);
gd.addCheckbox("'00:00' format:", digital);
- gd.addNumericField("Decimal Places:", decimalPlaces, 0);
- gd.addStringField("Or with a suffix Suffix:", suffix);
- gd.addCheckbox("Anti-Aliased text?", true);
- gd.addCheckbox("Overlay", false);
+ gd.addStringField("Or with a Suffix:", suffix);
+ gd.addStringField("Or Custom Format:", customFormat, 20);
+ gd.addMessage(" (e.g. ${d}d${hh}hr${mm}min${ss}sec)");
+ gd.addChoice("Time Unit:", timeUnits, timeUnitStr);
+ gd.addCheckbox("Anti-Aliased text?", AAtext);
+ gd.addCheckbox("Overlay", addToOverlay);
+ gd.addCheckbox("Create New Window", createNewWindow);
+
+ gd.addDialogListener(this);
+ Roi.addRoiListener(this);
gd.showDialog();
- if (gd.wasCanceled())
+
+ // Stop listening when dialog is closed
+ Roi.removeRoiListener(this);
+ gdInstance = null;
+
+ if (gd.wasCanceled()) {
+ // Remove preview and restore original ROI if canceled
+ if (originalRoi != null && originalRoi != previewRoi) {
+ imp.setRoi(originalRoi);
+ } else {
+ imp.killRoi();
+ }
return DONE;
+ }
+ // Confirm values
start = gd.getNextNumber();
interval = gd.getNextNumber();
- int timeindex = gd.getNextChoiceIndex();
- //String timeunits = timeunitsoptions[timeindex];
-
+ decimalPlaces = (int)gd.getNextNumber();
x = (int)gd.getNextNumber();
y = (int)gd.getNextNumber();
size = (int)gd.getNextNumber();
- digital = gd.getNextBoolean();
+ fontName = gd.getNextChoice();
+ fontStyleStr = gd.getNextChoice();
+ colorStr = gd.getNextChoice();
+ justificationStr = gd.getNextChoice();
- decimalPlaces = (int)gd.getNextNumber();
- AAtext = gd.getNextBoolean();
+ digital = gd.getNextBoolean();
suffix = gd.getNextString();
- font = new Font("SansSerif", Font.PLAIN, size);
- ip.setFont(font);
- if (y 4) {
+ TextField xField = (TextField) nFields.get(3);
+ TextField yField = (TextField) nFields.get(4);
+ if (xField != null && !xField.isFocusOwner()) {
+ xField.setText(String.valueOf(x));
+ }
+ if (yField != null && !yField.isFocusOwner()) {
+ yField.setText(String.valueOf(y));
+ }
+ }
+ }
+ }
}