diff --git a/Setup/Assets/Themes/Kobe-Light/Compare.svg b/Setup/Assets/Themes/Kobe-Light/Compare.svg
new file mode 100644
index 000000000..bcbd6a41a
--- /dev/null
+++ b/Setup/Assets/Themes/Kobe-Light/Compare.svg
@@ -0,0 +1 @@
+
diff --git a/Setup/Assets/Themes/Kobe-Light/CompareSlider.svg b/Setup/Assets/Themes/Kobe-Light/CompareSlider.svg
new file mode 100644
index 000000000..92bfb01a3
--- /dev/null
+++ b/Setup/Assets/Themes/Kobe-Light/CompareSlider.svg
@@ -0,0 +1,4 @@
+
diff --git a/Setup/Assets/Themes/Kobe-Light/igtheme.json b/Setup/Assets/Themes/Kobe-Light/igtheme.json
index c63acb547..01e4e664e 100644
--- a/Setup/Assets/Themes/Kobe-Light/igtheme.json
+++ b/Setup/Assets/Themes/Kobe-Light/igtheme.json
@@ -16,6 +16,7 @@
"IsShowTitlebarLogo": true,
"NavButtonLeft": "ViewPreviousImage.svg",
"NavButtonRight": "ViewNextImage.svg",
+ "CompareSliderHandle": "CompareSlider.svg",
"PreviewImage": "preview.webp",
"AppLogo": "Logo.svg"
},
@@ -47,6 +48,7 @@
"AutoZoom": "AutoZoom.svg",
"Checkerboard": "Checkerboard.svg",
"ColorPicker": "ColorPicker.svg",
+ "Compare": "Compare.svg",
"Crop": "Crop.svg",
"Delete": "Delete.svg",
"Edit": "Edit.svg",
diff --git a/Setup/Assets/Themes/Kobe/Compare.svg b/Setup/Assets/Themes/Kobe/Compare.svg
new file mode 100644
index 000000000..91484bcd2
--- /dev/null
+++ b/Setup/Assets/Themes/Kobe/Compare.svg
@@ -0,0 +1 @@
+
diff --git a/Setup/Assets/Themes/Kobe/CompareSlider.svg b/Setup/Assets/Themes/Kobe/CompareSlider.svg
new file mode 100644
index 000000000..c9e857fd5
--- /dev/null
+++ b/Setup/Assets/Themes/Kobe/CompareSlider.svg
@@ -0,0 +1,4 @@
+
diff --git a/Setup/Assets/Themes/Kobe/igtheme.json b/Setup/Assets/Themes/Kobe/igtheme.json
index 6bae91244..eb323e461 100644
--- a/Setup/Assets/Themes/Kobe/igtheme.json
+++ b/Setup/Assets/Themes/Kobe/igtheme.json
@@ -16,6 +16,7 @@
"IsShowTitlebarLogo": true,
"NavButtonLeft": "ViewPreviousImage.svg",
"NavButtonRight": "ViewNextImage.svg",
+ "CompareSliderHandle": "CompareSlider.svg",
"PreviewImage": "preview.webp",
"AppLogo": "Logo.svg"
},
@@ -37,6 +38,7 @@
"AutoZoom": "AutoZoom.svg",
"Checkerboard": "Checkerboard.svg",
"ColorPicker": "ColorPicker.svg",
+ "Compare": "Compare.svg",
"Crop": "Crop.svg",
"Delete": "Delete.svg",
"Edit": "Edit.svg",
diff --git a/Source/Components/ImageGlass.Base/Language/IgLang.cs b/Source/Components/ImageGlass.Base/Language/IgLang.cs
index 5d2cd2fd3..4866028d6 100644
--- a/Source/Components/ImageGlass.Base/Language/IgLang.cs
+++ b/Source/Components/ImageGlass.Base/Language/IgLang.cs
@@ -276,6 +276,7 @@ public Dictionary InitDefaultLanguage()
{ $"_._InvalidAction", "Invalid action" }, //v9.0
{ $"_._InvalidAction._Transformation", "ImageGlass does not support rotation, flipping for this image." }, //v9.0
+ { $"_._InvalidAction._ComparisonMode", "This feature is not available in comparison mode." }, //v9.1
{ "_._UserAction._MenuNotFound", "Cannot find menu '{0}' to invoke the action" }, // v9.0
@@ -510,6 +511,7 @@ public Dictionary InitDefaultLanguage()
{ "FrmMain.MnuTools", "Tools" }, //v3.0
{ "FrmMain.MnuColorPicker", "Color picker" }, //v5.0
{ "FrmMain.MnuCropTool", "Crop image" }, // v7.6
+ { "FrmMain.MnuCompareTool", "Compare images" }, // v9.1
{ "FrmMain.MnuResizeTool", "Resize image" }, // v9.2
{ "FrmMain.MnuFrameNav", "Frame navigation" }, // v7.5
{ "FrmMain.MnuGetMoreTools", "Get more tools…" }, // v9.0
@@ -890,6 +892,13 @@ public Dictionary InitDefaultLanguage()
#endregion // FrmCrop
+ #region FrmCompare
+ { "FrmCompare._DropToReplaceMainImage", "Drop to replace main image" }, // v9.1
+ { "FrmCompare._DropToSetComparisonImage", "Drop to set comparison image" }, // v9.1
+ { "FrmCompare._SelectImageToCompare", "Select an image to compare using the button below." }, // v9.1
+ #endregion // FrmCompare
+
+
#region FrmColorPicker
{ "FrmColorPicker.BtnSettings._Tooltip", "Open Color picker settings…" }, //v9.0
diff --git a/Source/Components/ImageGlass.Base/Photoing/Codecs/PhotoCodec.cs b/Source/Components/ImageGlass.Base/Photoing/Codecs/PhotoCodec.cs
index 4daf11d4a..97b54aa28 100644
--- a/Source/Components/ImageGlass.Base/Photoing/Codecs/PhotoCodec.cs
+++ b/Source/Components/ImageGlass.Base/Photoing/Codecs/PhotoCodec.cs
@@ -866,7 +866,7 @@ private static (bool loadSuccessful, IgImgData result, string ext, MagickReadSet
base64Content = fs.ReadToEnd();
}
- if (result.CanAnimate)
+ if (result.CanAnimate && options?.FirstFrameOnly != true)
{
result.Source = BHelper.ToGdiPlusBitmapFromBase64(base64Content);
}
@@ -887,16 +887,16 @@ private static (bool loadSuccessful, IgImgData result, string ext, MagickReadSet
try
{
// Note: Using WIC is much faster than using MagickImageCollection
- if (result.CanAnimate)
+ if (result.CanAnimate && options?.FirstFrameOnly != true)
{
result.Source = BHelper.ToGdiPlusBitmap(filePath);
}
- // multiple frame
- else if (result.FrameCount > 0)
+ // multiple frame (but not animating or FirstFrameOnly requested)
+ else if (result.FrameCount > 1 && options?.FirstFrameOnly != true)
{
result.Source = WicBitmapDecoder.Load(filePath);
}
- // single frame
+ // single frame or FirstFrameOnly requested
else
{
result.Image = WicBitmapSource.Load(filePath);
@@ -913,7 +913,7 @@ private static (bool loadSuccessful, IgImgData result, string ext, MagickReadSet
{
using var webp = new WebPWrapper();
- if (result.CanAnimate)
+ if (result.CanAnimate && options?.FirstFrameOnly != true)
{
var aniWebP = webp.AnimLoad(filePath);
var frames = aniWebP.Select(frame =>
diff --git a/Source/Components/ImageGlass.Base/Types/Const.cs b/Source/Components/ImageGlass.Base/Types/Const.cs
index c9649cc9e..3625d7de6 100644
--- a/Source/Components/ImageGlass.Base/Types/Const.cs
+++ b/Source/Components/ImageGlass.Base/Types/Const.cs
@@ -87,4 +87,9 @@ public static class Const
///
public const string IMAGE_FORMATS = ".3fr;.apng;.ari;.arw;.avif;.b64;.bay;.bmp;.cap;.cr2;.cr3;.crw;.cur;.cut;.dcr;.dcs;.dds;.dib;.dng;.drf;.eip;.emf;.erf;.exif;.exr;.fff;.fits;.flif;.gif;.gifv;.gpr;.hdp;.hdr;.heic;.heif;.hif;.ico;.iiq;.jfif;.jp2;.jpe;.jpeg;.jpg;.jxl;.jxr;.k25;.kdc;.mdc;.mef;.mjpeg;.mos;.mrw;.nef;.nrw;.obm;.orf;.pbm;.pcx;.pef;.pgm;.png;.ppm;.psb;.psd;.ptx;.pxn;.qoi;.r3d;.raf;.raw;.rw2;.rwl;.rwz;.sr2;.srf;.srw;.svg;.tga;.tif;.tiff;.viff;.wdp;.webp;.wmf;.wpg;.x3f;.xbm;.xpm;.xv";
+ ///
+ /// File extensions that may contain animation and should use WebView2 for rendering.
+ ///
+ public static readonly string[] ANIMATED_FORMATS = [".gif", ".gifv", ".webp", ".apng"];
+
}
\ No newline at end of file
diff --git a/Source/Components/ImageGlass.Gallery/ImageGallery.cs b/Source/Components/ImageGlass.Gallery/ImageGallery.cs
index 194738e4b..a0c2aa18f 100644
--- a/Source/Components/ImageGlass.Gallery/ImageGallery.cs
+++ b/Source/Components/ImageGlass.Gallery/ImageGallery.cs
@@ -764,6 +764,25 @@ public void ResetErrorImage()
mErrorImageChanged = false;
}
+
+ ///
+ /// Gets or sets whether comparison mode is active (shows A/B indicators).
+ ///
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public bool ComparisonMode { get; set; } = false;
+
+ ///
+ /// Gets or sets the index of the main comparison image (A).
+ ///
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public int ComparisonMainIndex { get; set; } = -1;
+
+ ///
+ /// Gets or sets the file path of the comparison image (B).
+ ///
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public string ComparisonImagePath { get; set; } = string.Empty;
+
#endregion
diff --git a/Source/Components/ImageGlass.Gallery/Renderer/StyleRenderer.cs b/Source/Components/ImageGlass.Gallery/Renderer/StyleRenderer.cs
index 72281d7ff..b73295fdb 100644
--- a/Source/Components/ImageGlass.Gallery/Renderer/StyleRenderer.cs
+++ b/Source/Components/ImageGlass.Gallery/Renderer/StyleRenderer.cs
@@ -1026,6 +1026,99 @@ public virtual void DrawItem(Graphics g, ImageGalleryItem item, ItemState state,
{
ControlPaint.DrawFocusRectangle(g, bounds);
}
+
+ // Draw comparison A/B badges
+ if (ImageGalleryOwner.ComparisonMode)
+ {
+ DrawComparisonBadge(g, item, bounds);
+ }
+ }
+
+ ///
+ /// Draws the A/B comparison badge for items in comparison mode.
+ ///
+ protected virtual void DrawComparisonBadge(Graphics g, ImageGalleryItem item, Rectangle bounds)
+ {
+ var isMainImage = item.Index == ImageGalleryOwner.ComparisonMainIndex;
+ var isCompareImage = !string.IsNullOrEmpty(ImageGalleryOwner.ComparisonImagePath) &&
+ string.Equals(item.FilePath, ImageGalleryOwner.ComparisonImagePath, StringComparison.OrdinalIgnoreCase);
+
+ if (!isMainImage && !isCompareImage) return;
+
+ var colorA = Color.FromArgb(220, 59, 130, 246); // Blue
+ var colorB = Color.FromArgb(220, 34, 197, 94); // Green
+
+ var badgePadding = 4;
+ var badgeHeight = 20;
+
+ g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
+
+ using var font = new Font("Segoe UI", 9f, FontStyle.Bold);
+ using var format = new StringFormat
+ {
+ Alignment = StringAlignment.Center,
+ LineAlignment = StringAlignment.Center
+ };
+
+ if (isMainImage && isCompareImage)
+ {
+ // Split pill: A on left (blue), B on right (green)
+ var pillWidth = 36;
+ var pillRect = new Rectangle(
+ bounds.Right - pillWidth - badgePadding,
+ bounds.Top + badgePadding,
+ pillWidth,
+ badgeHeight);
+
+ var halfWidth = pillWidth / 2;
+ var radius = badgeHeight / 2;
+
+ // Draw left half (A - blue) with rounded left side
+ using var pathLeft = new System.Drawing.Drawing2D.GraphicsPath();
+ pathLeft.AddArc(pillRect.Left, pillRect.Top, badgeHeight, badgeHeight, 90, 180);
+ pathLeft.AddLine(pillRect.Left + radius, pillRect.Top, pillRect.Left + halfWidth, pillRect.Top);
+ pathLeft.AddLine(pillRect.Left + halfWidth, pillRect.Top, pillRect.Left + halfWidth, pillRect.Bottom);
+ pathLeft.AddLine(pillRect.Left + halfWidth, pillRect.Bottom, pillRect.Left + radius, pillRect.Bottom);
+ pathLeft.CloseFigure();
+
+ using var brushA = new SolidBrush(colorA);
+ g.FillPath(brushA, pathLeft);
+
+ // Draw right half (B - green) with rounded right side
+ using var pathRight = new System.Drawing.Drawing2D.GraphicsPath();
+ pathRight.AddLine(pillRect.Left + halfWidth, pillRect.Top, pillRect.Right - radius, pillRect.Top);
+ pathRight.AddArc(pillRect.Right - badgeHeight, pillRect.Top, badgeHeight, badgeHeight, -90, 180);
+ pathRight.AddLine(pillRect.Right - radius, pillRect.Bottom, pillRect.Left + halfWidth, pillRect.Bottom);
+ pathRight.CloseFigure();
+
+ using var brushB = new SolidBrush(colorB);
+ g.FillPath(brushB, pathRight);
+
+ // Draw letters
+ var leftRect = new RectangleF(pillRect.Left, pillRect.Top, halfWidth, badgeHeight);
+ var rightRect = new RectangleF(pillRect.Left + halfWidth, pillRect.Top, halfWidth, badgeHeight);
+ g.DrawString("A", font, Brushes.White, leftRect, format);
+ g.DrawString("B", font, Brushes.White, rightRect, format);
+ }
+ else
+ {
+ // Single circular badge
+ var badgeSize = 20;
+ var badgeRect = new Rectangle(
+ bounds.Right - badgeSize - badgePadding,
+ bounds.Top + badgePadding,
+ badgeSize,
+ badgeSize);
+
+ var badgeText = isMainImage ? "A" : "B";
+ var badgeColor = isMainImage ? colorA : colorB;
+
+ using var brush = new SolidBrush(badgeColor);
+ g.FillEllipse(brush, badgeRect);
+ g.DrawString(badgeText, font, Brushes.White, badgeRect, format);
+ }
+
+ g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.Default;
}
///
diff --git a/Source/Components/ImageGlass.Settings/WebUI/DXCanvas_Webview2.html b/Source/Components/ImageGlass.Settings/WebUI/DXCanvas_Webview2.html
index c89662e6f..9a86b33eb 100644
--- a/Source/Components/ImageGlass.Settings/WebUI/DXCanvas_Webview2.html
+++ b/Source/Components/ImageGlass.Settings/WebUI/DXCanvas_Webview2.html
@@ -26,6 +26,33 @@
+
+
+
+
![Image A]()
+
+
+
![Image B]()
+
+
+
+
+
Drop to replace main image
+
+
+
+
Drop to set comparison image
+
+
+
+