diff --git a/CHANGELOG.md b/CHANGELOG.md
index 917493915..89e128ec6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -44,6 +44,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- [SIL.Media] BREAKING CHANGE (subtle and unlikely): WindowsAudioSession.OnPlaybackStopped now passes itself as the sender instead of a private implementation object, making the event arguments correct.
### Changed
+- [SIL.Windows.Forms] PalasoImage robust load/save helpers now allow callers to override retry defaults, and the built-in retry lists were expanded for additional read/save exceptions seen in the wild.
- [SIL.Core.Desktop, SIL.Windows.Forms, SIL.Windows.Forms.Keyboarding] Bumped L10NSharp to 10.0.0-beta0002 to support SIL.Core.Desktop with target `net8.0`; also updated the copyright to 2026 in each `AssemblyInfo.cs`.
- [SIL.Windows.Forms.i18n, SIL.Core.Desktop.i18n] BREAKING CHANGE: Move L10NSharpLocalizer from Windows.Forms to Core.Desktop so it can be accessed without Winforms dependency.
- [SIL.Windows.Forms.Clearshare] BREAKING CHANGE: Made LicenseInfo class independent of Windows Forms and moved it from SIL.Windows.Forms.Clearshare to SIL.Core.Clearshare.
diff --git a/SIL.Windows.Forms/Clipboarding/WindowsClipboard.cs b/SIL.Windows.Forms/Clipboarding/WindowsClipboard.cs
index 9a2994650..1471aa2f2 100644
--- a/SIL.Windows.Forms/Clipboarding/WindowsClipboard.cs
+++ b/SIL.Windows.Forms/Clipboarding/WindowsClipboard.cs
@@ -194,7 +194,7 @@ public PalasoImage GetImageFromClipboard()
// This line gets all the file paths that were selected in explorer
string[] files = dataObject.GetData(DataFormats.FileDrop) as string[];
- return files?.Where(RobustFile.Exists).Select(PalasoImage.FromFileRobustly).FirstOrDefault();
+ return files?.Where(RobustFile.Exists).Select(path => PalasoImage.FromFileRobustly(path)).FirstOrDefault();
}
if (Clipboard.ContainsText() && RobustFile.Exists(Clipboard.GetText()))
diff --git a/SIL.Windows.Forms/ImageToolbox/PalasoImage.cs b/SIL.Windows.Forms/ImageToolbox/PalasoImage.cs
index bfc990c3e..aad96345d 100644
--- a/SIL.Windows.Forms/ImageToolbox/PalasoImage.cs
+++ b/SIL.Windows.Forms/ImageToolbox/PalasoImage.cs
@@ -358,26 +358,39 @@ public static PalasoImage FromFile(string path)
///
/// This would logically belong in SIL.Core.IO.RobustIO except that PalasoImage is in SIL.Windows.Forms.
///
- public static PalasoImage FromFileRobustly(string path)
+ public static PalasoImage FromFileRobustly(
+ string path,
+ int maxRetryAttempts = RetryUtility.kDefaultMaxRetryAttempts,
+ int retryDelay = RetryUtility.kDefaultRetryDelay,
+ HashSet exceptionTypesToRetry = null
+ )
{
+ exceptionTypesToRetry ??= new HashSet
+ {
+ typeof(System.IO.IOException),
+ // Odd type to catch... but it seems that Image.FromFile (which is called in the bowels of PalasoImage.FromFile)
+ // throws OutOfMemoryException when the file is inaccessible.
+ // See http://stackoverflow.com/questions/2610416/is-there-a-reason-image-fromfile-throws-an-outofmemoryexception-for-an-invalid-i
+ typeof(System.OutOfMemoryException),
+ // Again you'd expect that if it's corrupt, it would stay that way, but
+ // experimentally, it seems we can get this if the file can't be read because it is (temporarily?) locked.
+ // (The text of the message reads, "File could not be read and is possible corrupted", which
+ // suggests they are using this to cover any case of not being able to read the file."
+ typeof(TagLib.CorruptFileException),
+ // Bloom saw this one in the wild. BL-16221
+ typeof(System.Collections.Generic.KeyNotFoundException),
+ // Adding this simply because I'm adding it on the Save side. I'm tempted to just retry everything...
+ typeof(System.ApplicationException),
+ };
+
try
{
- return RetryUtility.Retry(() => PalasoImage.FromFile(path),
- RetryUtility.kDefaultMaxRetryAttempts,
- RetryUtility.kDefaultRetryDelay,
- new HashSet
- {
- typeof(System.IO.IOException),
- // Odd type to catch... but it seems that Image.FromFile (which is called in the bowels of PalasoImage.FromFile)
- // throws OutOfMemoryException when the file is inaccessible.
- // See http://stackoverflow.com/questions/2610416/is-there-a-reason-image-fromfile-throws-an-outofmemoryexception-for-an-invalid-i
- typeof(System.OutOfMemoryException),
- // Again you'd expect that if it's corrupt, it would stay that way, but
- // experimentally, it seems we can get this if the file can't be read because it is (temporarily?) locked.
- // (The text of the message reads, "File could not be read and is possible corrupted", which
- // suggests they are using this to cover any case of not being able to read the file."
- typeof(TagLib.CorruptFileException)
- });
+ return RetryUtility.Retry(
+ () => PalasoImage.FromFile(path),
+ maxRetryAttempts,
+ retryDelay,
+ exceptionTypesToRetry
+ );
}
catch (Exception e)
{
@@ -395,16 +408,27 @@ public static PalasoImage FromFileRobustly(string path)
///
/// This would logically belong in SIL.Core.IO.RobustIO except that PalasoImage is in SIL.Windows.Forms.
///
- public static void SaveImageRobustly(PalasoImage image, string fileName)
+ public static void SaveImageRobustly(
+ PalasoImage image,
+ string fileName,
+ int maxRetryAttempts = RetryUtility.kDefaultMaxRetryAttempts,
+ int retryDelay = RetryUtility.kDefaultRetryDelay,
+ HashSet retryOnExceptions = null)
{
+
+ retryOnExceptions ??= new HashSet
+ {
+ typeof(System.IO.IOException),
+ typeof(System.Runtime.InteropServices.ExternalException),
+ // PalasoImage.SaveImageSafely can also throw ApplicationExceptions
+ // (See https://github.com/sillsdev/libpalaso/blob/f2482a5b3c6c75b50ec5672b1eb731b1a040a05a/SIL.Windows.Forms/ImageToolbox/PalasoImage.cs#L155)
+ typeof(System.ApplicationException),
+ };
+
RetryUtility.Retry(() => image.Save(fileName),
- RetryUtility.kDefaultMaxRetryAttempts,
- RetryUtility.kDefaultRetryDelay,
- new HashSet
- {
- Type.GetType("System.IO.IOException"),
- Type.GetType("System.Runtime.InteropServices.ExternalException")
- });
+ maxRetryAttempts,
+ retryDelay,
+ retryOnExceptions);
}
///