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/badge.svg)](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)); + } + } + } + } }