Merge branch 'bug/JAL-4353_cannot_output_multiple_different_structure_images_for_one_...
authorBen Soares <b.soares@dundee.ac.uk>
Tue, 19 Dec 2023 17:52:45 +0000 (17:52 +0000)
committerJames Procter <j.procter@dundee.ac.uk>
Tue, 23 Jan 2024 16:54:50 +0000 (16:54 +0000)
src/jalview/bin/Commands.java
src/jalview/bin/argparser/ArgValuesMap.java
src/jalview/gui/AlignmentPanel.java
src/jalview/gui/AnnotationLabels.java
src/jalview/gui/Desktop.java
src/jalview/gui/StructureChooser.java
src/jalview/structure/StructureSelectionManager.java
test/jalview/bin/CommandLineOperations.java
test/jalview/bin/CommandsTest.java

index e01d40a..a72da0d 100644 (file)
@@ -179,8 +179,7 @@ public class Commands
 
     if (argParser.getBoolean(Arg.QUIT))
     {
-      Jalview.getInstance().exit(
-              "Exiting due to " + Arg.QUIT.argString() + " argument.",
+      Jalview.exit("Exiting due to " + Arg.QUIT.argString() + " argument.",
               ExitCode.OK);
       return true;
     }
@@ -210,8 +209,13 @@ public class Commands
 
     Boolean isError = Boolean.valueOf(false);
 
-    // set wrap scope here so it can be applied after structures are opened
+    // set wrap, showSSAnnotations, showAnnotations and hideTFrows scope here so
+    // it can be applied after structures are opened
     boolean wrap = false;
+    boolean showSSAnnotations = false;
+    boolean showAnnotations = false;
+    boolean hideTFrows = false;
+    AlignFrame af = null;
 
     if (avm.containsArg(Arg.APPEND) || avm.containsArg(Arg.OPEN))
     {
@@ -220,7 +224,6 @@ public class Commands
 
       boolean first = true;
       boolean progressBarSet = false;
-      AlignFrame af;
       // Combine the APPEND and OPEN files into one list, along with whether it
       // was APPEND or OPEN
       List<ArgValue> openAvList = new ArrayList<>();
@@ -392,53 +395,17 @@ public class Commands
           }
 
           // Show secondary structure annotations?
-          boolean showSSAnnotations = avm.getFromSubValArgOrPref(
+          showSSAnnotations = avm.getFromSubValArgOrPref(
                   Arg.SHOWSSANNOTATIONS, av.getSubVals(), null,
                   "STRUCT_FROM_PDB", true);
-
           // Show sequence annotations?
-          boolean showAnnotations = avm.getFromSubValArgOrPref(
-                  Arg.SHOWANNOTATIONS, av.getSubVals(), null,
-                  "SHOW_ANNOTATIONS", true);
-
-          boolean hideTFrows = (avm.getBoolean(Arg.NOTEMPFAC));
-          final AlignFrame _af = af;
-          // many of jalview's format/layout methods are only thread safe on the
-          // swingworker thread.
-          // all these methods should be on the alignViewController so it can
-          // coordinate such details
-          try
-          {
-            SwingUtilities.invokeAndWait(new Runnable()
-            {
+          showAnnotations = avm.getFromSubValArgOrPref(Arg.SHOWANNOTATIONS,
+                  av.getSubVals(), null, "SHOW_ANNOTATIONS", true);
+          // hide the Temperature Factor row?
+          hideTFrows = (avm.getBoolean(Arg.NOTEMPFAC));
 
-              @Override
-              public void run()
-              {
-                _af.setAnnotationsVisibility(showSSAnnotations, true,
-                        false);
-
-                _af.setAnnotationsVisibility(showAnnotations, false, true);
-
-                // show temperature factor annotations?
-                if (hideTFrows)
-                {
-                  // do this better (annotation types?)
-                  List<String> hideThese = new ArrayList<>();
-                  hideThese.add("Temperature Factor");
-                  hideThese.add(AlphaFoldAnnotationRowBuilder.LABEL);
-                  AlignmentUtils.showOrHideSequenceAnnotations(
-                          _af.getCurrentView().getAlignment(), hideThese,
-                          null, false, false);
-                }
-              }
-            });
-          } catch (Exception x)
-          {
-            Console.warn(
-                    "Unexpected exception adjusting annotation row visibility.",
-                    x);
-          }
+          // showSSAnnotations, showAnnotations, hideTFrows used after opening
+          // structure
 
           // wrap alignment? do this last for formatting reasons
           wrap = avm.getFromSubValArgOrPref(Arg.WRAP, sv, null,
@@ -496,8 +463,10 @@ public class Commands
     // open the structure (from same PDB file or given PDBfile)
     if (!avm.getBoolean(Arg.NOSTRUCTURE))
     {
-
-      AlignFrame af = afMap.get(id);
+      if (af == null)
+      {
+        af = afMap.get(id);
+      }
       if (avm.containsArg(Arg.STRUCTURE))
       {
         commandArgsProvided = true;
@@ -867,10 +836,47 @@ public class Commands
       }
     }
 
-    if (wrap)
+    if (af == null)
     {
+      af = afMap.get(id);
+    }
+    // many of jalview's format/layout methods are only thread safe on the
+    // swingworker thread.
+    // all these methods should be on the alignViewController so it can
+    // coordinate such details
+    if (headless)
+    {
+      showOrHideAnnotations(af, showSSAnnotations, showAnnotations,
+              hideTFrows);
+    }
+    else
+    {
+      try
+      {
+        AlignFrame _af = af;
+        final boolean _showSSAnnotations = showSSAnnotations;
+        final boolean _showAnnotations = showAnnotations;
+        final boolean _hideTFrows = hideTFrows;
+        SwingUtilities.invokeAndWait(() -> {
+          showOrHideAnnotations(_af, _showSSAnnotations, _showAnnotations,
+                  _hideTFrows);
+        }
 
-      AlignFrame af = afMap.get(id);
+        );
+      } catch (Exception x)
+      {
+        Console.warn(
+                "Unexpected exception adjusting annotation row visibility.",
+                x);
+      }
+    }
+
+    if (wrap)
+    {
+      if (af == null)
+      {
+        af = afMap.get(id);
+      }
       if (af != null)
       {
         af.setWrapFormat(wrap, true);
@@ -897,6 +903,26 @@ public class Commands
     return theseArgsWereParsed && !isError;
   }
 
+  private static void showOrHideAnnotations(AlignFrame af,
+          boolean showSSAnnotations, boolean showAnnotations,
+          boolean hideTFrows)
+  {
+    af.setAnnotationsVisibility(showSSAnnotations, true, false);
+    af.setAnnotationsVisibility(showAnnotations, false, true);
+
+    // show temperature factor annotations?
+    if (hideTFrows)
+    {
+      // do this better (annotation types?)
+      List<String> hideThese = new ArrayList<>();
+      hideThese.add("Temperature Factor");
+      hideThese.add(AlphaFoldAnnotationRowBuilder.LABEL);
+      AlignmentUtils.showOrHideSequenceAnnotations(
+              af.getCurrentView().getAlignment(), hideThese, null, false,
+              false);
+    }
+  }
+
   protected void processGroovyScript(String id)
   {
     ArgValuesMap avm = argParser.getLinkedArgs(id);
index 219983f..7385db0 100644 (file)
@@ -653,6 +653,24 @@ public class ArgValuesMap
     return pref != null ? (invertPref ? !prefVal : prefVal) : def;
   }
 
+  @Override
+  public String toString()
+  {
+    StringBuilder sb = new StringBuilder();
+    for (Arg a : this.getArgKeys())
+    {
+      sb.append(a.argString());
+      sb.append(":\n");
+      for (ArgValue av : this.getArgValueList(a))
+      {
+        sb.append("  ");
+        sb.append(av.getValue());
+        sb.append("\n");
+      }
+    }
+    return sb.toString();
+  }
+
   public class ArgInfo implements Comparable<ArgInfo>
   {
     private Arg arg;
index 8e7c745..358560b 100644 (file)
@@ -1379,7 +1379,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
         // need to obtain default alignment width and then add in any
         // additional allowance for id margin
         // this duplicates the calculation in getWrappedHeight but adjusts for
-        // offscreen idWith
+        // offscreen idWidth
         width = alignFrame.getWidth() - vscroll.getPreferredSize().width
                 - alignFrame.getInsets().left - alignFrame.getInsets().right
                 - getVisibleIdWidth() + getVisibleIdWidth(false);
index 94f8790..70fef2a 100755 (executable)
@@ -1206,8 +1206,8 @@ public class AnnotationLabels extends JPanel
     if (ap != null)
     {
       iwa = ap.idwidthAdjuster;
-      if ((Cache.getDefault(ADJUST_ANNOTATION_LABELS_WIDTH_PREF, true)
-              || Jalview.isHeadlessMode()))
+      if (Cache.getDefault(ADJUST_ANNOTATION_LABELS_WIDTH_PREF, true)
+              || Jalview.isHeadlessMode())
       {
         Graphics2D g2d = (Graphics2D) g;
         Graphics dummy = g2d.create();
index 35c7818..bbd4dae 100644 (file)
@@ -680,22 +680,7 @@ public class Desktop extends jalview.jbgui.GDesktop
     // configure services
     StructureSelectionManager ssm = StructureSelectionManager
             .getStructureSelectionManager(this);
-    if (Cache.getDefault(Preferences.ADD_SS_ANN, true))
-    {
-      ssm.setAddTempFacAnnot(
-              Cache.getDefault(Preferences.ADD_TEMPFACT_ANN, true));
-      ssm.setProcessSecondaryStructure(
-              Cache.getDefault(Preferences.STRUCT_FROM_PDB, true));
-      // JAL-3915 - RNAView is no longer an option so this has no effect
-      ssm.setSecStructServices(
-              Cache.getDefault(Preferences.USE_RNAVIEW, false));
-    }
-    else
-    {
-      ssm.setAddTempFacAnnot(false);
-      ssm.setProcessSecondaryStructure(false);
-      ssm.setSecStructServices(false);
-    }
+    StructureSelectionManager.doConfigureStructurePrefs(ssm);
   }
 
   public void checkForNews()
index 666ff74..6132908 100644 (file)
@@ -1826,6 +1826,7 @@ public class StructureChooser extends GStructureChooser
     if (ssm == null)
     {
       ssm = ap.getStructureSelectionManager();
+      StructureSelectionManager.doConfigureStructurePrefs(ssm);
     }
 
     PDBEntry fileEntry = new AssociatePdbFileWithSeq().associatePdbWithSeq(
index ec3e0a0..9a9e2a2 100644 (file)
@@ -34,6 +34,7 @@ import java.util.Vector;
 
 import jalview.analysis.AlignSeq;
 import jalview.api.StructureSelectionManagerProvider;
+import jalview.bin.Cache;
 import jalview.bin.Console;
 import jalview.commands.CommandI;
 import jalview.commands.EditCommand;
@@ -50,6 +51,7 @@ import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceI;
 import jalview.ext.jmol.JmolParser;
 import jalview.gui.IProgressIndicator;
+import jalview.gui.Preferences;
 import jalview.io.AppletFormatAdapter;
 import jalview.io.DataSourceType;
 import jalview.io.StructureFile;
@@ -1665,4 +1667,34 @@ public class StructureSelectionManager
     return pdbIdFileName;
   }
 
+  public static void doConfigureStructurePrefs(
+          StructureSelectionManager ssm)
+  {
+    doConfigureStructurePrefs(ssm,
+            Cache.getDefault(Preferences.ADD_SS_ANN, true),
+            Cache.getDefault(Preferences.ADD_TEMPFACT_ANN, true),
+            Cache.getDefault(Preferences.STRUCT_FROM_PDB, true),
+            Cache.getDefault(Preferences.USE_RNAVIEW, false));
+  }
+
+  public static void doConfigureStructurePrefs(
+          StructureSelectionManager ssm, boolean add_ss_ann,
+          boolean add_tempfact_ann, boolean struct_from_pdb,
+          boolean use_rnaview)
+  {
+    if (add_ss_ann)
+    {
+      ssm.setAddTempFacAnnot(add_tempfact_ann);
+      ssm.setProcessSecondaryStructure(struct_from_pdb);
+      // JAL-3915 - RNAView is no longer an option so this has no effect
+      ssm.setSecStructServices(use_rnaview);
+    }
+    else
+    {
+      ssm.setAddTempFacAnnot(false);
+      ssm.setProcessSecondaryStructure(false);
+      ssm.setSecStructServices(false);
+    }
+  }
+
 }
index 77cbd92..3855dc7 100644 (file)
@@ -71,7 +71,7 @@ public class CommandLineOperations
    * @author jimp
    * 
    */
-  private static class Worker extends Thread
+  public static class Worker extends Thread
   {
     private final Process process;
 
@@ -156,7 +156,7 @@ public class CommandLineOperations
     return classpath;
   }
 
-  private Worker getJalviewDesktopRunner(boolean withAwt, String cmd,
+  public static Worker getJalviewDesktopRunner(boolean withAwt, String cmd,
           int timeout)
   {
     // Note: JAL-3065 - don't include quotes for lib/* because the arguments are
@@ -183,7 +183,7 @@ public class CommandLineOperations
               new InputStreamReader(ls2_proc.getInputStream()));
       BufferedReader errorReader = new BufferedReader(
               new InputStreamReader(ls2_proc.getErrorStream()));
-      worker = new Worker(ls2_proc);
+      worker = new CommandLineOperations.Worker(ls2_proc);
       worker.start();
       try
       {
index 7b42737..80354f9 100644 (file)
  */
 package jalview.bin;
 
+import java.awt.image.BufferedImage;
 import java.io.File;
 import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
 import java.nio.file.Files;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.Set;
 
+import javax.imageio.ImageIO;
 import javax.swing.SwingUtilities;
 
 import org.testng.Assert;
@@ -93,7 +94,12 @@ public class CommandsTest
 
   public static void callJalviewMain(String[] args)
   {
-    if (Jalview.getInstance() != null)
+    callJalviewMain(args, false);
+  }
+
+  public static void callJalviewMain(String[] args, boolean newJalview)
+  {
+    if (Jalview.getInstance() != null && !newJalview)
     {
       Jalview.getInstance().doMain(args);
     }
@@ -116,7 +122,11 @@ public class CommandsTest
   }
   */
 
-  @Test(groups = { "Functional", "testTask3" }, dataProvider = "cmdLines", singleThreaded = true)
+  @Test(
+    groups =
+    { "Functional", "testTask3" },
+    dataProvider = "cmdLines",
+    singleThreaded = true)
 
   public void commandsOpenTest(String cmdLine, boolean cmdArgs,
           int numFrames, String[] sequences)
@@ -169,40 +179,64 @@ public class CommandsTest
   @Test(
     groups =
     { "Functional", "testTask3" },
-    dataProvider = "structureImageOutputFiles",    singleThreaded = true)
+    dataProvider = "structureImageOutputFiles",
+    singleThreaded = true)
   public void structureImageOutputTest(String cmdLine, String[] filenames)
           throws IOException
   {
     cleanupFiles(filenames);
-    String[] args = (cmdLine + " --gui").split("\\s+");
+    String[] args = (cmdLine + "").split("\\s+");
     try
     {
       callJalviewMain(args);
       Commands cmds = Jalview.getInstance().getCommands();
       Assert.assertNotNull(cmds);
-      File lastFile = null;
-      for (String filename : filenames)
+      verifyIncreasingSize(cmdLine, filenames);
+    } catch (Exception x)
+    {
+      Assert.fail("Unexpected exception during structureImageOutputTest",
+              x);
+    } finally
+    {
+      // cleanupFiles(filenames);
+      tearDown();
+    }
+  }
+
+  /**
+   * given two command lines, compare the output files produced - they should
+   * exist and be equal in size
+   */
+  @Test(
+    groups =
+    { "Functional", "testTask3" },
+    dataProvider = "compareHeadlessAndGUIOps",
+    singleThreaded = true)
+  public void headlessOrGuiImageOutputTest(String[] cmdLines,
+          String[] filenames) throws IOException
+  {
+    cleanupFiles(filenames);
+    try
+    {
+      for (String cmdLine : cmdLines)
       {
-        File file = new File(filename);
-        Assert.assertTrue(file.exists(), "File '" + filename
-                + "' was not created by '" + cmdLine + "'");
-        Assert.assertTrue(file.isFile(), "File '" + filename
-                + "' is not a file from '" + cmdLine + "'");
-        Assert.assertTrue(Files.size(file.toPath()) > 0, "File '" + filename
-                + "' has no content from '" + cmdLine + "'");
-        // make sure the successive output files get bigger!
-        if (lastFile != null)
+        CommandLineOperations.Worker runner = CommandLineOperations
+                .getJalviewDesktopRunner(false, cmdLine, 1000);
+        long timeOut = 10000;
+        while (runner.isAlive() && timeOut > 0)
         {
-          waitForLastWrite(file,25);
-          
-          if (Files.size(file.toPath()) > Files
-                  .size(lastFile.toPath()))
-          Assert.assertTrue(Files.size(file.toPath()) > Files
-                  .size(lastFile.toPath()));
+          Thread.sleep(25);
+          timeOut -= 25;
         }
-        // remember it for next file
-        lastFile = file;
       }
+      /*
+       *  larger margin between IDs and alignment/annotations when in --gui mode
+       *  
+      verifyOrderedFileSet(cmdLines[0] + " vs " + cmdLines[1], filenames, false);
+       */
+
+      verifySimilarEnoughImages(cmdLines[0] + " vs " + cmdLines[1],
+              filenames, 0.6f, 0f);
     } catch (Exception x)
     {
       Assert.fail("Unexpected exception during structureImageOutputTest",
@@ -214,21 +248,161 @@ public class CommandsTest
     }
   }
 
+  @DataProvider(name = "compareHeadlessAndGUIOps")
+  public Object[][] compareHeadlessAndGUIOps()
+  {
+    return new Object[][] {
+        new Object[]
+        { new String[] { "--open examples/uniref50.fa "
+                + "--structure [seqid=FER1_SPIOL,tempfac=plddt,showssannotations,structureviewer=jmol]"
+                + "examples/AlphaFold/AF-P00221-F1-model_v4.pdb "
+                + "--paematrix examples/AlphaFold/AF-P00221-F1-predicted_aligned_error_v4.json --image="
+                + testfiles + "/"
+                + "test-al-pae-ss-gui.png --overwrite --gui --quit",
+            "--open examples/uniref50.fa "
+                    + "--structure [seqid=FER1_SPIOL,tempfac=plddt,showssannotations,structureviewer=jmol]"
+                    + "examples/AlphaFold/AF-P00221-F1-model_v4.pdb "
+                    + "--paematrix examples/AlphaFold/AF-P00221-F1-predicted_aligned_error_v4.json --image="
+                    + testfiles + "/"
+                    + "test-al-pae-ss-nogui.png --overwrite --nogui" },
+            new String[]
+            { testfiles + "/test-al-pae-ss-gui.png",
+                testfiles + "/test-al-pae-ss-nogui.png", } } };
+  }
+
+  private static void verifyIncreasingSize(String cmdLine,
+          String[] filenames) throws Exception
+  {
+    verifyOrderedFileSet(cmdLine, filenames, true);
+  }
+
+  private static void verifyOrderedFileSet(String cmdLine,
+          String[] filenames, boolean increasingSize) throws Exception
+  {
+    File lastFile = null;
+    for (String filename : filenames)
+    {
+      File file = new File(filename);
+      Assert.assertTrue(file.exists(), "File '" + filename
+              + "' was not created by '" + cmdLine + "'");
+      Assert.assertTrue(file.isFile(), "File '" + filename
+              + "' is not a file from '" + cmdLine + "'");
+      Assert.assertTrue(Files.size(file.toPath()) > 0, "File '" + filename
+              + "' has no content from '" + cmdLine + "'");
+      // make sure the successive output files get bigger!
+      if (lastFile != null)
+      {
+        waitForLastWrite(file, 25);
+
+        if (increasingSize)
+        {
+          Assert.assertTrue(
+                  Files.size(file.toPath()) > Files.size(lastFile.toPath()),
+                  "Expected " + file.toPath() + " to be larger than "
+                          + lastFile.toPath());
+        }
+        else
+        {
+          Assert.assertEquals(Files.size(file.toPath()),
+                  Files.size(lastFile.toPath()),
+                  "New file " + file.toPath()
+                          + " (actual size) not same as last file's size "
+                          + lastFile.toString());
+        }
+      }
+      // remember it for next file
+      lastFile = file;
+    }
+
+  }
+
+  private static void verifySimilarEnoughImages(String cmdLine,
+          String[] filenames, float w_tolerance_pc, float h_tolerance_pc)
+          throws Exception
+  {
+    int min_w = -1;
+    int max_w = -1;
+    int min_h = -1;
+    int max_h = -1;
+    for (String filename : filenames)
+    {
+      File file = new File(filename);
+      Assert.assertTrue(file.exists(), "File '" + filename
+              + "' was not created by '" + cmdLine + "'");
+      Assert.assertTrue(file.isFile(), "File '" + filename
+              + "' is not a file from '" + cmdLine + "'");
+      Assert.assertTrue(Files.size(file.toPath()) > 0, "File '" + filename
+              + "' has no content from '" + cmdLine + "'");
+
+      BufferedImage img = ImageIO.read(file);
+      if (img.getWidth() < min_w || min_w == -1)
+      {
+        min_w = img.getWidth();
+      }
+      if (img.getWidth() > max_w || max_w == -1)
+      {
+        max_w = img.getWidth();
+      }
+      if (img.getHeight() < min_h || min_h == -1)
+      {
+        min_h = img.getHeight();
+      }
+      if (img.getHeight() > max_h || max_h == -1)
+      {
+        max_h = img.getHeight();
+      }
+    }
+    Assert.assertTrue(min_w > 0,
+            "Minimum width is not positive (" + min_w + ")");
+    Assert.assertTrue(max_w > 0,
+            "Maximum width is not positive (" + max_w + ")");
+    Assert.assertTrue(min_h > 0,
+            "Minimum height is not positive (" + min_h + ")");
+    Assert.assertTrue(max_h > 0,
+            "Maximum height is not positive (" + max_h + ")");
+    // tolerance
+    Assert.assertTrue(100 * (max_w - min_w) / min_w < w_tolerance_pc,
+            "Width variation (" + (max_w - min_w)
+                    + " not within tolerance of minimum width (" + min_w
+                    + ")");
+    if (max_w != min_w)
+    {
+      System.out.println("Widths within tolerance (" + w_tolerance_pc
+              + "%), min_w=" + min_w + " < max_w=" + max_w);
+    }
+    Assert.assertTrue(100 * (max_h - min_h) / min_h < w_tolerance_pc,
+            "Height variation (" + (max_h - min_h)
+                    + " not within tolerance of minimum height (" + min_h
+                    + ")");
+    if (max_h != min_h)
+    {
+      System.out.println("Heights within tolerance (" + h_tolerance_pc
+              + "%), min_h=" + min_h + " < max_h=" + max_h);
+    }
+  }
+
   private static long waitForLastWrite(File file, int i) throws IOException
   {
-    long lastSize,stableSize =Files.size(file.toPath());
+    long lastSize, stableSize = Files.size(file.toPath());
     // wait around until we are sure the file has been completely written.
-    do {
+    do
+    {
       lastSize = stableSize;
-      try {
+      try
+      {
         Thread.sleep(i);
-      } catch (Exception x) {}
-      stableSize=Files.size(file.toPath());
-    } while (stableSize!=lastSize);
+      } catch (Exception x)
+      {
+      }
+      stableSize = Files.size(file.toPath());
+    } while (stableSize != lastSize);
     return stableSize;
   }
 
-  @Test(groups = "Functional", dataProvider = "argfileOutputFiles", singleThreaded = true)
+  @Test(
+    groups = "Functional",
+    dataProvider = "argfileOutputFiles",
+    singleThreaded = true)
 
   public void argFilesGlobAndSubstitutionsTest(String cmdLine,
           String[] filenames) throws IOException
@@ -251,10 +425,13 @@ public class CommandsTest
         Assert.assertTrue(Files.size(file.toPath()) > 0, "File '" + filename
                 + "' has no content from '" + cmdLine + "'");
         // make sure the successive output files get bigger!
-        if (lastFile != null) {
+        if (lastFile != null)
+        {
           Assert.assertTrue(Files.size(file.toPath()) > Files
                   .size(lastFile.toPath()));
-          System.out.println("this file: "+file+" +"+Files.size(file.toPath()) + " greater than " +Files.size(lastFile.toPath()));
+          System.out.println("this file: " + file + " +"
+                  + Files.size(file.toPath()) + " greater than "
+                  + Files.size(lastFile.toPath()));
         }
         // remember it for next file
         lastFile = file;
@@ -276,6 +453,7 @@ public class CommandsTest
   {
     return new Object[][] {
         //
+        /*
         { "--gui --nonews --nosplash --open=./examples/test_fab41.result/sample.a2m "
                 + "--structure=./examples/test_fab41.result/test_fab41_unrelaxed_rank_1_model_3.pdb "
                 + "--structureimage=" + testfiles + "/structureimage1.png "
@@ -306,20 +484,26 @@ public class CommandsTest
             { testfiles + "/structureimage1.png",
                 testfiles + "/structureimage2.png",
                 testfiles + "/structureimage3.png" } },
-        { "--gui --nonews --nosplash --open examples/1gaq.txt --append ./examples/3W5V.pdb "+"--structure examples/1gaq.txt --seqid \"1GAQ|A\" "+"--structureimage "+testfiles+"/1gaq.png --structure examples/3W5V.pdb "+"--seqid \"3W5V|A\" --structureimage "+testfiles+"/3w5v.png --overwrite",
-                       
-                new String[] {
-                               testfiles+"/1gaq.png",testfiles+"/3w5v.png"
-                }
-        },
-        { "--headless --noquit --open ./examples/1gaq.txt --append ./examples/3W5V.pdb "+"--structure examples/1gaq.txt --seqid \"1GAQ|A\" "+"--structureimage "+testfiles+"/1gaq.png --structure examples/3W5V.pdb "+"--seqid \"3W5V|A\" --structureimage "+testfiles+"/3w5v.png --overwrite",
-               
-            new String[] {
-                       testfiles+"/1gaq.png",testfiles+"/3w5v.png"
-            }
-    }
+                */
+        { "--gui --nonews --nosplash --open examples/1gaq.txt --append ./examples/3W5V.pdb "
+                + "--structure examples/1gaq.txt --seqid \"1GAQ|A\" "
+                + "--structureimage " + testfiles
+                + "/1gaq.png --structure examples/3W5V.pdb "
+                + "--seqid \"3W5V|A\" --structureimage " + testfiles
+                + "/3w5v.png --overwrite",
 
+            new String[]
+            { testfiles + "/1gaq.png", testfiles + "/3w5v.png" } },
         /*
+        { "--headless --noquit --open ./examples/1gaq.txt --append ./examples/3W5V.pdb "
+                + "--structure examples/1gaq.txt --seqid \"1GAQ|A\" "
+                + "--structureimage " + testfiles
+                + "/1gaq.png --structure examples/3W5V.pdb "
+                + "--seqid \"3W5V|A\" --structureimage " + testfiles
+                + "/3w5v.png --overwrite",
+        
+            new String[]
+            { testfiles + "/1gaq.png", testfiles + "/3w5v.png" } }
                 */
         //
     };
@@ -674,4 +858,105 @@ public class CommandsTest
     };
   }
 
+  @Test(
+    groups =
+    { "Functional", "testTask3" },
+    dataProvider = "structureImageAnnotationsOutputFiles",
+    singleThreaded = true)
+  public void structureImageAnnotationsOutputTest(String cmdLine,
+          String filename, int height) throws IOException
+  {
+    cleanupFiles(new String[] { filename });
+    String[] args = (cmdLine).split("\\s+");
+    callJalviewMain(args, true); // Create new instance of Jalview each time for
+                                 // linkedIds
+    BufferedImage img = ImageIO.read(new File(filename));
+    Assert.assertEquals(height, img.getHeight(), "Output image '" + filename
+            + "' is not in the expected height range, possibly because of the wrong number of annotations");
+
+    cleanupFiles(new String[] { filename });
+    tearDown();
+  }
+
+  @DataProvider(name = "structureImageAnnotationsOutputFiles")
+  public Object[][] structureImageAnnotationsOutputFiles()
+  {
+    String filename = "test/jalview/bin/argparser/testfiles/test_annotations.png";
+    return new Object[][] {
+        // MUST use --noquit with --headless to avoid a System.exit()
+        { "--noquit --headless --nonews --nosplash --open=./examples/uniref50.fa "
+                + "--structure=examples/AlphaFold/AF-P00221-F1-model_v4.pdb "
+                + "--seqid=FER1_SPIOL --structureviewer=jmol "
+                + "--paematrix examples/AlphaFold/AF-P00221-F1-predicted_aligned_error_v4.json "
+                + "--image=" + filename + " " + "--tempfac=plddt "
+                + "--overwrite " //
+                + "--noshowssannotations " + "--noshowannotations", //
+            filename, //
+            252 }, //
+        { "--noquit --headless --nonews --nosplash --open=./examples/uniref50.fa "
+                + "--structure=examples/AlphaFold/AF-P00221-F1-model_v4.pdb "
+                + "--seqid=FER1_SPIOL --structureviewer=jmol "
+                + "--paematrix examples/AlphaFold/AF-P00221-F1-predicted_aligned_error_v4.json "
+                + "--image=" + filename + " " + "--tempfac=plddt "
+                + "--overwrite " //
+                + "--showssannotations " + "--noshowannotations", //
+            filename, //
+            368 }, //
+        { "--noquit --headless --nonews --nosplash --open=./examples/uniref50.fa "
+                + "--structure=examples/AlphaFold/AF-P00221-F1-model_v4.pdb "
+                + "--seqid=FER1_SPIOL --structureviewer=jmol "
+                + "--paematrix examples/AlphaFold/AF-P00221-F1-predicted_aligned_error_v4.json "
+                + "--image=" + filename + " " + "--tempfac=plddt "
+                + "--overwrite " //
+                + "--noshowssannotations " + "--showannotations", //
+            filename, //
+            524 }, //
+        { "--noquit --headless --nonews --nosplash --open=./examples/uniref50.fa "
+                + "--structure=examples/AlphaFold/AF-P00221-F1-model_v4.pdb "
+                + "--seqid=FER1_SPIOL --structureviewer=jmol "
+                + "--paematrix examples/AlphaFold/AF-P00221-F1-predicted_aligned_error_v4.json "
+                + "--image=" + filename + " " + "--tempfac=plddt "
+                + "--overwrite " //
+                + "--showssannotations " + "--showannotations", //
+            filename, //
+            660 }, //
+        { "--gui --nonews --nosplash --open=./examples/uniref50.fa "
+                + "--structure=examples/AlphaFold/AF-P00221-F1-model_v4.pdb "
+                + "--seqid=FER1_SPIOL --structureviewer=jmol "
+                + "--paematrix examples/AlphaFold/AF-P00221-F1-predicted_aligned_error_v4.json "
+                + "--image=" + filename + " " + "--tempfac=plddt "
+                + "--overwrite " //
+                + "--noshowssannotations " + "--noshowannotations", //
+            filename, //
+            252 }, //
+        { "--gui --nonews --nosplash --open=./examples/uniref50.fa "
+                + "--structure=examples/AlphaFold/AF-P00221-F1-model_v4.pdb "
+                + "--seqid=FER1_SPIOL --structureviewer=jmol "
+                + "--paematrix examples/AlphaFold/AF-P00221-F1-predicted_aligned_error_v4.json "
+                + "--image=" + filename + " " + "--tempfac=plddt "
+                + "--overwrite " //
+                + "--showssannotations " + "--noshowannotations", //
+            filename, //
+            368 }, //
+        { "--gui --nonews --nosplash --open=./examples/uniref50.fa "
+                + "--structure=examples/AlphaFold/AF-P00221-F1-model_v4.pdb "
+                + "--seqid=FER1_SPIOL --structureviewer=jmol "
+                + "--paematrix examples/AlphaFold/AF-P00221-F1-predicted_aligned_error_v4.json "
+                + "--image=" + filename + " " + "--tempfac=plddt "
+                + "--overwrite " //
+                + "--noshowssannotations " + "--showannotations", //
+            filename, //
+            524 }, //
+        { "--gui --nonews --nosplash --open=./examples/uniref50.fa "
+                + "--structure=examples/AlphaFold/AF-P00221-F1-model_v4.pdb "
+                + "--seqid=FER1_SPIOL --structureviewer=jmol "
+                + "--paematrix examples/AlphaFold/AF-P00221-F1-predicted_aligned_error_v4.json "
+                + "--image=" + filename + " " + "--tempfac=plddt "
+                + "--overwrite " //
+                + "--showssannotations " + "--showannotations", //
+            filename, //
+            660 }, //
+    };
+  }
+
 }