diff --git a/Simple QR Code Maker.Core/Simple QR Code Maker.Core.csproj b/Simple QR Code Maker.Core/Simple QR Code Maker.Core.csproj
index d2f96c3..8c1cb13 100644
--- a/Simple QR Code Maker.Core/Simple QR Code Maker.Core.csproj
+++ b/Simple QR Code Maker.Core/Simple QR Code Maker.Core.csproj
@@ -11,6 +11,6 @@
-
+
diff --git a/Simple QR Code Maker/Controls/IconAndTextContent.xaml b/Simple QR Code Maker/Controls/IconAndTextContent.xaml
new file mode 100644
index 0000000..4583fe8
--- /dev/null
+++ b/Simple QR Code Maker/Controls/IconAndTextContent.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/Simple QR Code Maker/Controls/IconAndTextContent.xaml.cs b/Simple QR Code Maker/Controls/IconAndTextContent.xaml.cs
new file mode 100644
index 0000000..3f6f92e
--- /dev/null
+++ b/Simple QR Code Maker/Controls/IconAndTextContent.xaml.cs
@@ -0,0 +1,26 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Simple_QR_Code_Maker.Controls;
+
+public sealed partial class IconAndTextContent : StackPanel
+{
+ public IconAndTextContent()
+ {
+ InitializeComponent();
+ }
+
+ public Symbol Icon
+ { get => (Symbol)GetValue(IconProperty); set => SetValue(IconProperty, value);
+ }
+
+ public static readonly DependencyProperty IconProperty =
+ DependencyProperty.Register(nameof(Icon), typeof(Symbol), typeof(IconAndTextContent), new PropertyMetadata(Symbol.Placeholder));
+
+ public string ContentText
+ { get => (string)GetValue(ContentTextProperty); set => SetValue(ContentTextProperty, value);
+ }
+
+ public static readonly DependencyProperty ContentTextProperty =
+ DependencyProperty.Register(nameof(ContentText), typeof(string), typeof(IconAndTextContent), new PropertyMetadata(string.Empty));
+}
diff --git a/Simple QR Code Maker/Helpers/BarcodeHelpers.cs b/Simple QR Code Maker/Helpers/BarcodeHelpers.cs
index 51ff1eb..53f4bf6 100644
--- a/Simple QR Code Maker/Helpers/BarcodeHelpers.cs
+++ b/Simple QR Code Maker/Helpers/BarcodeHelpers.cs
@@ -14,7 +14,28 @@ namespace Simple_QR_Code_Maker.Helpers;
public static class BarcodeHelpers
{
- public static WriteableBitmap GetQrCodeBitmapFromText(string text, ErrorCorrectionLevel correctionLevel, System.Drawing.Color foreground, System.Drawing.Color background)
+ ///
+ /// Calculate the maximum safe logo size percentage based on QR code error correction level and version
+ ///
+ /// The text to encode in the QR code
+ /// The error correction level
+ /// Maximum safe logo size as a percentage (0-100)
+ // public static int GetMaxLogoSizePercentage(string text, ErrorCorrectionLevel correctionLevel)
+ public static int GetMaxLogoSizePercentage(ErrorCorrectionLevel correctionLevel)
+ {
+ // Error correction capacity by level (percentage of code that can be damaged and still readable)
+ // These are the theoretical maximum recovery percentages
+ return correctionLevel.ToString() switch
+ {
+ "L" => 20, // Low: ~7%
+ "M" => 23, // Medium: ~15%
+ "Q" => 35, // Quartile: ~25%
+ "H" => 40, // High: ~30%
+ _ => 20
+ };
+ }
+
+ public static WriteableBitmap GetQrCodeBitmapFromText(string text, ErrorCorrectionLevel correctionLevel, System.Drawing.Color foreground, System.Drawing.Color background, Bitmap? logoImage = null, double logoSizePercentage = 20.0, double logoPaddingPixels = 8.0)
{
BitmapRenderer bitmapRenderer = new()
{
@@ -38,6 +59,16 @@ public static WriteableBitmap GetQrCodeBitmapFromText(string text, ErrorCorrecti
barcodeWriter.Options = encodingOptions;
using Bitmap bitmap = barcodeWriter.Write(text);
+
+ // If a logo is provided, overlay it on the center of the QR code
+ if (logoImage != null)
+ {
+ // Get the QR code details to calculate module size
+ QRCode qrCode = ZXing.QrCode.Internal.Encoder.encode(text, correctionLevel);
+ int moduleCount = qrCode.Version.DimensionForVersion;
+ OverlayLogoOnQrCode(bitmap, logoImage, logoSizePercentage, moduleCount, encodingOptions.Margin, logoPaddingPixels, background);
+ }
+
using MemoryStream ms = new();
bitmap.Save(ms, ImageFormat.Png);
WriteableBitmap bitmapImage = new(encodingOptions.Width, encodingOptions.Height);
@@ -47,6 +78,81 @@ public static WriteableBitmap GetQrCodeBitmapFromText(string text, ErrorCorrecti
return bitmapImage;
}
+ private static void OverlayLogoOnQrCode(Bitmap qrCodeBitmap, Bitmap logo, double sizePercentage, int moduleCount, int margin, double logoPaddingPixels, System.Drawing.Color backgroundColor)
+ {
+ // Calculate the pixel size of each QR code module
+ // The total size includes the margin on both sides
+ int totalModules = moduleCount + (margin * 2);
+ double modulePixelSize = (double)qrCodeBitmap.Width / totalModules;
+
+ // Calculate the punchout space size based on the size percentage
+ // This is the area that will be covered (blocking QR code modules)
+ int punchoutSizePixels = (int)(Math.Min(qrCodeBitmap.Width, qrCodeBitmap.Height) * (sizePercentage / 100.0));
+
+ // Round the punchout size to the nearest module boundary
+ int punchoutSizeModules = (int)Math.Round(punchoutSizePixels / modulePixelSize);
+ // Ensure it's at least 1 module and odd number for better centering
+ if (punchoutSizeModules < 1) punchoutSizeModules = 1;
+ if (punchoutSizeModules % 2 == 0) punchoutSizeModules++; // Make it odd for symmetry
+
+ // Convert back to pixels, aligned to module boundaries
+ int punchoutSize = (int)(punchoutSizeModules * modulePixelSize);
+
+ // Calculate the position to center the punchout area
+ int punchoutX = (qrCodeBitmap.Width - punchoutSize) / 2;
+ int punchoutY = (qrCodeBitmap.Height - punchoutSize) / 2;
+
+ // Convert padding to actual pixels
+ // Positive padding = logo smaller than punchout (adds white space)
+ // Negative padding = logo larger than punchout (logo extends beyond white background)
+ int paddingPixels = (int)Math.Round(logoPaddingPixels);
+
+ // Calculate the actual logo display size
+ // Logo fills the punchout area minus the padding on all sides
+ int logoDisplayWidth = Math.Max(1, punchoutSize - (Math.Abs(paddingPixels) * 2));
+ int logoDisplayHeight = Math.Max(1, punchoutSize - (Math.Abs(paddingPixels) * 2));
+
+ // If padding is negative, logo is larger than punchout
+ if (paddingPixels < 0)
+ {
+ logoDisplayWidth = punchoutSize + (Math.Abs(paddingPixels) * 2);
+ logoDisplayHeight = punchoutSize + (Math.Abs(paddingPixels) * 2);
+ }
+
+ // Calculate logo dimensions preserving aspect ratio
+ float aspectRatio = (float)logo.Width / logo.Height;
+ int logoWidth, logoHeight;
+
+ if (aspectRatio > 1) // Wider than tall
+ {
+ logoWidth = logoDisplayWidth;
+ logoHeight = (int)(logoDisplayWidth / aspectRatio);
+ }
+ else // Taller than wide or square
+ {
+ logoHeight = logoDisplayHeight;
+ logoWidth = (int)(logoDisplayHeight * aspectRatio);
+ }
+
+ // Center the logo within the punchout area (or offset if larger)
+ int logoX = punchoutX + (punchoutSize - logoWidth) / 2;
+ int logoY = punchoutY + (punchoutSize - logoHeight) / 2;
+
+ using Graphics g = Graphics.FromImage(qrCodeBitmap);
+ // Set high quality rendering
+ g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
+ g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
+ g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
+ g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
+
+ // Draw the punchout background with the same color as the QR code background
+ using SolidBrush backgroundBrush = new(backgroundColor);
+ g.FillRectangle(backgroundBrush, punchoutX, punchoutY, punchoutSize, punchoutSize);
+
+ // Draw the logo scaled to fit within or extend beyond the punchout
+ g.DrawImage(logo, logoX, logoY, logoWidth, logoHeight);
+ }
+
///
/// Calculate the smallest side of a QR code based on the distance between the camera and the QR code
///
@@ -106,7 +212,7 @@ public static string SmallestSideWithUnits(double distance, int numberOfBlocks,
return $"{smallestSideCm:F2} x {smallestSideCm:F2} cm";
}
- public static SvgImage GetSvgQrCodeForText(string text, ErrorCorrectionLevel correctionLevel, System.Drawing.Color foreground, System.Drawing.Color background)
+ public static SvgImage GetSvgQrCodeForText(string text, ErrorCorrectionLevel correctionLevel, System.Drawing.Color foreground, System.Drawing.Color background, Bitmap? logoImage = null, double logoSizePercentage = 20.0, double logoPaddingPixels = 8.0)
{
SvgRenderer svgRenderer = new()
{
@@ -131,9 +237,114 @@ public static SvgImage GetSvgQrCodeForText(string text, ErrorCorrectionLevel cor
SvgImage svg = barcodeWriter.Write(text);
+ // If a logo is provided, embed it in the SVG
+ if (logoImage != null)
+ {
+ // Get the QR code details to calculate module size
+ QRCode qrCode = ZXing.QrCode.Internal.Encoder.encode(text, correctionLevel);
+ int moduleCount = qrCode.Version.DimensionForVersion;
+ svg = EmbedLogoInSvg(svg, logoImage, logoSizePercentage, moduleCount, encodingOptions.Margin, logoPaddingPixels, background);
+ }
+
return svg;
}
+ private static SvgImage EmbedLogoInSvg(SvgImage svg, Bitmap logo, double sizePercentage, int moduleCount, int margin, double logoPaddingPixels, System.Drawing.Color backgroundColor)
+ {
+ const int svgSize = 1024; // Should match the encoding options Width/Height
+
+ // Calculate the pixel size of each QR code module
+ int totalModules = moduleCount + (margin * 2);
+ double modulePixelSize = (double)svgSize / totalModules;
+
+ // Calculate the punchout space size based on the size percentage
+ // This is the area that will be covered (blocking QR code modules)
+ int punchoutSizePixels = (int)(svgSize * (sizePercentage / 100.0));
+
+ // Round the punchout size to the nearest module boundary
+ int punchoutSizeModules = (int)Math.Round(punchoutSizePixels / modulePixelSize);
+ if (punchoutSizeModules < 1) punchoutSizeModules = 1;
+ if (punchoutSizeModules % 2 == 0) punchoutSizeModules++;
+
+ int punchoutSize = (int)(punchoutSizeModules * modulePixelSize);
+
+ // Calculate the position to center the punchout area
+ int punchoutX = (svgSize - punchoutSize) / 2;
+ int punchoutY = (svgSize - punchoutSize) / 2;
+
+ // Convert padding to actual pixels
+ // Positive padding = logo smaller than punchout (adds white space)
+ // Negative padding = logo larger than punchout (logo extends beyond white background)
+ int paddingPixels = (int)Math.Round(logoPaddingPixels);
+
+ // Calculate the actual logo display size
+ // Logo fills the punchout area minus the padding on all sides
+ int logoDisplayWidth = Math.Max(1, punchoutSize - (Math.Abs(paddingPixels) * 2));
+ int logoDisplayHeight = Math.Max(1, punchoutSize - (Math.Abs(paddingPixels) * 2));
+
+ // If padding is negative, logo is larger than punchout
+ if (paddingPixels < 0)
+ {
+ logoDisplayWidth = punchoutSize + (Math.Abs(paddingPixels) * 2);
+ logoDisplayHeight = punchoutSize + (Math.Abs(paddingPixels) * 2);
+ }
+
+ // Calculate logo dimensions preserving aspect ratio
+ float aspectRatio = (float)logo.Width / logo.Height;
+ int logoWidth, logoHeight;
+
+ if (aspectRatio > 1) // Wider than tall
+ {
+ logoWidth = logoDisplayWidth;
+ logoHeight = (int)(logoDisplayWidth / aspectRatio);
+ }
+ else // Taller than wide or square
+ {
+ logoHeight = logoDisplayHeight;
+ logoWidth = (int)(logoDisplayHeight * aspectRatio);
+ }
+
+ // Center the logo within the punchout area (or offset if larger)
+ int logoX = punchoutX + (punchoutSize - logoWidth) / 2;
+ int logoY = punchoutY + (punchoutSize - logoHeight) / 2;
+
+ // Resize the logo to the display size before encoding to reduce SVG file size
+ Bitmap resizedLogo = new(logoWidth, logoHeight);
+ using (Graphics g = Graphics.FromImage(resizedLogo))
+ {
+ g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
+ g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
+ g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
+ g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
+ g.DrawImage(logo, 0, 0, logoWidth, logoHeight);
+ }
+
+ // Convert resized logo to base64 for embedding
+ string base64Logo;
+ using (MemoryStream ms = new())
+ {
+ resizedLogo.Save(ms, ImageFormat.Png);
+ byte[] imageBytes = ms.ToArray();
+ base64Logo = Convert.ToBase64String(imageBytes);
+ }
+ resizedLogo.Dispose();
+
+ // Build the SVG logo element with punchout background and logo
+ // Convert the background color to RGB format for SVG
+ string backgroundColorHex = $"rgb({backgroundColor.R},{backgroundColor.G},{backgroundColor.B})";
+
+ string logoSvgElement = $@"
+
+
+
+ ";
+
+ // Find the closing tag and insert the logo before it
+ string modifiedContent = svg.Content.Replace("", logoSvgElement + "\n");
+
+ return new SvgImage(modifiedContent);
+ }
+
public static IEnumerable<(string, Result)> GetStringsFromImageFile(StorageFile storageFile)
{
Bitmap bitmap = new(storageFile.Path);
@@ -148,14 +359,14 @@ public static SvgImage GetSvgQrCodeForText(string text, ErrorCorrectionLevel cor
AutoRotate = true,
Options =
{
- TryHarder = true,
- TryInverted = true,
- }
+ TryHarder = true,
+ TryInverted = true,
+}
};
Result[] results = barcodeReader.DecodeMultiple(bitmap);
- List<(string, Result)> strings = new();
+ List<(string, Result)> strings = [];
if (results == null || results.Length == 0)
return strings;
diff --git a/Simple QR Code Maker/Models/BarcodeImageItem.cs b/Simple QR Code Maker/Models/BarcodeImageItem.cs
index b943b94..f0dc4c0 100644
--- a/Simple QR Code Maker/Models/BarcodeImageItem.cs
+++ b/Simple QR Code Maker/Models/BarcodeImageItem.cs
@@ -34,6 +34,12 @@ public partial class BarcodeImageItem : ObservableRecipient
public Windows.UI.Color BackgroundColor { get; set; }
+ public System.Drawing.Bitmap? LogoImage { get; set; }
+
+ public double LogoSizePercentage { get; set; } = 20.0;
+
+ public double LogoPaddingPixels { get; set; } = 8.0;
+
public QRCode QRCodeDetails => Encoder.encode(CodeAsText, ErrorCorrection);
public string ToolTipText => $"Smallest recommended size {SmallestSide}, {CodeAsText}";
@@ -63,7 +69,7 @@ public async Task SaveCodeAsSvgFile(StorageFile file, System.Drawing.Color
{
try
{
- SvgImage svgImage = BarcodeHelpers.GetSvgQrCodeForText(CodeAsText, correctionLevel, foreground, background);
+ SvgImage svgImage = BarcodeHelpers.GetSvgQrCodeForText(CodeAsText, correctionLevel, foreground, background, LogoImage, LogoSizePercentage, LogoPaddingPixels);
using IRandomAccessStream randomAccessStream = await file.OpenAsync(FileAccessMode.ReadWrite);
DataWriter dataWriter = new(randomAccessStream);
dataWriter.WriteString(svgImage.Content);
@@ -81,7 +87,7 @@ public string GetCodeAsSvgText(System.Drawing.Color foreground, System.Drawing.C
{
try
{
- SvgImage svgImage = BarcodeHelpers.GetSvgQrCodeForText(CodeAsText, correctionLevel, foreground, background);
+ SvgImage svgImage = BarcodeHelpers.GetSvgQrCodeForText(CodeAsText, correctionLevel, foreground, background, LogoImage, LogoSizePercentage, LogoPaddingPixels);
return svgImage.Content;
}
catch
@@ -126,6 +132,12 @@ private async Task SaveCodeSvgContext()
[RelayCommand]
private async Task CopyCodePngContext()
{
+ if (CodeAsBitmap is null)
+ {
+ WeakReferenceMessenger.Default.Send(new RequestShowMessage("Failed to copy QR Code to the clipboard", "No QR Code to copy to the clipboard", InfoBarSeverity.Error));
+ return;
+ }
+
StorageFolder folder = ApplicationData.Current.LocalCacheFolder;
List files = [];
diff --git a/Simple QR Code Maker/Models/HistoryItem.cs b/Simple QR Code Maker/Models/HistoryItem.cs
index f413320..729c77f 100644
--- a/Simple QR Code Maker/Models/HistoryItem.cs
+++ b/Simple QR Code Maker/Models/HistoryItem.cs
@@ -39,6 +39,12 @@ public ErrorCorrectionLevel ErrorCorrection
[JsonConverter(typeof(JsonStringEnumConverter))]
public BarcodeFormat Format { get; set; } = BarcodeFormat.QR_CODE;
+ public string? LogoImagePath { get; set; }
+
+ public double LogoSizePercentage { get; set; } = 15;
+
+ public double LogoPaddingPixels { get; set; } = 4.0;
+
public HistoryItem()
{
diff --git a/Simple QR Code Maker/Simple QR Code Maker.csproj b/Simple QR Code Maker/Simple QR Code Maker.csproj
index fa262d1..b4bec46 100644
--- a/Simple QR Code Maker/Simple QR Code Maker.csproj
+++ b/Simple QR Code Maker/Simple QR Code Maker.csproj
@@ -33,6 +33,7 @@
+
@@ -47,15 +48,15 @@
-
+
-
-
+
+
-
-
-
-
+
+
+
+
@@ -65,6 +66,9 @@
Always
+
+ MSBuild:Compile
+
MSBuild:Compile
diff --git a/Simple QR Code Maker/ViewModels/DecodingViewModel.cs b/Simple QR Code Maker/ViewModels/DecodingViewModel.cs
index 2058538..50ba22f 100644
--- a/Simple QR Code Maker/ViewModels/DecodingViewModel.cs
+++ b/Simple QR Code Maker/ViewModels/DecodingViewModel.cs
@@ -36,7 +36,7 @@ public partial class DecodingViewModel : ObservableRecipient, INavigationAware
[ObservableProperty]
private ObservableCollection decodingImageItems = [];
- private string passedUrl = string.Empty;
+ private HistoryItem? navigationHistoryItem = null;
private INavigationService NavigationService { get; }
@@ -78,8 +78,16 @@ private void CheckIfCanPaste()
public void OnNavigatedTo(object parameter)
{
- if (parameter is string url)
- passedUrl = url;
+ // Store the HistoryItem to pass back when returning to main page
+ if (parameter is HistoryItem historyItem)
+ {
+ navigationHistoryItem = historyItem;
+ }
+ // For backward compatibility, also handle string parameter
+ else if (parameter is string url)
+ {
+ navigationHistoryItem = new HistoryItem { CodesContent = url };
+ }
}
[RelayCommand]
@@ -127,7 +135,7 @@ private async Task TryLaunchLink()
[RelayCommand]
private void GoBack()
{
- NavigationService.NavigateTo(typeof(MainViewModel).FullName!, passedUrl);
+ NavigationService.NavigateTo(typeof(MainViewModel).FullName!, navigationHistoryItem);
}
[RelayCommand]
@@ -174,7 +182,21 @@ private void EditQrCode(object parameter)
if (string.IsNullOrWhiteSpace(InfoBarMessage))
return;
- NavigationService.NavigateTo(typeof(MainViewModel).FullName!, InfoBarMessage);
+ // Create a new HistoryItem with the decoded text, preserving other state if available
+ var editHistoryItem = navigationHistoryItem != null
+ ? new HistoryItem
+ {
+ CodesContent = InfoBarMessage,
+ Foreground = navigationHistoryItem.Foreground,
+ Background = navigationHistoryItem.Background,
+ ErrorCorrection = navigationHistoryItem.ErrorCorrection,
+ LogoImagePath = navigationHistoryItem.LogoImagePath,
+ LogoSizePercentage = navigationHistoryItem.LogoSizePercentage,
+ LogoPaddingPixels = navigationHistoryItem.LogoPaddingPixels,
+ }
+ : new HistoryItem { CodesContent = InfoBarMessage };
+
+ NavigationService.NavigateTo(typeof(MainViewModel).FullName!, editHistoryItem);
}
private void OpenAndDecodeBitmap(IRandomAccessStreamWithContentType streamWithContentType)
diff --git a/Simple QR Code Maker/ViewModels/MainViewModel.cs b/Simple QR Code Maker/ViewModels/MainViewModel.cs
index dc55f19..8f1c6bf 100644
--- a/Simple QR Code Maker/ViewModels/MainViewModel.cs
+++ b/Simple QR Code Maker/ViewModels/MainViewModel.cs
@@ -10,9 +10,11 @@
using Simple_QR_Code_Maker.Helpers;
using Simple_QR_Code_Maker.Models;
using System.Collections.ObjectModel;
+using System.IO;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
using Windows.Storage.Pickers;
+using Windows.Storage.Streams;
using WinRT.Interop;
using ZXing.QrCode.Internal;
@@ -86,6 +88,23 @@ public partial class MainViewModel : ObservableRecipient, INavigationAware
[ObservableProperty]
private bool copySharePopupOpen = false;
+ [ObservableProperty]
+ private System.Drawing.Bitmap? logoImage = null;
+
+ [ObservableProperty]
+ private bool hasLogo = false;
+
+ [ObservableProperty]
+ private double logoPaddingPixels = 4.0;
+
+ [ObservableProperty]
+ private double logoSizePercentage = 15;
+
+ [ObservableProperty]
+ private double logoSizeMaxPercentage = 20;
+
+ private string? currentLogoPath = null;
+
private double MinSizeScanDistanceScaleFactor = 1;
private readonly DispatcherTimer copyInfoBarTimer = new();
@@ -100,11 +119,52 @@ partial void OnSelectedHistoryItemChanged(HistoryItem? value)
ForegroundColor = value.Foreground;
BackgroundColor = value.Background;
SelectedOption = ErrorCorrectionLevels.First(x => x.ErrorCorrectionLevel == value.ErrorCorrection);
+
+ // Restore logo image and size if available
+ if (!string.IsNullOrEmpty(value.LogoImagePath))
+ {
+ _ = LoadLogoFromHistory(value.LogoImagePath);
+ }
+ else
+ {
+ // Clear logo if history item has no logo
+ LogoImage?.Dispose();
+ LogoImage = null;
+ }
+
+ LogoSizePercentage = value.LogoSizePercentage;
+ LogoPaddingPixels = value.LogoPaddingPixels;
SelectedHistoryItem = null;
}
+
+ private async Task LoadLogoFromHistory(string logoPath)
+ {
+ try
+ {
+ if (File.Exists(logoPath))
+ {
+ // Dispose old logo first
+ LogoImage?.Dispose();
+
+ StorageFile file = await StorageFile.GetFileFromPathAsync(logoPath);
+ using var stream = await file.OpenReadAsync();
+ LogoImage = new System.Drawing.Bitmap(stream.AsStreamForRead());
+ currentLogoPath = logoPath;
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to load logo from history: {ex.Message}");
+ LogoImage?.Dispose();
+ LogoImage = null;
+ currentLogoPath = null;
+ }
+ }
- public List ErrorCorrectionLevels { get; } =
+ public ObservableCollection ErrorCorrectionLevels { get; set; } = new(allCorrectionLevels);
+
+ private static readonly List allCorrectionLevels =
[
new("Low 7%", ErrorCorrectionLevel.L),
new("Medium 15%", ErrorCorrectionLevel.M),
@@ -114,6 +174,15 @@ partial void OnSelectedHistoryItemChanged(HistoryItem? value)
partial void OnSelectedOptionChanged(ErrorCorrectionOptions value)
{
+ // Ensure logo size doesn't exceed the new error correction level's maximum
+ if (logoSizePercentage > logoSizeMaxPercentage)
+ {
+ logoSizePercentage = logoSizeMaxPercentage;
+ }
+ OnPropertyChanged(nameof(logoSizeMaxPercentage));
+
+ LogoSizeMaxPercentage = BarcodeHelpers.GetMaxLogoSizePercentage(value.ErrorCorrectionLevel);
+
debounceTimer.Stop();
debounceTimer.Start();
}
@@ -130,10 +199,58 @@ partial void OnForegroundColorChanged(Windows.UI.Color value)
debounceTimer.Start();
}
- public bool CanSaveImage { get => !string.IsNullOrWhiteSpace(UrlText); }
+ partial void OnLogoImageChanged(System.Drawing.Bitmap? value)
+ {
+ HasLogo = value != null;
+
+
+ if (HasLogo)
+ {
+ if (SelectedOption.ErrorCorrectionLevel == ErrorCorrectionLevel.L && ErrorCorrectionLevels.Count > 1)
+ {
+ SelectedOption = ErrorCorrectionLevels[1];
+ }
+
+ if (ErrorCorrectionLevels.Count > 0
+ && ErrorCorrectionLevels[0].ErrorCorrectionLevel == ErrorCorrectionLevel.L)
+ ErrorCorrectionLevels.RemoveAt(0);
+ }
+ else
+ {
+ if (ErrorCorrectionLevels.Count == 3)
+ ErrorCorrectionLevels.Insert(0,new("Low 7%", ErrorCorrectionLevel.L));
+ }
+
+
+ debounceTimer.Stop();
+ debounceTimer.Start();
+ }
+
+ partial void OnLogoSizePercentageChanged(double value)
+ {
+ debounceTimer.Stop();
+ debounceTimer.Start();
+ }
+
+ partial void OnLogoPaddingPixelsChanged(double value)
+ {
+ debounceTimer.Stop();
+ debounceTimer.Start();
+ }
+
+ public bool CanSaveImage => !string.IsNullOrWhiteSpace(UrlText);
partial void OnUrlTextChanged(string value)
{
+ // Update max logo size when text changes (affects QR version/density)
+ OnPropertyChanged(nameof(logoSizeMaxPercentage));
+
+ // Ensure current logo size doesn't exceed the new maximum
+ if (logoSizePercentage > logoSizeMaxPercentage)
+ {
+ logoSizePercentage = logoSizeMaxPercentage;
+ }
+
debounceTimer.Stop();
debounceTimer.Start();
}
@@ -207,10 +324,7 @@ private void OnRequestPaneChange(object recipient, RequestPaneChange message)
}
}
- private void OnSaveHistoryMessage(object recipient, SaveHistoryMessage message)
- {
- SaveCurrentStateToHistory();
- }
+ private async void OnSaveHistoryMessage(object recipient, SaveHistoryMessage message) => await SaveCurrentStateToHistory();
private void OnRequestShowMessage(object recipient, RequestShowMessage rsm)
{
@@ -249,6 +363,9 @@ private void CheckCanPasteText()
debounceTimer.Tick -= DebounceTimer_Tick;
Clipboard.ContentChanged -= Clipboard_ContentChanged;
+
+ // Dispose of the logo image
+ LogoImage?.Dispose();
}
private void PlaceholderTextTimer_Tick(object? sender, object e)
@@ -289,17 +406,23 @@ private void GenerateQrCodeFromOneLine(string text)
textToUse,
SelectedOption.ErrorCorrectionLevel,
ForegroundColor.ToSystemDrawingColor(),
- BackgroundColor.ToSystemDrawingColor());
+ BackgroundColor.ToSystemDrawingColor(),
+ LogoImage,
+ LogoSizePercentage,
+ LogoPaddingPixels);
BarcodeImageItem barcodeImageItem = new()
{
CodeAsBitmap = bitmap,
CodeAsText = textToUse,
IsAppShowingUrlWarnings = WarnWhenNotUrl,
- SizeTextVisible = HideMinimumSizeText ? Visibility.Collapsed : Visibility.Visible,
+ SizeTextVisible = (HideMinimumSizeText || LogoImage != null) ? Visibility.Collapsed : Visibility.Visible,
ErrorCorrection = SelectedOption.ErrorCorrectionLevel,
ForegroundColor = ForegroundColor,
BackgroundColor = BackgroundColor,
MaxSizeScaleFactor = MinSizeScanDistanceScaleFactor,
+ LogoImage = LogoImage,
+ LogoSizePercentage = LogoSizePercentage,
+ LogoPaddingPixels = LogoPaddingPixels,
};
double ratio = barcodeImageItem.ColorContrastRatio;
@@ -345,7 +468,7 @@ private async Task CopyPngToClipboard()
if (QrCodeBitmaps.Count == 0)
return;
- SaveCurrentStateToHistory();
+ await SaveCurrentStateToHistory();
StorageFolder folder = ApplicationData.Current.LocalCacheFolder;
List files = [];
@@ -394,7 +517,7 @@ private async Task CopySvgToClipboard()
if (QrCodeBitmaps.Count == 0)
return;
- SaveCurrentStateToHistory();
+ await SaveCurrentStateToHistory();
StorageFolder folder = ApplicationData.Current.LocalCacheFolder;
List files = [];
@@ -438,12 +561,12 @@ private async Task CopySvgToClipboard()
}
[RelayCommand]
- private void CopySvgTextToClipboard()
+ private async Task CopySvgTextToClipboard()
{
if (QrCodeBitmaps.Count == 0)
return;
- SaveCurrentStateToHistory();
+ await SaveCurrentStateToHistory();
List textStrings = [];
foreach (BarcodeImageItem qrCodeItem in QrCodeBitmaps)
@@ -492,35 +615,40 @@ private void CopyInfoBarTimer_Tick(object? sender, object e)
}
[RelayCommand]
- private void ToggleFaqPaneOpen()
- {
- IsFaqPaneOpen = !IsFaqPaneOpen;
- }
+ private void ToggleFaqPaneOpen() => IsFaqPaneOpen = !IsFaqPaneOpen;
[RelayCommand]
- private void ToggleHistoryPaneOpen()
- {
- IsHistoryPaneOpen = !IsHistoryPaneOpen;
- }
+ private void ToggleHistoryPaneOpen() => IsHistoryPaneOpen = !IsHistoryPaneOpen;
[RelayCommand]
- private void ShareApp()
- {
- CopySharePopupOpen = !CopySharePopupOpen;
- }
+ private void ShareApp() => CopySharePopupOpen = !CopySharePopupOpen;
[RelayCommand]
- private void OpenFile()
- {
- NavigationService.NavigateTo(typeof(DecodingViewModel).FullName!, UrlText);
- }
+ private void OpenFile() => NavigationService.NavigateTo(typeof(DecodingViewModel).FullName!, CreateCurrentStateHistoryItem());
[RelayCommand]
- private void GoToSettings()
+ private void GoToSettings() =>
+ // pass the current state as a HistoryItem to the settings page
+ // so when coming back it can be fully restored
+ NavigationService.NavigateTo(typeof(SettingsViewModel).FullName!, CreateCurrentStateHistoryItem());
+
+ private HistoryItem CreateCurrentStateHistoryItem()
+ {
+ return new HistoryItem
+ {
+ CodesContent = UrlText,
+ Foreground = ForegroundColor,
+ Background = BackgroundColor,
+ ErrorCorrection = SelectedOption.ErrorCorrectionLevel,
+ LogoImagePath = LogoImage != null ? GetCurrentLogoPath() : null,
+ LogoSizePercentage = LogoSizePercentage,
+ LogoPaddingPixels = LogoPaddingPixels,
+ };
+ }
+
+ private string? GetCurrentLogoPath()
{
- // pass the contents of the UrlText to the settings page
- // so when coming back it can be rehydrated
- NavigationService.NavigateTo(typeof(SettingsViewModel).FullName!, UrlText);
+ return currentLogoPath;
}
[RelayCommand]
@@ -529,7 +657,7 @@ private async Task SavePng()
if (QrCodeBitmaps.Count == 0)
return;
- SaveCurrentStateToHistory();
+ await SaveCurrentStateToHistory();
await SaveAllFiles(FileKind.PNG);
@@ -548,7 +676,7 @@ private async Task SaveSvg()
if (QrCodeBitmaps.Count == 0)
return;
- SaveCurrentStateToHistory();
+ await SaveCurrentStateToHistory();
await SaveAllFiles(FileKind.SVG);
@@ -574,6 +702,55 @@ private void AddNewLine()
UrlText += $"\r{stringToAdd}";
}
+ [RelayCommand]
+ private async Task SelectLogo()
+ {
+ FileOpenPicker openPicker = new()
+ {
+ SuggestedStartLocation = PickerLocationId.PicturesLibrary,
+ };
+ openPicker.FileTypeFilter.Add(".png");
+ openPicker.FileTypeFilter.Add(".jpg");
+ openPicker.FileTypeFilter.Add(".jpeg");
+ openPicker.FileTypeFilter.Add(".bmp");
+ openPicker.FileTypeFilter.Add(".gif");
+
+ Window window = new();
+ IntPtr windowHandle = WindowNative.GetWindowHandle(window);
+ InitializeWithWindow.Initialize(openPicker, windowHandle);
+
+ StorageFile file = await openPicker.PickSingleFileAsync();
+
+ if (file is null)
+ return;
+
+ try
+ {
+ // Dispose of the old logo if it exists
+ LogoImage?.Dispose();
+
+ // Use stream to access file instead of direct path for better compatibility
+ using IRandomAccessStreamWithContentType stream = await file.OpenReadAsync();
+ LogoImage = new System.Drawing.Bitmap(stream.AsStreamForRead());
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to load logo image: {ex.Message}");
+ CodeInfoBarMessage = "Failed to load the selected image";
+ ShowCodeInfoBar = true;
+ CodeInfoBarSeverity = InfoBarSeverity.Error;
+ CodeInfoBarTitle = "Error loading logo";
+ }
+ }
+
+ [RelayCommand]
+ private void RemoveLogo()
+ {
+ LogoImage?.Dispose();
+ LogoImage = null;
+ currentLogoPath = null;
+ }
+
private async Task WriteImageToFile(BarcodeImageItem imageItem, StorageFile file, FileKind kindOfFile)
{
switch (kindOfFile)
@@ -632,6 +809,7 @@ public async Task SaveAllFiles(FileKind kindOfFile)
public async void OnNavigatedTo(object parameter)
{
await LoadHistory();
+ CheckCanPasteText();
MultiLineCodeMode = await LocalSettingsService.ReadSettingAsync(nameof(MultiLineCodeMode));
BaseText = await LocalSettingsService.ReadSettingAsync(nameof(BaseText)) ?? string.Empty;
UrlText = BaseText;
@@ -645,47 +823,175 @@ public async void OnNavigatedTo(object parameter)
await LocalSettingsService.SaveSettingAsync(nameof(MinSizeScanDistanceScaleFactor), MinSizeScanDistanceScaleFactor);
}
- // check on text rehydration, could be coming from Reading or Settings
- if (parameter is string textParam && !string.IsNullOrWhiteSpace(textParam))
+ // Check if parameter is a HistoryItem with full state restoration
+ if (parameter is HistoryItem historyItem)
+ {
+ RestoreFromHistoryItem(historyItem);
+ }
+ // Otherwise check for text rehydration from other pages
+ else if (parameter is string textParam && !string.IsNullOrWhiteSpace(textParam))
+ {
UrlText = textParam;
+ }
+ }
+
+ private void RestoreFromHistoryItem(HistoryItem historyItem)
+ {
+ UrlText = historyItem.CodesContent;
+ ForegroundColor = historyItem.Foreground;
+ BackgroundColor = historyItem.Background;
+ SelectedOption = ErrorCorrectionLevels.First(x => x.ErrorCorrectionLevel == historyItem.ErrorCorrection);
+
+ // Restore logo image and settings if available
+ if (!string.IsNullOrEmpty(historyItem.LogoImagePath))
+ {
+ _ = LoadLogoFromHistory(historyItem.LogoImagePath);
+ }
+ else
+ {
+ // Clear logo if history item has no logo
+ LogoImage?.Dispose();
+ LogoImage = null;
+ currentLogoPath = null;
+ }
+
+ LogoSizePercentage = historyItem.LogoSizePercentage;
+ LogoPaddingPixels = historyItem.LogoPaddingPixels;
}
public void OnNavigatedFrom()
{
if (!string.IsNullOrWhiteSpace(UrlText))
- SaveCurrentStateToHistory();
+ _ = SaveCurrentStateToHistory();
}
- public void SaveCurrentStateToHistory()
+ public async Task SaveCurrentStateToHistory()
{
+ string? logoImagePath = null;
+
+ // Save logo image to local app storage if present
+ if (LogoImage != null)
+ {
+ try
+ {
+ StorageFolder logoFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync("LogoImages", CreationCollisionOption.OpenIfExists);
+ string fileName = $"logo_{DateTime.Now:yyyyMMddHHmmss}_{Guid.NewGuid():N}.png";
+ StorageFile logoFile = await logoFolder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);
+
+ using (var stream = await logoFile.OpenAsync(FileAccessMode.ReadWrite))
+ {
+ using (var outputStream = stream.GetOutputStreamAt(0))
+ {
+ using (var dataWriter = new DataWriter(outputStream))
+ {
+ using (MemoryStream ms = new())
+ {
+ LogoImage.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
+ byte[] bytes = ms.ToArray();
+ dataWriter.WriteBytes(bytes);
+ await dataWriter.StoreAsync();
+ }
+ }
+ }
+ }
+
+ logoImagePath = logoFile.Path;
+ currentLogoPath = logoImagePath; // Update current logo path
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to save logo image: {ex.Message}");
+ }
+ }
+
HistoryItem historyItem = new()
{
CodesContent = UrlText,
Foreground = ForegroundColor,
Background = BackgroundColor,
ErrorCorrection = SelectedOption.ErrorCorrectionLevel,
+ LogoImagePath = logoImagePath ?? currentLogoPath, // Use saved path or current path
+ LogoSizePercentage = LogoSizePercentage,
+ LogoPaddingPixels = LogoPaddingPixels,
};
HistoryItems.Remove(historyItem);
HistoryItems.Insert(0, historyItem);
- LocalSettingsService.SaveSettingAsync(nameof(HistoryItems), HistoryItems);
+ await SaveHistoryToFile();
}
- private async Task LoadHistory()
+ private async Task SaveHistoryToFile()
{
- ObservableCollection? prevHistory = null;
+ try
+ {
+ StorageFolder localFolder = ApplicationData.Current.LocalFolder;
+ StorageFile historyFile = await localFolder.CreateFileAsync("History.json", CreationCollisionOption.ReplaceExisting);
+
+ string json = System.Text.Json.JsonSerializer.Serialize(HistoryItems, new System.Text.Json.JsonSerializerOptions
+ {
+ WriteIndented = true
+ });
+
+ await FileIO.WriteTextAsync(historyFile, json);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to save history to file: {ex.Message}");
+ }
+ }
+ private async Task LoadHistory()
+ {
+ ObservableCollection? historyFromFile = null;
+
+ // First, try to load from file (new location)
try
{
- prevHistory = await LocalSettingsService.ReadSettingAsync>(nameof(HistoryItems));
+ StorageFolder localFolder = ApplicationData.Current.LocalFolder;
+ StorageFile historyFile = await localFolder.GetFileAsync("History.json");
+ string json = await FileIO.ReadTextAsync(historyFile);
+ historyFromFile = System.Text.Json.JsonSerializer.Deserialize>(json);
+ }
+ catch
+ {
+ // File doesn't exist yet, that's okay
}
- catch { }
- if (prevHistory is null || prevHistory.Count == 0)
+ // If we found history in the file, use it
+ if (historyFromFile != null && historyFromFile.Count > 0)
+ {
+ foreach (HistoryItem hisItem in historyFromFile)
+ HistoryItems.Add(hisItem);
return;
+ }
+
+ // Otherwise, try to migrate from old settings location
+ ObservableCollection? historyFromSettings = null;
+ try
+ {
+ historyFromSettings = await LocalSettingsService.ReadSettingAsync>(nameof(HistoryItems));
+ }
+ catch { }
- foreach (HistoryItem hisItem in prevHistory)
- HistoryItems.Add(hisItem);
+ if (historyFromSettings != null && historyFromSettings.Count > 0)
+ {
+ // Migrate: add to collection and save to file
+ foreach (HistoryItem hisItem in historyFromSettings)
+ HistoryItems.Add(hisItem);
+
+ // Save to new file location
+ await SaveHistoryToFile();
+
+ // Clear from old settings location to free up space
+ try
+ {
+ await LocalSettingsService.SaveSettingAsync(nameof(HistoryItems), new ObservableCollection());
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Failed to clear old history from settings: {ex.Message}");
+ }
+ }
}
}
diff --git a/Simple QR Code Maker/ViewModels/SettingsViewModel.cs b/Simple QR Code Maker/ViewModels/SettingsViewModel.cs
index 8f67bc4..1e6b8c3 100644
--- a/Simple QR Code Maker/ViewModels/SettingsViewModel.cs
+++ b/Simple QR Code Maker/ViewModels/SettingsViewModel.cs
@@ -4,6 +4,7 @@
using Simple_QR_Code_Maker.Contracts.Services;
using Simple_QR_Code_Maker.Contracts.ViewModels;
using Simple_QR_Code_Maker.Helpers;
+using Simple_QR_Code_Maker.Models;
using System.Globalization;
using System.Reflection;
using System.Windows.Input;
@@ -41,7 +42,7 @@ public partial class SettingsViewModel : ObservableRecipient, INavigationAware
private readonly DispatcherTimer settingChangedDebounceTimer = new();
- private string navigationText = string.Empty;
+ private HistoryItem? navigationHistoryItem = null;
private INavigationService NavigationService { get; }
public ILocalSettingsService LocalSettingsService { get; }
@@ -142,7 +143,7 @@ partial void OnMinSizeScanDistanceScaleFactorChanged(double value)
[RelayCommand]
private void GoHome()
{
- NavigationService.NavigateTo(typeof(MainViewModel).FullName!, navigationText);
+ NavigationService.NavigateTo(typeof(MainViewModel).FullName!, navigationHistoryItem);
}
[RelayCommand]
@@ -183,9 +184,15 @@ public async void OnNavigatedTo(object parameter)
HideMinimumSizeText = await LocalSettingsService.ReadSettingAsync(nameof(HideMinimumSizeText));
MinSizeScanDistanceScaleFactor = await LocalSettingsService.ReadSettingAsync(nameof(MinSizeScanDistanceScaleFactor));
- if (parameter is string urlText && !string.IsNullOrWhiteSpace(urlText))
+ // Store the HistoryItem to pass back when returning to main page
+ if (parameter is HistoryItem historyItem)
{
- navigationText = urlText;
+ navigationHistoryItem = historyItem;
+ }
+ // For backward compatibility, also handle string parameter
+ else if (parameter is string urlText && !string.IsNullOrWhiteSpace(urlText))
+ {
+ navigationHistoryItem = new HistoryItem { CodesContent = urlText };
}
}
diff --git a/Simple QR Code Maker/Views/MainPage.xaml b/Simple QR Code Maker/Views/MainPage.xaml
index d950138..2c239db 100644
--- a/Simple QR Code Maker/Views/MainPage.xaml
+++ b/Simple QR Code Maker/Views/MainPage.xaml
@@ -24,9 +24,7 @@
@@ -35,9 +33,7 @@
@@ -58,12 +54,9 @@
MinWidth="258"
MaxWidth="800"
AcceptsReturn="True"
- PlaceholderText="{x:Bind ViewModel.PlaceholderText,
- Mode=OneWay}"
+ PlaceholderText="{x:Bind ViewModel.PlaceholderText, Mode=OneWay}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
- Text="{x:Bind ViewModel.UrlText,
- Mode=TwoWay,
- UpdateSourceTrigger=PropertyChanged}"
+ Text="{x:Bind ViewModel.UrlText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
TextChanged="UrlTextBox_TextChanged"
TextWrapping="NoWrap" />
@@ -75,9 +68,7 @@
VerticalAlignment="Bottom"
Command="{x:Bind ViewModel.AddNewLineCommand}"
ToolTipService.ToolTip="Add a return and start making another QR Code"
- Visibility="{x:Bind ViewModel.CanSaveImage,
- Converter={StaticResource BoolToVisibility},
- Mode=OneWay}">
+ Visibility="{x:Bind ViewModel.CanSaveImage, Converter={StaticResource BoolToVisibility}, Mode=OneWay}">
@@ -89,13 +80,9 @@
Padding="0"
VerticalAlignment="Bottom"
Command="{x:Bind ViewModel.PasteTextIntoUrlTextCommand}"
- IsEnabled="{x:Bind ViewModel.CanPasteText,
- Mode=OneWay}"
+ IsEnabled="{x:Bind ViewModel.CanPasteText, Mode=OneWay}"
ToolTipService.ToolTip="Paste text from clipboard"
- Visibility="{x:Bind ViewModel.CanSaveImage,
- Converter={StaticResource BoolToVisibility},
- ConverterParameter=True,
- Mode=OneWay}">
+ Visibility="{x:Bind ViewModel.CanSaveImage, Converter={StaticResource BoolToVisibility}, ConverterParameter=True, Mode=OneWay}">
@@ -107,11 +94,9 @@
x:Name="QrCodeOptions"
HorizontalAlignment="Center"
Orientation="Horizontal"
- Spacing="12"
- Visibility="{x:Bind ViewModel.CanSaveImage,
- Mode=OneWay,
- Converter={StaticResource BoolToVisibility}}">
-
@@ -354,9 +388,7 @@
+ SelectedItem="{x:Bind ViewModel.SelectedHistoryItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">