From f39e154a77da30b31f91c58042e15daebeea267b Mon Sep 17 00:00:00 2001 From: kamilla11 Date: Tue, 26 May 2026 12:12:17 +0300 Subject: [PATCH] Replace string concatenation with StringBuilder and invariant interpolation --- .../InterpolatedStringHandlerAttributes.cs | 80 ++++++++ QRCoder/Extensions/StringExtensions.cs | 20 +- QRCoder/PayloadGenerator.cs | 15 +- QRCoder/PayloadGenerator/BezahlCode.cs | 58 +++--- .../BitcoinLikeCryptoCurrencyAddress.cs | 4 +- QRCoder/PayloadGenerator/ContactData.cs | 188 ++++++++---------- QRCoder/PayloadGenerator/Girocode.cs | 34 ++-- QRCoder/PayloadGenerator/Mail.cs | 4 +- QRCoder/PayloadGenerator/OneTimePassword.cs | 14 +- .../PayloadGenerator/RussiaPaymentOrder.cs | 15 +- QRCoder/PayloadGenerator/SwissQrCode.cs | 76 +++---- QRCoder/PayloadGenerator/Url.cs | 2 +- QRCoder/PayloadGenerator/WiFi.cs | 4 +- QRCoder/PdfByteQRCode.cs | 134 ++++++------- QRCoder/QRCodeGenerator/Polynom.cs | 2 +- 15 files changed, 374 insertions(+), 276 deletions(-) create mode 100644 QRCoder/Attributes/InterpolatedStringHandlerAttributes.cs diff --git a/QRCoder/Attributes/InterpolatedStringHandlerAttributes.cs b/QRCoder/Attributes/InterpolatedStringHandlerAttributes.cs new file mode 100644 index 00000000..5a91b22f --- /dev/null +++ b/QRCoder/Attributes/InterpolatedStringHandlerAttributes.cs @@ -0,0 +1,80 @@ +#if !NET6_0_OR_GREATER +namespace System.Runtime.CompilerServices; + +/// +/// Indicates the attributed type is an interpolated string handler. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)] +internal sealed class InterpolatedStringHandlerAttribute : Attribute +{ +} + +/// +/// Indicates which arguments an interpolated string handler passes through to the underlying handler constructor. +/// +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +internal sealed class InterpolatedStringHandlerArgumentAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the argument that should be passed to the handler. + public InterpolatedStringHandlerArgumentAttribute(string argument) + { + Arguments = new[] { argument }; + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the first argument that should be passed to the handler. + /// The name of the second argument that should be passed to the handler. + public InterpolatedStringHandlerArgumentAttribute(string argument1, string argument2) + { + Arguments = new[] { argument1, argument2 }; + } + + /// + /// Gets the arguments that should be passed to the handler. + /// + public string[] Arguments { get; } +} + +/// +/// Indicates that compiler support for a particular feature is required for the location where this attribute is applied. +/// +[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] +internal sealed class CompilerFeatureRequiredAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the compiler feature. + public CompilerFeatureRequiredAttribute(string featureName) + { + FeatureName = featureName; + } + + /// + /// Gets the name of the compiler feature. + /// + public string FeatureName { get; } + + /// + /// Gets a value that indicates whether the compiler can choose to allow access if it does not understand . + /// + public bool IsOptional { get; set; } + + /// + /// The feature name used for ref structs. + /// +#pragma warning disable IDE1006 // Must match the BCL constant names + public const string RefStructs = nameof(RefStructs); + + /// + /// The feature name used for required members. + /// + public const string RequiredMembers = nameof(RequiredMembers); +#pragma warning restore IDE1006 +} +#endif diff --git a/QRCoder/Extensions/StringExtensions.cs b/QRCoder/Extensions/StringExtensions.cs index 6ad02de9..8857fb0c 100644 --- a/QRCoder/Extensions/StringExtensions.cs +++ b/QRCoder/Extensions/StringExtensions.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace QRCoder; @@ -67,6 +68,21 @@ internal static string ToString(this char c, CultureInfo _) => c.ToString(); #endif + /// + /// Appends an interpolated string using invariant culture formatting. + /// On .NET 6+ appends via the invariant-culture interpolated-string handler overload (no extra string allocation). + /// Must be a static method (not an extension) so the handler can reference the parameter (CS8944). + /// +#if NET6_0_OR_GREATER + internal static void AppendInvariant( + StringBuilder sb, + [InterpolatedStringHandlerArgument(nameof(sb))] ref StringBuilder.AppendInterpolatedStringHandler handler) + => sb.Append(CultureInfo.InvariantCulture, ref handler); +#else + internal static void AppendInvariant(StringBuilder sb, string value) + => sb.Append(value); +#endif + /// /// Appends an integer value to the StringBuilder using invariant culture formatting. /// @@ -75,7 +91,7 @@ internal static string ToString(this char c, CultureInfo _) internal static void AppendInvariant(this StringBuilder sb, int num) { #if NET6_0_OR_GREATER - sb.Append(CultureInfo.InvariantCulture, $"{num}"); + AppendInvariant(sb, $"{num}"); #else #if HAS_SPAN Span buffer = stackalloc char[16]; @@ -97,7 +113,7 @@ internal static void AppendInvariant(this StringBuilder sb, int num) internal static void AppendInvariant(this StringBuilder sb, float num) { #if NET6_0_OR_GREATER - sb.Append(CultureInfo.InvariantCulture, $"{num:G7}"); + AppendInvariant(sb, $"{num:G7}"); #else #if HAS_SPAN Span buffer = stackalloc char[16]; diff --git a/QRCoder/PayloadGenerator.cs b/QRCoder/PayloadGenerator.cs index 1bb15d81..71681a19 100644 --- a/QRCoder/PayloadGenerator.cs +++ b/QRCoder/PayloadGenerator.cs @@ -24,7 +24,16 @@ internal static bool IsValidIban(string iban) //Check IBAN checksum var checksumValid = false; - var sum = $"{ibanCleared.Substring(4)}{ibanCleared.Substring(0, 4)}".ToCharArray().Aggregate("", (current, c) => current + (char.IsLetter(c) ? (c - 55).ToString(CultureInfo.InvariantCulture) : c.ToString(CultureInfo.InvariantCulture))); + var sumChars = $"{ibanCleared.Substring(4)}{ibanCleared.Substring(0, 4)}".ToCharArray(); + var sumBuilder = new StringBuilder(sumChars.Length * 2); + foreach (var c in sumChars) + { + if (char.IsLetter(c)) + sumBuilder.Append((c - 55).ToString(CultureInfo.InvariantCulture)); + else + sumBuilder.Append(c); + } + var sum = sumBuilder.ToString(); int m = 0; for (int i = 0; i < (int)Math.Ceiling((sum.Length - 2) / 7d); i++) { @@ -33,7 +42,7 @@ internal static bool IsValidIban(string iban) #if NET5_0_OR_GREATER var n = string.Concat(i == 0 ? "" : m.ToString(CultureInfo.InvariantCulture), sum.AsSpan(start, Math.Min(9 - offset, sum.Length - start))); #else - var n = (i == 0 ? "" : m.ToString(CultureInfo.InvariantCulture)) + sum.Substring(start, Math.Min(9 - offset, sum.Length - start)); + var n = $"{(i == 0 ? "" : m.ToString(CultureInfo.InvariantCulture))}{sum.Substring(start, Math.Min(9 - offset, sum.Length - start))}"; #endif if (!int.TryParse(n, NumberStyles.Any, CultureInfo.InvariantCulture, out m)) break; @@ -112,7 +121,7 @@ private static string EscapeInput(string inp, bool simple = false) } foreach (var c in forbiddenChars) { - inp = inp.Replace(c.ToString(), "\\" + c); + inp = inp.Replace(c.ToString(), $"\\{c}"); } return inp; } diff --git a/QRCoder/PayloadGenerator/BezahlCode.cs b/QRCoder/PayloadGenerator/BezahlCode.cs index ba617fb5..63d398ad 100644 --- a/QRCoder/PayloadGenerator/BezahlCode.cs +++ b/QRCoder/PayloadGenerator/BezahlCode.cs @@ -148,7 +148,7 @@ public BezahlCode(AuthorityType authority, string name, string account, string b _name = name; //Limit reason length depending on payment type - //140 chars for SEPA payments and 27 chars for others + //140 chars for SEPA payments and 27 chars for others var reasonLength = authority == AuthorityType.periodicsinglepaymentsepa || authority == AuthorityType.singledirectdebitsepa || authority == AuthorityType.singlepaymentsepa || (authority == AuthorityType.contact_v2 && newWayFilled) ? 140 : 27; if (reason.Length > reasonLength) throw new BezahlCodeException($"Reasons texts have to be shorter than {reasonLength + 1} chars."); @@ -246,9 +246,8 @@ public BezahlCode(AuthorityType authority, string name, string account, string b /// public override string ToString() { - var bezahlCodePayload = $"bank://{_authority}?"; - - bezahlCodePayload += $"name={Uri.EscapeDataString(_name)}&"; + var bezahlCodePayload = new StringBuilder($"bank://{_authority}?"); + StringExtensions.AppendInvariant(bezahlCodePayload,$"name={Uri.EscapeDataString(_name)}&"); if (_authority != AuthorityType.contact && _authority != AuthorityType.contact_v2) { @@ -257,44 +256,44 @@ public override string ToString() if (_authority == AuthorityType.periodicsinglepayment || _authority == AuthorityType.singledirectdebit || _authority == AuthorityType.singlepayment) #pragma warning restore CS0618 { - bezahlCodePayload += $"account={_account}&"; - bezahlCodePayload += $"bnc={_bnc}&"; + StringExtensions.AppendInvariant(bezahlCodePayload,$"account={_account}&"); + StringExtensions.AppendInvariant(bezahlCodePayload,$"bnc={_bnc}&"); if (_postingKey > 0) - bezahlCodePayload += $"postingkey={_postingKey}&"; + StringExtensions.AppendInvariant(bezahlCodePayload,$"postingkey={_postingKey}&"); } else { - bezahlCodePayload += $"iban={_iban}&"; - bezahlCodePayload += $"bic={_bic}&"; + StringExtensions.AppendInvariant(bezahlCodePayload,$"iban={_iban}&"); + StringExtensions.AppendInvariant(bezahlCodePayload,$"bic={_bic}&"); if (!string.IsNullOrEmpty(_sepaReference)) - bezahlCodePayload += $"separeference={Uri.EscapeDataString(_sepaReference)}&"; + StringExtensions.AppendInvariant(bezahlCodePayload,$"separeference={Uri.EscapeDataString(_sepaReference)}&"); if (_authority == AuthorityType.singledirectdebitsepa) { if (!string.IsNullOrEmpty(_creditorId)) - bezahlCodePayload += $"creditorid={Uri.EscapeDataString(_creditorId)}&"; + StringExtensions.AppendInvariant(bezahlCodePayload,$"creditorid={Uri.EscapeDataString(_creditorId)}&"); if (!string.IsNullOrEmpty(_mandateId)) - bezahlCodePayload += $"mandateid={Uri.EscapeDataString(_mandateId)}&"; + StringExtensions.AppendInvariant(bezahlCodePayload,$"mandateid={Uri.EscapeDataString(_mandateId)}&"); if (_dateOfSignature != DateTime.MinValue) - bezahlCodePayload += $"dateofsignature={_dateOfSignature.ToString("ddMMyyyy", CultureInfo.InvariantCulture)}&"; + StringExtensions.AppendInvariant(bezahlCodePayload,$"dateofsignature={_dateOfSignature.ToString("ddMMyyyy", CultureInfo.InvariantCulture)}&"); } } - bezahlCodePayload += string.Format(CultureInfo.InvariantCulture, "amount={0:0.00}&", _amount).Replace(".", ","); + bezahlCodePayload.Append(string.Format(CultureInfo.InvariantCulture, "amount={0:0.00}&", _amount).Replace(".", ",")); if (!string.IsNullOrEmpty(_reason)) - bezahlCodePayload += $"reason={Uri.EscapeDataString(_reason)}&"; - bezahlCodePayload += $"currency={_currency}&"; - bezahlCodePayload += $"executiondate={_executionDate.ToString("ddMMyyyy", CultureInfo.InvariantCulture)}&"; + StringExtensions.AppendInvariant(bezahlCodePayload,$"reason={Uri.EscapeDataString(_reason)}&"); + StringExtensions.AppendInvariant(bezahlCodePayload,$"currency={_currency}&"); + StringExtensions.AppendInvariant(bezahlCodePayload,$"executiondate={_executionDate.ToString("ddMMyyyy", CultureInfo.InvariantCulture)}&"); #pragma warning disable CS0618 if (_authority == AuthorityType.periodicsinglepayment || _authority == AuthorityType.periodicsinglepaymentsepa) { - bezahlCodePayload += $"periodictimeunit={_periodicTimeunit}&"; - bezahlCodePayload += $"periodictimeunitrotation={_periodicTimeunitRotation}&"; + StringExtensions.AppendInvariant(bezahlCodePayload,$"periodictimeunit={_periodicTimeunit}&"); + StringExtensions.AppendInvariant(bezahlCodePayload,$"periodictimeunitrotation={_periodicTimeunitRotation}&"); if (_periodicFirstExecutionDate != DateTime.MinValue) - bezahlCodePayload += $"periodicfirstexecutiondate={_periodicFirstExecutionDate.ToString("ddMMyyyy", CultureInfo.InvariantCulture)}&"; + StringExtensions.AppendInvariant(bezahlCodePayload,$"periodicfirstexecutiondate={_periodicFirstExecutionDate.ToString("ddMMyyyy", CultureInfo.InvariantCulture)}&"); if (_periodicLastExecutionDate != DateTime.MinValue) - bezahlCodePayload += $"periodiclastexecutiondate={_periodicLastExecutionDate.ToString("ddMMyyyy", CultureInfo.InvariantCulture)}&"; + StringExtensions.AppendInvariant(bezahlCodePayload,$"periodiclastexecutiondate={_periodicLastExecutionDate.ToString("ddMMyyyy", CultureInfo.InvariantCulture)}&"); } #pragma warning restore CS0618 } @@ -303,28 +302,29 @@ public override string ToString() //Handle what is same for all contacts if (_authority == AuthorityType.contact) { - bezahlCodePayload += $"account={_account}&"; - bezahlCodePayload += $"bnc={_bnc}&"; + StringExtensions.AppendInvariant(bezahlCodePayload,$"account={_account}&"); + StringExtensions.AppendInvariant(bezahlCodePayload,$"bnc={_bnc}&"); } else if (_authority == AuthorityType.contact_v2) { if (!string.IsNullOrEmpty(_account) && !string.IsNullOrEmpty(_bnc)) { - bezahlCodePayload += $"account={_account}&"; - bezahlCodePayload += $"bnc={_bnc}&"; + StringExtensions.AppendInvariant(bezahlCodePayload,$"account={_account}&"); + StringExtensions.AppendInvariant(bezahlCodePayload,$"bnc={_bnc}&"); } else { - bezahlCodePayload += $"iban={_iban}&"; - bezahlCodePayload += $"bic={_bic}&"; + StringExtensions.AppendInvariant(bezahlCodePayload,$"iban={_iban}&"); + StringExtensions.AppendInvariant(bezahlCodePayload,$"bic={_bic}&"); } } if (!string.IsNullOrEmpty(_reason)) - bezahlCodePayload += $"reason={Uri.EscapeDataString(_reason)}&"; + StringExtensions.AppendInvariant(bezahlCodePayload,$"reason={Uri.EscapeDataString(_reason)}&"); } - return bezahlCodePayload.Trim('&'); + string result = bezahlCodePayload.ToString(); + return result.TrimEnd('&'); } /// diff --git a/QRCoder/PayloadGenerator/BitcoinLikeCryptoCurrencyAddress.cs b/QRCoder/PayloadGenerator/BitcoinLikeCryptoCurrencyAddress.cs index d9f05245..4d1ea0d5 100644 --- a/QRCoder/PayloadGenerator/BitcoinLikeCryptoCurrencyAddress.cs +++ b/QRCoder/PayloadGenerator/BitcoinLikeCryptoCurrencyAddress.cs @@ -55,10 +55,10 @@ public override string ToString() if (queryValues.Any(keyPair => !string.IsNullOrEmpty(keyPair.Value))) { - query = "?" + string.Join("&", queryValues + query = $"?{string.Join("&", queryValues .Where(keyPair => !string.IsNullOrEmpty(keyPair.Value)) .Select(keyPair => $"{keyPair.Key}={keyPair.Value}") - .ToArray()); + .ToArray())}"; } return $"{Enum.GetName(typeof(BitcoinLikeCryptoCurrencyType), _currencyType)!.ToLowerInvariant()}:{_address}{query}"; diff --git a/QRCoder/PayloadGenerator/ContactData.cs b/QRCoder/PayloadGenerator/ContactData.cs index e2d3e111..89a420d7 100644 --- a/QRCoder/PayloadGenerator/ContactData.cs +++ b/QRCoder/PayloadGenerator/ContactData.cs @@ -114,139 +114,127 @@ public ContactData(ContactOutputType outputType, string firstname, string lastna /// A string representation of the contact data in the specified format. public override string ToString() { - string payload = string.Empty; + var payload = new StringBuilder(); if (_outputType == ContactOutputType.MeCard) { - payload += "MECARD+\r\n"; + payload.Append("MECARD+\r\n"); if (!string.IsNullOrEmpty(_firstname) && !string.IsNullOrEmpty(_lastname)) - payload += $"N:{_lastname}, {_firstname}\r\n"; + StringExtensions.AppendInvariant(payload,$"N:{_lastname}, {_firstname}\r\n"); else if (!string.IsNullOrEmpty(_firstname) || !string.IsNullOrEmpty(_lastname)) - payload += $"N:{_firstname}{_lastname}\r\n"; + StringExtensions.AppendInvariant(payload,$"N:{_firstname}{_lastname}\r\n"); if (!string.IsNullOrEmpty(_org)) - payload += $"ORG:{_org}\r\n"; + StringExtensions.AppendInvariant(payload,$"ORG:{_org}\r\n"); if (!string.IsNullOrEmpty(_orgTitle)) - payload += $"TITLE:{_orgTitle}\r\n"; + StringExtensions.AppendInvariant(payload,$"TITLE:{_orgTitle}\r\n"); if (!string.IsNullOrEmpty(_phone)) - payload += $"TEL:{_phone}\r\n"; + StringExtensions.AppendInvariant(payload,$"TEL:{_phone}\r\n"); if (!string.IsNullOrEmpty(_mobilePhone)) - payload += $"TEL:{_mobilePhone}\r\n"; + StringExtensions.AppendInvariant(payload,$"TEL:{_mobilePhone}\r\n"); if (!string.IsNullOrEmpty(_workPhone)) - payload += $"TEL:{_workPhone}\r\n"; + StringExtensions.AppendInvariant(payload,$"TEL:{_workPhone}\r\n"); if (!string.IsNullOrEmpty(_email)) - payload += $"EMAIL:{_email}\r\n"; + StringExtensions.AppendInvariant(payload,$"EMAIL:{_email}\r\n"); if (!string.IsNullOrEmpty(_note)) - payload += $"NOTE:{_note}\r\n"; + StringExtensions.AppendInvariant(payload,$"NOTE:{_note}\r\n"); if (_birthday != null) - payload += $"BDAY:{((DateTime)_birthday).ToString("yyyyMMdd", CultureInfo.InvariantCulture)}\r\n"; + StringExtensions.AppendInvariant(payload,$"BDAY:{((DateTime)_birthday).ToString("yyyyMMdd", CultureInfo.InvariantCulture)}\r\n"); // RFC 2426 Section 3.2.1: ADR format is PO Box; Extended Address; Street; Locality (City); Region; Postal Code; Country - string addressString = string.Empty; if (_addressOrder == AddressOrder.Default) { - addressString = $"ADR:,,{(!string.IsNullOrEmpty(_street) ? _street + " " : "")}{(!string.IsNullOrEmpty(_houseNumber) ? _houseNumber : "")},{(!string.IsNullOrEmpty(_city) ? _city : "")},{(!string.IsNullOrEmpty(_stateRegion) ? _stateRegion : "")},{(!string.IsNullOrEmpty(_zipCode) ? _zipCode : "")},{(!string.IsNullOrEmpty(_country) ? _country : "")}\r\n"; + StringExtensions.AppendInvariant(payload,$"ADR:,,{(!string.IsNullOrEmpty(_street) ? $"{_street} " : "")}{(!string.IsNullOrEmpty(_houseNumber) ? _houseNumber : "")},{(!string.IsNullOrEmpty(_city) ? _city : "")},{(!string.IsNullOrEmpty(_stateRegion) ? _stateRegion : "")},{(!string.IsNullOrEmpty(_zipCode) ? _zipCode : "")},{(!string.IsNullOrEmpty(_country) ? _country : "")}\r\n"); } else { - addressString = $"ADR:,,{(!string.IsNullOrEmpty(_houseNumber) ? _houseNumber + " " : "")}{(!string.IsNullOrEmpty(_street) ? _street : "")},{(!string.IsNullOrEmpty(_city) ? _city : "")},{(!string.IsNullOrEmpty(_stateRegion) ? _stateRegion : "")},{(!string.IsNullOrEmpty(_zipCode) ? _zipCode : "")},{(!string.IsNullOrEmpty(_country) ? _country : "")}\r\n"; + StringExtensions.AppendInvariant(payload,$"ADR:,,{(!string.IsNullOrEmpty(_houseNumber) ? $"{_houseNumber} " : "")}{(!string.IsNullOrEmpty(_street) ? _street : "")},{(!string.IsNullOrEmpty(_city) ? _city : "")},{(!string.IsNullOrEmpty(_stateRegion) ? _stateRegion : "")},{(!string.IsNullOrEmpty(_zipCode) ? _zipCode : "")},{(!string.IsNullOrEmpty(_country) ? _country : "")}\r\n"); } - payload += addressString; if (!string.IsNullOrEmpty(_website)) - payload += $"URL:{_website}\r\n"; + StringExtensions.AppendInvariant(payload,$"URL:{_website}\r\n"); if (!string.IsNullOrEmpty(_nickname)) - payload += $"NICKNAME:{_nickname}\r\n"; - payload = payload.Trim(_trimChars); + StringExtensions.AppendInvariant(payload,$"NICKNAME:{_nickname}\r\n"); + return payload.ToString().Trim(_trimChars); } - else - { - var version = _outputType.ToString().Substring(5); - if (version.Length > 1) - version = version.Insert(1, "."); - else - version += ".0"; - - payload += "BEGIN:VCARD\r\n"; - payload += $"VERSION:{version}\r\n"; - payload += $"N:{(!string.IsNullOrEmpty(_lastname) ? _lastname : "")};{(!string.IsNullOrEmpty(_firstname) ? _firstname : "")};;;\r\n"; - payload += $"FN:{(!string.IsNullOrEmpty(_firstname) ? _firstname + " " : "")}{(!string.IsNullOrEmpty(_lastname) ? _lastname : "")}\r\n"; - if (!string.IsNullOrEmpty(_org)) - { - payload += $"ORG:" + _org + "\r\n"; - } - if (!string.IsNullOrEmpty(_orgTitle)) - { - payload += $"TITLE:" + _orgTitle + "\r\n"; - } - if (!string.IsNullOrEmpty(_phone)) - { - payload += $"TEL;"; - if (_outputType == ContactOutputType.VCard21) - payload += $"HOME;VOICE:{_phone}"; - else if (_outputType == ContactOutputType.VCard3) - payload += $"TYPE=HOME,VOICE:{_phone}"; - else - payload += $"TYPE=home,voice;VALUE=uri:tel:{_phone}"; - payload += "\r\n"; - } - - if (!string.IsNullOrEmpty(_mobilePhone)) - { - payload += $"TEL;"; - if (_outputType == ContactOutputType.VCard21) - payload += $"HOME;CELL:{_mobilePhone}"; - else if (_outputType == ContactOutputType.VCard3) - payload += $"TYPE=HOME,CELL:{_mobilePhone}"; - else - payload += $"TYPE=home,cell;VALUE=uri:tel:{_mobilePhone}"; - payload += "\r\n"; - } - - if (!string.IsNullOrEmpty(_workPhone)) - { - payload += $"TEL;"; - if (_outputType == ContactOutputType.VCard21) - payload += $"WORK;VOICE:{_workPhone}"; - else if (_outputType == ContactOutputType.VCard3) - payload += $"TYPE=WORK,VOICE:{_workPhone}"; - else - payload += $"TYPE=work,voice;VALUE=uri:tel:{_workPhone}"; - payload += "\r\n"; - } + var version = _outputType.ToString().Substring(5); + if (version.Length > 1) + version = version.Insert(1, "."); + else + version += ".0"; + payload.Append("BEGIN:VCARD\r\n"); + StringExtensions.AppendInvariant(payload,$"VERSION:{version}\r\n"); - // RFC 2426 Section 3.2.1: ADR format is PO Box; Extended Address; Street; Locality (City); Region; Postal Code; Country - payload += "ADR;"; + StringExtensions.AppendInvariant(payload,$"N:{(!string.IsNullOrEmpty(_lastname) ? _lastname : "")};{(!string.IsNullOrEmpty(_firstname) ? _firstname : "")};;;\r\n"); + StringExtensions.AppendInvariant(payload,$"FN:{(!string.IsNullOrEmpty(_firstname) ? $"{_firstname} " : "")}{(!string.IsNullOrEmpty(_lastname) ? _lastname : "")}\r\n"); + if (!string.IsNullOrEmpty(_org)) + StringExtensions.AppendInvariant(payload,$"ORG:{_org}\r\n"); + if (!string.IsNullOrEmpty(_orgTitle)) + StringExtensions.AppendInvariant(payload,$"TITLE:{_orgTitle}\r\n"); + if (!string.IsNullOrEmpty(_phone)) + { + payload.Append("TEL;"); if (_outputType == ContactOutputType.VCard21) - payload += GetAddressTypeString21() + ":"; + StringExtensions.AppendInvariant(payload,$"HOME;VOICE:{_phone}"); else if (_outputType == ContactOutputType.VCard3) - payload += "TYPE=" + GetAddressTypeString3() + ":"; + StringExtensions.AppendInvariant(payload,$"TYPE=HOME,VOICE:{_phone}"); else - payload += "TYPE=" + GetAddressTypeString4() + ":"; - string addressString = string.Empty; - if (_addressOrder == AddressOrder.Default) - { - addressString = $";;{(!string.IsNullOrEmpty(_street) ? _street + " " : "")}{(!string.IsNullOrEmpty(_houseNumber) ? _houseNumber : "")};{(!string.IsNullOrEmpty(_city) ? _city : "")};{(!string.IsNullOrEmpty(_stateRegion) ? _stateRegion : "")};{(!string.IsNullOrEmpty(_zipCode) ? _zipCode : "")};{(!string.IsNullOrEmpty(_country) ? _country : "")}\r\n"; - } + StringExtensions.AppendInvariant(payload,$"TYPE=home,voice;VALUE=uri:tel:{_phone}"); + payload.Append("\r\n"); + } + + if (!string.IsNullOrEmpty(_mobilePhone)) + { + payload.Append("TEL;"); + if (_outputType == ContactOutputType.VCard21) + StringExtensions.AppendInvariant(payload,$"HOME;CELL:{_mobilePhone}"); + else if (_outputType == ContactOutputType.VCard3) + StringExtensions.AppendInvariant(payload,$"TYPE=HOME,CELL:{_mobilePhone}"); else - { - addressString = $";;{(!string.IsNullOrEmpty(_houseNumber) ? _houseNumber + " " : "")}{(!string.IsNullOrEmpty(_street) ? _street : "")};{(!string.IsNullOrEmpty(_city) ? _city : "")};{(!string.IsNullOrEmpty(_stateRegion) ? _stateRegion : "")};{(!string.IsNullOrEmpty(_zipCode) ? _zipCode : "")};{(!string.IsNullOrEmpty(_country) ? _country : "")}\r\n"; - } - payload += addressString; + StringExtensions.AppendInvariant(payload,$"TYPE=home,cell;VALUE=uri:tel:{_mobilePhone}"); + payload.Append("\r\n"); + } - if (_birthday != null) - payload += $"BDAY:{((DateTime)_birthday).ToString("yyyyMMdd", CultureInfo.InvariantCulture)}\r\n"; - if (!string.IsNullOrEmpty(_website)) - payload += $"URL:{_website}\r\n"; - if (!string.IsNullOrEmpty(_email)) - payload += $"EMAIL:{_email}\r\n"; - if (!string.IsNullOrEmpty(_note)) - payload += $"NOTE:{_note}\r\n"; - if (_outputType != ContactOutputType.VCard21 && !string.IsNullOrEmpty(_nickname)) - payload += $"NICKNAME:{_nickname}\r\n"; + if (!string.IsNullOrEmpty(_workPhone)) + { + payload.Append("TEL;"); + if (_outputType == ContactOutputType.VCard21) + StringExtensions.AppendInvariant(payload,$"WORK;VOICE:{_workPhone}"); + else if (_outputType == ContactOutputType.VCard3) + StringExtensions.AppendInvariant(payload,$"TYPE=WORK,VOICE:{_workPhone}"); + else + StringExtensions.AppendInvariant(payload,$"TYPE=work,voice;VALUE=uri:tel:{_workPhone}"); + payload.Append("\r\n"); + } - payload += "END:VCARD"; + // RFC 2426 Section 3.2.1: ADR format is PO Box; Extended Address; Street; Locality (City); Region; Postal Code; Country + payload.Append("ADR;"); + if (_outputType == ContactOutputType.VCard21) + StringExtensions.AppendInvariant(payload,$"{GetAddressTypeString21()}:"); + else if (_outputType == ContactOutputType.VCard3) + StringExtensions.AppendInvariant(payload,$"TYPE={GetAddressTypeString3()}:"); + else + StringExtensions.AppendInvariant(payload,$"TYPE={GetAddressTypeString4()}:"); + if (_addressOrder == AddressOrder.Default) + { + StringExtensions.AppendInvariant(payload,$";;{(!string.IsNullOrEmpty(_street) ? $"{_street} " : "")}{(!string.IsNullOrEmpty(_houseNumber) ? _houseNumber : "")};{(!string.IsNullOrEmpty(_city) ? _city : "")};{(!string.IsNullOrEmpty(_stateRegion) ? _stateRegion : "")};{(!string.IsNullOrEmpty(_zipCode) ? _zipCode : "")};{(!string.IsNullOrEmpty(_country) ? _country : "")}\r\n"); + } + else + { + StringExtensions.AppendInvariant(payload,$";;{(!string.IsNullOrEmpty(_houseNumber) ? $"{_houseNumber} " : "")}{(!string.IsNullOrEmpty(_street) ? _street : "")};{(!string.IsNullOrEmpty(_city) ? _city : "")};{(!string.IsNullOrEmpty(_stateRegion) ? _stateRegion : "")};{(!string.IsNullOrEmpty(_zipCode) ? _zipCode : "")};{(!string.IsNullOrEmpty(_country) ? _country : "")}\r\n"); } - return payload; + if (_birthday != null) + StringExtensions.AppendInvariant(payload,$"BDAY:{((DateTime)_birthday).ToString("yyyyMMdd", CultureInfo.InvariantCulture)}\r\n"); + if (!string.IsNullOrEmpty(_website)) + StringExtensions.AppendInvariant(payload,$"URL:{_website}\r\n"); + if (!string.IsNullOrEmpty(_email)) + StringExtensions.AppendInvariant(payload,$"EMAIL:{_email}\r\n"); + if (!string.IsNullOrEmpty(_note)) + StringExtensions.AppendInvariant(payload,$"NOTE:{_note}\r\n"); + if (_outputType != ContactOutputType.VCard21 && !string.IsNullOrEmpty(_nickname)) + StringExtensions.AppendInvariant(payload,$"NICKNAME:{_nickname}\r\n"); + + payload.Append("END:VCARD"); + return payload.ToString(); } /// diff --git a/QRCoder/PayloadGenerator/Girocode.cs b/QRCoder/PayloadGenerator/Girocode.cs index fedd794d..9d989560 100644 --- a/QRCoder/PayloadGenerator/Girocode.cs +++ b/QRCoder/PayloadGenerator/Girocode.cs @@ -74,24 +74,22 @@ public Girocode(string iban, string? bic, string name, decimal amount, string re /// The Girocode payload as a string. public override string ToString() { - var girocodePayload = "BCD" + _br; - girocodePayload += ((_version == GirocodeVersion.Version1) ? "001" : "002") + _br; - girocodePayload += (int)_encoding + 1 + _br; - girocodePayload += "SCT" + _br; - girocodePayload += _bic + _br; - girocodePayload += _name + _br; - girocodePayload += _iban + _br; - girocodePayload += string.Format(CultureInfo.InvariantCulture, "EUR{0:0.00}", _amount) + _br; - girocodePayload += _purposeOfCreditTransfer + _br; - girocodePayload += ((_typeOfRemittance == TypeOfRemittance.Structured) - ? _remittanceInformation - : string.Empty) + _br; - girocodePayload += ((_typeOfRemittance == TypeOfRemittance.Unstructured) - ? _remittanceInformation - : string.Empty) + _br; - girocodePayload += _messageToGirocodeUser; - - return ConvertStringToEncoding(girocodePayload, _encoding.ToString().Replace("_", "-")); + var girocodePayload = new StringBuilder(); + StringExtensions.AppendInvariant(girocodePayload,$"BCD{_br}"); + StringExtensions.AppendInvariant(girocodePayload,$"{(_version == GirocodeVersion.Version1 ? "001" : "002")}{_br}"); + StringExtensions.AppendInvariant(girocodePayload,$"{(int)_encoding + 1}{_br}"); + StringExtensions.AppendInvariant(girocodePayload,$"SCT{_br}"); + StringExtensions.AppendInvariant(girocodePayload,$"{_bic}{_br}"); + StringExtensions.AppendInvariant(girocodePayload,$"{_name}{_br}"); + StringExtensions.AppendInvariant(girocodePayload,$"{_iban}{_br}"); + girocodePayload.Append(string.Format(CultureInfo.InvariantCulture, "EUR{0:0.00}", _amount)); + girocodePayload.Append(_br); + StringExtensions.AppendInvariant(girocodePayload,$"{_purposeOfCreditTransfer}{_br}"); + StringExtensions.AppendInvariant(girocodePayload,$"{(_typeOfRemittance == TypeOfRemittance.Structured ? _remittanceInformation : string.Empty)}{_br}"); + StringExtensions.AppendInvariant(girocodePayload,$"{(_typeOfRemittance == TypeOfRemittance.Unstructured ? _remittanceInformation : string.Empty)}{_br}"); + girocodePayload.Append(_messageToGirocodeUser); + + return ConvertStringToEncoding(girocodePayload.ToString(), _encoding.ToString().Replace("_", "-")); } /// diff --git a/QRCoder/PayloadGenerator/Mail.cs b/QRCoder/PayloadGenerator/Mail.cs index 76fbf7d8..6b211104 100644 --- a/QRCoder/PayloadGenerator/Mail.cs +++ b/QRCoder/PayloadGenerator/Mail.cs @@ -37,9 +37,9 @@ public override string ToString() case MailEncoding.MAILTO: var parts = new List(); if (!string.IsNullOrEmpty(_subject)) - parts.Add("subject=" + Uri.EscapeDataString(_subject)); + parts.Add($"subject={Uri.EscapeDataString(_subject)}"); if (!string.IsNullOrEmpty(_message)) - parts.Add("body=" + Uri.EscapeDataString(_message)); + parts.Add($"body={Uri.EscapeDataString(_message)}"); var queryString = parts.Count > 0 ? $"?{string.Join("&", parts.ToArray())}" : ""; return $"mailto:{_mailReceiver}{queryString}"; case MailEncoding.MATMSG: diff --git a/QRCoder/PayloadGenerator/OneTimePassword.cs b/QRCoder/PayloadGenerator/OneTimePassword.cs index e81eeb87..536e4ec6 100644 --- a/QRCoder/PayloadGenerator/OneTimePassword.cs +++ b/QRCoder/PayloadGenerator/OneTimePassword.cs @@ -141,7 +141,7 @@ private string HMACToString() var sb = new StringBuilder("otpauth://hotp/"); ProcessCommonFields(sb); var actualCounter = Counter ?? 1; - sb.Append("&counter=" + actualCounter); + StringExtensions.AppendInvariant(sb,$"&counter={actualCounter}"); return sb.ToString(); } @@ -162,7 +162,7 @@ private string TimeToString() if (Period != 30) { - sb.Append("&period=" + Period); + StringExtensions.AppendInvariant(sb,$"&period={Period}"); } return sb.ToString(); @@ -203,7 +203,7 @@ private void ProcessCommonFields(StringBuilder sb) if (escapedLabel != null && escapedIssuer != null) { - label = escapedIssuer + ":" + escapedLabel; + label = $"{escapedIssuer}:{escapedLabel}"; } else if (escapedIssuer != null) { @@ -215,21 +215,21 @@ private void ProcessCommonFields(StringBuilder sb) sb.Append(label); } - sb.Append("?secret=" + strippedSecret); + StringExtensions.AppendInvariant(sb,$"?secret={strippedSecret}"); if (escapedIssuer != null) { - sb.Append("&issuer=" + escapedIssuer); + StringExtensions.AppendInvariant(sb,$"&issuer={escapedIssuer}"); } if (AuthAlgorithm != OneTimePasswordAuthAlgorithm.SHA1) { - sb.Append("&algorithm=" + AuthAlgorithm.ToString()); + StringExtensions.AppendInvariant(sb,$"&algorithm={AuthAlgorithm}"); } if (Digits != 6) { - sb.Append("&digits=" + Digits); + StringExtensions.AppendInvariant(sb,$"&digits={Digits}"); } } } diff --git a/QRCoder/PayloadGenerator/RussiaPaymentOrder.cs b/QRCoder/PayloadGenerator/RussiaPaymentOrder.cs index adc6d159..2239b3cf 100644 --- a/QRCoder/PayloadGenerator/RussiaPaymentOrder.cs +++ b/QRCoder/PayloadGenerator/RussiaPaymentOrder.cs @@ -90,12 +90,13 @@ public byte[] ToBytes() _separator = DetermineSeparator(); //Create the payload string - string ret = $"ST0001" + ((int)_characterSet).ToString(CultureInfo.InvariantCulture) + //(separator != "|" ? separator : "") + - $"{_separator}Name={_mFields.Name}" + - $"{_separator}PersonalAcc={_mFields.PersonalAcc}" + - $"{_separator}BankName={_mFields.BankName}" + - $"{_separator}BIC={_mFields.BIC}" + - $"{_separator}CorrespAcc={_mFields.CorrespAcc}"; + var retBuilder = new StringBuilder($"ST0001{(int)_characterSet}"); + StringExtensions.AppendInvariant(retBuilder,$"{_separator}Name={_mFields.Name}"); + StringExtensions.AppendInvariant(retBuilder,$"{_separator}PersonalAcc={_mFields.PersonalAcc}"); + StringExtensions.AppendInvariant(retBuilder,$"{_separator}BankName={_mFields.BankName}"); + StringExtensions.AppendInvariant(retBuilder,$"{_separator}BIC={_mFields.BIC}"); + StringExtensions.AppendInvariant(retBuilder,$"{_separator}CorrespAcc={_mFields.CorrespAcc}"); + var ret = retBuilder.ToString(); //Check length of mandatory field block (-8 => Removing service data block bytes from ret length) int bytesMandatoryLen = Encoding.Convert(Encoding.UTF8, Encoding.GetEncoding(cp), Encoding.UTF8.GetBytes(ret)).Length - 8; @@ -576,7 +577,7 @@ public string? TaxPaytKind } #pragma warning disable CA1707 // Underscore in identifier - /// + /// /// (List of values of the technical code of the payment) /// Перечень значений технического кода платежа /// diff --git a/QRCoder/PayloadGenerator/SwissQrCode.cs b/QRCoder/PayloadGenerator/SwissQrCode.cs index 57384154..2cd6aadc 100644 --- a/QRCoder/PayloadGenerator/SwissQrCode.cs +++ b/QRCoder/PayloadGenerator/SwissQrCode.cs @@ -8,7 +8,7 @@ public static partial class PayloadGenerator public class SwissQrCode : Payload { //Keep in mind, that the ECC level has to be set to "M" when generating a SwissQrCode! - //SwissQrCode specification: + //SwissQrCode specification: // - (de) https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-de.pdf // - (en) https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf //Changes between version 1.0 and 2.0: https://www.paymentstandards.ch/dam/downloads/change-documentation-qrr-de.pdf @@ -157,7 +157,7 @@ public class Reference /// /// Type of the reference (QRR, SCOR or NON) /// Reference text - /// Type of the reference text (QR-reference or Creditor Reference) + /// Type of the reference text (QR-reference or Creditor Reference) public Reference(ReferenceType referenceType, string? reference = null, ReferenceTextType? referenceTextType = null) { RefType = referenceType; @@ -479,14 +479,15 @@ private static HashSet ValidTwoLetterCodes() /// A string representing the contact information. public override string ToString() { - string contactData = $"{(AddressType.StructuredAddress == _adrType ? "S" : "K")}{_br}"; //AdrTp - contactData += _name.Replace("\n", "") + _br; //Name - contactData += (!string.IsNullOrEmpty(_streetOrAddressline1) ? _streetOrAddressline1!.Replace("\n", "") : string.Empty) + _br; //StrtNmOrAdrLine1 - contactData += (!string.IsNullOrEmpty(_houseNumberOrAddressline2) ? _houseNumberOrAddressline2!.Replace("\n", "") : string.Empty) + _br; //BldgNbOrAdrLine2 - contactData += _zipCode.Replace("\n", "") + _br; //PstCd - contactData += _city.Replace("\n", "") + _br; //TwnNm - contactData += _country + _br; //Ctry - return contactData; + var contactData = new StringBuilder(); + StringExtensions.AppendInvariant(contactData,$"{(AddressType.StructuredAddress == _adrType ? "S" : "K")}{_br}"); //AdrTp + StringExtensions.AppendInvariant(contactData,$"{_name.Replace("\n", "")}{_br}"); //Name + StringExtensions.AppendInvariant(contactData,$"{(!string.IsNullOrEmpty(_streetOrAddressline1) ? _streetOrAddressline1!.Replace("\n", "") : string.Empty)}{_br}"); //StrtNmOrAdrLine1 + StringExtensions.AppendInvariant(contactData,$"{(!string.IsNullOrEmpty(_houseNumberOrAddressline2) ? _houseNumberOrAddressline2!.Replace("\n", "") : string.Empty)}{_br}"); //BldgNbOrAdrLine2 + StringExtensions.AppendInvariant(contactData,$"{_zipCode.Replace("\n", "")}{_br}"); //PstCd + StringExtensions.AppendInvariant(contactData,$"{_city.Replace("\n", "")}{_br}"); //TwnNm + StringExtensions.AppendInvariant(contactData,$"{_country}{_br}"); //Ctry + return contactData.ToString(); } /// @@ -543,58 +544,65 @@ public SwissQrCodeContactException(string message, Exception inner) /// A string representing the Swiss QR code payload. public override string ToString() { + static void AppendEmptyAddressLines(StringBuilder builder, string lineBreak, int count) + { + for (int i = 0; i < count; i++) + builder.Append(lineBreak); + } + //Header "logical" element - var SwissQrCodePayload = "SPC" + _br; //QRType - SwissQrCodePayload += "0200" + _br; //Version - SwissQrCodePayload += "1" + _br; //Coding + var swissQrCodePayload = new StringBuilder(); + StringExtensions.AppendInvariant(swissQrCodePayload,$"SPC{_br}"); //QRType + StringExtensions.AppendInvariant(swissQrCodePayload,$"0200{_br}"); //Version + StringExtensions.AppendInvariant(swissQrCodePayload,$"1{_br}"); //Coding //CdtrInf "logical" element - SwissQrCodePayload += _iban.ToString() + _br; //IBAN - + StringExtensions.AppendInvariant(swissQrCodePayload,$"{_iban}{_br}"); //IBAN //Cdtr "logical" element - SwissQrCodePayload += _creditor.ToString(); + swissQrCodePayload.Append(_creditor.ToString()); //UltmtCdtr "logical" element //Since version 2.0 ultimate creditor was marked as "for future use" and has to be delivered empty in any case! - SwissQrCodePayload += string.Concat(Enumerable.Repeat(_br, 7).ToArray()); + AppendEmptyAddressLines(swissQrCodePayload, _br, 7); //CcyAmtDate "logical" element //Amoutn has to use . as decimal seperator in any case. See https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf page 27. - SwissQrCodePayload += (_amount != null ? $"{_amount:0.00}".Replace(",", ".") : string.Empty) + _br; //Amt - SwissQrCodePayload += _currency + _br; //Ccy + StringExtensions.AppendInvariant(swissQrCodePayload,$"{(_amount != null ? $"{_amount:0.00}".Replace(",", ".") : string.Empty)}{_br}"); //Amt + StringExtensions.AppendInvariant(swissQrCodePayload,$"{_currency}{_br}"); //Ccy //Removed in S-QR version 2.0 - //SwissQrCodePayload += (requestedDateOfPayment != null ? ((DateTime)requestedDateOfPayment).ToString("yyyy-MM-dd") : string.Empty) + br; //ReqdExctnDt + //StringExtensions.AppendInvariant(swissQrCodePayload,$"{(requestedDateOfPayment != null ? ((DateTime)requestedDateOfPayment).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) : string.Empty)}{_br}"); //ReqdExctnDt //UltmtDbtr "logical" element if (_debitor != null) - SwissQrCodePayload += _debitor.ToString(); + swissQrCodePayload.Append(_debitor.ToString()); else - SwissQrCodePayload += string.Concat(Enumerable.Repeat(_br, 7).ToArray()); - + AppendEmptyAddressLines(swissQrCodePayload, _br, 7); //RmtInf "logical" element - SwissQrCodePayload += _reference.RefType.ToString() + _br; //Tp - SwissQrCodePayload += (!string.IsNullOrEmpty(_reference.ReferenceText) ? _reference.ReferenceText : string.Empty) + _br; //Ref - + StringExtensions.AppendInvariant(swissQrCodePayload,$"{_reference.RefType}{_br}"); //Tp + StringExtensions.AppendInvariant(swissQrCodePayload,$"{(!string.IsNullOrEmpty(_reference.ReferenceText) ? _reference.ReferenceText : string.Empty)}{_br}"); //Ref //AddInf "logical" element - SwissQrCodePayload += (!string.IsNullOrEmpty(_additionalInformation.UnstructureMessage) ? _additionalInformation.UnstructureMessage : string.Empty) + _br; //Ustrd - SwissQrCodePayload += _additionalInformation.Trailer + _br; //Trailer + StringExtensions.AppendInvariant(swissQrCodePayload,$"{(!string.IsNullOrEmpty(_additionalInformation.UnstructureMessage) ? _additionalInformation.UnstructureMessage : string.Empty)}{_br}"); //Ustrd + StringExtensions.AppendInvariant(swissQrCodePayload,$"{_additionalInformation.Trailer}{_br}"); //Trailer // Bugfix PR #399 If BillInformation is empty, insert no linebreak - SwissQrCodePayload += (!string.IsNullOrEmpty(_additionalInformation.BillInformation) ? _additionalInformation.BillInformation + _br : string.Empty); //StrdBkgInf + if (!string.IsNullOrEmpty(_additionalInformation.BillInformation)) + StringExtensions.AppendInvariant(swissQrCodePayload,$"{_additionalInformation.BillInformation}{_br}"); //StrdBkgInf //AltPmtInf "logical" element if (!string.IsNullOrEmpty(_alternativeProcedure1)) - SwissQrCodePayload += _alternativeProcedure1!.Replace("\n", "") + _br; //AltPmt + StringExtensions.AppendInvariant(swissQrCodePayload,$"{_alternativeProcedure1!.Replace("\n", "")}{_br}"); //AltPmt if (!string.IsNullOrEmpty(_alternativeProcedure2)) - SwissQrCodePayload += _alternativeProcedure2!.Replace("\n", "") + _br; //AltPmt + StringExtensions.AppendInvariant(swissQrCodePayload,$"{_alternativeProcedure2!.Replace("\n", "")}{_br}"); //AltPmt + + var result = swissQrCodePayload.ToString(); //S-QR specification 2.0, chapter 4.2.3 - if (SwissQrCodePayload.EndsWith(_br, StringComparison.Ordinal)) - SwissQrCodePayload = SwissQrCodePayload.Remove(SwissQrCodePayload.Length - _br.Length); + if (result.EndsWith(_br, StringComparison.Ordinal)) + result = result.Remove(result.Length - _br.Length); - return SwissQrCodePayload; + return result; } diff --git a/QRCoder/PayloadGenerator/Url.cs b/QRCoder/PayloadGenerator/Url.cs index 08cd43a3..a94dee8f 100644 --- a/QRCoder/PayloadGenerator/Url.cs +++ b/QRCoder/PayloadGenerator/Url.cs @@ -23,6 +23,6 @@ public Url(string url) /// /// The URL payload as a string, ensuring it starts with "http://" if no protocol is specified. public override string ToString() - => !_url.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? "http://" + _url : _url; + => !_url.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? $"http://{_url}" : _url; } } diff --git a/QRCoder/PayloadGenerator/WiFi.cs b/QRCoder/PayloadGenerator/WiFi.cs index f9c45781..27631a37 100644 --- a/QRCoder/PayloadGenerator/WiFi.cs +++ b/QRCoder/PayloadGenerator/WiFi.cs @@ -21,9 +21,9 @@ public class WiFi : Payload public WiFi(string ssid, string password, Authentication authenticationMode, bool isHiddenSSID = false, bool escapeHexStrings = true) { _ssid = EscapeInput(ssid); - _ssid = escapeHexStrings && isHexStyle(_ssid) ? "\"" + _ssid + "\"" : _ssid; + _ssid = escapeHexStrings && isHexStyle(_ssid) ? $"\"{_ssid}\"" : _ssid; _password = EscapeInput(password); - _password = escapeHexStrings && isHexStyle(_password) ? "\"" + _password + "\"" : _password; + _password = escapeHexStrings && isHexStyle(_password) ? $"\"{_password}\"" : _password; _authenticationMode = authenticationMode.ToString(); _isHiddenSsid = isHiddenSSID; } diff --git a/QRCoder/PdfByteQRCode.cs b/QRCoder/PdfByteQRCode.cs index ae994813..d2edc437 100644 --- a/QRCoder/PdfByteQRCode.cs +++ b/QRCoder/PdfByteQRCode.cs @@ -78,97 +78,100 @@ public byte[] GetGraphic(int pixelsPerModule, string darkColorHtmlHex, string li xrefs.Add(stream.Position); // Object 1: Catalog - root of PDF document structure - writer.Write( - ToStr(xrefs.Count) + " 0 obj\r\n" + // Object number and generation number (0) - "<<\r\n" + // Begin dictionary - "/Type /Catalog\r\n" + // Declares this as the document catalog - "/Pages 2 0 R\r\n" + // References the Pages object (object 2) - ">>\r\n" + // End dictionary - "endobj\r\n" // End object - ); + var catalogObject = new StringBuilder(); + StringExtensions.AppendInvariant(catalogObject, $"{ToStr(xrefs.Count)} 0 obj\r\n"); // Object number and generation number (0) + catalogObject.Append("<<\r\n"); // Begin dictionary + catalogObject.Append("/Type /Catalog\r\n"); // Declares this as the document catalog + catalogObject.Append("/Pages 2 0 R\r\n"); // References the Pages object (object 2) + catalogObject.Append(">>\r\n"); // End dictionary + catalogObject.Append("endobj\r\n"); // End object + writer.Write(catalogObject.ToString()); writer.Flush(); xrefs.Add(stream.Position); // Object 2: Pages - defines page tree structure - writer.Write( - ToStr(xrefs.Count) + " 0 obj\r\n" + // Object number and generation number (0) - "<<\r\n" + // Begin dictionary - "/Count 1\r\n" + // Number of pages in document - "/Kids [ 3 0 R ]\r\n" + // Kids must contain indirect references to Page objects - ">>\r\n" + // End dictionary - "endobj\r\n" // End object - ); + var pagesObject = new StringBuilder(); + StringExtensions.AppendInvariant(pagesObject, $"{ToStr(xrefs.Count)} 0 obj\r\n"); // Object number and generation number (0) + pagesObject.Append("<<\r\n"); // Begin dictionary + pagesObject.Append("/Count 1\r\n"); // Number of pages in document + pagesObject.Append("/Kids [ 3 0 R ]\r\n"); // Kids must contain indirect references to Page objects + pagesObject.Append(">>\r\n"); // End dictionary + pagesObject.Append("endobj\r\n"); // End object + writer.Write(pagesObject.ToString()); // Content stream - PDF drawing instructions - var scale = ToStr(imgSize * 72 / (float)dpi / moduleCount); // Scale factor to convert module units to PDF points - var pathCommands = CreatePathFromModules(); // Create path from dark modules - var content = "q\r\n" + // 'q' = Save graphics state - scale + " 0 0 -" + scale + " 0 " + pdfMediaSize + " cm\r\n" + // 'cm' = Transformation matrix: scale X, scale & flip Y, translate to top - lightColorPdf + " rg\r\n" + // 'rg' = Set RGB fill color for background - "0 0 " + ToStr(moduleCount) + " " + ToStr(moduleCount) + " re\r\n" + // 're' = Rectangle covering entire QR code - "f\r\n" + // 'f' = Fill background - darkColorPdf + " rg\r\n" + // 'rg' = Set RGB fill color for dark modules - pathCommands + // Add all dark module rectangles to path - "f*\r\n" + // 'f*' = Fill with even-odd rule - "Q"; // 'Q' = Restore graphics state + var scale = ToStr(imgSize * 72 / (float)dpi / moduleCount); // Scale factor to convert module units to PDF points + var pathCommands = CreatePathFromModules(); // Create path from dark modules + var content = new StringBuilder(); + content.Append("q\r\n"); // 'q' = Save graphics state + StringExtensions.AppendInvariant(content, $"{scale} 0 0 -{scale} 0 {pdfMediaSize} cm\r\n"); // 'cm' = Transformation matrix: scale X, scale & flip Y, translate to top + StringExtensions.AppendInvariant(content, $"{lightColorPdf} rg\r\n"); // 'rg' = Set RGB fill color for background + StringExtensions.AppendInvariant(content, $"0 0 {ToStr(moduleCount)} {ToStr(moduleCount)} re\r\n"); // 're' = Rectangle covering entire QR code + content.Append("f\r\n"); // 'f' = Fill background + StringExtensions.AppendInvariant(content, $"{darkColorPdf} rg\r\n"); // 'rg' = Set RGB fill color for dark modules + StringExtensions.AppendInvariant(content, $"{pathCommands}f*\r\n"); // Add all dark module rectangles to path; 'f*' = Fill with even-odd rule + content.Append('Q'); // 'Q' = Restore graphics state + var contentString = content.ToString(); writer.Flush(); xrefs.Add(stream.Position); // Object 3: Page - indirect page object (Kids array must reference pages indirectly) - writer.Write( - ToStr(xrefs.Count) + " 0 obj\r\n" + // Object number and generation number (0) - "<<\r\n" + // Begin dictionary - "/Type /Page\r\n" + // Declares this as a page - "/Parent 2 0 R\r\n" + // References parent Pages object - "/MediaBox [0 0 " + pdfMediaSize + " " + pdfMediaSize + "]\r\n" + // Page dimensions [x1 y1 x2 y2] - "/Resources << /ProcSet [ /PDF ] >>\r\n" + // Required resources: PDF operations only (no images) - "/Contents 4 0 R\r\n" + // References content stream (object 4) - ">>\r\n" + // End dictionary - "endobj\r\n" // End object - ); + var pageObject = new StringBuilder(); + StringExtensions.AppendInvariant(pageObject, $"{ToStr(xrefs.Count)} 0 obj\r\n"); // Object number and generation number (0) + pageObject.Append("<<\r\n"); // Begin dictionary + pageObject.Append("/Type /Page\r\n"); // Declares this as a page + pageObject.Append("/Parent 2 0 R\r\n"); // References parent Pages object + StringExtensions.AppendInvariant(pageObject, $"/MediaBox [0 0 {pdfMediaSize} {pdfMediaSize}]\r\n"); // Page dimensions [x1 y1 x2 y2] + pageObject.Append("/Resources << /ProcSet [ /PDF ] >>\r\n"); // Required resources: PDF operations only (no images) + pageObject.Append("/Contents 4 0 R\r\n"); // References content stream (object 4) + pageObject.Append(">>\r\n"); // End dictionary + pageObject.Append("endobj\r\n"); // End object + writer.Write(pageObject.ToString()); writer.Flush(); xrefs.Add(stream.Position); // Object 4: Content stream - contains the drawing instructions - writer.Write( - ToStr(xrefs.Count) + " 0 obj\r\n" + // Object number and generation number (0) - "<< /Length " + ToStr(System.Text.Encoding.ASCII.GetByteCount(content)) + " >>\r\n" + // Dictionary with stream length in bytes - "stream\r\n" + // Begin stream data - content + "endstream\r\n" + // Stream content followed by end stream marker - "endobj\r\n" // End object - ); + var contentStreamObject = new StringBuilder(); + StringExtensions.AppendInvariant(contentStreamObject, $"{ToStr(xrefs.Count)} 0 obj\r\n"); // Object number and generation number (0) + StringExtensions.AppendInvariant(contentStreamObject, $"<< /Length {ToStr(System.Text.Encoding.ASCII.GetByteCount(contentString))} >>\r\n"); // Dictionary with stream length in bytes + StringExtensions.AppendInvariant(contentStreamObject, $"stream\r\n{contentString}endstream\r\n"); // Begin stream data; stream content followed by end stream marker + contentStreamObject.Append("endobj\r\n"); // End object + writer.Write(contentStreamObject.ToString()); writer.Flush(); var startxref = checked((int)stream.Position); // Cross-reference table - maps object numbers to byte offsets - writer.Write( - "xref\r\n" + // Cross-reference table keyword - "0 " + ToStr(xrefs.Count + 1) + "\r\n" + // First object number (0) and count of entries - "0000000000 65535 f\r\n" // Entry 0: always free, generation 65535, 'f' = free - ); + var xrefTable = new StringBuilder(); + xrefTable.Append("xref\r\n"); // Cross-reference table keyword + StringExtensions.AppendInvariant(xrefTable, $"0 {ToStr(xrefs.Count + 1)}\r\n"); // First object number (0) and count of entries + xrefTable.Append("0000000000 65535 f\r\n"); // Entry 0: always free, generation 65535, 'f' = free + writer.Write(xrefTable.ToString()); // Write byte offset for each object + var xrefEntry = new StringBuilder(); foreach (var refValue in xrefs) { // Write each entry as a 10-digit zero-padded byte offset, 5-digit zero-padded generation number (0), and 'n' = in use - writer.Write(checked((int)refValue).ToString("0000000000", CultureInfo.InvariantCulture) + " 00000 n\r\n"); + xrefEntry.Length = 0; + StringExtensions.AppendInvariant(xrefEntry, $"{checked((int)refValue).ToString("0000000000", CultureInfo.InvariantCulture)} 00000 n\r\n"); + writer.Write(xrefEntry.ToString()); } // Trailer - provides location of catalog and xref table - writer.Write( - "trailer\r\n" + // Trailer keyword - "<<\r\n" + // Begin trailer dictionary - "/Size " + ToStr(xrefs.Count + 1) + "\r\n" + // Total number of entries in xref table - "/Root 1 0 R\r\n" + // Reference to catalog object - ">>\r\n" + // End trailer dictionary - "startxref\r\n" + // Start of xref keyword - ToStr(startxref) + "\r\n" + // Byte offset of xref table - "%%EOF" // End of file marker - ); + var trailer = new StringBuilder(); + trailer.Append("trailer\r\n"); // Trailer keyword + trailer.Append("<<\r\n"); // Begin trailer dictionary + StringExtensions.AppendInvariant(trailer, $"/Size {ToStr(xrefs.Count + 1)}\r\n"); // Total number of entries in xref table + trailer.Append("/Root 1 0 R\r\n"); // Reference to catalog object + trailer.Append(">>\r\n"); // End trailer dictionary + trailer.Append("startxref\r\n"); // Start of xref keyword + StringExtensions.AppendInvariant(trailer, $"{ToStr(startxref)}\r\n"); // Byte offset of xref table + trailer.Append("%%EOF"); // End of file marker + writer.Write(trailer.ToString()); writer.Flush(); } @@ -214,12 +217,7 @@ private string CreatePathFromModules() // Create a single rectangle for the entire run of dark modules // Format: x y width height re - pathCommands.AppendInvariant(startX); - pathCommands.Append(' '); - pathCommands.AppendInvariant(y); - pathCommands.Append(' '); - pathCommands.AppendInvariant(x - startX); - pathCommands.Append(" 1 re\r\n"); + StringExtensions.AppendInvariant(pathCommands, $"{startX} {y} {x - startX} 1 re\r\n"); } } @@ -241,7 +239,7 @@ private static string ColorToPdfRgb(byte[] color) var g = ToStr(color[1] * inv255); var b = ToStr(color[2] * inv255); - return r + " " + g + " " + b; + return $"{r} {g} {b}"; } /// diff --git a/QRCoder/QRCodeGenerator/Polynom.cs b/QRCoder/QRCodeGenerator/Polynom.cs index 90237945..a1ed208d 100644 --- a/QRCoder/QRCodeGenerator/Polynom.cs +++ b/QRCoder/QRCodeGenerator/Polynom.cs @@ -154,7 +154,7 @@ public override string ToString() for (int i = 0; i < Count; i++) { var polyItem = _polyItems[i]; - sb.Append("a^" + polyItem.Coefficient + "*x^" + polyItem.Exponent + " + "); + StringExtensions.AppendInvariant(sb,$"a^{polyItem.Coefficient}*x^{polyItem.Exponent} + "); } // Remove the trailing " + " if the string builder has added terms