From 9582904008a733d0cde5d807a481edf941a536d2 Mon Sep 17 00:00:00 2001 From: Nate Hitze Date: Fri, 3 Sep 2021 18:51:32 -0400 Subject: [PATCH] Added QRCoder2 --- QRCoder.sln | 36 + .../ImageSharpByteQRCode.cs | 7 + .../QRCoder2.Renderers.ImageSharp.csproj | 12 + QRCoder2/Exceptions/DataTooLongException.cs | 16 + QRCoder2/Payloads/BezahlCodePayload.cs | 567 ++++++ ...BitcoinLikeCryptoCurrencyAddressPayload.cs | 86 + QRCoder2/Payloads/BookmarkPayload.cs | 23 + QRCoder2/Payloads/CalendarEventPayload.cs | 53 + QRCoder2/Payloads/ContactDataPayload.cs | 224 +++ QRCoder2/Payloads/ECCLevel.cs | 25 + QRCoder2/Payloads/EciMode.cs | 10 + QRCoder2/Payloads/GeolocationPayload.cs | 40 + QRCoder2/Payloads/GirocodePayload.cs | 138 ++ QRCoder2/Payloads/MMSPayload.cs | 52 + QRCoder2/Payloads/MailPayload.cs | 74 + QRCoder2/Payloads/MoneroTransactionPayload.cs | 59 + QRCoder2/Payloads/OneTimePasswordPayload.cs | 149 ++ QRCoder2/Payloads/PayloadBase.cs | 67 + QRCoder2/Payloads/PhoneNumberPayload.cs | 21 + QRCoder2/Payloads/PlainTextPayload.cs | 21 + QRCoder2/Payloads/SMSPayload.cs | 55 + QRCoder2/Payloads/ShadowSocksConfigPayload.cs | 227 +++ QRCoder2/Payloads/SkypeCallPayload.cs | 21 + QRCoder2/Payloads/SlovenianUpnQrPayload.cs | 101 ++ QRCoder2/Payloads/SwissQrCodePayload.cs | 599 +++++++ QRCoder2/Payloads/UrlPayload.cs | 21 + QRCoder2/Payloads/WhatsAppMessagePayload.cs | 35 + QRCoder2/Payloads/WiFiPayload.cs | 38 + QRCoder2/QRCodeData.cs | 172 ++ QRCoder2/QRCodeGenerator.cs | 1566 +++++++++++++++++ QRCoder2/QRCoder2.csproj | 8 + QRCoder2/Renderers/BitmapByteQRCode.cs | 123 ++ QRCoder2/Renderers/PngByteQRCode.cs | 338 ++++ QRCoder2/Renderers/RendererBase.cs | 12 + 34 files changed, 4996 insertions(+) create mode 100644 QRCoder2.Renderers.ImageSharp/ImageSharpByteQRCode.cs create mode 100644 QRCoder2.Renderers.ImageSharp/QRCoder2.Renderers.ImageSharp.csproj create mode 100644 QRCoder2/Exceptions/DataTooLongException.cs create mode 100644 QRCoder2/Payloads/BezahlCodePayload.cs create mode 100644 QRCoder2/Payloads/BitcoinLikeCryptoCurrencyAddressPayload.cs create mode 100644 QRCoder2/Payloads/BookmarkPayload.cs create mode 100644 QRCoder2/Payloads/CalendarEventPayload.cs create mode 100644 QRCoder2/Payloads/ContactDataPayload.cs create mode 100644 QRCoder2/Payloads/ECCLevel.cs create mode 100644 QRCoder2/Payloads/EciMode.cs create mode 100644 QRCoder2/Payloads/GeolocationPayload.cs create mode 100644 QRCoder2/Payloads/GirocodePayload.cs create mode 100644 QRCoder2/Payloads/MMSPayload.cs create mode 100644 QRCoder2/Payloads/MailPayload.cs create mode 100644 QRCoder2/Payloads/MoneroTransactionPayload.cs create mode 100644 QRCoder2/Payloads/OneTimePasswordPayload.cs create mode 100644 QRCoder2/Payloads/PayloadBase.cs create mode 100644 QRCoder2/Payloads/PhoneNumberPayload.cs create mode 100644 QRCoder2/Payloads/PlainTextPayload.cs create mode 100644 QRCoder2/Payloads/SMSPayload.cs create mode 100644 QRCoder2/Payloads/ShadowSocksConfigPayload.cs create mode 100644 QRCoder2/Payloads/SkypeCallPayload.cs create mode 100644 QRCoder2/Payloads/SlovenianUpnQrPayload.cs create mode 100644 QRCoder2/Payloads/SwissQrCodePayload.cs create mode 100644 QRCoder2/Payloads/UrlPayload.cs create mode 100644 QRCoder2/Payloads/WhatsAppMessagePayload.cs create mode 100644 QRCoder2/Payloads/WiFiPayload.cs create mode 100644 QRCoder2/QRCodeData.cs create mode 100644 QRCoder2/QRCodeGenerator.cs create mode 100644 QRCoder2/QRCoder2.csproj create mode 100644 QRCoder2/Renderers/BitmapByteQRCode.cs create mode 100644 QRCoder2/Renderers/PngByteQRCode.cs create mode 100644 QRCoder2/Renderers/RendererBase.cs diff --git a/QRCoder.sln b/QRCoder.sln index 2df93136..909b787b 100644 --- a/QRCoder.sln +++ b/QRCoder.sln @@ -13,6 +13,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QRCoderDemoUWP", "QRCoderDe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QRCoderTests", "QRCoderTests\QRCoderTests.csproj", "{1B51624B-9915-4ED6-8FC1-1B7C472246E5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QRCoder2", "QRCoder2\QRCoder2.csproj", "{990202D3-C323-433A-881D-382C4F5BD2D5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QRCoder2.Renderers.ImageSharp", "QRCoder2.Renderers.ImageSharp\QRCoder2.Renderers.ImageSharp.csproj", "{F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -101,6 +105,38 @@ Global {1B51624B-9915-4ED6-8FC1-1B7C472246E5}.Release|x64.Build.0 = Release|Any CPU {1B51624B-9915-4ED6-8FC1-1B7C472246E5}.Release|x86.ActiveCfg = Release|Any CPU {1B51624B-9915-4ED6-8FC1-1B7C472246E5}.Release|x86.Build.0 = Release|Any CPU + {990202D3-C323-433A-881D-382C4F5BD2D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {990202D3-C323-433A-881D-382C4F5BD2D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {990202D3-C323-433A-881D-382C4F5BD2D5}.Debug|ARM.ActiveCfg = Debug|Any CPU + {990202D3-C323-433A-881D-382C4F5BD2D5}.Debug|ARM.Build.0 = Debug|Any CPU + {990202D3-C323-433A-881D-382C4F5BD2D5}.Debug|x64.ActiveCfg = Debug|Any CPU + {990202D3-C323-433A-881D-382C4F5BD2D5}.Debug|x64.Build.0 = Debug|Any CPU + {990202D3-C323-433A-881D-382C4F5BD2D5}.Debug|x86.ActiveCfg = Debug|Any CPU + {990202D3-C323-433A-881D-382C4F5BD2D5}.Debug|x86.Build.0 = Debug|Any CPU + {990202D3-C323-433A-881D-382C4F5BD2D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {990202D3-C323-433A-881D-382C4F5BD2D5}.Release|Any CPU.Build.0 = Release|Any CPU + {990202D3-C323-433A-881D-382C4F5BD2D5}.Release|ARM.ActiveCfg = Release|Any CPU + {990202D3-C323-433A-881D-382C4F5BD2D5}.Release|ARM.Build.0 = Release|Any CPU + {990202D3-C323-433A-881D-382C4F5BD2D5}.Release|x64.ActiveCfg = Release|Any CPU + {990202D3-C323-433A-881D-382C4F5BD2D5}.Release|x64.Build.0 = Release|Any CPU + {990202D3-C323-433A-881D-382C4F5BD2D5}.Release|x86.ActiveCfg = Release|Any CPU + {990202D3-C323-433A-881D-382C4F5BD2D5}.Release|x86.Build.0 = Release|Any CPU + {F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}.Debug|ARM.ActiveCfg = Debug|Any CPU + {F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}.Debug|ARM.Build.0 = Debug|Any CPU + {F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}.Debug|x64.ActiveCfg = Debug|Any CPU + {F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}.Debug|x64.Build.0 = Debug|Any CPU + {F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}.Debug|x86.ActiveCfg = Debug|Any CPU + {F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}.Debug|x86.Build.0 = Debug|Any CPU + {F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}.Release|Any CPU.Build.0 = Release|Any CPU + {F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}.Release|ARM.ActiveCfg = Release|Any CPU + {F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}.Release|ARM.Build.0 = Release|Any CPU + {F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}.Release|x64.ActiveCfg = Release|Any CPU + {F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}.Release|x64.Build.0 = Release|Any CPU + {F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}.Release|x86.ActiveCfg = Release|Any CPU + {F2BEC8CB-B151-40BC-80B0-99FED2BEDD05}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/QRCoder2.Renderers.ImageSharp/ImageSharpByteQRCode.cs b/QRCoder2.Renderers.ImageSharp/ImageSharpByteQRCode.cs new file mode 100644 index 00000000..b5a76ce8 --- /dev/null +++ b/QRCoder2.Renderers.ImageSharp/ImageSharpByteQRCode.cs @@ -0,0 +1,7 @@ +namespace QRCoder2.Renderers.ImageSharp +{ + public class ImageSharpByteQRCode + { + + } +} \ No newline at end of file diff --git a/QRCoder2.Renderers.ImageSharp/QRCoder2.Renderers.ImageSharp.csproj b/QRCoder2.Renderers.ImageSharp/QRCoder2.Renderers.ImageSharp.csproj new file mode 100644 index 00000000..cfc582f2 --- /dev/null +++ b/QRCoder2.Renderers.ImageSharp/QRCoder2.Renderers.ImageSharp.csproj @@ -0,0 +1,12 @@ + + + + net6.0 + + + + + + + + diff --git a/QRCoder2/Exceptions/DataTooLongException.cs b/QRCoder2/Exceptions/DataTooLongException.cs new file mode 100644 index 00000000..c4327607 --- /dev/null +++ b/QRCoder2/Exceptions/DataTooLongException.cs @@ -0,0 +1,16 @@ +using System; + +namespace QRCoder2.Exceptions +{ + public class DataTooLongException : Exception + { + public DataTooLongException(string eccLevel, string encodingMode, int maxSizeByte) : base( + $"The given payload exceeds the maximum size of the QR code standard. The maximum size allowed for the choosen paramters (ECC level={eccLevel}, EncodingMode={encodingMode}) is {maxSizeByte} byte." + ){} + + public DataTooLongException(string eccLevel, string encodingMode, int version, int maxSizeByte) : base( + $"The given payload exceeds the maximum size of the QR code standard. The maximum size allowed for the choosen paramters (ECC level={eccLevel}, EncodingMode={encodingMode}, FixedVersion={version}) is {maxSizeByte} byte." + ) + { } + } +} diff --git a/QRCoder2/Payloads/BezahlCodePayload.cs b/QRCoder2/Payloads/BezahlCodePayload.cs new file mode 100644 index 00000000..cf81c769 --- /dev/null +++ b/QRCoder2/Payloads/BezahlCodePayload.cs @@ -0,0 +1,567 @@ +using System; +using System.Text.RegularExpressions; + +namespace QRCoder2.Payloads +{ + public class BezahlCodePayload : PayloadBase + { + //BezahlCode specification: http://www.bezahlcode.de/wp-content/uploads/BezahlCode_TechDok.pdf + + private readonly string name, iban, bic, account, bnc, sepaReference, reason, creditorId, mandateId, periodicTimeunit; + private readonly decimal amount; + private readonly int postingKey, periodicTimeunitRotation; + private readonly Currency currency; + private readonly AuthorityType authority; + private readonly DateTime executionDate, dateOfSignature, periodicFirstExecutionDate, periodicLastExecutionDate; + + + /// + /// Constructor for contact data + /// + /// Type of the bank transfer + /// Name of the receiver (Empfänger) + /// Bank account (Kontonummer) + /// Bank institute (Bankleitzahl) + /// IBAN + /// BIC + /// Reason (Verwendungszweck) + public BezahlCodePayload(AuthorityType authority, string name, string account = "", string bnc = "", string iban = "", string bic = "", string reason = "") : this(authority, name, account, bnc, iban, bic, 0, string.Empty, 0, null, null, string.Empty, string.Empty, null, reason, 0, string.Empty, Currency.EUR, null, 1) + { + } + + + /// + /// Constructor for non-SEPA payments + /// + /// Type of the bank transfer + /// Name of the receiver (Empfänger) + /// Bank account (Kontonummer) + /// Bank institute (Bankleitzahl) + /// Amount (Betrag) + /// Unit of intervall for payment ('M' = monthly, 'W' = weekly) + /// Intervall for payment. This value is combined with 'periodicTimeunit' + /// Date of first periodic execution + /// Date of last periodic execution + /// Reason (Verwendungszweck) + /// Transfer Key (Textschlüssel, z.B. Spendenzahlung = 69) + /// Currency (Währung) + /// Execution date (Ausführungsdatum) + public BezahlCodePayload(AuthorityType authority, string name, string account, string bnc, decimal amount, string periodicTimeunit = "", int periodicTimeunitRotation = 0, DateTime? periodicFirstExecutionDate = null, DateTime? periodicLastExecutionDate = null, string reason = "", int postingKey = 0, Currency currency = Currency.EUR, DateTime? executionDate = null) : this(authority, name, account, bnc, string.Empty, string.Empty, amount, periodicTimeunit, periodicTimeunitRotation, periodicFirstExecutionDate, periodicLastExecutionDate, string.Empty, string.Empty, null, reason, postingKey, string.Empty, currency, executionDate, 2) + { + } + + /// + /// Constructor for SEPA payments + /// + /// Type of the bank transfer + /// Name of the receiver (Empfänger) + /// IBAN + /// BIC + /// Amount (Betrag) + /// Unit of intervall for payment ('M' = monthly, 'W' = weekly) + /// Intervall for payment. This value is combined with 'periodicTimeunit' + /// Date of first periodic execution + /// Date of last periodic execution + /// Creditor id (Gläubiger ID) + /// Manadate id (Mandatsreferenz) + /// Signature date (Erteilungsdatum des Mandats) + /// Reason (Verwendungszweck) + /// Transfer Key (Textschlüssel, z.B. Spendenzahlung = 69) + /// SEPA reference (SEPA-Referenz) + /// Currency (Währung) + /// Execution date (Ausführungsdatum) + public BezahlCodePayload(AuthorityType authority, string name, string iban, string bic, decimal amount, string periodicTimeunit = "", int periodicTimeunitRotation = 0, DateTime? periodicFirstExecutionDate = null, DateTime? periodicLastExecutionDate = null, string creditorId = "", string mandateId = "", DateTime? dateOfSignature = null, string reason = "", string sepaReference = "", Currency currency = Currency.EUR, DateTime? executionDate = null) : this(authority, name, string.Empty, string.Empty, iban, bic, amount, periodicTimeunit, periodicTimeunitRotation, periodicFirstExecutionDate, periodicLastExecutionDate, creditorId, mandateId, dateOfSignature, reason, 0, sepaReference, currency, executionDate, 3) + { + } + + + + + /// + /// Generic constructor. Please use specific (non-SEPA or SEPA) constructor + /// + /// Type of the bank transfer + /// Name of the receiver (Empfänger) + /// Bank account (Kontonummer) + /// Bank institute (Bankleitzahl) + /// IBAN + /// BIC + /// Amount (Betrag) + /// Unit of intervall for payment ('M' = monthly, 'W' = weekly) + /// Intervall for payment. This value is combined with 'periodicTimeunit' + /// Date of first periodic execution + /// Date of last periodic execution + /// Creditor id (Gläubiger ID) + /// Manadate id (Mandatsreferenz) + /// Signature date (Erteilungsdatum des Mandats) + /// Reason (Verwendungszweck) + /// Transfer Key (Textschlüssel, z.B. Spendenzahlung = 69) + /// SEPA reference (SEPA-Referenz) + /// Currency (Währung) + /// Execution date (Ausführungsdatum) + /// Only used for internal state handdling + public BezahlCodePayload(AuthorityType authority, string name, string account, string bnc, string iban, string bic, decimal amount, string periodicTimeunit = "", int periodicTimeunitRotation = 0, DateTime? periodicFirstExecutionDate = null, DateTime? periodicLastExecutionDate = null, string creditorId = "", string mandateId = "", DateTime? dateOfSignature = null, string reason = "", int postingKey = 0, string sepaReference = "", Currency currency = Currency.EUR, DateTime? executionDate = null, int internalMode = 0) + { + //Loaded via "contact-constructor" + if (internalMode == 1) + { + if (authority != AuthorityType.contact && authority != AuthorityType.contact_v2) + throw new BezahlCodeException("The constructor without an amount may only ne used with authority types 'contact' and 'contact_v2'."); + if (authority == AuthorityType.contact && (string.IsNullOrEmpty(account) || string.IsNullOrEmpty(bnc))) + throw new BezahlCodeException("When using authority type 'contact' the parameters 'account' and 'bnc' must be set."); + + if (authority != AuthorityType.contact_v2) + { + var oldFilled = (!string.IsNullOrEmpty(account) && !string.IsNullOrEmpty(bnc)); + var newFilled = (!string.IsNullOrEmpty(iban) && !string.IsNullOrEmpty(bic)); + if ((!oldFilled && !newFilled) || (oldFilled && newFilled)) + throw new BezahlCodeException("When using authority type 'contact_v2' either the parameters 'account' and 'bnc' or the parameters 'iban' and 'bic' must be set. Leave the other parameter pair empty."); + } + } + else if (internalMode == 2) + { +#pragma warning disable CS0612 + if (authority != AuthorityType.periodicsinglepayment && authority != AuthorityType.singledirectdebit && authority != AuthorityType.singlepayment) + throw new BezahlCodeException("The constructor with 'account' and 'bnc' may only be used with 'non SEPA' authority types. Either choose another authority type or switch constructor."); + if (authority == AuthorityType.periodicsinglepayment && (string.IsNullOrEmpty(periodicTimeunit) || periodicTimeunitRotation == 0)) + throw new BezahlCodeException("When using 'periodicsinglepayment' as authority type, the parameters 'periodicTimeunit' and 'periodicTimeunitRotation' must be set."); +#pragma warning restore CS0612 + } + else if (internalMode == 3) + { + if (authority != AuthorityType.periodicsinglepaymentsepa && authority != AuthorityType.singledirectdebitsepa && authority != AuthorityType.singlepaymentsepa) + throw new BezahlCodeException("The constructor with 'iban' and 'bic' may only be used with 'SEPA' authority types. Either choose another authority type or switch constructor."); + if (authority == AuthorityType.periodicsinglepaymentsepa && (string.IsNullOrEmpty(periodicTimeunit) || periodicTimeunitRotation == 0)) + throw new BezahlCodeException("When using 'periodicsinglepaymentsepa' as authority type, the parameters 'periodicTimeunit' and 'periodicTimeunitRotation' must be set."); + } + + this.authority = authority; + + if (name.Length > 70) + throw new BezahlCodeException("(Payee-)Name must be shorter than 71 chars."); + this.name = name; + + if (reason.Length > 27) + throw new BezahlCodeException("Reasons texts have to be shorter than 28 chars."); + this.reason = reason; + + var oldWayFilled = (!string.IsNullOrEmpty(account) && !string.IsNullOrEmpty(bnc)); + var newWayFilled = (!string.IsNullOrEmpty(iban) && !string.IsNullOrEmpty(bic)); + + //Non-SEPA payment types +#pragma warning disable CS0612 + if (authority == AuthorityType.periodicsinglepayment || authority == AuthorityType.singledirectdebit || authority == AuthorityType.singlepayment || authority == AuthorityType.contact || (authority == AuthorityType.contact_v2 && oldWayFilled)) + { +#pragma warning restore CS0612 + if (!Regex.IsMatch(account.Replace(" ", ""), @"^[0-9]{1,9}$")) + throw new BezahlCodeException("The account entered isn't valid."); + this.account = account.Replace(" ", "").ToUpper(); + if(!Regex.IsMatch(bnc.Replace(" ", ""), @"^[0-9]{1,9}$")) + throw new BezahlCodeException("The bnc entered isn't valid."); + this.bnc = bnc.Replace(" ", "").ToUpper(); + + if (authority != AuthorityType.contact && authority != AuthorityType.contact_v2) + { + if (postingKey < 0 || postingKey >= 100) + throw new BezahlCodeException("PostingKey must be within 0 and 99."); + this.postingKey = postingKey; + } + } + + //SEPA payment types + if (authority == AuthorityType.periodicsinglepaymentsepa || authority == AuthorityType.singledirectdebitsepa || authority == AuthorityType.singlepaymentsepa || (authority == AuthorityType.contact_v2 && newWayFilled)) + { + if (!IsValidIban(iban)) + throw new BezahlCodeException("The IBAN entered isn't valid."); + this.iban = iban.Replace(" ", "").ToUpper(); + if (!IsValidBic(bic)) + throw new BezahlCodeException("The BIC entered isn't valid."); + this.bic = bic.Replace(" ", "").ToUpper(); + + if (authority != AuthorityType.contact_v2) + { + if (sepaReference.Length > 35) + throw new BezahlCodeException("SEPA reference texts have to be shorter than 36 chars."); + this.sepaReference = sepaReference; + + if (!string.IsNullOrEmpty(creditorId) && !Regex.IsMatch(creditorId.Replace(" ", ""), @"^[a-zA-Z]{2,2}[0-9]{2,2}([A-Za-z0-9]|[\+|\?|/|\-|:|\(|\)|\.|,|']){3,3}([A-Za-z0-9]|[\+|\?|/|\-|:|\(|\)|\.|,|']){1,28}$")) + throw new BezahlCodeException("The creditorId entered isn't valid."); + this.creditorId = creditorId; + if (!string.IsNullOrEmpty(mandateId) && !Regex.IsMatch(mandateId.Replace(" ", ""), @"^([A-Za-z0-9]|[\+|\?|/|\-|:|\(|\)|\.|,|']){1,35}$")) + throw new BezahlCodeException("The mandateId entered isn't valid."); + this.mandateId = mandateId; + if (dateOfSignature != null) + this.dateOfSignature = (DateTime)dateOfSignature; + } + } + + //Checks for all payment types + if (authority != AuthorityType.contact && authority != AuthorityType.contact_v2) + { + if (amount.ToString().Replace(",", ".").Contains(".") && amount.ToString().Replace(",", ".").Split('.')[1].TrimEnd('0').Length > 2) + throw new BezahlCodeException("Amount must have less than 3 digits after decimal point."); + if (amount < 0.01m || amount > 999999999.99m) + throw new BezahlCodeException("Amount has to at least 0.01 and must be smaller or equal to 999999999.99."); + this.amount = amount; + + this.currency = currency; + + if (executionDate == null) + this.executionDate = DateTime.Now; + else + { + if (DateTime.Today.Ticks > executionDate.Value.Ticks) + throw new BezahlCodeException("Execution date must be today or in future."); + this.executionDate = (DateTime)executionDate; + } +#pragma warning disable CS0612 + if (authority == AuthorityType.periodicsinglepayment || authority == AuthorityType.periodicsinglepaymentsepa) +#pragma warning restore CS0612 + { + if (periodicTimeunit.ToUpper() != "M" && periodicTimeunit.ToUpper() != "W") + throw new BezahlCodeException("The periodicTimeunit must be either 'M' (monthly) or 'W' (weekly)."); + this.periodicTimeunit = periodicTimeunit; + if (periodicTimeunitRotation < 1 || periodicTimeunitRotation > 52) + throw new BezahlCodeException("The periodicTimeunitRotation must be 1 or greater. (It means repeat the payment every 'periodicTimeunitRotation' weeks/months."); + this.periodicTimeunitRotation = periodicTimeunitRotation; + if (periodicFirstExecutionDate != null) + this.periodicFirstExecutionDate = (DateTime)periodicFirstExecutionDate; + if (periodicLastExecutionDate != null) + this.periodicLastExecutionDate = (DateTime)periodicLastExecutionDate; + } + + } + + + + } + + public override string ToString() + { + var bezahlCodePayload = $"bank://{authority}?"; + + bezahlCodePayload += $"name={Uri.EscapeDataString(name)}&"; + + if (authority != AuthorityType.contact && authority != AuthorityType.contact_v2) + { + //Handle what is same for all payments +#pragma warning disable CS0612 + if (authority == AuthorityType.periodicsinglepayment || authority == AuthorityType.singledirectdebit || authority == AuthorityType.singlepayment) +#pragma warning restore CS0612 + { + bezahlCodePayload += $"account={account}&"; + bezahlCodePayload += $"bnc={bnc}&"; + if (postingKey > 0) + bezahlCodePayload += $"postingkey={postingKey}&"; + } + else + { + bezahlCodePayload += $"iban={iban}&"; + bezahlCodePayload += $"bic={bic}&"; + + if (!string.IsNullOrEmpty(sepaReference)) + bezahlCodePayload += $"separeference={ Uri.EscapeDataString(sepaReference)}&"; + + if (authority == AuthorityType.singledirectdebitsepa) + { + if (!string.IsNullOrEmpty(creditorId)) + bezahlCodePayload += $"creditorid={ Uri.EscapeDataString(creditorId)}&"; + if (!string.IsNullOrEmpty(mandateId)) + bezahlCodePayload += $"mandateid={ Uri.EscapeDataString(mandateId)}&"; + if (dateOfSignature != DateTime.MinValue) + bezahlCodePayload += $"dateofsignature={dateOfSignature.ToString("ddMMyyyy")}&"; + } + } + bezahlCodePayload += $"amount={amount:0.00}&".Replace(".", ","); + + if (!string.IsNullOrEmpty(reason)) + bezahlCodePayload += $"reason={ Uri.EscapeDataString(reason)}&"; + bezahlCodePayload += $"currency={currency}&"; + bezahlCodePayload += $"executiondate={executionDate.ToString("ddMMyyyy")}&"; +#pragma warning disable CS0612 + if (authority == AuthorityType.periodicsinglepayment || authority == AuthorityType.periodicsinglepaymentsepa) + { + bezahlCodePayload += $"periodictimeunit={periodicTimeunit}&"; + bezahlCodePayload += $"periodictimeunitrotation={periodicTimeunitRotation}&"; + if (periodicFirstExecutionDate != DateTime.MinValue) + bezahlCodePayload += $"periodicfirstexecutiondate={periodicFirstExecutionDate.ToString("ddMMyyyy")}&"; + if (periodicLastExecutionDate != DateTime.MinValue) + bezahlCodePayload += $"periodiclastexecutiondate={periodicLastExecutionDate.ToString("ddMMyyyy")}&"; + } +#pragma warning restore CS0612 + } + else + { + //Handle what is same for all contacts + if (authority == AuthorityType.contact) + { + bezahlCodePayload += $"account={account}&"; + bezahlCodePayload += $"bnc={bnc}&"; + } + else if (authority == AuthorityType.contact_v2) + { + if (!string.IsNullOrEmpty(account) && !string.IsNullOrEmpty(bnc)) + { + bezahlCodePayload += $"account={account}&"; + bezahlCodePayload += $"bnc={bnc}&"; + } + else + { + bezahlCodePayload += $"iban={iban}&"; + bezahlCodePayload += $"bic={bic}&"; + } + } + + if (!string.IsNullOrEmpty(reason)) + bezahlCodePayload += $"reason={ Uri.EscapeDataString(reason)}&"; + } + + return bezahlCodePayload.Trim('&'); + } + + /// + /// ISO 4217 currency codes + /// + public enum Currency + { + AED = 784, + AFN = 971, + ALL = 008, + AMD = 051, + ANG = 532, + AOA = 973, + ARS = 032, + AUD = 036, + AWG = 533, + AZN = 944, + BAM = 977, + BBD = 052, + BDT = 050, + BGN = 975, + BHD = 048, + BIF = 108, + BMD = 060, + BND = 096, + BOB = 068, + BOV = 984, + BRL = 986, + BSD = 044, + BTN = 064, + BWP = 072, + BYR = 974, + BZD = 084, + CAD = 124, + CDF = 976, + CHE = 947, + CHF = 756, + CHW = 948, + CLF = 990, + CLP = 152, + CNY = 156, + COP = 170, + COU = 970, + CRC = 188, + CUC = 931, + CUP = 192, + CVE = 132, + CZK = 203, + DJF = 262, + DKK = 208, + DOP = 214, + DZD = 012, + EGP = 818, + ERN = 232, + ETB = 230, + EUR = 978, + FJD = 242, + FKP = 238, + GBP = 826, + GEL = 981, + GHS = 936, + GIP = 292, + GMD = 270, + GNF = 324, + GTQ = 320, + GYD = 328, + HKD = 344, + HNL = 340, + HRK = 191, + HTG = 332, + HUF = 348, + IDR = 360, + ILS = 376, + INR = 356, + IQD = 368, + IRR = 364, + ISK = 352, + JMD = 388, + JOD = 400, + JPY = 392, + KES = 404, + KGS = 417, + KHR = 116, + KMF = 174, + KPW = 408, + KRW = 410, + KWD = 414, + KYD = 136, + KZT = 398, + LAK = 418, + LBP = 422, + LKR = 144, + LRD = 430, + LSL = 426, + LYD = 434, + MAD = 504, + MDL = 498, + MGA = 969, + MKD = 807, + MMK = 104, + MNT = 496, + MOP = 446, + MRO = 478, + MUR = 480, + MVR = 462, + MWK = 454, + MXN = 484, + MXV = 979, + MYR = 458, + MZN = 943, + NAD = 516, + NGN = 566, + NIO = 558, + NOK = 578, + NPR = 524, + NZD = 554, + OMR = 512, + PAB = 590, + PEN = 604, + PGK = 598, + PHP = 608, + PKR = 586, + PLN = 985, + PYG = 600, + QAR = 634, + RON = 946, + RSD = 941, + RUB = 643, + RWF = 646, + SAR = 682, + SBD = 090, + SCR = 690, + SDG = 938, + SEK = 752, + SGD = 702, + SHP = 654, + SLL = 694, + SOS = 706, + SRD = 968, + SSP = 728, + STD = 678, + SVC = 222, + SYP = 760, + SZL = 748, + THB = 764, + TJS = 972, + TMT = 934, + TND = 788, + TOP = 776, + TRY = 949, + TTD = 780, + TWD = 901, + TZS = 834, + UAH = 980, + UGX = 800, + USD = 840, + USN = 997, + UYI = 940, + UYU = 858, + UZS = 860, + VEF = 937, + VND = 704, + VUV = 548, + WST = 882, + XAF = 950, + XAG = 961, + XAU = 959, + XBA = 955, + XBB = 956, + XBC = 957, + XBD = 958, + XCD = 951, + XDR = 960, + XOF = 952, + XPD = 964, + XPF = 953, + XPT = 962, + XSU = 994, + XTS = 963, + XUA = 965, + XXX = 999, + YER = 886, + ZAR = 710, + ZMW = 967, + ZWL = 932 + } + + + /// + /// Operation modes of the BezahlCode + /// + public enum AuthorityType + { + /// + /// Single payment (Überweisung) + /// + [Obsolete] + singlepayment, + /// + /// Single SEPA payment (SEPA-Überweisung) + /// + singlepaymentsepa, + /// + /// Single debit (Lastschrift) + /// + [Obsolete] + singledirectdebit, + /// + /// Single SEPA debit (SEPA-Lastschrift) + /// + singledirectdebitsepa, + /// + /// Periodic payment (Dauerauftrag) + /// + [Obsolete] + periodicsinglepayment, + /// + /// Periodic SEPA payment (SEPA-Dauerauftrag) + /// + periodicsinglepaymentsepa, + /// + /// Contact data + /// + contact, + /// + /// Contact data V2 + /// + contact_v2 + } + + public class BezahlCodeException : Exception + { + public BezahlCodeException() + { + } + + public BezahlCodeException(string message) + : base(message) + { + } + + public BezahlCodeException(string message, Exception inner) + : base(message, inner) + { + } + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/BitcoinLikeCryptoCurrencyAddressPayload.cs b/QRCoder2/Payloads/BitcoinLikeCryptoCurrencyAddressPayload.cs new file mode 100644 index 00000000..327bca9a --- /dev/null +++ b/QRCoder2/Payloads/BitcoinLikeCryptoCurrencyAddressPayload.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace QRCoder2.Payloads +{ + public class BitcoinLikeCryptoCurrencyAddressPayload : PayloadBase + { + private readonly BitcoinLikeCryptoCurrencyType currencyType; + private readonly string address, label, message; + private readonly double? amount; + + /// + /// Generates a Bitcoin like cryptocurrency payment payload. QR Codes with this payload can open a payment app. + /// + /// Bitcoin like cryptocurrency address of the payment receiver + /// Bitcoin like cryptocurrency address of the payment receiver + /// Amount of coins to transfer + /// Reference label + /// Referece text aka message + public BitcoinLikeCryptoCurrencyAddressPayload(BitcoinLikeCryptoCurrencyType currencyType, string address, double? amount, string label = null, string message = null) + { + this.currencyType = currencyType; + this.address = address; + + if (!string.IsNullOrEmpty(label)) + { + this.label = Uri.EscapeUriString(label); + } + + if (!string.IsNullOrEmpty(message)) + { + this.message = Uri.EscapeUriString(message); + } + + this.amount = amount; + } + + public override string ToString() + { + string query = null; + + var queryValues = new KeyValuePair[]{ + new KeyValuePair(nameof(label), label), + new KeyValuePair(nameof(message), message), + new KeyValuePair(nameof(amount), amount.HasValue ? amount.Value.ToString("#.########", CultureInfo.InvariantCulture) : null) + }; + + if (queryValues.Any(keyPair => !string.IsNullOrEmpty(keyPair.Value))) + { + query = "?" + string.Join("&", queryValues + .Where(keyPair => !string.IsNullOrEmpty(keyPair.Value)) + .Select(keyPair => $"{keyPair.Key}={keyPair.Value}") + .ToArray()); + } + + return $"{Enum.GetName(typeof(BitcoinLikeCryptoCurrencyType), currencyType).ToLower()}:{address}{query}"; + } + + public enum BitcoinLikeCryptoCurrencyType + { + Bitcoin, + BitcoinCash, + Litecoin + } + } + + public class BitcoinAddressPayload : BitcoinLikeCryptoCurrencyAddressPayload + { + public BitcoinAddressPayload(string address, double? amount, string label = null, string message = null) + : base(BitcoinLikeCryptoCurrencyType.Bitcoin, address, amount, label, message) { } + } + + public class BitcoinCashAddressPayload : BitcoinLikeCryptoCurrencyAddressPayload + { + public BitcoinCashAddressPayload(string address, double? amount, string label = null, string message = null) + : base(BitcoinLikeCryptoCurrencyType.BitcoinCash, address, amount, label, message) { } + } + + public class LitecoinAddressPayload : BitcoinLikeCryptoCurrencyAddressPayload + { + public LitecoinAddressPayload(string address, double? amount, string label = null, string message = null) + : base(BitcoinLikeCryptoCurrencyType.Litecoin, address, amount, label, message) { } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/BookmarkPayload.cs b/QRCoder2/Payloads/BookmarkPayload.cs new file mode 100644 index 00000000..b7b3b0fa --- /dev/null +++ b/QRCoder2/Payloads/BookmarkPayload.cs @@ -0,0 +1,23 @@ +namespace QRCoder2.Payloads +{ + public class BookmarkPayload : PayloadBase + { + private readonly string url, title; + + /// + /// Generates a bookmark payload. Scanned by an QR Code reader, this one creates a browser bookmark. + /// + /// Url of the bookmark + /// Title of the bookmark + public BookmarkPayload(string url, string title) + { + this.url = EscapeInput(url); + this.title = EscapeInput(title); + } + + public override string ToString() + { + return $"MEBKM:TITLE:{this.title};URL:{this.url};;"; + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/CalendarEventPayload.cs b/QRCoder2/Payloads/CalendarEventPayload.cs new file mode 100644 index 00000000..0d349163 --- /dev/null +++ b/QRCoder2/Payloads/CalendarEventPayload.cs @@ -0,0 +1,53 @@ +using System; + +namespace QRCoder2.Payloads +{ + public class CalendarEventPayload : PayloadBase + { + private readonly string subject, description, location, start, end; + private readonly EventEncoding encoding; + + /// + /// Generates a calender entry/event payload. + /// + /// Subject/title of the calender event + /// Description of the event + /// Location (lat:long or address) of the event + /// Start time of the event + /// End time of the event + /// Is it a full day event? + /// Type of encoding (universal or iCal) + public CalendarEventPayload(string subject, string description, string location, DateTime start, DateTime end, bool allDayEvent, EventEncoding encoding = EventEncoding.Universal) + { + this.subject = subject; + this.description = description; + this.location = location; + this.encoding = encoding; + string dtFormat = allDayEvent ? "yyyyMMdd" : "yyyyMMddTHHmmss"; + this.start = start.ToString(dtFormat); + this.end = end.ToString(dtFormat); + } + + public override string ToString() + { + var vEvent = $"BEGIN:VEVENT{Environment.NewLine}"; + vEvent += $"SUMMARY:{this.subject}{Environment.NewLine}"; + vEvent += !string.IsNullOrEmpty(this.description) ? $"DESCRIPTION:{this.description}{Environment.NewLine}" : ""; + vEvent += !string.IsNullOrEmpty(this.location) ? $"LOCATION:{this.location}{Environment.NewLine}" : ""; + vEvent += $"DTSTART:{this.start}{Environment.NewLine}"; + vEvent += $"DTEND:{this.end}{Environment.NewLine}"; + vEvent += "END:VEVENT"; + + if (this.encoding == EventEncoding.iCalComplete) + vEvent = $@"BEGIN:VCALENDAR{Environment.NewLine}VERSION:2.0{Environment.NewLine}{vEvent}{Environment.NewLine}END:VCALENDAR"; + + return vEvent; + } + + public enum EventEncoding + { + iCalComplete, + Universal + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/ContactDataPayload.cs b/QRCoder2/Payloads/ContactDataPayload.cs new file mode 100644 index 00000000..ed9c910a --- /dev/null +++ b/QRCoder2/Payloads/ContactDataPayload.cs @@ -0,0 +1,224 @@ +using System; + +namespace QRCoder2.Payloads +{ + public class ContactDataPayload : PayloadBase + { + private readonly string firstname; + private readonly string lastname; + private readonly string nickname; + private readonly string org; + private readonly string phone; + private readonly string mobilePhone; + private readonly string workPhone; + private readonly string email; + private readonly DateOnly? birthday; + private readonly string website; + private readonly string street; + private readonly string houseNumber; + private readonly string city; + private readonly string zipCode; + private readonly string stateRegion; + private readonly string country; + private readonly string note; + private readonly ContactOutputType outputType; + private readonly AddressOrder addressOrder; + + + /// + /// Generates a vCard or meCard contact dataset + /// + /// Payload output type + /// The firstname + /// The lastname + /// The displayname + /// Normal phone number + /// Mobile phone + /// Office phone number + /// E-Mail address + /// Birthday + /// Website / Homepage + /// Street + /// Housenumber + /// City + /// State or Region + /// Zip code + /// Country + /// The address order format to use + /// Memo text / notes + public ContactDataPayload(ContactOutputType outputType, string firstname, string lastname, string nickname = null, string phone = null, string mobilePhone = null, string workPhone = null, string email = null, DateOnly? birthday = null, string website = null, string street = null, string houseNumber = null, string city = null, string zipCode = null, string country = null, string note = null, string stateRegion = null, AddressOrder addressOrder = AddressOrder.Default, string org = null) + { + this.firstname = firstname; + this.lastname = lastname; + this.nickname = nickname; + this.org = org; + this.phone = phone; + this.mobilePhone = mobilePhone; + this.workPhone = workPhone; + this.email = email; + this.birthday = birthday; + this.website = website; + this.street = street; + this.houseNumber = houseNumber; + this.city = city; + this.stateRegion = stateRegion; + this.zipCode = zipCode; + this.country = country; + this.addressOrder = addressOrder; + this.note = note; + this.outputType = outputType; + } + + public override string ToString() + { + string payload = string.Empty; + if (outputType == ContactOutputType.MeCard) + { + payload += "MECARD+\r\n"; + if (!string.IsNullOrEmpty(firstname) && !string.IsNullOrEmpty(lastname)) + payload += $"N:{lastname}, {firstname}\r\n"; + else if (!string.IsNullOrEmpty(firstname) || !string.IsNullOrEmpty(lastname)) + payload += $"N:{firstname}{lastname}\r\n"; + if (!string.IsNullOrEmpty(org)) + payload += $"ORG:{org}\r\n"; + if (!string.IsNullOrEmpty(phone)) + payload += $"TEL:{phone}\r\n"; + if (!string.IsNullOrEmpty(mobilePhone)) + payload += $"TEL:{mobilePhone}\r\n"; + if (!string.IsNullOrEmpty(workPhone)) + payload += $"TEL:{workPhone}\r\n"; + if (!string.IsNullOrEmpty(email)) + payload += $"EMAIL:{email}\r\n"; + if (!string.IsNullOrEmpty(note)) + payload += $"NOTE:{note}\r\n"; + if (birthday != null) + payload += $"BDAY:{((DateOnly)birthday).ToString("yyyyMMdd")}\r\n"; + string addressString = string.Empty; + if(addressOrder == AddressOrder.Default) + { + addressString = $"ADR:,,{(!string.IsNullOrEmpty(street) ? street + " " : "")}{(!string.IsNullOrEmpty(houseNumber) ? houseNumber : "")},{(!string.IsNullOrEmpty(zipCode) ? zipCode : "")},{(!string.IsNullOrEmpty(city) ? city : "")},{(!string.IsNullOrEmpty(stateRegion) ? stateRegion : "")},{(!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"; + } + payload += addressString; + if (!string.IsNullOrEmpty(website)) + payload += $"URL:{website}\r\n"; + if (!string.IsNullOrEmpty(nickname)) + payload += $"NICKNAME:{nickname}\r\n"; + payload = payload.Trim(new char[] { '\r', '\n' }); + } + 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(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"; + } + + + payload += "ADR;"; + if (outputType == ContactOutputType.VCard21) + payload += "HOME;PREF:"; + else if (outputType == ContactOutputType.VCard3) + payload += "TYPE=HOME,PREF:"; + else + payload += "TYPE=home,pref:"; + string addressString = string.Empty; + if(addressOrder == AddressOrder.Default) + { + addressString = $";;{(!string.IsNullOrEmpty(street) ? street + " " : "")}{(!string.IsNullOrEmpty(houseNumber) ? houseNumber : "")};{(!string.IsNullOrEmpty(zipCode) ? zipCode : "")};{(!string.IsNullOrEmpty(city) ? city : "")};{(!string.IsNullOrEmpty(stateRegion) ? stateRegion : "")};{(!string.IsNullOrEmpty(country) ? country : "")}\r\n"; + } + 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; + + if (birthday != null) + payload += $"BDAY:{((DateOnly)birthday).ToString("yyyyMMdd")}\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"; + + payload += "END:VCARD"; + } + + return payload; + } + + /// + /// Possible output types. Either vCard 2.1, vCard 3.0, vCard 4.0 or MeCard. + /// + public enum ContactOutputType + { + MeCard, + VCard21, + VCard3, + VCard4 + } + + + /// + /// define the address format + /// Default: European format, ([Street] [House Number] and [Postal Code] [City] + /// Reversed: North American and others format ([House Number] [Street] and [City] [Postal Code]) + /// + public enum AddressOrder + { + Default, + Reversed + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/ECCLevel.cs b/QRCoder2/Payloads/ECCLevel.cs new file mode 100644 index 00000000..0bef4cfd --- /dev/null +++ b/QRCoder2/Payloads/ECCLevel.cs @@ -0,0 +1,25 @@ +namespace QRCoder2.Payloads +{ + /// + /// Error correction level. These define the tolerance levels for how much of the code can be lost before the code cannot be recovered. + /// + public enum ECCLevel + { + /// + /// 7% may be lost before recovery is not possible + /// + L, + /// + /// 15% may be lost before recovery is not possible + /// + M, + /// + /// 25% may be lost before recovery is not possible + /// + Q, + /// + /// 30% may be lost before recovery is not possible + /// + H, + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/EciMode.cs b/QRCoder2/Payloads/EciMode.cs new file mode 100644 index 00000000..2d4f2e0d --- /dev/null +++ b/QRCoder2/Payloads/EciMode.cs @@ -0,0 +1,10 @@ +namespace QRCoder2.Payloads +{ + public enum EciMode + { + Default = 0, + Iso8859_1 = 3, + Iso8859_2 = 4, + Utf8 = 26 + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/GeolocationPayload.cs b/QRCoder2/Payloads/GeolocationPayload.cs new file mode 100644 index 00000000..54bfbe0a --- /dev/null +++ b/QRCoder2/Payloads/GeolocationPayload.cs @@ -0,0 +1,40 @@ +namespace QRCoder2.Payloads +{ + public class GeolocationPayload : PayloadBase + { + private readonly string latitude, longitude; + private readonly GeolocationEncoding encoding; + + /// + /// Generates a geo location payload. Supports raw location (GEO encoding) or Google Maps link (GoogleMaps encoding) + /// + /// Latitude with . as splitter + /// Longitude with . as splitter + /// Encoding type - GEO or GoogleMaps + public GeolocationPayload(string latitude, string longitude, GeolocationEncoding encoding = GeolocationEncoding.GEO) + { + this.latitude = latitude.Replace(",","."); + this.longitude = longitude.Replace(",", "."); + this.encoding = encoding; + } + + public override string ToString() + { + switch (this.encoding) + { + case GeolocationEncoding.GEO: + return $"geo:{this.latitude},{this.longitude}"; + case GeolocationEncoding.GoogleMaps: + return $"http://maps.google.com/maps?q={this.latitude},{this.longitude}"; + default: + return "geo:"; + } + } + + public enum GeolocationEncoding + { + GEO, + GoogleMaps + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/GirocodePayload.cs b/QRCoder2/Payloads/GirocodePayload.cs new file mode 100644 index 00000000..89015c63 --- /dev/null +++ b/QRCoder2/Payloads/GirocodePayload.cs @@ -0,0 +1,138 @@ +using System; +using System.Text; + +namespace QRCoder2.Payloads +{ + public class GirocodePayload : PayloadBase + { + //Keep in mind, that the ECC level has to be set to "M" when generating a Girocode! + //Girocode specification: http://www.europeanpaymentscouncil.eu/index.cfm/knowledge-bank/epc-documents/quick-response-code-guidelines-to-enable-data-capture-for-the-initiation-of-a-sepa-credit-transfer/epc069-12-quick-response-code-guidelines-to-enable-data-capture-for-the-initiation-of-a-sepa-credit-transfer1/ + + private string br = "\n"; + private readonly string iban, bic, name, purposeOfCreditTransfer, remittanceInformation, messageToGirocodeUser; + private readonly decimal amount; + private readonly GirocodeVersion version; + private readonly GirocodeEncoding encoding; + private readonly TypeOfRemittance typeOfRemittance; + + + /// + /// Generates the payload for a Girocode (QR-Code with credit transfer information). + /// Attention: When using Girocode payload, QR code must be generated with ECC level M! + /// + /// Account number of the Beneficiary. Only IBAN is allowed. + /// BIC of the Beneficiary Bank. + /// Name of the Beneficiary. + /// Amount of the Credit Transfer in Euro. + /// (Amount must be more than 0.01 and less than 999999999.99) + /// Remittance Information (Purpose-/reference text). (optional) + /// Type of remittance information. Either structured (e.g. ISO 11649 RF Creditor Reference) and max. 35 chars or unstructured and max. 140 chars. + /// Purpose of the Credit Transfer (optional) + /// Beneficiary to originator information. (optional) + /// Girocode version. Either 001 or 002. Default: 001. + /// Encoding of the Girocode payload. Default: ISO-8859-1 + public GirocodePayload(string iban, string bic, string name, decimal amount, string remittanceInformation = "", TypeOfRemittance typeOfRemittance = TypeOfRemittance.Unstructured, string purposeOfCreditTransfer = "", string messageToGirocodeUser = "", GirocodeVersion version = GirocodeVersion.Version1, GirocodeEncoding encoding = GirocodeEncoding.ISO_8859_1) + { + this.version = version; + this.encoding = encoding; + if (!IsValidIban(iban)) + throw new GirocodeException("The IBAN entered isn't valid."); + this.iban = iban.Replace(" ","").ToUpper(); + if (!IsValidBic(bic)) + throw new GirocodeException("The BIC entered isn't valid."); + this.bic = bic.Replace(" ", "").ToUpper(); + if (name.Length > 70) + throw new GirocodeException("(Payee-)Name must be shorter than 71 chars."); + this.name = name; + if (amount.ToString().Replace(",", ".").Contains(".") && amount.ToString().Replace(",",".").Split('.')[1].TrimEnd('0').Length > 2) + throw new GirocodeException("Amount must have less than 3 digits after decimal point."); + if (amount < 0.01m || amount > 999999999.99m) + throw new GirocodeException("Amount has to at least 0.01 and must be smaller or equal to 999999999.99."); + this.amount = amount; + if (purposeOfCreditTransfer.Length > 4) + throw new GirocodeException("Purpose of credit transfer can only have 4 chars at maximum."); + this.purposeOfCreditTransfer = purposeOfCreditTransfer; + if (typeOfRemittance == TypeOfRemittance.Unstructured && remittanceInformation.Length > 140) + throw new GirocodeException("Unstructured reference texts have to shorter than 141 chars."); + if (typeOfRemittance == TypeOfRemittance.Structured && remittanceInformation.Length > 35) + throw new GirocodeException("Structured reference texts have to shorter than 36 chars."); + this.typeOfRemittance = typeOfRemittance; + this.remittanceInformation = remittanceInformation; + if (messageToGirocodeUser.Length > 70) + throw new GirocodeException("Message to the Girocode-User reader texts have to shorter than 71 chars."); + this.messageToGirocodeUser = messageToGirocodeUser; + } + + 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 += $"EUR{amount:0.00}".Replace(",",".") + 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("_","-")); + } + + private static string ConvertStringToEncoding(string message, string encoding) + { + Encoding iso = Encoding.GetEncoding(encoding); + Encoding utf8 = Encoding.UTF8; + byte[] utfBytes = utf8.GetBytes(message); + byte[] isoBytes = Encoding.Convert(utf8, iso, utfBytes); + return iso.GetString(isoBytes,0, isoBytes.Length); + } + + public enum GirocodeVersion + { + Version1, + Version2 + } + + public enum TypeOfRemittance + { + Structured, + Unstructured + } + + public enum GirocodeEncoding + { + UTF_8, + ISO_8859_1, + ISO_8859_2, + ISO_8859_4, + ISO_8859_5, + ISO_8859_7, + ISO_8859_10, + ISO_8859_15 + } + + public class GirocodeException : Exception + { + public GirocodeException() + { + } + + public GirocodeException(string message) + : base(message) + { + } + + public GirocodeException(string message, Exception inner) + : base(message, inner) + { + } + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/MMSPayload.cs b/QRCoder2/Payloads/MMSPayload.cs new file mode 100644 index 00000000..9d90ed28 --- /dev/null +++ b/QRCoder2/Payloads/MMSPayload.cs @@ -0,0 +1,52 @@ +namespace QRCoder2.Payloads +{ + public class MMSPayload : PayloadBase + { + private readonly string number, subject; + private readonly MMSEncoding encoding; + + /// + /// Creates a MMS payload without text + /// + /// Receiver phone number + /// Encoding type + public MMSPayload(string number, MMSEncoding encoding = MMSEncoding.MMS) + { + this.number = number; + this.subject = string.Empty; + this.encoding = encoding; + } + + /// + /// Creates a MMS payload with text (subject) + /// + /// Receiver phone number + /// Text of the MMS + /// Encoding type + public MMSPayload(string number, string subject, MMSEncoding encoding = MMSEncoding.MMS) + { + this.number = number; + this.subject = subject; + this.encoding = encoding; + } + + public override string ToString() + { + switch (this.encoding) + { + case MMSEncoding.MMSTO: + return $"mmsto:{this.number}?subject={System.Uri.EscapeDataString(this.subject)}"; + case MMSEncoding.MMS: + return $"mms:{this.number}?body={System.Uri.EscapeDataString(this.subject)}"; + default: + return "mms:"; + } + } + + public enum MMSEncoding + { + MMS, + MMSTO + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/MailPayload.cs b/QRCoder2/Payloads/MailPayload.cs new file mode 100644 index 00000000..fd169b59 --- /dev/null +++ b/QRCoder2/Payloads/MailPayload.cs @@ -0,0 +1,74 @@ +namespace QRCoder2.Payloads +{ + public class MailPayload : PayloadBase + { + private readonly string mailReceiver, subject, message; + private readonly MailEncoding encoding; + + /// + /// Creates an empty email payload + /// + /// Receiver's email address + /// Payload encoding type. Choose dependent on your QR Code scanner app. + public MailPayload(string mailReceiver, MailEncoding encoding = MailEncoding.MAILTO) + { + this.mailReceiver = mailReceiver; + this.subject = this.message = string.Empty; + this.encoding = encoding; + } + + /// + /// Creates an email payload with subject + /// + /// Receiver's email address + /// Subject line of the email + /// Payload encoding type. Choose dependent on your QR Code scanner app. + public MailPayload(string mailReceiver, string subject, MailEncoding encoding = MailEncoding.MAILTO) + { + this.mailReceiver = mailReceiver; + this.subject = subject; + this.message = string.Empty; + this.encoding = encoding; + } + + /// + /// Creates an email payload with subject and message/text + /// + /// Receiver's email address + /// Subject line of the email + /// Message content of the email + /// Payload encoding type. Choose dependent on your QR Code scanner app. + public MailPayload(string mailReceiver, string subject, string message, MailEncoding encoding = MailEncoding.MAILTO) + { + this.mailReceiver = mailReceiver; + this.subject = subject; + this.message = message; + this.encoding = encoding; + } + + public override string ToString() + { + switch (this.encoding) + { + case MailEncoding.MAILTO: + return + $"mailto:{this.mailReceiver}?subject={System.Uri.EscapeDataString(this.subject)}&body={System.Uri.EscapeDataString(this.message)}"; + case MailEncoding.MATMSG: + return + $"MATMSG:TO:{this.mailReceiver};SUB:{EscapeInput(this.subject)};BODY:{EscapeInput(this.message)};;"; + case MailEncoding.SMTP: + return + $"SMTP:{this.mailReceiver}:{EscapeInput(this.subject, true)}:{EscapeInput(this.message, true)}"; + default: + return this.mailReceiver; + } + } + + public enum MailEncoding + { + MAILTO, + MATMSG, + SMTP + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/MoneroTransactionPayload.cs b/QRCoder2/Payloads/MoneroTransactionPayload.cs new file mode 100644 index 00000000..03287137 --- /dev/null +++ b/QRCoder2/Payloads/MoneroTransactionPayload.cs @@ -0,0 +1,59 @@ +using System; + +namespace QRCoder2.Payloads +{ + public class MoneroTransactionPayload : PayloadBase + { + private readonly string address, txPaymentId, recipientName, txDescription; + private readonly float? txAmount; + + /// + /// Creates a monero transaction payload + /// + /// Receiver's monero address + /// Amount to transfer + /// Payment id + /// Receipient's name + /// Reference text / payment description + public MoneroTransactionPayload(string address, float? txAmount = null, string txPaymentId = null, string recipientName = null, string txDescription = null) + { + if (string.IsNullOrEmpty(address)) + throw new MoneroTransactionException("The address is mandatory and has to be set."); + this.address = address; + if (txAmount != null && txAmount <= 0) + throw new MoneroTransactionException("Value of 'txAmount' must be greater than 0."); + this.txAmount = txAmount; + this.txPaymentId = txPaymentId; + this.recipientName = recipientName; + this.txDescription = txDescription; + } + + public override string ToString() + { + var moneroUri = $"monero://{address}{(!string.IsNullOrEmpty(txPaymentId) || !string.IsNullOrEmpty(recipientName) || !string.IsNullOrEmpty(txDescription) || txAmount != null ? "?" : string.Empty)}"; + moneroUri += (!string.IsNullOrEmpty(txPaymentId) ? $"tx_payment_id={Uri.EscapeDataString(txPaymentId)}&" : string.Empty); + moneroUri += (!string.IsNullOrEmpty(recipientName) ? $"recipient_name={Uri.EscapeDataString(recipientName)}&" : string.Empty); + moneroUri += (txAmount != null ? $"tx_amount={txAmount.ToString().Replace(",",".")}&" : string.Empty); + moneroUri += (!string.IsNullOrEmpty(txDescription) ? $"tx_description={Uri.EscapeDataString(txDescription)}" : string.Empty); + return moneroUri.TrimEnd('&'); + } + + + public class MoneroTransactionException : Exception + { + public MoneroTransactionException() + { + } + + public MoneroTransactionException(string message) + : base(message) + { + } + + public MoneroTransactionException(string message, Exception inner) + : base(message, inner) + { + } + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/OneTimePasswordPayload.cs b/QRCoder2/Payloads/OneTimePasswordPayload.cs new file mode 100644 index 00000000..ccfc18c2 --- /dev/null +++ b/QRCoder2/Payloads/OneTimePasswordPayload.cs @@ -0,0 +1,149 @@ +using System; +using System.Text; + +namespace QRCoder2.Payloads +{ + public class OneTimePasswordPayload : PayloadBase + { + //https://github.com/google/google-authenticator/wiki/Key-Uri-Format + public OneTimePasswordAuthType Type { get; set; } = OneTimePasswordAuthType.TOTP; + public string Secret { get; set; } + + public OneTimePasswordAuthAlgorithm AuthAlgorithm { get; set; } = OneTimePasswordAuthAlgorithm.SHA1; + + [Obsolete("This property is obsolete, use " + nameof(AuthAlgorithm) + " instead", false)] + public OoneTimePasswordAuthAlgorithm Algorithm + { + get { return (OoneTimePasswordAuthAlgorithm)Enum.Parse(typeof(OoneTimePasswordAuthAlgorithm), AuthAlgorithm.ToString()); } + set { AuthAlgorithm = (OneTimePasswordAuthAlgorithm)Enum.Parse(typeof(OneTimePasswordAuthAlgorithm), value.ToString()); } + } + + public string Issuer { get; set; } + public string Label { get; set; } + public int Digits { get; set; } = 6; + public int? Counter { get; set; } = null; + public int? Period { get; set; } = 30; + + public enum OneTimePasswordAuthType + { + TOTP, + HOTP, + } + + public enum OneTimePasswordAuthAlgorithm + { + SHA1, + SHA256, + SHA512, + } + + [Obsolete("This enum is obsolete, use " + nameof(OneTimePasswordAuthAlgorithm) + " instead", false)] + public enum OoneTimePasswordAuthAlgorithm + { + SHA1, + SHA256, + SHA512, + } + + public override string ToString() + { + switch (Type) + { + case OneTimePasswordAuthType.TOTP: + return TimeToString(); + case OneTimePasswordAuthType.HOTP: + return HMACToString(); + default: + throw new ArgumentOutOfRangeException(); + } + } + + // Note: Issuer:Label must only contain 1 : if either of the Issuer or the Label has a : then it is invalid. + // Defaults are 6 digits and 30 for Period + private string HMACToString() + { + var sb = new StringBuilder("otpauth://hotp/"); + ProcessCommonFields(sb); + var actualCounter = Counter ?? 1; + sb.Append("&counter=" + actualCounter); + return sb.ToString(); + } + + private string TimeToString() + { + if (Period == null) + { + throw new Exception("Period must be set when using OneTimePasswordAuthType.TOTP"); + } + + var sb = new StringBuilder("otpauth://totp/"); + + ProcessCommonFields(sb); + + if (Period != 30) + { + sb.Append("&period=" + Period); + } + + return sb.ToString(); + } + + private void ProcessCommonFields(StringBuilder sb) + { + if (string.IsNullOrWhiteSpace(Secret)) + { + throw new Exception("Secret must be a filled out base32 encoded string"); + } + string strippedSecret = Secret.Replace(" ", ""); + string escapedIssuer = null; + string escapedLabel = null; + + if (!string.IsNullOrWhiteSpace(Issuer)) + { + if (Issuer.Contains(":")) + { + throw new Exception("Issuer must not have a ':'"); + } + escapedIssuer = Uri.EscapeUriString(Issuer); + } + + if (!string.IsNullOrWhiteSpace(Label)) + { + if (Label.Contains(":")) + { + throw new Exception("Label must not have a ':'"); + } + escapedLabel = Uri.EscapeUriString(Label); + } + + if (escapedLabel != null) + { + if (escapedIssuer != null) + { + escapedLabel = escapedIssuer + ":" + escapedLabel; + } + } + else if (escapedIssuer != null) + { + escapedLabel = escapedIssuer; + } + + if (escapedLabel != null) + { + sb.Append(escapedLabel); + } + + sb.Append("?secret=" + strippedSecret); + + if (escapedIssuer != null) + { + sb.Append("&issuer=" + escapedIssuer); + } + + if (Digits != 6) + { + sb.Append("&digits=" + Digits); + } + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/PayloadBase.cs b/QRCoder2/Payloads/PayloadBase.cs new file mode 100644 index 00000000..903e11c8 --- /dev/null +++ b/QRCoder2/Payloads/PayloadBase.cs @@ -0,0 +1,67 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; + +namespace QRCoder2.Payloads +{ + public abstract class PayloadBase + { + public virtual int Version => -1; + public virtual ECCLevel EccLevel => ECCLevel.M; + public virtual EciMode EciMode => EciMode.Default; + public abstract override string ToString(); + + protected static string EscapeInput(string inp, bool simple = false) + { + char[] forbiddenChars = {'\\', ';', ',', ':'}; + if (simple) + { + forbiddenChars = new char[1] {':'}; + } + foreach (var c in forbiddenChars) + { + inp = inp.Replace(c.ToString(), "\\" + c); + } + return inp; + } + + protected static bool IsHexStyle(string inp) + { + return (System.Text.RegularExpressions.Regex.IsMatch(inp, @"\A\b[0-9a-fA-F]+\b\Z") || System.Text.RegularExpressions.Regex.IsMatch(inp, @"\A\b(0[xX])?[0-9a-fA-F]+\b\Z")); + } + + protected static bool IsValidIban(string iban) + { + //Clean IBAN + var ibanCleared = iban.ToUpper().Replace(" ", "").Replace("-", ""); + + //Check for general structure + var structurallyValid = Regex.IsMatch(ibanCleared, @"^[a-zA-Z]{2}[0-9]{2}([a-zA-Z0-9]?){16,30}$"); + + //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() : c.ToString())); + int m = 0; + for (int i = 0; i < (int)Math.Ceiling((sum.Length - 2) / 7d); i++) + { + var offset = (i == 0 ? 0 : 2); + var start = i * 7 + offset; + var n = (i == 0 ? "" : m.ToString()) + + sum.Substring(start, Math.Min(9 - offset, sum.Length - start)); + if (!int.TryParse(n, NumberStyles.Any, CultureInfo.InvariantCulture, out m)) + break; + m = m % 97; + } + + checksumValid = m == 1; + return structurallyValid && checksumValid; + } + + protected static bool IsValidBic(string bic) + { + return Regex.IsMatch(bic.Replace(" ", ""), @"^([a-zA-Z]{4}[a-zA-Z]{2}[a-zA-Z0-9]{2}([a-zA-Z0-9]{3})?)$"); + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/PhoneNumberPayload.cs b/QRCoder2/Payloads/PhoneNumberPayload.cs new file mode 100644 index 00000000..6532e70c --- /dev/null +++ b/QRCoder2/Payloads/PhoneNumberPayload.cs @@ -0,0 +1,21 @@ +namespace QRCoder2.Payloads +{ + public class PhoneNumberPayload : PayloadBase + { + private readonly string number; + + /// + /// Generates a phone call payload + /// + /// Phonenumber of the receiver + public PhoneNumberPayload(string number) + { + this.number = number; + } + + public override string ToString() + { + return $"tel:{this.number}"; + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/PlainTextPayload.cs b/QRCoder2/Payloads/PlainTextPayload.cs new file mode 100644 index 00000000..c6c67c7a --- /dev/null +++ b/QRCoder2/Payloads/PlainTextPayload.cs @@ -0,0 +1,21 @@ +namespace QRCoder2.Payloads +{ + public class PlainTextPayload : PayloadBase + { + private readonly string text; + + /// + /// Generates a plain text payload + /// + /// + public PlainTextPayload(string text) + { + this.text = text; + } + + public override string ToString() + { + return text; + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/SMSPayload.cs b/QRCoder2/Payloads/SMSPayload.cs new file mode 100644 index 00000000..3c14f850 --- /dev/null +++ b/QRCoder2/Payloads/SMSPayload.cs @@ -0,0 +1,55 @@ +namespace QRCoder2.Payloads +{ + public class SMSPayload : PayloadBase + { + private readonly string number, subject; + private readonly SMSEncoding encoding; + + /// + /// Creates a SMS payload without text + /// + /// Receiver phone number + /// Encoding type + public SMSPayload(string number, SMSEncoding encoding = SMSEncoding.SMS) + { + this.number = number; + this.subject = string.Empty; + this.encoding = encoding; + } + + /// + /// Creates a SMS payload with text (subject) + /// + /// Receiver phone number + /// Text of the SMS + /// Encoding type + public SMSPayload(string number, string subject, SMSEncoding encoding = SMSEncoding.SMS) + { + this.number = number; + this.subject = subject; + this.encoding = encoding; + } + + public override string ToString() + { + switch (this.encoding) + { + case SMSEncoding.SMS: + return $"sms:{this.number}?body={System.Uri.EscapeDataString(this.subject)}"; + case SMSEncoding.SMS_iOS: + return $"sms:{this.number};body={System.Uri.EscapeDataString(this.subject)}"; + case SMSEncoding.SMSTO: + return $"SMSTO:{this.number}:{this.subject}"; + default: + return "sms:"; + } + } + + public enum SMSEncoding + { + SMS, + SMSTO, + SMS_iOS + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/ShadowSocksConfigPayload.cs b/QRCoder2/Payloads/ShadowSocksConfigPayload.cs new file mode 100644 index 00000000..5c88f24e --- /dev/null +++ b/QRCoder2/Payloads/ShadowSocksConfigPayload.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace QRCoder2.Payloads +{ + public class ShadowSocksConfigPayload : PayloadBase + { + private readonly string hostname, password, tag, methodStr, parameter; + private readonly Method method; + private readonly int port; + private Dictionary encryptionTexts = new Dictionary() { + { "Chacha20IetfPoly1305", "chacha20-ietf-poly1305" }, + { "Aes128Gcm", "aes-128-gcm" }, + { "Aes192Gcm", "aes-192-gcm" }, + { "Aes256Gcm", "aes-256-gcm" }, + + { "XChacha20IetfPoly1305", "xchacha20-ietf-poly1305" }, + + { "Aes128Cfb", "aes-128-cfb" }, + { "Aes192Cfb", "aes-192-cfb" }, + { "Aes256Cfb", "aes-256-cfb" }, + { "Aes128Ctr", "aes-128-ctr" }, + { "Aes192Ctr", "aes-192-ctr" }, + { "Aes256Ctr", "aes-256-ctr" }, + { "Camellia128Cfb", "camellia-128-cfb" }, + { "Camellia192Cfb", "camellia-192-cfb" }, + { "Camellia256Cfb", "camellia-256-cfb" }, + { "Chacha20Ietf", "chacha20-ietf" }, + + { "Aes256Cb", "aes-256-cfb" }, + + { "Aes128Ofb", "aes-128-ofb" }, + { "Aes192Ofb", "aes-192-ofb" }, + { "Aes256Ofb", "aes-256-ofb" }, + { "Aes128Cfb1", "aes-128-cfb1" }, + { "Aes192Cfb1", "aes-192-cfb1" }, + { "Aes256Cfb1", "aes-256-cfb1" }, + { "Aes128Cfb8", "aes-128-cfb8" }, + { "Aes192Cfb8", "aes-192-cfb8" }, + { "Aes256Cfb8", "aes-256-cfb8" }, + + { "Chacha20", "chacha20" }, + { "BfCfb", "bf-cfb" }, + { "Rc4Md5", "rc4-md5" }, + { "Salsa20", "salsa20" }, + + { "DesCfb", "des-cfb" }, + { "IdeaCfb", "idea-cfb" }, + { "Rc2Cfb", "rc2-cfb" }, + { "Cast5Cfb", "cast5-cfb" }, + { "Salsa20Ctr", "salsa20-ctr" }, + { "Rc4", "rc4" }, + { "SeedCfb", "seed-cfb" }, + { "Table", "table" } + }; + + /// + /// Generates a ShadowSocks proxy config payload. + /// + /// Hostname of the ShadowSocks proxy + /// Port of the ShadowSocks proxy + /// Password of the SS proxy + /// Encryption type + /// Optional tag line + public ShadowSocksConfigPayload(string hostname, int port, string password, Method method, string tag = null) : + this(hostname, port, password, method, null, tag) + { } + + public ShadowSocksConfigPayload(string hostname, int port, string password, Method method, string plugin, string pluginOption, string tag = null) : + this(hostname, port, password, method, new Dictionary + { + ["plugin"] = plugin + ( + string.IsNullOrEmpty(pluginOption) + ? "" + : $";{pluginOption}" + ) + }, tag) + { } + private Dictionary UrlEncodeTable = new Dictionary + { + [" "] = "+", + ["\0"] = "%00", + ["\t"] = "%09", + ["\n"] = "%0a", + ["\r"] = "%0d", + ["\""] = "%22", + ["#"] = "%23", + ["$"] = "%24", + ["%"] = "%25", + ["&"] = "%26", + ["'"] = "%27", + ["+"] = "%2b", + [","] = "%2c", + ["/"] = "%2f", + [":"] = "%3a", + [";"] = "%3b", + ["<"] = "%3c", + ["="] = "%3d", + [">"] = "%3e", + ["?"] = "%3f", + ["@"] = "%40", + ["["] = "%5b", + ["\\"] = "%5c", + ["]"] = "%5d", + ["^"] = "%5e", + ["`"] = "%60", + ["{"] = "%7b", + ["|"] = "%7c", + ["}"] = "%7d", + ["~"] = "%7e", + }; + + private string UrlEncode(string i) + { + string j = i; + foreach (var kv in UrlEncodeTable) + { + j = j.Replace(kv.Key, kv.Value); + } + return j; + } + + public ShadowSocksConfigPayload(string hostname, int port, string password, Method method, Dictionary parameters, string tag = null) + { + this.hostname = Uri.CheckHostName(hostname) == UriHostNameType.IPv6 + ? $"[{hostname}]" + : hostname; + if (port < 1 || port > 65535) + throw new ShadowSocksConfigException("Value of 'port' must be within 0 and 65535."); + this.port = port; + this.password = password; + this.method = method; + this.methodStr = encryptionTexts[method.ToString()]; + this.tag = tag; + + if (parameters != null) + this.parameter = + string.Join("&", + parameters.Select( + kv => $"{UrlEncode(kv.Key)}={UrlEncode(kv.Value)}" + ).ToArray()); + } + + public override string ToString() + { + if (string.IsNullOrEmpty(parameter)) + { + var connectionString = $"{methodStr}:{password}@{hostname}:{port}"; + var connectionStringEncoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(connectionString)); + return $"ss://{connectionStringEncoded}{(!string.IsNullOrEmpty(tag) ? $"#{tag}" : string.Empty)}"; + } + var authString = $"{methodStr}:{password}"; + var authStringEncoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString)) + .Replace('+', '-') + .Replace('/', '_') + .TrimEnd('='); + return $"ss://{authStringEncoded}@{hostname}:{port}/?{parameter}{(!string.IsNullOrEmpty(tag) ? $"#{tag}" : string.Empty)}"; + } + + public enum Method + { + // AEAD + Chacha20IetfPoly1305, + Aes128Gcm, + Aes192Gcm, + Aes256Gcm, + // AEAD, not standard + XChacha20IetfPoly1305, + // Stream cipher + Aes128Cfb, + Aes192Cfb, + Aes256Cfb, + Aes128Ctr, + Aes192Ctr, + Aes256Ctr, + Camellia128Cfb, + Camellia192Cfb, + Camellia256Cfb, + Chacha20Ietf, + // alias of Aes256Cfb + Aes256Cb, + // Stream cipher, not standard + Aes128Ofb, + Aes192Ofb, + Aes256Ofb, + Aes128Cfb1, + Aes192Cfb1, + Aes256Cfb1, + Aes128Cfb8, + Aes192Cfb8, + Aes256Cfb8, + // Stream cipher, deprecated + Chacha20, + BfCfb, + Rc4Md5, + Salsa20, + // Not standard and not in acitve use + DesCfb, + IdeaCfb, + Rc2Cfb, + Cast5Cfb, + Salsa20Ctr, + Rc4, + SeedCfb, + Table + } + + public class ShadowSocksConfigException : Exception + { + public ShadowSocksConfigException() + { + } + + public ShadowSocksConfigException(string message) + : base(message) + { + } + + public ShadowSocksConfigException(string message, Exception inner) + : base(message, inner) + { + } + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/SkypeCallPayload.cs b/QRCoder2/Payloads/SkypeCallPayload.cs new file mode 100644 index 00000000..4814c2be --- /dev/null +++ b/QRCoder2/Payloads/SkypeCallPayload.cs @@ -0,0 +1,21 @@ +namespace QRCoder2.Payloads +{ + public class SkypeCallPayload : PayloadBase + { + private readonly string skypeUsername; + + /// + /// Generates a Skype call payload + /// + /// Skype username which will be called + public SkypeCallPayload(string skypeUsername) + { + this.skypeUsername = skypeUsername; + } + + public override string ToString() + { + return $"skype:{this.skypeUsername}?call"; + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/SlovenianUpnQrPayload.cs b/QRCoder2/Payloads/SlovenianUpnQrPayload.cs new file mode 100644 index 00000000..b9e0025b --- /dev/null +++ b/QRCoder2/Payloads/SlovenianUpnQrPayload.cs @@ -0,0 +1,101 @@ +using System; +using System.Text; + +namespace QRCoder2.Payloads +{ + public class SlovenianUpnQrPayload : PayloadBase + { + //Keep in mind, that the ECC level has to be set to "M", version to 15 and ECI to EciMode.Iso8859_2 when generating a SlovenianUpnQr! + //SlovenianUpnQr specification: https://www.upn-qr.si/uploads/files/NavodilaZaProgramerjeUPNQR.pdf + + private string _payerName = ""; + private string _payerAddress = ""; + private string _payerPlace = ""; + private string _amount = ""; + private string _code = ""; + private string _purpose = ""; + private string _deadLine = ""; + private string _recipientIban = ""; + private string _recipientName = ""; + private string _recipientAddress = ""; + private string _recipientPlace = ""; + private string _recipientSiModel = ""; + private string _recipientSiReference = ""; + + public override int Version { get { return 15; } } + public override ECCLevel EccLevel { get { return ECCLevel.M; } } + public override EciMode EciMode { get { return EciMode.Iso8859_2; } } + + private string LimitLength(string value, int maxLength) + { + return (value.Length <= maxLength) ? value : value.Substring(0, maxLength); + } + + public SlovenianUpnQrPayload(string payerName, string payerAddress, string payerPlace, string recipientName, string recipientAddress, string recipientPlace, string recipientIban, string description, double amount, string recipientSiModel = "SI00", string recipientSiReference = "", string code = "OTHR") : + this(payerName, payerAddress, payerPlace, recipientName, recipientAddress, recipientPlace, recipientIban, description, amount, null, recipientSiModel, recipientSiReference, code) + { } + + public SlovenianUpnQrPayload(string payerName, string payerAddress, string payerPlace, string recipientName, string recipientAddress, string recipientPlace, string recipientIban, string description, double amount, DateTime? deadline, string recipientSiModel = "SI99", string recipientSiReference = "", string code = "OTHR") + { + _payerName = LimitLength(payerName.Trim(), 33); + _payerAddress = LimitLength(payerAddress.Trim(), 33); + _payerPlace = LimitLength(payerPlace.Trim(), 33); + _amount = FormatAmount(amount); + _code = LimitLength(code.Trim().ToUpper(), 4); + _purpose = LimitLength(description.Trim(), 42); + _deadLine = (deadline == null) ? "" : deadline?.ToString("dd.MM.yyyy"); + _recipientIban = LimitLength(recipientIban.Trim(), 34); + _recipientName = LimitLength(recipientName.Trim(), 33); + _recipientAddress = LimitLength(recipientAddress.Trim(), 33); + _recipientPlace = LimitLength(recipientPlace.Trim(), 33); + _recipientSiModel = LimitLength(recipientSiModel.Trim().ToUpper(), 4); + _recipientSiReference = LimitLength(recipientSiReference.Trim(), 22); + } + + private string FormatAmount(double amount) + { + int _amt = (int)Math.Round(amount * 100.0); + return String.Format("{0:00000000000}", _amt); + } + + private int CalculateChecksum() + { + int _cs = 5 + _payerName.Length; //5 = UPNQR constant Length + _cs += _payerAddress.Length; + _cs += _payerPlace.Length; + _cs += _amount.Length; + _cs += _code.Length; + _cs += _purpose.Length; + _cs += _deadLine.Length; + _cs += _recipientIban.Length; + _cs += _recipientName.Length; + _cs += _recipientAddress.Length; + _cs += _recipientPlace.Length; + _cs += _recipientSiModel.Length; + _cs += _recipientSiReference.Length; + _cs += 19; + return _cs; + } + + public override string ToString() + { + var _sb = new StringBuilder(); + _sb.Append("UPNQR"); + _sb.Append('\n').Append('\n').Append('\n').Append('\n').Append('\n'); + _sb.Append(_payerName).Append('\n'); + _sb.Append(_payerAddress).Append('\n'); + _sb.Append(_payerPlace).Append('\n'); + _sb.Append(_amount).Append('\n').Append('\n').Append('\n'); + _sb.Append(_code.ToUpper()).Append('\n'); + _sb.Append(_purpose).Append('\n'); + _sb.Append(_deadLine).Append('\n'); + _sb.Append(_recipientIban.ToUpper()).Append('\n'); + _sb.Append(_recipientSiModel).Append(_recipientSiReference).Append('\n'); + _sb.Append(_recipientName).Append('\n'); + _sb.Append(_recipientAddress).Append('\n'); + _sb.Append(_recipientPlace).Append('\n'); + _sb.AppendFormat("{0:000}", CalculateChecksum()).Append('\n'); + return _sb.ToString(); + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/SwissQrCodePayload.cs b/QRCoder2/Payloads/SwissQrCodePayload.cs new file mode 100644 index 00000000..47e82e83 --- /dev/null +++ b/QRCoder2/Payloads/SwissQrCodePayload.cs @@ -0,0 +1,599 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; + +namespace QRCoder2.Payloads +{ + public class SwissQrCodePayload : PayloadBase + { + //Keep in mind, that the ECC level has to be set to "M" when generating a SwissQrCode! + //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 + + private readonly string br = "\r\n"; + private readonly string alternativeProcedure1, alternativeProcedure2; + private readonly Iban iban; + private readonly decimal? amount; + private readonly Contact creditor, ultimateCreditor, debitor; + private readonly Currency currency; + private readonly DateTime? requestedDateOfPayment; + private readonly Reference reference; + private readonly AdditionalInformation additionalInformation; + + /// + /// Generates the payload for a SwissQrCode v2.0. (Don't forget to use ECC-Level=M, EncodingMode=UTF-8 and to set the Swiss flag icon to the final QR code.) + /// + /// IBAN object + /// Currency (either EUR or CHF) + /// Creditor (payee) information + /// Reference information + /// Debitor (payer) information + /// Amount + /// Requested date of debitor's payment + /// Ultimate creditor information (use only in consultation with your bank - for future use only!) + /// Optional command for alternative processing mode - line 1 + /// Optional command for alternative processing mode - line 2 + public SwissQrCodePayload(Iban iban, Currency currency, Contact creditor, Reference reference, + AdditionalInformation additionalInformation = null, Contact debitor = null, decimal? amount = null, + DateTime? requestedDateOfPayment = null, Contact ultimateCreditor = null, + string alternativeProcedure1 = null, string alternativeProcedure2 = null) + { + this.iban = iban; + + this.creditor = creditor; + this.ultimateCreditor = ultimateCreditor; + + this.additionalInformation = + additionalInformation != null ? additionalInformation : new AdditionalInformation(); + + if (amount != null && amount.ToString().Length > 12) + throw new SwissQrCodeException("Amount (including decimals) must be shorter than 13 places."); + this.amount = amount; + + this.currency = currency; + this.requestedDateOfPayment = requestedDateOfPayment; + this.debitor = debitor; + + if (iban.IsQrIban && reference.RefType != Reference.ReferenceType.QRR) + throw new SwissQrCodeException("If QR-IBAN is used, you have to choose \"QRR\" as reference type!"); + if (!iban.IsQrIban && reference.RefType == Reference.ReferenceType.QRR) + throw new SwissQrCodeException( + "If non QR-IBAN is used, you have to choose either \"SCOR\" or \"NON\" as reference type!"); + this.reference = reference; + + if (alternativeProcedure1 != null && alternativeProcedure1.Length > 100) + throw new SwissQrCodeException( + "Alternative procedure information block 1 must be shorter than 101 chars."); + this.alternativeProcedure1 = alternativeProcedure1; + if (alternativeProcedure2 != null && alternativeProcedure2.Length > 100) + throw new SwissQrCodeException( + "Alternative procedure information block 2 must be shorter than 101 chars."); + this.alternativeProcedure2 = alternativeProcedure2; + } + + public class AdditionalInformation + { + private readonly string unstructuredMessage, billInformation, trailer; + + /// + /// Creates an additional information object. Both parameters are optional and must be shorter than 141 chars in combination. + /// + /// Unstructured text message + /// Bill information + public AdditionalInformation(string unstructuredMessage = null, string billInformation = null) + { + if (((unstructuredMessage != null ? unstructuredMessage.Length : 0) + + (billInformation != null ? billInformation.Length : 0)) > 140) + throw new SwissQrCodeAdditionalInformationException( + "Unstructured message and bill information must be shorter than 141 chars in total/combined."); + this.unstructuredMessage = unstructuredMessage; + this.billInformation = billInformation; + this.trailer = "EPD"; + } + + public string UnstructureMessage + { + get + { + return !string.IsNullOrEmpty(unstructuredMessage) ? unstructuredMessage.Replace("\n", "") : null; + } + } + + public string BillInformation + { + get { return !string.IsNullOrEmpty(billInformation) ? billInformation.Replace("\n", "") : null; } + } + + public string Trailer + { + get { return trailer; } + } + + + public class SwissQrCodeAdditionalInformationException : Exception + { + public SwissQrCodeAdditionalInformationException() + { + } + + public SwissQrCodeAdditionalInformationException(string message) + : base(message) + { + } + + public SwissQrCodeAdditionalInformationException(string message, Exception inner) + : base(message, inner) + { + } + } + } + + public class Reference + { + private readonly ReferenceType referenceType; + private readonly string reference; + private readonly ReferenceTextType? referenceTextType; + + /// + /// Creates a reference object which must be passed to the SwissQrCode instance + /// + /// Type of the reference (QRR, SCOR or NON) + /// Reference text + /// Type of the reference text (QR-reference or Creditor Reference) + public Reference(ReferenceType referenceType, string reference = null, + ReferenceTextType? referenceTextType = null) + { + this.referenceType = referenceType; + this.referenceTextType = referenceTextType; + + if (referenceType == ReferenceType.NON && reference != null) + throw new SwissQrCodeReferenceException( + "Reference is only allowed when referenceType not equals \"NON\""); + if (referenceType != ReferenceType.NON && reference != null && referenceTextType == null) + throw new SwissQrCodeReferenceException( + "You have to set an ReferenceTextType when using the reference text."); + if (referenceTextType == ReferenceTextType.QrReference && reference != null && (reference.Length > 27)) + throw new SwissQrCodeReferenceException("QR-references have to be shorter than 28 chars."); + if (referenceTextType == ReferenceTextType.QrReference && reference != null && + !Regex.IsMatch(reference, @"^[0-9]+$")) + throw new SwissQrCodeReferenceException("QR-reference must exist out of digits only."); + if (referenceTextType == ReferenceTextType.QrReference && reference != null && + !ChecksumMod10(reference)) + throw new SwissQrCodeReferenceException("QR-references is invalid. Checksum error."); + if (referenceTextType == ReferenceTextType.CreditorReferenceIso11649 && reference != null && + (reference.Length > 25)) + throw new SwissQrCodeReferenceException( + "Creditor references (ISO 11649) have to be shorter than 26 chars."); + + this.reference = reference; + } + + public ReferenceType RefType + { + get { return referenceType; } + } + + public string ReferenceText + { + get { return !string.IsNullOrEmpty(reference) ? reference.Replace("\n", "") : null; } + } + + /// + /// Reference type. When using a QR-IBAN you have to use either "QRR" or "SCOR" + /// + public enum ReferenceType + { + QRR, + SCOR, + NON + } + + public enum ReferenceTextType + { + QrReference, + CreditorReferenceIso11649 + } + + private static bool ChecksumMod10(string digits) + { + if (string.IsNullOrEmpty(digits) || digits.Length < 2) + return false; + int[] mods = new int[] { 0, 9, 4, 6, 8, 2, 7, 1, 3, 5 }; + + int remainder = 0; + for (int i = 0; i < digits.Length - 1; i++) + { + var num = Convert.ToInt32(digits[i]) - 48; + remainder = mods[(num + remainder) % 10]; + } + + var checksum = (10 - remainder) % 10; + return checksum == Convert.ToInt32(digits[digits.Length - 1]) - 48; + } + + public class SwissQrCodeReferenceException : Exception + { + public SwissQrCodeReferenceException() + { + } + + public SwissQrCodeReferenceException(string message) + : base(message) + { + } + + public SwissQrCodeReferenceException(string message, Exception inner) + : base(message, inner) + { + } + } + } + + public class Iban + { + private string iban; + private IbanType ibanType; + + /// + /// IBAN object with type information + /// + /// IBAN + /// Type of IBAN (normal or QR-IBAN) + public Iban(string iban, IbanType ibanType) + { + if (ibanType == IbanType.Iban && !IsValidIban(iban)) + throw new SwissQrCodeIbanException("The IBAN entered isn't valid."); + if (ibanType == IbanType.QrIban && !IsValidQRIban(iban)) + throw new SwissQrCodeIbanException("The QR-IBAN entered isn't valid."); + if (!iban.StartsWith("CH") && !iban.StartsWith("LI")) + throw new SwissQrCodeIbanException("The IBAN must start with \"CH\" or \"LI\"."); + this.iban = iban; + this.ibanType = ibanType; + } + + public bool IsQrIban + { + get { return ibanType == IbanType.QrIban; } + } + + public override string ToString() + { + return iban.Replace("-", "").Replace("\n", "").Replace(" ", ""); + } + + public enum IbanType + { + Iban, + QrIban + } + + private static bool IsValidQRIban(string iban) + { + var foundQrIid = false; + try + { + var ibanCleared = iban.ToUpper().Replace(" ", "").Replace("-", ""); + var possibleQrIid = Convert.ToInt32(ibanCleared.Substring(4, 5)); + foundQrIid = possibleQrIid >= 30000 && possibleQrIid <= 31999; + } + catch + { + } + + return IsValidIban(iban) && foundQrIid; + } + + public class SwissQrCodeIbanException : Exception + { + public SwissQrCodeIbanException() + { + } + + public SwissQrCodeIbanException(string message) + : base(message) + { + } + + public SwissQrCodeIbanException(string message, Exception inner) + : base(message, inner) + { + } + } + } + + public class Contact + { + private static readonly HashSet twoLetterCodes = ValidTwoLetterCodes(); + private string br = "\r\n"; + private string name, streetOrAddressline1, houseNumberOrAddressline2, zipCode, city, country; + private AddressType adrType; + + /// + /// Contact type. Can be used for payee, ultimate payee, etc. with address in structured mode (S). + /// + /// Last name or company (optional first name) + /// Zip-/Postcode + /// City name + /// Two-letter country code as defined in ISO 3166-1 + /// Streetname without house number + /// House number + [Obsolete("This constructor is deprecated. Use WithStructuredAddress instead.")] + public Contact(string name, string zipCode, string city, string country, string street = null, + string houseNumber = null) : this(name, zipCode, city, country, street, houseNumber, + AddressType.StructuredAddress) + { + } + + + /// + /// Contact type. Can be used for payee, ultimate payee, etc. with address in combined mode (K). + /// + /// Last name or company (optional first name) + /// Two-letter country code as defined in ISO 3166-1 + /// Adress line 1 + /// Adress line 2 + [Obsolete("This constructor is deprecated. Use WithCombinedAddress instead.")] + public Contact(string name, string country, string addressLine1, string addressLine2) : this(name, null, + null, country, addressLine1, addressLine2, AddressType.CombinedAddress) + { + } + + public static Contact WithStructuredAddress(string name, string zipCode, string city, string country, + string street = null, string houseNumber = null) + { + return new Contact(name, zipCode, city, country, street, houseNumber, AddressType.StructuredAddress); + } + + public static Contact WithCombinedAddress(string name, string country, string addressLine1, + string addressLine2) + { + return new Contact(name, null, null, country, addressLine1, addressLine2, AddressType.CombinedAddress); + } + + + private Contact(string name, string zipCode, string city, string country, string streetOrAddressline1, + string houseNumberOrAddressline2, AddressType addressType) + { + //Pattern extracted from https://qr-validation.iso-payments.ch as explained in https://github.com/codebude/QRCoder/issues/97 + var charsetPattern = + @"^([a-zA-Z0-9\.,;:'\ \+\-/\(\)?\*\[\]\{\}\\`´~ ]|[!""#%&<>÷=@_$£]|[àáâäçèéêëìíîïñòóôöùúûüýßÀÁÂÄÇÈÉÊËÌÍÎÏÒÓÔÖÙÚÛÜÑ])*$"; + + this.adrType = addressType; + + if (string.IsNullOrEmpty(name)) + throw new SwissQrCodeContactException("Name must not be empty."); + if (name.Length > 70) + throw new SwissQrCodeContactException("Name must be shorter than 71 chars."); + if (!Regex.IsMatch(name, charsetPattern)) + throw new SwissQrCodeContactException( + $"Name must match the following pattern as defined in pain.001: {charsetPattern}"); + this.name = name; + + if (AddressType.StructuredAddress == this.adrType) + { + if (!string.IsNullOrEmpty(streetOrAddressline1) && (streetOrAddressline1.Length > 70)) + throw new SwissQrCodeContactException("Street must be shorter than 71 chars."); + if (!string.IsNullOrEmpty(streetOrAddressline1) && + !Regex.IsMatch(streetOrAddressline1, charsetPattern)) + throw new SwissQrCodeContactException( + $"Street must match the following pattern as defined in pain.001: {charsetPattern}"); + this.streetOrAddressline1 = streetOrAddressline1; + + if (!string.IsNullOrEmpty(houseNumberOrAddressline2) && houseNumberOrAddressline2.Length > 16) + throw new SwissQrCodeContactException("House number must be shorter than 17 chars."); + this.houseNumberOrAddressline2 = houseNumberOrAddressline2; + } + else + { + if (!string.IsNullOrEmpty(streetOrAddressline1) && (streetOrAddressline1.Length > 70)) + throw new SwissQrCodeContactException("Address line 1 must be shorter than 71 chars."); + if (!string.IsNullOrEmpty(streetOrAddressline1) && + !Regex.IsMatch(streetOrAddressline1, charsetPattern)) + throw new SwissQrCodeContactException( + $"Address line 1 must match the following pattern as defined in pain.001: {charsetPattern}"); + this.streetOrAddressline1 = streetOrAddressline1; + + if (string.IsNullOrEmpty(houseNumberOrAddressline2)) + throw new SwissQrCodeContactException( + "Address line 2 must be provided for combined addresses (address line-based addresses)."); + if (!string.IsNullOrEmpty(houseNumberOrAddressline2) && (houseNumberOrAddressline2.Length > 70)) + throw new SwissQrCodeContactException("Address line 2 must be shorter than 71 chars."); + if (!string.IsNullOrEmpty(houseNumberOrAddressline2) && + !Regex.IsMatch(houseNumberOrAddressline2, charsetPattern)) + throw new SwissQrCodeContactException( + $"Address line 2 must match the following pattern as defined in pain.001: {charsetPattern}"); + this.houseNumberOrAddressline2 = houseNumberOrAddressline2; + } + + if (AddressType.StructuredAddress == this.adrType) + { + if (string.IsNullOrEmpty(zipCode)) + throw new SwissQrCodeContactException("Zip code must not be empty."); + if (zipCode.Length > 16) + throw new SwissQrCodeContactException("Zip code must be shorter than 17 chars."); + if (!Regex.IsMatch(zipCode, charsetPattern)) + throw new SwissQrCodeContactException( + $"Zip code must match the following pattern as defined in pain.001: {charsetPattern}"); + this.zipCode = zipCode; + + if (string.IsNullOrEmpty(city)) + throw new SwissQrCodeContactException("City must not be empty."); + if (city.Length > 35) + throw new SwissQrCodeContactException("City name must be shorter than 36 chars."); + if (!Regex.IsMatch(city, charsetPattern)) + throw new SwissQrCodeContactException( + $"City name must match the following pattern as defined in pain.001: {charsetPattern}"); + this.city = city; + } + else + { + this.zipCode = this.city = string.Empty; + } + + if (!IsValidTwoLetterCode(country)) + throw new SwissQrCodeContactException( + "Country must be a valid \"two letter\" country code as defined by ISO 3166-1, but it isn't."); + + this.country = country; + } + + private static bool IsValidTwoLetterCode(string code) => twoLetterCodes.Contains(code); + + private static HashSet ValidTwoLetterCodes() + { + string[] codes = new string[] + { + "AF", "AL", "DZ", "AS", "AD", "AO", "AI", "AQ", "AG", "AR", "AM", "AW", "AU", "AT", "AZ", "BS", + "BH", "BD", "BB", "BY", "BE", "BZ", "BJ", "BM", "BT", "BO", "BQ", "BA", "BW", "BV", "BR", "IO", + "BN", "BG", "BF", "BI", "CV", "KH", "CM", "CA", "KY", "CF", "TD", "CL", "CN", "CX", "CC", "CO", + "KM", "CG", "CD", "CK", "CR", "CI", "HR", "CU", "CW", "CY", "CZ", "DK", "DJ", "DM", "DO", "EC", + "EG", "SV", "GQ", "ER", "EE", "SZ", "ET", "FK", "FO", "FJ", "FI", "FR", "GF", "PF", "TF", "GA", + "GM", "GE", "DE", "GH", "GI", "GR", "GL", "GD", "GP", "GU", "GT", "GG", "GN", "GW", "GY", "HT", + "HM", "VA", "HN", "HK", "HU", "IS", "IN", "ID", "IR", "IQ", "IE", "IM", "IL", "IT", "JM", "JP", + "JE", "JO", "KZ", "KE", "KI", "KP", "KR", "KW", "KG", "LA", "LV", "LB", "LS", "LR", "LY", "LI", + "LT", "LU", "MO", "MG", "MW", "MY", "MV", "ML", "MT", "MH", "MQ", "MR", "MU", "YT", "MX", "FM", + "MD", "MC", "MN", "ME", "MS", "MA", "MZ", "MM", "NA", "NR", "NP", "NL", "NC", "NZ", "NI", "NE", + "NG", "NU", "NF", "MP", "MK", "NO", "OM", "PK", "PW", "PS", "PA", "PG", "PY", "PE", "PH", "PN", + "PL", "PT", "PR", "QA", "RE", "RO", "RU", "RW", "BL", "SH", "KN", "LC", "MF", "PM", "VC", "WS", + "SM", "ST", "SA", "SN", "RS", "SC", "SL", "SG", "SX", "SK", "SI", "SB", "SO", "ZA", "GS", "SS", + "ES", "LK", "SD", "SR", "SJ", "SE", "CH", "SY", "TW", "TJ", "TZ", "TH", "TL", "TG", "TK", "TO", + "TT", "TN", "TR", "TM", "TC", "TV", "UG", "UA", "AE", "GB", "US", "UM", "UY", "UZ", "VU", "VE", + "VN", "VG", "VI", "WF", "EH", "YE", "ZM", "ZW", "AX" + }; + return new HashSet(codes, StringComparer.OrdinalIgnoreCase); + } + + 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; + } + + public enum AddressType + { + StructuredAddress, + CombinedAddress + } + + public class SwissQrCodeContactException : Exception + { + public SwissQrCodeContactException() + { + } + + public SwissQrCodeContactException(string message) + : base(message) + { + } + + public SwissQrCodeContactException(string message, Exception inner) + : base(message, inner) + { + } + } + } + + public override string ToString() + { + //Header "logical" element + var SwissQrCodePayload = "SPC" + br; //QRType + SwissQrCodePayload += "0200" + br; //Version + SwissQrCodePayload += "1" + br; //Coding + + //CdtrInf "logical" element + SwissQrCodePayload += iban.ToString() + br; //IBAN + + + //Cdtr "logical" element + SwissQrCodePayload += 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()); + + //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 + //Removed in S-QR version 2.0 + //SwissQrCodePayload += (requestedDateOfPayment != null ? ((DateTime)requestedDateOfPayment).ToString("yyyy-MM-dd") : string.Empty) + br; //ReqdExctnDt + + //UltmtDbtr "logical" element + if (debitor != null) + SwissQrCodePayload += debitor.ToString(); + else + SwissQrCodePayload += string.Concat(Enumerable.Repeat(br, 7).ToArray()); + + + //RmtInf "logical" element + SwissQrCodePayload += reference.RefType.ToString() + br; //Tp + 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 + SwissQrCodePayload += (!string.IsNullOrEmpty(additionalInformation.BillInformation) + ? additionalInformation.BillInformation + : string.Empty) + br; //StrdBkgInf + + //AltPmtInf "logical" element + if (!string.IsNullOrEmpty(alternativeProcedure1)) + SwissQrCodePayload += alternativeProcedure1.Replace("\n", "") + br; //AltPmt + if (!string.IsNullOrEmpty(alternativeProcedure2)) + SwissQrCodePayload += alternativeProcedure2.Replace("\n", "") + br; //AltPmt + + //S-QR specification 2.0, chapter 4.2.3 + if (SwissQrCodePayload.EndsWith(br)) + SwissQrCodePayload = SwissQrCodePayload.Remove(SwissQrCodePayload.Length - br.Length); + + return SwissQrCodePayload; + } + + + + + /// + /// ISO 4217 currency codes + /// + public enum Currency + { + CHF = 756, + EUR = 978 + } + + public class SwissQrCodeException : Exception + { + public SwissQrCodeException() + { + } + + public SwissQrCodeException(string message) + : base(message) + { + } + + public SwissQrCodeException(string message, Exception inner) + : base(message, inner) + { + } + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/UrlPayload.cs b/QRCoder2/Payloads/UrlPayload.cs new file mode 100644 index 00000000..8807080b --- /dev/null +++ b/QRCoder2/Payloads/UrlPayload.cs @@ -0,0 +1,21 @@ +namespace QRCoder2.Payloads +{ + public class UrlPayload : PayloadBase + { + private readonly string url; + + /// + /// Generates a link. If not given, http/https protocol will be added. + /// + /// Link url target + public UrlPayload(string url) + { + this.url = url; + } + + public override string ToString() + { + return (!this.url.StartsWith("http") ? "http://" + this.url : this.url); + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/WhatsAppMessagePayload.cs b/QRCoder2/Payloads/WhatsAppMessagePayload.cs new file mode 100644 index 00000000..7f6cb8f6 --- /dev/null +++ b/QRCoder2/Payloads/WhatsAppMessagePayload.cs @@ -0,0 +1,35 @@ +using System; + +namespace QRCoder2.Payloads +{ + public class WhatsAppMessagePayload : PayloadBase + { + private readonly string number, message; + + /// + /// Let's you compose a WhatApp message and send it the receiver number. + /// + /// Receiver phone number + /// The message + public WhatsAppMessagePayload(string number, string message) + { + this.number = number; + this.message = message; + } + + /// + /// Let's you compose a WhatApp message. When scanned the user is asked to choose a contact who will receive the message. + /// + /// The message + public WhatsAppMessagePayload(string message) + { + this.number = string.Empty; + this.message = message; + } + + public override string ToString() + { + return ($"whatsapp://send?phone={this.number}&text={Uri.EscapeDataString(message)}"); + } + } +} \ No newline at end of file diff --git a/QRCoder2/Payloads/WiFiPayload.cs b/QRCoder2/Payloads/WiFiPayload.cs new file mode 100644 index 00000000..5c7daa86 --- /dev/null +++ b/QRCoder2/Payloads/WiFiPayload.cs @@ -0,0 +1,38 @@ +namespace QRCoder2.Payloads +{ + public class WiFiPayload : PayloadBase + { + private readonly string ssid, password, authenticationMode; + private readonly bool isHiddenSsid; + + /// + /// Generates a WiFi payload. Scanned by a QR Code scanner app, the device will connect to the WiFi. + /// + /// SSID of the WiFi network + /// Password of the WiFi network + /// Authentication mode (WEP, WPA, WPA2) + /// Set flag, if the WiFi network hides its SSID + public WiFiPayload(string ssid, string password, Authentication authenticationMode, bool isHiddenSSID = false) + { + this.ssid = EscapeInput(ssid); + this.ssid = IsHexStyle(this.ssid) ? "\"" + this.ssid + "\"" : this.ssid; + this.password = EscapeInput(password); + this.password = IsHexStyle(this.password) ? "\"" + this.password + "\"" : this.password; + this.authenticationMode = authenticationMode.ToString(); + this.isHiddenSsid = isHiddenSSID; + } + + public override string ToString() + { + return + $"WIFI:T:{this.authenticationMode};S:{this.ssid};P:{this.password};{(this.isHiddenSsid ? "H:true" : string.Empty)};"; + } + + public enum Authentication + { + WEP, + WPA, + nopass + } + } +} \ No newline at end of file diff --git a/QRCoder2/QRCodeData.cs b/QRCoder2/QRCodeData.cs new file mode 100644 index 00000000..f39a8654 --- /dev/null +++ b/QRCoder2/QRCodeData.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; + +namespace QRCoder2 +{ + public class QRCodeData : IDisposable + { + public List ModuleMatrix { get; set; } + + public QRCodeData(int version) + { + this.Version = version; + var size = ModulesPerSideFromVersion(version); + this.ModuleMatrix = new List(); + for (var i = 0; i < size; i++) + this.ModuleMatrix.Add(new BitArray(size)); + } + + public QRCodeData(byte[] rawData, Compression compressMode) + { + var bytes = new List(rawData); + + //Decompress + if (compressMode == Compression.Deflate) + { + using (var input = new MemoryStream(bytes.ToArray())) + { + using (var output = new MemoryStream()) + { + using (var dstream = new DeflateStream(input, CompressionMode.Decompress)) + { + dstream.CopyTo(output); + } + bytes = new List(output.ToArray()); + } + } + } + else if (compressMode == Compression.GZip) + { + using (var input = new MemoryStream(bytes.ToArray())) + { + using (var output = new MemoryStream()) + { + using (var dstream = new GZipStream(input, CompressionMode.Decompress)) + { + dstream.CopyTo(output); + } + bytes = new List(output.ToArray()); + } + } + } + + if (bytes[0] != 0x51 || bytes[1] != 0x52 || bytes[2] != 0x52) + throw new Exception("Invalid raw data file. Filetype doesn't match \"QRR\"."); + + //Set QR code version + var sideLen = (int)bytes[4]; + bytes.RemoveRange(0, 5); + this.Version = (sideLen - 21 - 8) / 4 + 1; + + //Unpack + var modules = new Queue(8 * bytes.Count); + foreach (var b in bytes) + { + var bArr = new BitArray(new byte[] { b }); + for (int i = 7; i >= 0; i--) + { + modules.Enqueue((b & (1 << i)) != 0); + } + } + + //Build module matrix + this.ModuleMatrix = new List(sideLen); + for (int y = 0; y < sideLen; y++) + { + this.ModuleMatrix.Add(new BitArray(sideLen)); + for (int x = 0; x < sideLen; x++) + { + this.ModuleMatrix[y][x] = modules.Dequeue(); + } + } + + } + + public byte[] GetRawData(Compression compressMode) + { + var bytes = new List(); + + //Add header - signature ("QRR") + bytes.AddRange(new byte[]{ 0x51, 0x52, 0x52, 0x00 }); + + //Add header - rowsize + bytes.Add((byte)ModuleMatrix.Count); + + //Build data queue + var dataQueue = new Queue(); + foreach (var row in ModuleMatrix) + { + foreach (var module in row) + { + dataQueue.Enqueue((bool)module ? 1 : 0); + } + } + for (int i = 0; i < 8 - (ModuleMatrix.Count * ModuleMatrix.Count) % 8; i++) + { + dataQueue.Enqueue(0); + } + + //Process queue + while (dataQueue.Count > 0) + { + byte b = 0; + for (int i = 7; i >= 0; i--) + { + b += (byte)(dataQueue.Dequeue() << i); + } + bytes.Add(b); + } + var rawData = bytes.ToArray(); + + //Compress stream (optional) + if (compressMode == Compression.Deflate) + { + using (var output = new MemoryStream()) + { + using (var dstream = new DeflateStream(output, CompressionMode.Compress)) + { + dstream.Write(rawData, 0, rawData.Length); + } + rawData = output.ToArray(); + } + } + else if (compressMode == Compression.GZip) + { + using (var output = new MemoryStream()) + { + using (GZipStream gzipStream = new GZipStream(output, CompressionMode.Compress, true)) + { + gzipStream.Write(rawData, 0, rawData.Length); + } + rawData = output.ToArray(); + } + } + return rawData; + } + + public int Version { get; private set; } + + private static int ModulesPerSideFromVersion(int version) + { + return 21 + (version - 1) * 4; + } + + public void Dispose() + { + this.ModuleMatrix = null; + this.Version = 0; + + } + + public enum Compression + { + Uncompressed, + Deflate, + GZip + } + } +} diff --git a/QRCoder2/QRCodeGenerator.cs b/QRCoder2/QRCodeGenerator.cs new file mode 100644 index 00000000..18bbb69a --- /dev/null +++ b/QRCoder2/QRCodeGenerator.cs @@ -0,0 +1,1566 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Collections; +using QRCoder2.Payloads; + +namespace QRCoder2 +{ + public class QRCodeGenerator : IDisposable + { + private static readonly char[] alphanumEncTable = { ' ', '$', '%', '*', '+', '-', '.', '/', ':' }; + private static readonly int[] capacityBaseValues = { 41, 25, 17, 10, 34, 20, 14, 8, 27, 16, 11, 7, 17, 10, 7, 4, 77, 47, 32, 20, 63, 38, 26, 16, 48, 29, 20, 12, 34, 20, 14, 8, 127, 77, 53, 32, 101, 61, 42, 26, 77, 47, 32, 20, 58, 35, 24, 15, 187, 114, 78, 48, 149, 90, 62, 38, 111, 67, 46, 28, 82, 50, 34, 21, 255, 154, 106, 65, 202, 122, 84, 52, 144, 87, 60, 37, 106, 64, 44, 27, 322, 195, 134, 82, 255, 154, 106, 65, 178, 108, 74, 45, 139, 84, 58, 36, 370, 224, 154, 95, 293, 178, 122, 75, 207, 125, 86, 53, 154, 93, 64, 39, 461, 279, 192, 118, 365, 221, 152, 93, 259, 157, 108, 66, 202, 122, 84, 52, 552, 335, 230, 141, 432, 262, 180, 111, 312, 189, 130, 80, 235, 143, 98, 60, 652, 395, 271, 167, 513, 311, 213, 131, 364, 221, 151, 93, 288, 174, 119, 74, 772, 468, 321, 198, 604, 366, 251, 155, 427, 259, 177, 109, 331, 200, 137, 85, 883, 535, 367, 226, 691, 419, 287, 177, 489, 296, 203, 125, 374, 227, 155, 96, 1022, 619, 425, 262, 796, 483, 331, 204, 580, 352, 241, 149, 427, 259, 177, 109, 1101, 667, 458, 282, 871, 528, 362, 223, 621, 376, 258, 159, 468, 283, 194, 120, 1250, 758, 520, 320, 991, 600, 412, 254, 703, 426, 292, 180, 530, 321, 220, 136, 1408, 854, 586, 361, 1082, 656, 450, 277, 775, 470, 322, 198, 602, 365, 250, 154, 1548, 938, 644, 397, 1212, 734, 504, 310, 876, 531, 364, 224, 674, 408, 280, 173, 1725, 1046, 718, 442, 1346, 816, 560, 345, 948, 574, 394, 243, 746, 452, 310, 191, 1903, 1153, 792, 488, 1500, 909, 624, 384, 1063, 644, 442, 272, 813, 493, 338, 208, 2061, 1249, 858, 528, 1600, 970, 666, 410, 1159, 702, 482, 297, 919, 557, 382, 235, 2232, 1352, 929, 572, 1708, 1035, 711, 438, 1224, 742, 509, 314, 969, 587, 403, 248, 2409, 1460, 1003, 618, 1872, 1134, 779, 480, 1358, 823, 565, 348, 1056, 640, 439, 270, 2620, 1588, 1091, 672, 2059, 1248, 857, 528, 1468, 890, 611, 376, 1108, 672, 461, 284, 2812, 1704, 1171, 721, 2188, 1326, 911, 561, 1588, 963, 661, 407, 1228, 744, 511, 315, 3057, 1853, 1273, 784, 2395, 1451, 997, 614, 1718, 1041, 715, 440, 1286, 779, 535, 330, 3283, 1990, 1367, 842, 2544, 1542, 1059, 652, 1804, 1094, 751, 462, 1425, 864, 593, 365, 3517, 2132, 1465, 902, 2701, 1637, 1125, 692, 1933, 1172, 805, 496, 1501, 910, 625, 385, 3669, 2223, 1528, 940, 2857, 1732, 1190, 732, 2085, 1263, 868, 534, 1581, 958, 658, 405, 3909, 2369, 1628, 1002, 3035, 1839, 1264, 778, 2181, 1322, 908, 559, 1677, 1016, 698, 430, 4158, 2520, 1732, 1066, 3289, 1994, 1370, 843, 2358, 1429, 982, 604, 1782, 1080, 742, 457, 4417, 2677, 1840, 1132, 3486, 2113, 1452, 894, 2473, 1499, 1030, 634, 1897, 1150, 790, 486, 4686, 2840, 1952, 1201, 3693, 2238, 1538, 947, 2670, 1618, 1112, 684, 2022, 1226, 842, 518, 4965, 3009, 2068, 1273, 3909, 2369, 1628, 1002, 2805, 1700, 1168, 719, 2157, 1307, 898, 553, 5253, 3183, 2188, 1347, 4134, 2506, 1722, 1060, 2949, 1787, 1228, 756, 2301, 1394, 958, 590, 5529, 3351, 2303, 1417, 4343, 2632, 1809, 1113, 3081, 1867, 1283, 790, 2361, 1431, 983, 605, 5836, 3537, 2431, 1496, 4588, 2780, 1911, 1176, 3244, 1966, 1351, 832, 2524, 1530, 1051, 647, 6153, 3729, 2563, 1577, 4775, 2894, 1989, 1224, 3417, 2071, 1423, 876, 2625, 1591, 1093, 673, 6479, 3927, 2699, 1661, 5039, 3054, 2099, 1292, 3599, 2181, 1499, 923, 2735, 1658, 1139, 701, 6743, 4087, 2809, 1729, 5313, 3220, 2213, 1362, 3791, 2298, 1579, 972, 2927, 1774, 1219, 750, 7089, 4296, 2953, 1817, 5596, 3391, 2331, 1435, 3993, 2420, 1663, 1024, 3057, 1852, 1273, 784 }; + private static readonly int[] capacityECCBaseValues = { 19, 7, 1, 19, 0, 0, 16, 10, 1, 16, 0, 0, 13, 13, 1, 13, 0, 0, 9, 17, 1, 9, 0, 0, 34, 10, 1, 34, 0, 0, 28, 16, 1, 28, 0, 0, 22, 22, 1, 22, 0, 0, 16, 28, 1, 16, 0, 0, 55, 15, 1, 55, 0, 0, 44, 26, 1, 44, 0, 0, 34, 18, 2, 17, 0, 0, 26, 22, 2, 13, 0, 0, 80, 20, 1, 80, 0, 0, 64, 18, 2, 32, 0, 0, 48, 26, 2, 24, 0, 0, 36, 16, 4, 9, 0, 0, 108, 26, 1, 108, 0, 0, 86, 24, 2, 43, 0, 0, 62, 18, 2, 15, 2, 16, 46, 22, 2, 11, 2, 12, 136, 18, 2, 68, 0, 0, 108, 16, 4, 27, 0, 0, 76, 24, 4, 19, 0, 0, 60, 28, 4, 15, 0, 0, 156, 20, 2, 78, 0, 0, 124, 18, 4, 31, 0, 0, 88, 18, 2, 14, 4, 15, 66, 26, 4, 13, 1, 14, 194, 24, 2, 97, 0, 0, 154, 22, 2, 38, 2, 39, 110, 22, 4, 18, 2, 19, 86, 26, 4, 14, 2, 15, 232, 30, 2, 116, 0, 0, 182, 22, 3, 36, 2, 37, 132, 20, 4, 16, 4, 17, 100, 24, 4, 12, 4, 13, 274, 18, 2, 68, 2, 69, 216, 26, 4, 43, 1, 44, 154, 24, 6, 19, 2, 20, 122, 28, 6, 15, 2, 16, 324, 20, 4, 81, 0, 0, 254, 30, 1, 50, 4, 51, 180, 28, 4, 22, 4, 23, 140, 24, 3, 12, 8, 13, 370, 24, 2, 92, 2, 93, 290, 22, 6, 36, 2, 37, 206, 26, 4, 20, 6, 21, 158, 28, 7, 14, 4, 15, 428, 26, 4, 107, 0, 0, 334, 22, 8, 37, 1, 38, 244, 24, 8, 20, 4, 21, 180, 22, 12, 11, 4, 12, 461, 30, 3, 115, 1, 116, 365, 24, 4, 40, 5, 41, 261, 20, 11, 16, 5, 17, 197, 24, 11, 12, 5, 13, 523, 22, 5, 87, 1, 88, 415, 24, 5, 41, 5, 42, 295, 30, 5, 24, 7, 25, 223, 24, 11, 12, 7, 13, 589, 24, 5, 98, 1, 99, 453, 28, 7, 45, 3, 46, 325, 24, 15, 19, 2, 20, 253, 30, 3, 15, 13, 16, 647, 28, 1, 107, 5, 108, 507, 28, 10, 46, 1, 47, 367, 28, 1, 22, 15, 23, 283, 28, 2, 14, 17, 15, 721, 30, 5, 120, 1, 121, 563, 26, 9, 43, 4, 44, 397, 28, 17, 22, 1, 23, 313, 28, 2, 14, 19, 15, 795, 28, 3, 113, 4, 114, 627, 26, 3, 44, 11, 45, 445, 26, 17, 21, 4, 22, 341, 26, 9, 13, 16, 14, 861, 28, 3, 107, 5, 108, 669, 26, 3, 41, 13, 42, 485, 30, 15, 24, 5, 25, 385, 28, 15, 15, 10, 16, 932, 28, 4, 116, 4, 117, 714, 26, 17, 42, 0, 0, 512, 28, 17, 22, 6, 23, 406, 30, 19, 16, 6, 17, 1006, 28, 2, 111, 7, 112, 782, 28, 17, 46, 0, 0, 568, 30, 7, 24, 16, 25, 442, 24, 34, 13, 0, 0, 1094, 30, 4, 121, 5, 122, 860, 28, 4, 47, 14, 48, 614, 30, 11, 24, 14, 25, 464, 30, 16, 15, 14, 16, 1174, 30, 6, 117, 4, 118, 914, 28, 6, 45, 14, 46, 664, 30, 11, 24, 16, 25, 514, 30, 30, 16, 2, 17, 1276, 26, 8, 106, 4, 107, 1000, 28, 8, 47, 13, 48, 718, 30, 7, 24, 22, 25, 538, 30, 22, 15, 13, 16, 1370, 28, 10, 114, 2, 115, 1062, 28, 19, 46, 4, 47, 754, 28, 28, 22, 6, 23, 596, 30, 33, 16, 4, 17, 1468, 30, 8, 122, 4, 123, 1128, 28, 22, 45, 3, 46, 808, 30, 8, 23, 26, 24, 628, 30, 12, 15, 28, 16, 1531, 30, 3, 117, 10, 118, 1193, 28, 3, 45, 23, 46, 871, 30, 4, 24, 31, 25, 661, 30, 11, 15, 31, 16, 1631, 30, 7, 116, 7, 117, 1267, 28, 21, 45, 7, 46, 911, 30, 1, 23, 37, 24, 701, 30, 19, 15, 26, 16, 1735, 30, 5, 115, 10, 116, 1373, 28, 19, 47, 10, 48, 985, 30, 15, 24, 25, 25, 745, 30, 23, 15, 25, 16, 1843, 30, 13, 115, 3, 116, 1455, 28, 2, 46, 29, 47, 1033, 30, 42, 24, 1, 25, 793, 30, 23, 15, 28, 16, 1955, 30, 17, 115, 0, 0, 1541, 28, 10, 46, 23, 47, 1115, 30, 10, 24, 35, 25, 845, 30, 19, 15, 35, 16, 2071, 30, 17, 115, 1, 116, 1631, 28, 14, 46, 21, 47, 1171, 30, 29, 24, 19, 25, 901, 30, 11, 15, 46, 16, 2191, 30, 13, 115, 6, 116, 1725, 28, 14, 46, 23, 47, 1231, 30, 44, 24, 7, 25, 961, 30, 59, 16, 1, 17, 2306, 30, 12, 121, 7, 122, 1812, 28, 12, 47, 26, 48, 1286, 30, 39, 24, 14, 25, 986, 30, 22, 15, 41, 16, 2434, 30, 6, 121, 14, 122, 1914, 28, 6, 47, 34, 48, 1354, 30, 46, 24, 10, 25, 1054, 30, 2, 15, 64, 16, 2566, 30, 17, 122, 4, 123, 1992, 28, 29, 46, 14, 47, 1426, 30, 49, 24, 10, 25, 1096, 30, 24, 15, 46, 16, 2702, 30, 4, 122, 18, 123, 2102, 28, 13, 46, 32, 47, 1502, 30, 48, 24, 14, 25, 1142, 30, 42, 15, 32, 16, 2812, 30, 20, 117, 4, 118, 2216, 28, 40, 47, 7, 48, 1582, 30, 43, 24, 22, 25, 1222, 30, 10, 15, 67, 16, 2956, 30, 19, 118, 6, 119, 2334, 28, 18, 47, 31, 48, 1666, 30, 34, 24, 34, 25, 1276, 30, 20, 15, 61, 16 }; + private static readonly int[] alignmentPatternBaseValues = { 0, 0, 0, 0, 0, 0, 0, 6, 18, 0, 0, 0, 0, 0, 6, 22, 0, 0, 0, 0, 0, 6, 26, 0, 0, 0, 0, 0, 6, 30, 0, 0, 0, 0, 0, 6, 34, 0, 0, 0, 0, 0, 6, 22, 38, 0, 0, 0, 0, 6, 24, 42, 0, 0, 0, 0, 6, 26, 46, 0, 0, 0, 0, 6, 28, 50, 0, 0, 0, 0, 6, 30, 54, 0, 0, 0, 0, 6, 32, 58, 0, 0, 0, 0, 6, 34, 62, 0, 0, 0, 0, 6, 26, 46, 66, 0, 0, 0, 6, 26, 48, 70, 0, 0, 0, 6, 26, 50, 74, 0, 0, 0, 6, 30, 54, 78, 0, 0, 0, 6, 30, 56, 82, 0, 0, 0, 6, 30, 58, 86, 0, 0, 0, 6, 34, 62, 90, 0, 0, 0, 6, 28, 50, 72, 94, 0, 0, 6, 26, 50, 74, 98, 0, 0, 6, 30, 54, 78, 102, 0, 0, 6, 28, 54, 80, 106, 0, 0, 6, 32, 58, 84, 110, 0, 0, 6, 30, 58, 86, 114, 0, 0, 6, 34, 62, 90, 118, 0, 0, 6, 26, 50, 74, 98, 122, 0, 6, 30, 54, 78, 102, 126, 0, 6, 26, 52, 78, 104, 130, 0, 6, 30, 56, 82, 108, 134, 0, 6, 34, 60, 86, 112, 138, 0, 6, 30, 58, 86, 114, 142, 0, 6, 34, 62, 90, 118, 146, 0, 6, 30, 54, 78, 102, 126, 150, 6, 24, 50, 76, 102, 128, 154, 6, 28, 54, 80, 106, 132, 158, 6, 32, 58, 84, 110, 136, 162, 6, 26, 54, 82, 110, 138, 166, 6, 30, 58, 86, 114, 142, 170 }; + private static readonly int[] remainderBits = { 0, 7, 7, 7, 7, 7, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0 }; + + private static readonly List alignmentPatternTable = CreateAlignmentPatternTable(); + private static readonly List capacityECCTable = CreateCapacityECCTable(); + private static readonly List capacityTable = CreateCapacityTable(); + private static readonly List galoisField = CreateAntilogTable(); + private static readonly Dictionary alphanumEncDict = CreateAlphanumEncDict(); + + /// + /// Initializes the QR code generator + /// + public QRCodeGenerator() + { + } + + /// + /// Calculates the QR code data which than can be used in one of the rendering classes to generate a graphical representation. + /// + /// A payload object, generated by the PayloadGenerator-class + /// Thrown when the payload is too big to be encoded in a QR code. + /// Returns the raw QR code data which can be used for rendering. + public QRCodeData CreateQrCode(PayloadBase payload) + { + return GenerateQrCode(payload); + } + + /// + /// Calculates the QR code data which than can be used in one of the rendering classes to generate a graphical representation. + /// + /// A payload object, generated by the PayloadGenerator-class + /// The level of error correction data + /// Thrown when the payload is too big to be encoded in a QR code. + /// Returns the raw QR code data which can be used for rendering. + public QRCodeData CreateQrCode(PayloadBase payload, ECCLevel eccLevel) + { + return GenerateQrCode(payload, eccLevel); + } + + /// + /// Calculates the QR code data which than can be used in one of the rendering classes to generate a graphical representation. + /// + /// The payload which shall be encoded in the QR code + /// The level of error correction data + /// Shall the generator be forced to work in UTF-8 mode? + /// Should the byte-order-mark be used? + /// Which ECI mode shall be used? + /// Set fixed QR code target version. + /// Thrown when the payload is too big to be encoded in a QR code. + /// Returns the raw QR code data which can be used for rendering. + public QRCodeData CreateQrCode(string plainText, ECCLevel eccLevel, bool forceUtf8 = false, bool utf8BOM = false, EciMode eciMode = EciMode.Default, int requestedVersion = -1) + { + return GenerateQrCode(plainText, eccLevel, forceUtf8, utf8BOM, eciMode, requestedVersion); + } + + /// + /// Calculates the QR code data which than can be used in one of the rendering classes to generate a graphical representation. + /// + /// A byte array which shall be encoded/stored in the QR code + /// The level of error correction data + /// Thrown when the payload is too big to be encoded in a QR code. + /// Returns the raw QR code data which can be used for rendering. + public QRCodeData CreateQrCode(byte[] binaryData, ECCLevel eccLevel) + { + return GenerateQrCode(binaryData, eccLevel); + } + + + /// + /// Calculates the QR code data which than can be used in one of the rendering classes to generate a graphical representation. + /// + /// A payload object, generated by the PayloadGenerator-class + /// Thrown when the payload is too big to be encoded in a QR code. + /// Returns the raw QR code data which can be used for rendering. + public static QRCodeData GenerateQrCode(PayloadBase payload) + { + return GenerateQrCode(payload.ToString(), payload.EccLevel, false, false, payload.EciMode, payload.Version); + } + + /// + /// Calculates the QR code data which than can be used in one of the rendering classes to generate a graphical representation. + /// + /// A payload object, generated by the PayloadGenerator-class + /// The level of error correction data + /// Thrown when the payload is too big to be encoded in a QR code. + /// Returns the raw QR code data which can be used for rendering. + public static QRCodeData GenerateQrCode(PayloadBase payload, ECCLevel eccLevel) + { + return GenerateQrCode(payload.ToString(), eccLevel, false, false, payload.EciMode, payload.Version); + } + + /// + /// Calculates the QR code data which than can be used in one of the rendering classes to generate a graphical representation. + /// + /// The payload which shall be encoded in the QR code + /// The level of error correction data + /// Shall the generator be forced to work in UTF-8 mode? + /// Should the byte-order-mark be used? + /// Which ECI mode shall be used? + /// Set fixed QR code target version. + /// Thrown when the payload is too big to be encoded in a QR code. + /// Returns the raw QR code data which can be used for rendering. + public static QRCodeData GenerateQrCode(string plainText, ECCLevel eccLevel, bool forceUtf8 = false, bool utf8BOM = false, EciMode eciMode = EciMode.Default, int requestedVersion = -1) + { + EncodingMode encoding = GetEncodingFromPlaintext(plainText, forceUtf8); + var codedText = PlainTextToBinary(plainText, encoding, eciMode, utf8BOM, forceUtf8); + var dataInputLength = GetDataLength(encoding, plainText, codedText, forceUtf8); + int version = requestedVersion; + if (version == -1) + { + version = GetVersion(dataInputLength+(eciMode != EciMode.Default?2:0), encoding, eccLevel); + } + else + { + //Version was passed as fixed version via parameter. Thus let's check if chosen version is valid. + var minVersion = GetVersion(dataInputLength + (eciMode != EciMode.Default ? 2 : 0), encoding, eccLevel); + if (minVersion > version) + { + var maxSizeByte = capacityTable[version - 1].Details.First(x => x.ErrorCorrectionLevel == eccLevel).CapacityDict[encoding]; + throw new QRCoder2.Exceptions.DataTooLongException(eccLevel.ToString(), encoding.ToString(), version, maxSizeByte); + } + } + + string modeIndicator = String.Empty; + if (eciMode != EciMode.Default) + { + modeIndicator = DecToBin((int)EncodingMode.ECI, 4); + modeIndicator += DecToBin((int)eciMode, 8); + } + modeIndicator += DecToBin((int)encoding, 4); + var countIndicator = DecToBin(dataInputLength, GetCountIndicatorLength(version, encoding)); + var bitString = modeIndicator + countIndicator; + + bitString += codedText; + + return GenerateQrCode(bitString, eccLevel, version); + } + + + /// + /// Calculates the QR code data which than can be used in one of the rendering classes to generate a graphical representation. + /// + /// A byte array which shall be encoded/stored in the QR code + /// The level of error correction data + /// Thrown when the payload is too big to be encoded in a QR code. + /// Returns the raw QR code data which can be used for rendering. + public static QRCodeData GenerateQrCode(byte[] binaryData, ECCLevel eccLevel) + { + int version = GetVersion(binaryData.Length, EncodingMode.Byte, eccLevel); + + string modeIndicator = DecToBin((int)EncodingMode.Byte, 4); + string countIndicator = DecToBin(binaryData.Length, GetCountIndicatorLength(version, EncodingMode.Byte)); + + string bitString = modeIndicator + countIndicator; + foreach (byte b in binaryData) + { + bitString += DecToBin(b, 8); + } + + return GenerateQrCode(bitString, eccLevel, version); + } + + private static QRCodeData GenerateQrCode(string bitString, ECCLevel eccLevel, int version) + { + //Fill up data code word + var eccInfo = capacityECCTable.Single(x => x.Version == version && x.ErrorCorrectionLevel == eccLevel); + var dataLength = eccInfo.TotalDataCodewords * 8; + var lengthDiff = dataLength - bitString.Length; + if (lengthDiff > 0) + bitString += new string('0', Math.Min(lengthDiff, 4)); + if ((bitString.Length % 8) != 0) + bitString += new string('0', 8 - (bitString.Length % 8)); + while (bitString.Length < dataLength) + bitString += "1110110000010001"; + if (bitString.Length > dataLength) + bitString = bitString.Substring(0, dataLength); + + //Calculate error correction words + var codeWordWithECC = new List(eccInfo.BlocksInGroup1 + eccInfo.BlocksInGroup2); + for (var i = 0; i < eccInfo.BlocksInGroup1; i++) + { + var bitStr = bitString.Substring(i * eccInfo.CodewordsInGroup1 * 8, eccInfo.CodewordsInGroup1 * 8); + var bitBlockList = BinaryStringToBitBlockList(bitStr); + var bitBlockListDec = BinaryStringListToDecList(bitBlockList); + var eccWordList = CalculateECCWords(bitStr, eccInfo); + var eccWordListDec = BinaryStringListToDecList(eccWordList); + codeWordWithECC.Add( + new CodewordBlock(1, + i + 1, + bitStr, + bitBlockList, + eccWordList, + bitBlockListDec, + eccWordListDec) + ); + } + bitString = bitString.Substring(eccInfo.BlocksInGroup1 * eccInfo.CodewordsInGroup1 * 8); + for (var i = 0; i < eccInfo.BlocksInGroup2; i++) + { + var bitStr = bitString.Substring(i * eccInfo.CodewordsInGroup2 * 8, eccInfo.CodewordsInGroup2 * 8); + var bitBlockList = BinaryStringToBitBlockList(bitStr); + var bitBlockListDec = BinaryStringListToDecList(bitBlockList); + var eccWordList = CalculateECCWords(bitStr, eccInfo); + var eccWordListDec = BinaryStringListToDecList(eccWordList); + codeWordWithECC.Add(new CodewordBlock(2, + i + 1, + bitStr, + bitBlockList, + eccWordList, + bitBlockListDec, + eccWordListDec) + ); + } + + + //Interleave code words + var interleavedWordsSb = new StringBuilder(); + for (var i = 0; i < Math.Max(eccInfo.CodewordsInGroup1, eccInfo.CodewordsInGroup2); i++) + { + foreach (var codeBlock in codeWordWithECC) + if (codeBlock.CodeWords.Count > i) + interleavedWordsSb.Append(codeBlock.CodeWords[i]); + } + + + for (var i = 0; i < eccInfo.ECCPerBlock; i++) + { + foreach (var codeBlock in codeWordWithECC) + if (codeBlock.ECCWords.Count > i) + interleavedWordsSb.Append(codeBlock.ECCWords[i]); + } + interleavedWordsSb.Append(new string('0', remainderBits[version - 1])); + var interleavedData = interleavedWordsSb.ToString(); + + + //Place interleaved data on module matrix + var qr = new QRCodeData(version); + var blockedModules = new List(); + ModulePlacer.PlaceFinderPatterns(ref qr, ref blockedModules); + ModulePlacer.ReserveSeperatorAreas(qr.ModuleMatrix.Count, ref blockedModules); + ModulePlacer.PlaceAlignmentPatterns(ref qr, alignmentPatternTable.Where(x => x.Version == version).Select(x => x.PatternPositions).First(), ref blockedModules); + ModulePlacer.PlaceTimingPatterns(ref qr, ref blockedModules); + ModulePlacer.PlaceDarkModule(ref qr, version, ref blockedModules); + ModulePlacer.ReserveVersionAreas(qr.ModuleMatrix.Count, version, ref blockedModules); + ModulePlacer.PlaceDataWords(ref qr, interleavedData, ref blockedModules); + var maskVersion = ModulePlacer.MaskCode(ref qr, version, ref blockedModules, eccLevel); + var formatStr = GetFormatString(eccLevel, maskVersion); + + ModulePlacer.PlaceFormat(ref qr, formatStr); + if (version >= 7) + { + var versionString = GetVersionString(version); + ModulePlacer.PlaceVersion(ref qr, versionString); + } + + + ModulePlacer.AddQuietZone(ref qr); + return qr; + } + + private static string GetFormatString(ECCLevel level, int maskVersion) + { + var generator = "10100110111"; + var fStrMask = "101010000010010"; + + var fStr = (level == ECCLevel.L) ? "01" : (level == ECCLevel.M) ? "00" : (level == ECCLevel.Q) ? "11" : "10"; + fStr += DecToBin(maskVersion, 3); + var fStrEcc = fStr.PadRight(15, '0').TrimStart('0'); + while (fStrEcc.Length > 10) + { + var sb = new StringBuilder(); + generator = generator.PadRight(fStrEcc.Length, '0'); + for (var i = 0; i < fStrEcc.Length; i++) + sb.Append((Convert.ToInt32(fStrEcc[i]) ^ Convert.ToInt32(generator[i])).ToString()); + fStrEcc = sb.ToString().TrimStart('0'); + } + fStrEcc = fStrEcc.PadLeft(10, '0'); + fStr += fStrEcc; + + var sbMask = new StringBuilder(); + for (var i = 0; i < fStr.Length; i++) + sbMask.Append((Convert.ToInt32(fStr[i]) ^ Convert.ToInt32(fStrMask[i])).ToString()); + return sbMask.ToString(); + } + + private static string GetVersionString(int version) + { + var generator = "1111100100101"; + + var vStr = DecToBin(version, 6); + var vStrEcc = vStr.PadRight(18, '0').TrimStart('0'); + while (vStrEcc.Length > 12) + { + var sb = new StringBuilder(); + generator = generator.PadRight(vStrEcc.Length, '0'); + for (var i = 0; i < vStrEcc.Length; i++) + sb.Append((Convert.ToInt32(vStrEcc[i]) ^ Convert.ToInt32(generator[i])).ToString()); + vStrEcc = sb.ToString().TrimStart('0'); + } + vStrEcc = vStrEcc.PadLeft(12, '0'); + vStr += vStrEcc; + + return vStr; + } + + private static class ModulePlacer + { + public static void AddQuietZone(ref QRCodeData qrCode) + { + var quietLine = new bool[qrCode.ModuleMatrix.Count + 8]; + for (var i = 0; i < quietLine.Length; i++) + quietLine[i] = false; + for (var i = 0; i < 4; i++) + qrCode.ModuleMatrix.Insert(0, new BitArray(quietLine)); + for (var i = 0; i < 4; i++) + qrCode.ModuleMatrix.Add(new BitArray(quietLine)); + for (var i = 4; i < qrCode.ModuleMatrix.Count - 4; i++) + { + bool[] quietPart = { false, false, false, false }; + var tmpLine = new List(quietPart); + tmpLine.AddRange(qrCode.ModuleMatrix[i].Cast()); + tmpLine.AddRange(quietPart); + qrCode.ModuleMatrix[i] = new BitArray(tmpLine.ToArray()); + } + } + + private static string ReverseString(string inp) + { + string newStr = string.Empty; + if (inp.Length > 0) + { + for (int i = inp.Length - 1; i >= 0; i--) + newStr += inp[i]; + } + return newStr; + } + + public static void PlaceVersion(ref QRCodeData qrCode, string versionStr) + { + var size = qrCode.ModuleMatrix.Count; + + var vStr = ReverseString(versionStr); + + for (var x = 0; x < 6; x++) + { + for (var y = 0; y < 3; y++) + { + qrCode.ModuleMatrix[y + size - 11][x] = vStr[x * 3 + y] == '1'; + qrCode.ModuleMatrix[x][y + size - 11] = vStr[x * 3 + y] == '1'; + } + } + } + + public static void PlaceFormat(ref QRCodeData qrCode, string formatStr) + { + var size = qrCode.ModuleMatrix.Count; + var fStr = ReverseString(formatStr); + var modules = new[,] { + { 8, 0, size - 1, 8 }, + { 8, 1, size - 2, 8 }, + { 8, 2, size - 3, 8 }, + { 8, 3, size - 4, 8 }, + { 8, 4, size - 5, 8 }, + { 8, 5, size - 6, 8 }, + { 8, 7, size - 7, 8 }, + { 8, 8, size - 8, 8 }, + { 7, 8, 8, size - 7 }, + { 5, 8, 8, size - 6 }, + { 4, 8, 8, size - 5 }, + { 3, 8, 8, size - 4 }, + { 2, 8, 8, size - 3 }, + { 1, 8, 8, size - 2 }, + { 0, 8, 8, size - 1 } }; + for (var i = 0; i < 15; i++) + { + var p1 = new Point(modules[i, 0], modules[i, 1]); + var p2 = new Point(modules[i, 2], modules[i, 3]); + qrCode.ModuleMatrix[p1.Y][p1.X] = fStr[i] == '1'; + qrCode.ModuleMatrix[p2.Y][p2.X] = fStr[i] == '1'; + } + } + + + public static int MaskCode(ref QRCodeData qrCode, int version, ref List blockedModules, ECCLevel eccLevel) + { + int? selectedPattern = null; + var patternScore = 0; + + var size = qrCode.ModuleMatrix.Count; + + var methods = new Dictionary>(8) { + { 1, MaskPattern.Pattern1 }, {2, MaskPattern.Pattern2 }, {3, MaskPattern.Pattern3 }, {4, MaskPattern.Pattern4 }, + {5, MaskPattern.Pattern5 }, {6, MaskPattern.Pattern6 }, {7, MaskPattern.Pattern7 }, {8, MaskPattern.Pattern8 } + }; + + foreach (var pattern in methods) + { + var qrTemp = new QRCodeData(version); + for (var y = 0; y < size; y++) + { + for (var x = 0; x < size; x++) + { + qrTemp.ModuleMatrix[y][x] = qrCode.ModuleMatrix[y][x]; + } + + } + + var formatStr = GetFormatString(eccLevel, pattern.Key - 1); + ModulePlacer.PlaceFormat(ref qrTemp, formatStr); + if (version >= 7) + { + var versionString = GetVersionString(version); + ModulePlacer.PlaceVersion(ref qrTemp, versionString); + } + + for (var x = 0; x < size; x++) + { + for (var y = 0; y < x; y++) + { + if (!IsBlocked(new Rectangle(x, y, 1, 1), blockedModules)) + { + qrTemp.ModuleMatrix[y][x] ^= pattern.Value(x, y); + qrTemp.ModuleMatrix[x][y] ^= pattern.Value(y, x); + } + } + + if (!IsBlocked(new Rectangle(x, x, 1, 1), blockedModules)) + { + qrTemp.ModuleMatrix[x][x] ^= pattern.Value(x, x); + } + } + + var score = MaskPattern.Score(ref qrTemp); + if (!selectedPattern.HasValue || patternScore > score) + { + selectedPattern = pattern.Key; + patternScore = score; + } + } + + for (var x = 0; x < size; x++) + { + for (var y = 0; y < x; y++) + { + if (!IsBlocked(new Rectangle(x, y, 1, 1), blockedModules)) + { + qrCode.ModuleMatrix[y][x] ^= methods[selectedPattern.Value](x, y); + qrCode.ModuleMatrix[x][y] ^= methods[selectedPattern.Value](y, x); + } + } + + if (!IsBlocked(new Rectangle(x, x, 1, 1), blockedModules)) + { + qrCode.ModuleMatrix[x][x] ^= methods[selectedPattern.Value](x, x); + } + } + return selectedPattern.Value - 1; + } + + + public static void PlaceDataWords(ref QRCodeData qrCode, string data, ref List blockedModules) + { + var size = qrCode.ModuleMatrix.Count; + var up = true; + var datawords = new Queue(); + for (int i = 0; i< data.Length; i++) + { + datawords.Enqueue(data[i] != '0'); + } + for (var x = size - 1; x >= 0; x = x - 2) + { + if (x == 6) + x = 5; + for (var yMod = 1; yMod <= size; yMod++) + { + int y; + if (up) + { + y = size - yMod; + if (datawords.Count > 0 && !IsBlocked(new Rectangle(x, y, 1, 1), blockedModules)) + qrCode.ModuleMatrix[y][x] = datawords.Dequeue(); + if (datawords.Count > 0 && x > 0 && !IsBlocked(new Rectangle(x - 1, y, 1, 1), blockedModules)) + qrCode.ModuleMatrix[y][x - 1] = datawords.Dequeue(); + } + else + { + y = yMod - 1; + if (datawords.Count > 0 && !IsBlocked(new Rectangle(x, y, 1, 1), blockedModules)) + qrCode.ModuleMatrix[y][x] = datawords.Dequeue(); + if (datawords.Count > 0 && x > 0 && !IsBlocked(new Rectangle(x - 1, y, 1, 1), blockedModules)) + qrCode.ModuleMatrix[y][x - 1] = datawords.Dequeue(); + } + } + up = !up; + } + } + + public static void ReserveSeperatorAreas(int size, ref List blockedModules) + { + blockedModules.AddRange(new[]{ + new Rectangle(7, 0, 1, 8), + new Rectangle(0, 7, 7, 1), + new Rectangle(0, size-8, 8, 1), + new Rectangle(7, size-7, 1, 7), + new Rectangle(size-8, 0, 1, 8), + new Rectangle(size-7, 7, 7, 1) + }); + } + + public static void ReserveVersionAreas(int size, int version, ref List blockedModules) + { + blockedModules.AddRange(new[]{ + new Rectangle(8, 0, 1, 6), + new Rectangle(8, 7, 1, 1), + new Rectangle(0, 8, 6, 1), + new Rectangle(7, 8, 2, 1), + new Rectangle(size-8, 8, 8, 1), + new Rectangle(8, size-7, 1, 7) + }); + + if (version >= 7) + { + blockedModules.AddRange(new[]{ + new Rectangle(size-11, 0, 3, 6), + new Rectangle(0, size-11, 6, 3) + }); + } + } + public static void PlaceDarkModule(ref QRCodeData qrCode, int version, ref List blockedModules) + { + qrCode.ModuleMatrix[4 * version + 9][8] = true; + blockedModules.Add(new Rectangle(8, 4 * version + 9, 1, 1)); + } + + public static void PlaceFinderPatterns(ref QRCodeData qrCode, ref List blockedModules) + { + var size = qrCode.ModuleMatrix.Count; + int[] locations = { 0, 0, size - 7, 0, 0, size - 7 }; + + for (var i = 0; i < 6; i = i + 2) + { + for (var x = 0; x < 7; x++) + { + for (var y = 0; y < 7; y++) + { + if (!(((x == 1 || x == 5) && y > 0 && y < 6) || (x > 0 && x < 6 && (y == 1 || y == 5)))) + { + qrCode.ModuleMatrix[y + locations[i + 1]][x + locations[i]] = true; + } + } + } + blockedModules.Add(new Rectangle(locations[i], locations[i + 1], 7, 7)); + } + } + + public static void PlaceAlignmentPatterns(ref QRCodeData qrCode, List alignmentPatternLocations, ref List blockedModules) + { + foreach (var loc in alignmentPatternLocations) + { + var alignmentPatternRect = new Rectangle(loc.X, loc.Y, 5, 5); + var blocked = false; + foreach (var blockedRect in blockedModules) + { + if (Intersects(alignmentPatternRect, blockedRect)) + { + blocked = true; + break; + } + } + if (blocked) + continue; + + for (var x = 0; x < 5; x++) + { + for (var y = 0; y < 5; y++) + { + if (y == 0 || y == 4 || x == 0 || x == 4 || (x == 2 && y == 2)) + { + qrCode.ModuleMatrix[loc.Y + y][loc.X + x] = true; + } + } + } + blockedModules.Add(new Rectangle(loc.X, loc.Y, 5, 5)); + } + } + + public static void PlaceTimingPatterns(ref QRCodeData qrCode, ref List blockedModules) + { + var size = qrCode.ModuleMatrix.Count; + for (var i = 8; i < size - 8; i++) + { + if (i % 2 == 0) + { + qrCode.ModuleMatrix[6][i] = true; + qrCode.ModuleMatrix[i][6] = true; + } + } + blockedModules.AddRange(new[]{ + new Rectangle(6, 8, 1, size-16), + new Rectangle(8, 6, size-16, 1) + }); + } + + private static bool Intersects(Rectangle r1, Rectangle r2) + { + return r2.X < r1.X + r1.Width && r1.X < r2.X + r2.Width && r2.Y < r1.Y + r1.Height && r1.Y < r2.Y + r2.Height; + } + + private static bool IsBlocked(Rectangle r1, List blockedModules) + { + foreach (var blockedMod in blockedModules) + { + if (Intersects(blockedMod, r1)) + return true; + } + return false; + } + + private static class MaskPattern + { + public static bool Pattern1(int x, int y) + { + return (x + y) % 2 == 0; + } + + public static bool Pattern2(int x, int y) + { + return y % 2 == 0; + } + + public static bool Pattern3(int x, int y) + { + return x % 3 == 0; + } + + public static bool Pattern4(int x, int y) + { + return (x + y) % 3 == 0; + } + + public static bool Pattern5(int x, int y) + { + return ((int)(Math.Floor(y / 2d) + Math.Floor(x / 3d)) % 2) == 0; + } + + public static bool Pattern6(int x, int y) + { + return ((x * y) % 2) + ((x * y) % 3) == 0; + } + + public static bool Pattern7(int x, int y) + { + return (((x * y) % 2) + ((x * y) % 3)) % 2 == 0; + } + + public static bool Pattern8(int x, int y) + { + return (((x + y) % 2) + ((x * y) % 3)) % 2 == 0; + } + + public static int Score(ref QRCodeData qrCode) + { + int score1 = 0, + score2 = 0, + score3 = 0, + score4 = 0; + var size = qrCode.ModuleMatrix.Count; + + //Penalty 1 + for (var y = 0; y < size; y++) + { + var modInRow = 0; + var modInColumn = 0; + var lastValRow = qrCode.ModuleMatrix[y][0]; + var lastValColumn = qrCode.ModuleMatrix[0][y]; + for (var x = 0; x < size; x++) + { + if (qrCode.ModuleMatrix[y][x] == lastValRow) + modInRow++; + else + modInRow = 1; + if (modInRow == 5) + score1 += 3; + else if (modInRow > 5) + score1++; + lastValRow = qrCode.ModuleMatrix[y][x]; + + + if (qrCode.ModuleMatrix[x][y] == lastValColumn) + modInColumn++; + else + modInColumn = 1; + if (modInColumn == 5) + score1 += 3; + else if (modInColumn > 5) + score1++; + lastValColumn = qrCode.ModuleMatrix[x][y]; + } + } + + + //Penalty 2 + for (var y = 0; y < size - 1; y++) + { + for (var x = 0; x < size - 1; x++) + { + if (qrCode.ModuleMatrix[y][x] == qrCode.ModuleMatrix[y][x + 1] && + qrCode.ModuleMatrix[y][x] == qrCode.ModuleMatrix[y + 1][x] && + qrCode.ModuleMatrix[y][x] == qrCode.ModuleMatrix[y + 1][x + 1]) + score2 += 3; + } + } + + //Penalty 3 + for (var y = 0; y < size; y++) + { + for (var x = 0; x < size - 10; x++) + { + if ((qrCode.ModuleMatrix[y][x] && + !qrCode.ModuleMatrix[y][x + 1] && + qrCode.ModuleMatrix[y][x + 2] && + qrCode.ModuleMatrix[y][x + 3] && + qrCode.ModuleMatrix[y][x + 4] && + !qrCode.ModuleMatrix[y][x + 5] && + qrCode.ModuleMatrix[y][x + 6] && + !qrCode.ModuleMatrix[y][x + 7] && + !qrCode.ModuleMatrix[y][x + 8] && + !qrCode.ModuleMatrix[y][x + 9] && + !qrCode.ModuleMatrix[y][x + 10]) || + (!qrCode.ModuleMatrix[y][x] && + !qrCode.ModuleMatrix[y][x + 1] && + !qrCode.ModuleMatrix[y][x + 2] && + !qrCode.ModuleMatrix[y][x + 3] && + qrCode.ModuleMatrix[y][x + 4] && + !qrCode.ModuleMatrix[y][x + 5] && + qrCode.ModuleMatrix[y][x + 6] && + qrCode.ModuleMatrix[y][x + 7] && + qrCode.ModuleMatrix[y][x + 8] && + !qrCode.ModuleMatrix[y][x + 9] && + qrCode.ModuleMatrix[y][x + 10])) + { + score3 += 40; + } + + if ((qrCode.ModuleMatrix[x][y] && + !qrCode.ModuleMatrix[x + 1][y] && + qrCode.ModuleMatrix[x + 2][y] && + qrCode.ModuleMatrix[x + 3][y] && + qrCode.ModuleMatrix[x + 4][y] && + !qrCode.ModuleMatrix[x + 5][y] && + qrCode.ModuleMatrix[x + 6][y] && + !qrCode.ModuleMatrix[x + 7][y] && + !qrCode.ModuleMatrix[x + 8][y] && + !qrCode.ModuleMatrix[x + 9][y] && + !qrCode.ModuleMatrix[x + 10][y]) || + (!qrCode.ModuleMatrix[x][y] && + !qrCode.ModuleMatrix[x + 1][y] && + !qrCode.ModuleMatrix[x + 2][y] && + !qrCode.ModuleMatrix[x + 3][y] && + qrCode.ModuleMatrix[x + 4][y] && + !qrCode.ModuleMatrix[x + 5][y] && + qrCode.ModuleMatrix[x + 6][y] && + qrCode.ModuleMatrix[x + 7][y] && + qrCode.ModuleMatrix[x + 8][y] && + !qrCode.ModuleMatrix[x + 9][y] && + qrCode.ModuleMatrix[x + 10][y])) + { + score3 += 40; + } + } + } + + //Penalty 4 + double blackModules = 0; + foreach (var row in qrCode.ModuleMatrix) + foreach (bool bit in row) + if (bit) + blackModules++; + + var percent = (blackModules / (qrCode.ModuleMatrix.Count * qrCode.ModuleMatrix.Count)) * 100; + var prevMultipleOf5 = Math.Abs((int) Math.Floor(percent/5)*5 - 50)/5; + var nextMultipleOf5 = Math.Abs((int)Math.Floor(percent / 5) * 5 -45)/5; + score4 = Math.Min(prevMultipleOf5, nextMultipleOf5)*10; + + return score1 + score2 + score3 + score4; + } + } + + } + + private static List CalculateECCWords(string bitString, ECCInfo eccInfo) + { + var eccWords = eccInfo.ECCPerBlock; + var messagePolynom = CalculateMessagePolynom(bitString); + var generatorPolynom = CalculateGeneratorPolynom(eccWords); + + for (var i = 0; i < messagePolynom.PolyItems.Count; i++) + messagePolynom.PolyItems[i] = new PolynomItem(messagePolynom.PolyItems[i].Coefficient, + messagePolynom.PolyItems[i].Exponent + eccWords); + + for (var i = 0; i < generatorPolynom.PolyItems.Count; i++) + generatorPolynom.PolyItems[i] = new PolynomItem(generatorPolynom.PolyItems[i].Coefficient, + generatorPolynom.PolyItems[i].Exponent + (messagePolynom.PolyItems.Count-1)); + + var leadTermSource = messagePolynom; + for (var i = 0; (leadTermSource.PolyItems.Count > 0 && leadTermSource.PolyItems[leadTermSource.PolyItems.Count - 1].Exponent > 0); i++) + { + if (leadTermSource.PolyItems[0].Coefficient == 0) + { + leadTermSource.PolyItems.RemoveAt(0); + leadTermSource.PolyItems.Add(new PolynomItem(0, leadTermSource.PolyItems[leadTermSource.PolyItems.Count - 1].Exponent - 1)); + } + else + { + var resPoly = MultiplyGeneratorPolynomByLeadterm(generatorPolynom, ConvertToAlphaNotation(leadTermSource).PolyItems[0], i); + resPoly = ConvertToDecNotation(resPoly); + resPoly = XORPolynoms(leadTermSource, resPoly); + leadTermSource = resPoly; + } + } + return leadTermSource.PolyItems.Select(x => DecToBin(x.Coefficient, 8)).ToList(); + } + + private static Polynom ConvertToAlphaNotation(Polynom poly) + { + var newPoly = new Polynom(); + for (var i = 0; i < poly.PolyItems.Count; i++) + newPoly.PolyItems.Add( + new PolynomItem( + (poly.PolyItems[i].Coefficient != 0 + ? GetAlphaExpFromIntVal(poly.PolyItems[i].Coefficient) + : 0), poly.PolyItems[i].Exponent)); + return newPoly; + } + + private static Polynom ConvertToDecNotation(Polynom poly) + { + var newPoly = new Polynom(); + for (var i = 0; i < poly.PolyItems.Count; i++) + newPoly.PolyItems.Add(new PolynomItem(GetIntValFromAlphaExp(poly.PolyItems[i].Coefficient), poly.PolyItems[i].Exponent)); + return newPoly; + } + + private static int GetVersion(int length, EncodingMode encMode, ECCLevel eccLevel) + { + + var fittingVersions = capacityTable.Where( + x => x.Details.Any( + y => (y.ErrorCorrectionLevel == eccLevel + && y.CapacityDict[encMode] >= Convert.ToInt32(length) + ) + ) + ).Select(x => new + { + version = x.Version, + capacity = x.Details.Single(y => y.ErrorCorrectionLevel == eccLevel) + .CapacityDict[encMode] + }); + + if (fittingVersions.Any()) + return fittingVersions.Min(x => x.version); + + var maxSizeByte = capacityTable.Where( + x => x.Details.Any( + y => (y.ErrorCorrectionLevel == eccLevel)) + ).Max(x => x.Details.Single(y => y.ErrorCorrectionLevel == eccLevel).CapacityDict[encMode]); + throw new QRCoder2.Exceptions.DataTooLongException(eccLevel.ToString(), encMode.ToString(), maxSizeByte); + } + + private static EncodingMode GetEncodingFromPlaintext(string plainText, bool forceUtf8) + { + if (forceUtf8) return EncodingMode.Byte; + EncodingMode result = EncodingMode.Numeric; // assume numeric + foreach (char c in plainText) + { + if (IsInRange(c, '0', '9')) continue; // numeric - char.IsDigit() for Latin1 + result = EncodingMode.Alphanumeric; // not numeric, assume alphanumeric + if (IsInRange(c, 'A', 'Z') || alphanumEncTable.Contains(c)) continue; // alphanumeric + return EncodingMode.Byte; // not numeric or alphanumeric, assume byte + } + return result; // either numeric or alphanumeric + } + + private static bool IsInRange(char c, char min, char max) + { + return (uint)(c - min) <= (uint)(max - min); + } + + private static Polynom CalculateMessagePolynom(string bitString) + { + var messagePol = new Polynom(); + for (var i = bitString.Length / 8 - 1; i >= 0; i--) + { + messagePol.PolyItems.Add(new PolynomItem(BinToDec(bitString.Substring(0, 8)), i)); + bitString = bitString.Remove(0, 8); + } + return messagePol; + } + + + private static Polynom CalculateGeneratorPolynom(int numEccWords) + { + var generatorPolynom = new Polynom(); + generatorPolynom.PolyItems.AddRange(new[]{ + new PolynomItem(0,1), + new PolynomItem(0,0) + }); + for (var i = 1; i <= numEccWords - 1; i++) + { + var multiplierPolynom = new Polynom(); + multiplierPolynom.PolyItems.AddRange(new[]{ + new PolynomItem(0,1), + new PolynomItem(i,0) + }); + + generatorPolynom = MultiplyAlphaPolynoms(generatorPolynom, multiplierPolynom); + } + + return generatorPolynom; + } + + private static List BinaryStringToBitBlockList(string bitString) + { + const int blockSize = 8; + var numberOfBlocks = (int)Math.Ceiling(bitString.Length / (double)blockSize); + var blocklist = new List(numberOfBlocks); + + for (int i = 0; i < bitString.Length; i += blockSize) + { + blocklist.Add(bitString.Substring(i, blockSize)); + } + + return blocklist; + } + + private static List BinaryStringListToDecList(List binaryStringList) + { + return binaryStringList.Select(binaryString => BinToDec(binaryString)).ToList(); + } + + private static int BinToDec(string binStr) + { + return Convert.ToInt32(binStr, 2); + } + + private static string DecToBin(int decNum) + { + return Convert.ToString(decNum, 2); + } + + private static string DecToBin(int decNum, int padLeftUpTo) + { + var binStr = DecToBin(decNum); + return binStr.PadLeft(padLeftUpTo, '0'); + } + + private static int GetCountIndicatorLength(int version, EncodingMode encMode) + { + if (version < 10) + { + if (encMode == EncodingMode.Numeric) + return 10; + else if (encMode == EncodingMode.Alphanumeric) + return 9; + else + return 8; + } + else if (version < 27) + { + if (encMode == EncodingMode.Numeric) + return 12; + else if (encMode == EncodingMode.Alphanumeric) + return 11; + else if (encMode == EncodingMode.Byte) + return 16; + else + return 10; + } + else + { + if (encMode == EncodingMode.Numeric) + return 14; + else if (encMode == EncodingMode.Alphanumeric) + return 13; + else if (encMode == EncodingMode.Byte) + return 16; + else + return 12; + } + } + + private static int GetDataLength(EncodingMode encoding, string plainText, string codedText, bool forceUtf8) + { + return forceUtf8 || IsUtf8(encoding, plainText, forceUtf8) ? (codedText.Length / 8) : plainText.Length; + } + + private static bool IsUtf8(EncodingMode encoding, string plainText, bool forceUtf8) + { + return (encoding == EncodingMode.Byte && (!IsValidISO(plainText) || forceUtf8)); + } + + private static bool IsValidISO(string input) + { + var bytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(input); + //var result = Encoding.GetEncoding("ISO-8859-1").GetString(bytes); + var result = Encoding.GetEncoding("ISO-8859-1").GetString(bytes,0,bytes.Length); + return String.Equals(input, result); + } + + private static string PlainTextToBinary(string plainText, EncodingMode encMode, EciMode eciMode, bool utf8BOM, bool forceUtf8) + { + switch(encMode) + { + case EncodingMode.Alphanumeric: + return PlainTextToBinaryAlphanumeric(plainText); + case EncodingMode.Numeric: + return PlainTextToBinaryNumeric(plainText); + case EncodingMode.Byte: + return PlainTextToBinaryByte(plainText, eciMode, utf8BOM, forceUtf8); + case EncodingMode.Kanji: + return string.Empty; + case EncodingMode.ECI: + default: + return string.Empty; + } + } + + private static string PlainTextToBinaryNumeric(string plainText) + { + var codeText = string.Empty; + while (plainText.Length >= 3) + { + var dec = Convert.ToInt32(plainText.Substring(0, 3)); + codeText += DecToBin(dec, 10); + plainText = plainText.Substring(3); + + } + if (plainText.Length == 2) + { + var dec = Convert.ToInt32(plainText); + codeText += DecToBin(dec, 7); + } + else if (plainText.Length == 1) + { + var dec = Convert.ToInt32(plainText); + codeText += DecToBin(dec, 4); + } + return codeText; + } + + private static string PlainTextToBinaryAlphanumeric(string plainText) + { + var codeText = string.Empty; + while (plainText.Length >= 2) + { + var token = plainText.Substring(0, 2); + var dec = alphanumEncDict[token[0]] * 45 + alphanumEncDict[token[1]]; + codeText += DecToBin(dec, 11); + plainText = plainText.Substring(2); + + } + if (plainText.Length > 0) + { + codeText += DecToBin(alphanumEncDict[plainText[0]], 6); + } + return codeText; + } + + private string PlainTextToBinaryECI(string plainText) + { + var codeText = string.Empty; + byte[] _bytes = Encoding.GetEncoding("ascii").GetBytes(plainText); + foreach(byte _byte in _bytes) + { + codeText += DecToBin(_byte, 8); + } + return codeText; + } + + private static string ConvertToIso8859(string value, string Iso = "ISO-8859-2") + { + Encoding iso = Encoding.GetEncoding(Iso); + Encoding utf8 = Encoding.UTF8; + byte[] utfBytes = utf8.GetBytes(value); + byte[] isoBytes = Encoding.Convert(utf8, iso, utfBytes); +#if NETFRAMEWORK || NETSTANDARD2_0 || NET5_0 + return iso.GetString(isoBytes); +#else + return iso.GetString(isoBytes, 0, isoBytes.Length); +#endif + } + + private static string PlainTextToBinaryByte(string plainText, EciMode eciMode, bool utf8BOM, bool forceUtf8) + { + byte[] codeBytes; + var codeText = string.Empty; + + if (IsValidISO(plainText) && !forceUtf8) + codeBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(plainText); + else + { + switch(eciMode) + { + case EciMode.Iso8859_1: + codeBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(ConvertToIso8859(plainText, "ISO-8859-1")); + break; + case EciMode.Iso8859_2: + codeBytes = Encoding.GetEncoding("ISO-8859-2").GetBytes(ConvertToIso8859(plainText, "ISO-8859-2")); + break; + case EciMode.Default: + case EciMode.Utf8: + default: + codeBytes = utf8BOM ? Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(plainText)).ToArray() : Encoding.UTF8.GetBytes(plainText); + break; + } + } + + foreach (var b in codeBytes) + codeText += DecToBin(b, 8); + + return codeText; + } + + + private static Polynom XORPolynoms(Polynom messagePolynom, Polynom resPolynom) + { + var resultPolynom = new Polynom(); + Polynom longPoly, shortPoly; + if (messagePolynom.PolyItems.Count >= resPolynom.PolyItems.Count) + { + longPoly = messagePolynom; + shortPoly = resPolynom; + } + else + { + longPoly = resPolynom; + shortPoly = messagePolynom; + } + + for (var i = 0; i < longPoly.PolyItems.Count; i++) + { + var polItemRes = new PolynomItem + ( + + longPoly.PolyItems[i].Coefficient ^ + (shortPoly.PolyItems.Count > i ? shortPoly.PolyItems[i].Coefficient : 0), + messagePolynom.PolyItems[0].Exponent - i + ); + resultPolynom.PolyItems.Add(polItemRes); + } + resultPolynom.PolyItems.RemoveAt(0); + return resultPolynom; + } + + + private static Polynom MultiplyGeneratorPolynomByLeadterm(Polynom genPolynom, PolynomItem leadTerm, int lowerExponentBy) + { + var resultPolynom = new Polynom(); + foreach (var polItemBase in genPolynom.PolyItems) + { + var polItemRes = new PolynomItem( + + (polItemBase.Coefficient + leadTerm.Coefficient) % 255, + polItemBase.Exponent - lowerExponentBy + ); + resultPolynom.PolyItems.Add(polItemRes); + } + return resultPolynom; + } + + + private static Polynom MultiplyAlphaPolynoms(Polynom polynomBase, Polynom polynomMultiplier) + { + var resultPolynom = new Polynom(); + foreach (var polItemBase in polynomMultiplier.PolyItems) + { + foreach (var polItemMulti in polynomBase.PolyItems) + { + var polItemRes = new PolynomItem + ( + ShrinkAlphaExp(polItemBase.Coefficient + polItemMulti.Coefficient), + (polItemBase.Exponent + polItemMulti.Exponent) + ); + resultPolynom.PolyItems.Add(polItemRes); + } + } + var exponentsToGlue = resultPolynom.PolyItems.GroupBy(x => x.Exponent).Where(x => x.Count() > 1).Select(x => x.First().Exponent); + var toGlue = exponentsToGlue as IList ?? exponentsToGlue.ToList(); + var gluedPolynoms = new List(toGlue.Count); + foreach (var exponent in toGlue) + { + var coefficient = resultPolynom.PolyItems.Where(x => x.Exponent == exponent).Aggregate(0, (current, polynomOld) + => current ^ GetIntValFromAlphaExp(polynomOld.Coefficient)); + var polynomFixed = new PolynomItem(GetAlphaExpFromIntVal(coefficient), exponent); + gluedPolynoms.Add(polynomFixed); + } + resultPolynom.PolyItems.RemoveAll(x => toGlue.Contains(x.Exponent)); + resultPolynom.PolyItems.AddRange(gluedPolynoms); + resultPolynom.PolyItems.Sort((x, y) => -x.Exponent.CompareTo(y.Exponent)); + return resultPolynom; + } + + private static int GetIntValFromAlphaExp(int exp) + { + return galoisField.Find(alog => alog.ExponentAlpha == exp).IntegerValue; + } + + private static int GetAlphaExpFromIntVal(int intVal) + { + return galoisField.Find(alog => alog.IntegerValue == intVal).ExponentAlpha; + } + + private static int ShrinkAlphaExp(int alphaExp) + { + // ReSharper disable once PossibleLossOfFraction + return (int)((alphaExp % 256) + Math.Floor((double)(alphaExp / 256))); + } + + private static Dictionary CreateAlphanumEncDict() + { + var localAlphanumEncDict = new Dictionary(45); + //Add numbers + for (int i = 0; i < 10; i++) + localAlphanumEncDict.Add($"{i}"[0], i); + //Add chars + for (char c = 'A'; c <= 'Z'; c++) + localAlphanumEncDict.Add(c, localAlphanumEncDict.Count()); + //Add special chars + for (int i = 0; i < alphanumEncTable.Length; i++) + localAlphanumEncDict.Add(alphanumEncTable[i], localAlphanumEncDict.Count()); + return localAlphanumEncDict; + } + + private static List CreateAlignmentPatternTable() + { + var localAlignmentPatternTable = new List(40); + + for (var i = 0; i < (7 * 40); i = i + 7) + { + var points = new List(); + for (var x = 0; x < 7; x++) + { + if (alignmentPatternBaseValues[i + x] != 0) + { + for (var y = 0; y < 7; y++) + { + if (alignmentPatternBaseValues[i + y] != 0) + { + var p = new Point(alignmentPatternBaseValues[i + x] - 2, alignmentPatternBaseValues[i + y] - 2); + if (!points.Contains(p)) + points.Add(p); + } + } + } + } + + localAlignmentPatternTable.Add(new AlignmentPattern() + { + Version = (i + 7) / 7, + PatternPositions = points + } + ); + } + return localAlignmentPatternTable; + } + + + private static List CreateCapacityECCTable() + { + var localCapacityECCTable = new List(160); + for (var i = 0; i < (4 * 6 * 40); i = i + (4 * 6)) + { + localCapacityECCTable.AddRange( + new[] + { + new ECCInfo( + (i+24) / 24, + ECCLevel.L, + capacityECCBaseValues[i], + capacityECCBaseValues[i+1], + capacityECCBaseValues[i+2], + capacityECCBaseValues[i+3], + capacityECCBaseValues[i+4], + capacityECCBaseValues[i+5]), + new ECCInfo + ( + version: (i + 24) / 24, + errorCorrectionLevel: ECCLevel.M, + totalDataCodewords: capacityECCBaseValues[i+6], + eccPerBlock: capacityECCBaseValues[i+7], + blocksInGroup1: capacityECCBaseValues[i+8], + codewordsInGroup1: capacityECCBaseValues[i+9], + blocksInGroup2: capacityECCBaseValues[i+10], + codewordsInGroup2: capacityECCBaseValues[i+11] + ), + new ECCInfo + ( + version: (i + 24) / 24, + errorCorrectionLevel: ECCLevel.Q, + totalDataCodewords: capacityECCBaseValues[i+12], + eccPerBlock: capacityECCBaseValues[i+13], + blocksInGroup1: capacityECCBaseValues[i+14], + codewordsInGroup1: capacityECCBaseValues[i+15], + blocksInGroup2: capacityECCBaseValues[i+16], + codewordsInGroup2: capacityECCBaseValues[i+17] + ), + new ECCInfo + ( + version: (i + 24) / 24, + errorCorrectionLevel: ECCLevel.H, + totalDataCodewords: capacityECCBaseValues[i+18], + eccPerBlock: capacityECCBaseValues[i+19], + blocksInGroup1: capacityECCBaseValues[i+20], + codewordsInGroup1: capacityECCBaseValues[i+21], + blocksInGroup2: capacityECCBaseValues[i+22], + codewordsInGroup2: capacityECCBaseValues[i+23] + ) + }); + } + return localCapacityECCTable; + } + + private static List CreateCapacityTable() + { + var localCapacityTable = new List(40); + for (var i = 0; i < (16 * 40); i = i + 16) + { + localCapacityTable.Add(new VersionInfo( + + (i + 16) / 16, + new List(4) + { + new VersionInfoDetails( + ECCLevel.L, + new Dictionary(){ + { EncodingMode.Numeric, capacityBaseValues[i] }, + { EncodingMode.Alphanumeric, capacityBaseValues[i+1] }, + { EncodingMode.Byte, capacityBaseValues[i+2] }, + { EncodingMode.Kanji, capacityBaseValues[i+3] }, + } + ), + new VersionInfoDetails( + ECCLevel.M, + new Dictionary(){ + { EncodingMode.Numeric, capacityBaseValues[i+4] }, + { EncodingMode.Alphanumeric, capacityBaseValues[i+5] }, + { EncodingMode.Byte, capacityBaseValues[i+6] }, + { EncodingMode.Kanji, capacityBaseValues[i+7] }, + } + ), + new VersionInfoDetails( + ECCLevel.Q, + new Dictionary(){ + { EncodingMode.Numeric, capacityBaseValues[i+8] }, + { EncodingMode.Alphanumeric, capacityBaseValues[i+9] }, + { EncodingMode.Byte, capacityBaseValues[i+10] }, + { EncodingMode.Kanji, capacityBaseValues[i+11] }, + } + ), + new VersionInfoDetails( + ECCLevel.H, + new Dictionary(){ + { EncodingMode.Numeric, capacityBaseValues[i+12] }, + { EncodingMode.Alphanumeric, capacityBaseValues[i+13] }, + { EncodingMode.Byte, capacityBaseValues[i+14] }, + { EncodingMode.Kanji, capacityBaseValues[i+15] }, + } + ) + } + )); + } + return localCapacityTable; + } + + private static List CreateAntilogTable() + { + var localGaloisField = new List(256); + + int gfItem = 1; + for (var i = 0; i < 256; i++) + { + localGaloisField.Add(new Antilog(i, gfItem)); + gfItem *= 2; + if (gfItem > 255) + gfItem ^= 285; + } + return localGaloisField; + } + + private enum EncodingMode + { + Numeric = 1, + Alphanumeric = 2, + Byte = 4, + Kanji = 8, + ECI = 7 + } + + private struct AlignmentPattern + { + public int Version; + public List PatternPositions; + } + + private struct CodewordBlock + { + public CodewordBlock(int groupNumber, int blockNumber, string bitString, List codeWords, + List eccWords, List codeWordsInt, List eccWordsInt) + { + this.GroupNumber = groupNumber; + this.BlockNumber = blockNumber; + this.BitString = bitString; + this.CodeWords = codeWords; + this.ECCWords = eccWords; + this.CodeWordsInt = codeWordsInt; + this.ECCWordsInt = eccWordsInt; + } + + public int GroupNumber { get; } + public int BlockNumber { get; } + public string BitString { get; } + public List CodeWords { get; } + public List CodeWordsInt { get; } + public List ECCWords { get; } + public List ECCWordsInt { get; } + } + + private struct ECCInfo + { + public ECCInfo(int version, ECCLevel errorCorrectionLevel, int totalDataCodewords, int eccPerBlock, int blocksInGroup1, + int codewordsInGroup1, int blocksInGroup2, int codewordsInGroup2) + { + this.Version = version; + this.ErrorCorrectionLevel = errorCorrectionLevel; + this.TotalDataCodewords = totalDataCodewords; + this.ECCPerBlock = eccPerBlock; + this.BlocksInGroup1 = blocksInGroup1; + this.CodewordsInGroup1 = codewordsInGroup1; + this.BlocksInGroup2 = blocksInGroup2; + this.CodewordsInGroup2 = codewordsInGroup2; + } + public int Version { get; } + public ECCLevel ErrorCorrectionLevel { get; } + public int TotalDataCodewords { get; } + public int ECCPerBlock { get; } + public int BlocksInGroup1 { get; } + public int CodewordsInGroup1 { get; } + public int BlocksInGroup2 { get; } + public int CodewordsInGroup2 { get; } + } + + private struct VersionInfo + { + public VersionInfo(int version, List versionInfoDetails) + { + this.Version = version; + this.Details = versionInfoDetails; + } + public int Version { get; } + public List Details { get; } + } + + private struct VersionInfoDetails + { + public VersionInfoDetails(ECCLevel errorCorrectionLevel, Dictionary capacityDict) + { + this.ErrorCorrectionLevel = errorCorrectionLevel; + this.CapacityDict = capacityDict; + } + + public ECCLevel ErrorCorrectionLevel { get; } + public Dictionary CapacityDict { get; } + } + + private struct Antilog + { + public Antilog(int exponentAlpha, int integerValue) + { + this.ExponentAlpha = exponentAlpha; + this.IntegerValue = integerValue; + } + public int ExponentAlpha { get; } + public int IntegerValue { get; } + } + + private struct PolynomItem + { + public PolynomItem(int coefficient, int exponent) + { + this.Coefficient = coefficient; + this.Exponent = exponent; + } + + public int Coefficient { get; } + public int Exponent { get; } + } + + private class Polynom + { + public Polynom() + { + this.PolyItems = new List(); + } + + public List PolyItems { get; set; } + + public override string ToString() + { + var sb = new StringBuilder(); + //this.PolyItems.ForEach(x => sb.Append("a^" + x.Coefficient + "*x^" + x.Exponent + " + ")); + foreach (var polyItem in this.PolyItems) + { + sb.Append("a^" + polyItem.Coefficient + "*x^" + polyItem.Exponent + " + "); + } + + return sb.ToString().TrimEnd(new[] { ' ', '+' }); + } + } + + private class Point + { + public int X { get; } + public int Y { get; } + public Point(int x, int y) + { + this.X = x; + this.Y = y; + } + } + + private class Rectangle + { + public int X { get; } + public int Y { get; } + public int Width { get; } + public int Height { get; } + + public Rectangle(int x, int y, int w, int h) + { + this.X = x; + this.Y = y; + this.Width = w; + this.Height = h; + } + } + + public void Dispose() + { + // left for back-compat + } + } +} diff --git a/QRCoder2/QRCoder2.csproj b/QRCoder2/QRCoder2.csproj new file mode 100644 index 00000000..7e24b13a --- /dev/null +++ b/QRCoder2/QRCoder2.csproj @@ -0,0 +1,8 @@ + + + + net6.0 + + + + diff --git a/QRCoder2/Renderers/BitmapByteQRCode.cs b/QRCoder2/Renderers/BitmapByteQRCode.cs new file mode 100644 index 00000000..0e0f92e7 --- /dev/null +++ b/QRCoder2/Renderers/BitmapByteQRCode.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using System.Linq; +using QRCoder2.Payloads; + +namespace QRCoder2.Renderers +{ + + // ReSharper disable once InconsistentNaming + public class BitmapByteQRCode : QRCodeRendererBase + { + public BitmapByteQRCode(QRCodeData data) : base(data) { } + + public byte[] GetGraphic(int pixelsPerModule) + { + return GetGraphic(pixelsPerModule, new byte[] { 0x00, 0x00, 0x00 }, new byte[] { 0xFF, 0xFF, 0xFF }); + } + + public byte[] GetGraphic(int pixelsPerModule, string darkColorHtmlHex, string lightColorHtmlHex) + { + return GetGraphic(pixelsPerModule, HexColorToByteArray(darkColorHtmlHex), HexColorToByteArray(lightColorHtmlHex)); + } + + public byte[] GetGraphic(int pixelsPerModule, byte[] darkColorRgb, byte[] lightColorRgb) + { + var sideLength = this.QrCodeData.ModuleMatrix.Count * pixelsPerModule; + + var moduleDark = darkColorRgb.Reverse(); + var moduleLight = lightColorRgb.Reverse(); + + List bmp = new List(); + + //header + bmp.AddRange(new byte[] { 0x42, 0x4D, 0x4C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1A, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00 }); + + //width + bmp.AddRange(IntTo4Byte(sideLength)); + //height + bmp.AddRange(IntTo4Byte(sideLength)); + + //header end + bmp.AddRange(new byte[] { 0x01, 0x00, 0x18, 0x00 }); + + //draw qr code + for (var x = sideLength-1; x >= 0; x = x - pixelsPerModule) + { + for (int pm = 0; pm < pixelsPerModule; pm++) + { + for (var y = 0; y < sideLength; y = y + pixelsPerModule) + { + var module = + this.QrCodeData.ModuleMatrix[(x + pixelsPerModule)/pixelsPerModule - 1][(y + pixelsPerModule)/pixelsPerModule - 1]; + for (int i = 0; i < pixelsPerModule; i++) + { + bmp.AddRange(module ? moduleDark : moduleLight); + } + } + if (sideLength%4 != 0) + { + for (int i = 0; i < sideLength%4; i++) + { + bmp.Add(0x00); + } + } + } + } + + //finalize with terminator + bmp.AddRange(new byte[] { 0x00, 0x00 }); + + return bmp.ToArray(); + } + + private byte[] HexColorToByteArray(string colorString) + { + if (colorString.StartsWith("#")) + colorString = colorString.Substring(1); + byte[] byteColor = new byte[colorString.Length / 2]; + for (int i = 0; i < byteColor.Length; i++) + byteColor[i] = byte.Parse(colorString.Substring(i * 2, 2), System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture); + return byteColor; + } + + private byte[] IntTo4Byte(int inp) + { + byte[] bytes = new byte[2]; + unchecked + { + bytes[1] = (byte)(inp >> 8); + bytes[0] = (byte)(inp); + } + return bytes; + } + } + + + public static class BitmapByteQRCodeHelper + { + public static byte[] GetQRCode(string plainText, int pixelsPerModule, string darkColorHtmlHex, + string lightColorHtmlHex, ECCLevel eccLevel, bool forceUtf8 = false, bool utf8BOM = false, + EciMode eciMode = EciMode.Default, int requestedVersion = -1) + { + using (var qrGenerator = new QRCodeGenerator()) + using ( + var qrCodeData = qrGenerator.CreateQrCode(plainText, eccLevel, forceUtf8, utf8BOM, eciMode, + requestedVersion)) + { + var qrCode = new BitmapByteQRCode(qrCodeData); + return qrCode.GetGraphic(pixelsPerModule, darkColorHtmlHex, lightColorHtmlHex); + } + + } + + public static byte[] GetQRCode(string txt, ECCLevel eccLevel, int size) + { + using (var qrGen = new QRCodeGenerator()) + using (var qrCode = qrGen.CreateQrCode(txt, eccLevel)) + { + var qrBmp = new BitmapByteQRCode(qrCode); + return qrBmp.GetGraphic(size); + } + } + } +} diff --git a/QRCoder2/Renderers/PngByteQRCode.cs b/QRCoder2/Renderers/PngByteQRCode.cs new file mode 100644 index 00000000..78b69b77 --- /dev/null +++ b/QRCoder2/Renderers/PngByteQRCode.cs @@ -0,0 +1,338 @@ +using System; +using System.IO; +using System.IO.Compression; +using QRCoder2.Payloads; + +namespace QRCoder2.Renderers +{ + public sealed class PngByteQRCode : QRCodeRendererBase + { + public PngByteQRCode(QRCodeData data) : base(data) + { + } + + + /// + /// Creates a black & white PNG of the QR code, using 1-bit grayscale. + /// + public byte[] GetGraphic(int pixelsPerModule) + { + using (var png = new PngBuilder()) + { + var size = this.QrCodeData.ModuleMatrix.Count * pixelsPerModule; + png.WriteHeader(size, size, 1, PngBuilder.ColorType.Greyscale); + png.WriteScanlines(this.DrawScanlines(pixelsPerModule)); + png.WriteEnd(); + return png.GetBytes(); + } + } + + /// + /// Creates 2-color PNG of the QR code, using 1-bit indexed color. Accepts 3-byte RGB colors for normal images and 4-byte RGBA-colors for transparent images. + /// + public byte[] GetGraphic(int pixelsPerModule, byte[] darkColorRgba, byte[] lightColorRgba) + { + using (var png = new PngBuilder()) + { + var size = this.QrCodeData.ModuleMatrix.Count * pixelsPerModule; + png.WriteHeader(size, size, 1, PngBuilder.ColorType.Indexed); + png.WritePalette(darkColorRgba, lightColorRgba); + png.WriteScanlines(this.DrawScanlines(pixelsPerModule)); + png.WriteEnd(); + return png.GetBytes(); + } + } + + /// + /// Creates a bitmap where each pixel is represented by a single bit, dark = 0 and light = 1. + /// + private byte[] DrawScanlines(int pixelsPerModule) + { + var moduleMatrix = this.QrCodeData.ModuleMatrix; + var matrixSize = moduleMatrix.Count; + var bytesPerScanline = (matrixSize * pixelsPerModule + 7) / 8 + 1; // A monochrome scanline is one byte for filter type then one bit per pixel. + var scanlines = new byte[bytesPerScanline * matrixSize * pixelsPerModule]; + + for (var y = 0; y < matrixSize; y++) + { + var modules = moduleMatrix[y]; + var scanlineOffset = y * pixelsPerModule * bytesPerScanline; + + // Draw a scanline with the modules from the QR code. + for (var x = 0; x < matrixSize; x++) + { + if (modules[x]) + { + continue; + } + + var pixelIndex = x * pixelsPerModule; + var endIndex = pixelIndex + pixelsPerModule; + for (; pixelIndex < endIndex; pixelIndex++) + { + scanlines[scanlineOffset + 1 + pixelIndex / 8] |= (byte)(0x80 >> (pixelIndex % 8)); + } + } + + // Copy the scanline required number of times. + for (var copyCount = 1; copyCount < pixelsPerModule; copyCount++) + { + Array.Copy(scanlines, scanlineOffset, scanlines, scanlineOffset + copyCount * bytesPerScanline, bytesPerScanline); + } + } + + return scanlines; + } + + /// + /// Writes the chunks that make up a PNG file. + /// + /// + /// See https://www.w3.org/TR/2003/REC-PNG-20031110 and https://www.ietf.org/rfc/rfc1950.txt. + /// + private sealed class PngBuilder : IDisposable + { + private static readonly byte[] PngSignature = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; + + private static readonly uint[] CrcTable = { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, + 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, + 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D + }; + + // ReSharper disable InconsistentNaming + // Chunk types + private static readonly byte[] IHDR = { 73, 72, 68, 82 }; + + private static readonly byte[] IDAT = { 73, 68, 65, 84 }; + + private static readonly byte[] IEND = { 73, 69, 78, 68 }; + + private static readonly byte[] PLTE = { 80, 76, 84, 69 }; + + private static readonly byte[] tRNS = { 116, 82, 78, 83 }; + // ReSharper enable InconsistentNaming + + public enum ColorType : byte + { + Greyscale = 0, + Indexed = 3 + } + + private MemoryStream stream = new MemoryStream(); + + public void Dispose() + { + this.stream?.Dispose(); + this.stream = null; + } + + public byte[] GetBytes() + { + var bytes = this.stream.ToArray(); + + // Enumerate chunks in file and insert their CRC32 checksums. + var chunkOffset = PngSignature.Length; + while (chunkOffset < bytes.Length) + { + // Read length field. + var dataLength = (bytes[chunkOffset] << 24) | (bytes[chunkOffset + 1] << 16) | (bytes[chunkOffset + 2] << 8) | bytes[chunkOffset + 3]; + + // CRC is computed from type and data fields. + var crc = Crc32(bytes, chunkOffset + 4, dataLength + 4); + + // Write CRC to end of chunk. + var crcOffset = chunkOffset + 8 + dataLength; + bytes[crcOffset + 0] = (byte)(crc >> 24); + bytes[crcOffset + 1] = (byte)(crc >> 16); + bytes[crcOffset + 2] = (byte)(crc >> 8); + bytes[crcOffset + 3] = (byte)crc; + + // Seek to next chunk. + chunkOffset = crcOffset + 4; + } + + return bytes; + } + + /// + /// Writes the IHDR chunk. This must be the first chunk in the file. + /// + public void WriteHeader(int width, int height, byte bitDepth, ColorType colorType) + { + this.stream.Write(PngSignature, 0, PngSignature.Length); + this.WriteChunkStart(IHDR, 13); + + // Size. + this.WriteIntBigEndian((uint)width); + this.WriteIntBigEndian((uint)height); + + // Color. + this.stream.WriteByte(bitDepth); + this.stream.WriteByte((byte)colorType); + + // Constants. + this.stream.WriteByte(0); + this.stream.WriteByte(0); + this.stream.WriteByte(0); + + this.WriteChunkEnd(); + } + + /// + /// Writes the PLTE chunk, and also the tRNS chunk if necessary. Must come before the IDAT chunk. + /// + public void WritePalette(params byte[][] rgbaColors) + { + const int Red = 0, Green = 1, Blue = 2, Alpha = 3; + const byte Opaque = 255; + var hasAlpha = false; + + this.WriteChunkStart(PLTE, 3 * rgbaColors.Length); + foreach (var color in rgbaColors) + { + hasAlpha |= color.Length > Alpha && color[Alpha] < Opaque; + this.stream.WriteByte(color[Red]); + this.stream.WriteByte(color[Green]); + this.stream.WriteByte(color[Blue]); + } + this.WriteChunkEnd(); + + if (!hasAlpha) + { + return; + } + + this.WriteChunkStart(tRNS, rgbaColors.Length); + foreach (var color in rgbaColors) + { + this.stream.WriteByte(color.Length > Alpha ? color[Alpha] : Opaque); + } + this.WriteChunkEnd(); + } + + /// + /// Writes the IDAT chunk with the actual picture. + /// + public void WriteScanlines(byte[] scanlines) + { + using (var idatStream = new MemoryStream()) + { + Deflate(idatStream, scanlines); + + this.WriteChunkStart(IDAT, (int)(idatStream.Length + 6)); + + // Deflate header. + this.stream.WriteByte(0x78); // 8 Deflate algorithm, 7 max window size + this.stream.WriteByte(0x9C); // Check bits. + + // Compressed data. + idatStream.Position = 0; +#if NET35 + idatStream.WriteTo(this.stream); +#else + idatStream.CopyTo(this.stream); +#endif + // Deflate checksum. + var adler = Adler32(scanlines, 0, scanlines.Length); + this.WriteIntBigEndian(adler); + + this.WriteChunkEnd(); + } + } + + /// + /// Writes the IEND chunk. This must be the last chunk in the file. + /// + public void WriteEnd() + { + this.WriteChunkStart(IEND, 0); + this.WriteChunkEnd(); + } + + private void WriteChunkStart(byte[] type, int length) + { + this.WriteIntBigEndian((uint)length); + this.stream.Write(type, 0, 4); + } + + private void WriteChunkEnd() + { + // Reserves 4 bytes space for crc32 so GetBytes can add it later. + this.stream.SetLength(this.stream.Length + 4); + this.stream.Position += 4; + } + + private void WriteIntBigEndian(uint value) + { + this.stream.WriteByte((byte)(value >> 24)); + this.stream.WriteByte((byte)(value >> 16)); + this.stream.WriteByte((byte)(value >> 8)); + this.stream.WriteByte((byte)value); + } + + private static void Deflate(Stream output, byte[] bytes) + { + using (var deflateStream = new DeflateStream(output, CompressionMode.Compress, leaveOpen: true)) + { + deflateStream.Write(bytes, 0, bytes.Length); + } + } + + // Reference implementation from RFC 1950. Not optimized. + private static uint Adler32(byte[] data, int index, int length) + { + const uint Base = 65521; + uint s1 = 1, s2 = 0; + + var end = index + length; + for (var n = index; n < end; n++) + { + s1 = (s1 + data[n]) % Base; + s2 = (s2 + s1) % Base; + } + + return (s2 << 16) + s1; + } + + // Reference implementation from REC-PNG-20031110. Not optimized. + private static uint Crc32(byte[] data, int index, int length) + { + var c = 0xffffffff; + + var end = index + length; + for (var n = index; n < end; n++) + { + c = CrcTable[(c ^ data[n]) & 0xff] ^ (c >> 8); + } + + return c ^ 0xffffffff; + } + } + } + + public static class PngByteQRCodeHelper + { + public static byte[] GetQRCode(string plainText, int pixelsPerModule, byte[] darkColorRgba, byte[] lightColorRgba, ECCLevel eccLevel, bool forceUtf8 = false, bool utf8BOM = false, EciMode eciMode = EciMode.Default, int requestedVersion = -1) + { + using (var qrGenerator = new QRCodeGenerator()) + using (var qrCodeData = + qrGenerator.CreateQrCode(plainText, eccLevel, forceUtf8, utf8BOM, eciMode, requestedVersion)) + { + var qrCode = new PngByteQRCode(qrCodeData); + return qrCode.GetGraphic(pixelsPerModule, darkColorRgba, lightColorRgba); + } + } + + public static byte[] GetQRCode(string txt, ECCLevel eccLevel, int size) + { + using (var qrGen = new QRCodeGenerator()) + using (var qrCode = qrGen.CreateQrCode(txt, eccLevel)) + { + var qrPng = new PngByteQRCode(qrCode); + return qrPng.GetGraphic(size); + } + } + } +} diff --git a/QRCoder2/Renderers/RendererBase.cs b/QRCoder2/Renderers/RendererBase.cs new file mode 100644 index 00000000..62be13a0 --- /dev/null +++ b/QRCoder2/Renderers/RendererBase.cs @@ -0,0 +1,12 @@ +namespace QRCoder2.Renderers +{ + public abstract class QRCodeRendererBase + { + protected QRCodeData QrCodeData { get; set; } + + protected QRCodeRendererBase(QRCodeData data) + { + this.QrCodeData = data; + } + } +} \ No newline at end of file