diff --git a/.github/workflows/wf-build-release-ci.yml b/.github/workflows/wf-build-release-ci.yml index 124e9b18..8bf0c577 100644 --- a/.github/workflows/wf-build-release-ci.yml +++ b/.github/workflows/wf-build-release-ci.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: jobs: build: - runs-on: windows-2019 + runs-on: windows-latest env: ACTIONS_ALLOW_UNSECURE_COMMANDS: true steps: @@ -33,17 +33,17 @@ jobs: uses: actions/upload-artifact@v4 with: name: Compiled project - path: D:\a\qrcoder\qrcoder + path: ${{ github.workspace }} test: needs: build - runs-on: windows-2019 + runs-on: windows-latest steps: - name: Download artifacts uses: actions/download-artifact@v4 with: name: Compiled project - path: D:\a\qrcoder\qrcoder + path: ${{ github.workspace }} - name: Install additional .NET SDKs uses: actions/setup-dotnet@v4 @@ -139,7 +139,7 @@ jobs: pack-push-ci: needs: test - runs-on: windows-2019 + runs-on: windows-latest env: GH_PKG_SEC: ${{ secrets.GITHUB_TOKEN }} steps: @@ -147,7 +147,7 @@ jobs: uses: actions/download-artifact@v4 with: name: Compiled project - path: D:\a\qrcoder\qrcoder + path: ${{ github.workspace }} - name: Install additional .NET SDKs uses: actions/setup-dotnet@v4 @@ -193,7 +193,7 @@ jobs: clean: needs: [build, test, pack-push-ci] if: always() - runs-on: windows-2019 + runs-on: windows-latest steps: - name: Delete artifacts uses: GeekyEggo/delete-artifact@v5 diff --git a/.github/workflows/wf-build-release.yml b/.github/workflows/wf-build-release.yml index 1b898d11..ad7a5f21 100644 --- a/.github/workflows/wf-build-release.yml +++ b/.github/workflows/wf-build-release.yml @@ -7,7 +7,7 @@ on: required: true jobs: build: - runs-on: windows-2019 + runs-on: windows-latest env: ACTIONS_ALLOW_UNSECURE_COMMANDS: true steps: @@ -34,17 +34,17 @@ jobs: uses: actions/upload-artifact@v4 with: name: Compiled project - path: D:\a\qrcoder\qrcoder + path: ${{ github.workspace }} test: needs: build - runs-on: windows-2019 + runs-on: windows-latest steps: - name: Download artifacts uses: actions/download-artifact@v4 with: name: Compiled project - path: D:\a\qrcoder\qrcoder + path: ${{ github.workspace }} - name: Install additional .NET SDKs uses: actions/setup-dotnet@v4 @@ -139,7 +139,7 @@ jobs: pack-push-release: needs: test - runs-on: windows-2019 + runs-on: windows-latest env: GH_PKG_SEC: ${{ secrets.GITHUB_TOKEN }} steps: @@ -147,7 +147,7 @@ jobs: uses: actions/download-artifact@v4 with: name: Compiled project - path: D:\a\qrcoder\qrcoder + path: ${{ github.workspace }} - name: Install additional .NET SDKs uses: actions/setup-dotnet@v4 @@ -187,7 +187,7 @@ jobs: clean: needs: [build, test, pack-push-release] if: always() - runs-on: windows-2019 + runs-on: windows-latest steps: - name: Delete artifacts uses: GeekyEggo/delete-artifact@v5 diff --git a/.github/workflows/wf-build-test.yml b/.github/workflows/wf-build-test.yml index eca43ce8..64cc56d3 100644 --- a/.github/workflows/wf-build-test.yml +++ b/.github/workflows/wf-build-test.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: jobs: build: - runs-on: windows-2019 + runs-on: windows-latest env: ACTIONS_ALLOW_UNSECURE_COMMANDS: true steps: @@ -33,17 +33,17 @@ jobs: uses: actions/upload-artifact@v4 with: name: Compiled project - path: D:\a\qrcoder\qrcoder + path: ${{ github.workspace }} test: needs: build - runs-on: windows-2019 + runs-on: windows-latest steps: - name: Download artifacts uses: actions/download-artifact@v4 with: name: Compiled project - path: D:\a\qrcoder\qrcoder + path: ${{ github.workspace }} - name: Install additional .NET SDKs uses: actions/setup-dotnet@v4 @@ -98,7 +98,7 @@ jobs: clean: needs: [build, test] if: always() - runs-on: windows-2019 + runs-on: windows-latest steps: - name: Delete artifacts uses: GeekyEggo/delete-artifact@v5 diff --git a/.github/workflows/wf-verify-formatting.yml b/.github/workflows/wf-verify-formatting.yml index acc21c54..3dbb788e 100644 --- a/.github/workflows/wf-verify-formatting.yml +++ b/.github/workflows/wf-verify-formatting.yml @@ -7,7 +7,9 @@ on: jobs: format: - runs-on: windows-2019 + runs-on: ubuntu-latest + env: + EnableWindowsTargeting: true steps: - uses: actions/checkout@v4 name: Checkout Code @@ -15,11 +17,7 @@ jobs: - name: Install additional .NET SDKs uses: actions/setup-dotnet@v4 with: - dotnet-version: | - 2.0.x - 5.0.x - 6.0.x - 8.0.x + dotnet-version: 8.0.x - name: Restore NuGet Packages run: dotnet restore diff --git a/QRCoder/QRCodeData.cs b/QRCoder/QRCodeData.cs index 0c27ff55..7fb8a68b 100644 --- a/QRCoder/QRCodeData.cs +++ b/QRCoder/QRCodeData.cs @@ -194,10 +194,12 @@ public void SaveRawData(string filePath, Compression compressMode) /// /// Gets the number of modules per side from the specified version. /// - /// The version of the QR code. + /// The version of the QR code (1 to 40, or -1 to -4 for Micro QR codes). /// Returns the number of modules per side. private static int ModulesPerSideFromVersion(int version) - => 21 + (version - 1) * 4; + => version > 0 + ? 21 + (version - 1) * 4 + : 11 + (-version - 1) * 2; /// /// Releases all resources used by the . diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index 8dcf7928..5847082c 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -150,6 +150,85 @@ public static QRCodeData GenerateQrCode(string plainText, ECCLevel eccLevel, boo return GenerateQrCode(completeBitArray, eccLevel, version); } + /// + /// Calculates the Micro QR code data which then 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 + /// Set fixed Micro QR code target version; must be -1 to -4 representing M1 to M4, or 0 for default. + /// 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 GenerateMicroQrCode(string plainText, ECCLevel eccLevel = ECCLevel.Default, int requestedVersion = 0) + { + if (requestedVersion is < -4 or > 0) + throw new ArgumentOutOfRangeException(nameof(requestedVersion), requestedVersion, "Requested version must be -1 to -4 representing M1 to M4, or 0 for default."); + ValidateECCLevel(eccLevel); + if (eccLevel == ECCLevel.H) + throw new ArgumentOutOfRangeException(nameof(eccLevel), eccLevel, "Micro QR codes does not support error correction level H."); + if (eccLevel == ECCLevel.Q && requestedVersion != -4) + throw new ArgumentOutOfRangeException(nameof(eccLevel), eccLevel, "Micro QR codes only supports error correction level Q for version M4."); + if (eccLevel != ECCLevel.Default && requestedVersion == -1) + throw new ArgumentOutOfRangeException(nameof(eccLevel), eccLevel, "Please specify ECCLevel.Default for version M1."); + if (plainText == null) + throw new ArgumentNullException(nameof(plainText)); + + var encoding = GetEncodingFromPlaintext(plainText, false); + var codedText = PlainTextToBinary(plainText, encoding, EciMode.Default, false, false); + var dataInputLength = GetDataLength(encoding, plainText, codedText, false); + int version = requestedVersion; + int minVersion = CapacityTables.CalculateMinimumMicroVersion(dataInputLength, encoding, eccLevel); + + if (version == 0) + { + version = minVersion; + } + else + { + //Version was passed as fixed version via parameter. Thus let's check if chosen version is valid. + if (minVersion < version) + { + var matchedEncoding = CapacityTables.GetVersionInfo(version).Details + .First(x => (x.ErrorCorrectionLevel == eccLevel || (eccLevel == ECCLevel.Default && x.ErrorCorrectionLevel == ECCLevel.L))) + .CapacityDict.TryGetValue(encoding, out var maxSizeByte); + if (!matchedEncoding) + throw new ArgumentOutOfRangeException(nameof(encoding), encoding, "Encoding not supported for this version."); + throw new QRCoder.Exceptions.DataTooLongException(eccLevel.ToString(), encoding.ToString(), version, maxSizeByte); + } + } + if (version < -1 && eccLevel == ECCLevel.Default) + eccLevel = ECCLevel.L; + + var modeIndicatorLength = -version - 1; // 0 for M1, 1 for M2, 2 for M3, 3 for M4 + var countIndicatorLength = GetCountIndicatorLength(version, encoding); + var completeBitArrayLength = modeIndicatorLength + countIndicatorLength + codedText.Length; + + var completeBitArray = new BitArray(completeBitArrayLength); + + // write mode indicator + var completeBitArrayIndex = 0; + if (version < 0) + { + var encodingValue = + encoding == EncodingMode.Numeric ? 0 : + encoding == EncodingMode.Alphanumeric ? 1 : + encoding == EncodingMode.Byte ? 2 : 3; + completeBitArrayIndex = DecToBin(encodingValue, modeIndicatorLength, completeBitArray, completeBitArrayIndex); + } + else + { + completeBitArrayIndex = DecToBin((int)encoding, 4, completeBitArray, completeBitArrayIndex); + } + // write count indicator + completeBitArrayIndex = DecToBin(dataInputLength, countIndicatorLength, completeBitArray, completeBitArrayIndex); + // write data + for (int i = 0; i < codedText.Length; i++) + { + completeBitArray[completeBitArrayIndex++] = codedText[i]; + } + + return GenerateQrCode(completeBitArray, eccLevel, version); + } + /// /// Calculates the QR code data which than can be used in one of the rendering classes to generate a graphical representation. /// @@ -223,7 +302,7 @@ private static QRCodeData GenerateQrCode(BitArray bitArray, ECCLevel eccLevel, i // fills the bit array with a repeating pattern to reach the required length void PadData() { - var dataLength = eccInfo.TotalDataCodewords * 8; + var dataLength = eccInfo.TotalDataBits; var lengthDiff = dataLength - bitArray.Length; if (lengthDiff > 0) { @@ -231,11 +310,23 @@ void PadData() var index = bitArray.Length; // extend bit array to required length bitArray.Length = dataLength; - // pad with 4 zeros (or less if lengthDiff < 4) - index += Math.Min(lengthDiff, 4); + // compute padding length + int padLength = version switch + { + > 0 => 4, + -1 => 3, + -2 => 5, + -3 => 7, + _ => 9 + }; + // pad with zeros (or less if not enough room) + index += padLength; // pad to nearest 8 bit boundary if ((uint)index % 8 != 0) index += 8 - (int)((uint)index % 8); + // for m1 and m3 sizes don't fill last 4 bits with repeating pattern + if (version == -1 || version == -3) + dataLength -= 4; // pad with repeating pattern var repeatingPatternIndex = 0; while (index < dataLength) @@ -264,6 +355,7 @@ List CalculateECCBlocks() void AddCodeWordBlocks(int blockNum, int blocksInGroup, int codewordsInGroup, int offset2, int count, Polynom generatorPolynom) { var groupLength = codewordsInGroup * 8; + groupLength = groupLength > count ? count : groupLength; for (var i = 0; i < blocksInGroup; i++) { var eccWordList = CalculateECCWords(bitArray, offset2, groupLength, eccInfo, generatorPolynom); @@ -277,7 +369,13 @@ void AddCodeWordBlocks(int blockNum, int blocksInGroup, int codewordsInGroup, in int CalculateInterleavedLength() { var length = 0; - for (var i = 0; i < Math.Max(eccInfo.CodewordsInGroup1, eccInfo.CodewordsInGroup2); i++) + var codewords = Math.Max(eccInfo.CodewordsInGroup1, eccInfo.CodewordsInGroup2); + if (version == -1 || version == -3) + { + codewords--; + length += 4; + } + for (var i = 0; i < codewords; i++) { foreach (var codeBlock in codeWordWithECC) if ((uint)codeBlock.CodeWordsLength / 8 > i) @@ -298,6 +396,9 @@ BitArray InterleaveData() { var data = new BitArray(interleavedLength); int pos = 0; + int codewords = Math.Max(eccInfo.CodewordsInGroup1, eccInfo.CodewordsInGroup2); + if (version == -1 || version == -3) + codewords--; for (var i = 0; i < Math.Max(eccInfo.CodewordsInGroup1, eccInfo.CodewordsInGroup2); i++) { foreach (var codeBlock in codeWordWithECC) @@ -306,6 +407,10 @@ BitArray InterleaveData() pos = bitArray.CopyTo(data, (int)((uint)i * 8) + codeBlock.CodeWordsOffset, pos, 8); } } + if (version == -1 || version == -3) + { + pos = bitArray.CopyTo(data, (int)((uint)codewords * 8) + codeWordWithECC[0].CodeWordsOffset, pos, 4); + } for (var i = 0; i < eccInfo.ECCPerBlock; i++) { foreach (var codeBlock in codeWordWithECC) @@ -325,14 +430,14 @@ QRCodeData PlaceModules() using (var blockedModules = new ModulePlacer.BlockedModules(size)) { ModulePlacer.PlaceFinderPatterns(qr, blockedModules); - ModulePlacer.ReserveSeperatorAreas(size, blockedModules); + ModulePlacer.ReserveSeperatorAreas(version, size, blockedModules); ModulePlacer.PlaceAlignmentPatterns(qr, AlignmentPatterns.FromVersion(version).PatternPositions, blockedModules); ModulePlacer.PlaceTimingPatterns(qr, blockedModules); ModulePlacer.PlaceDarkModule(qr, version, blockedModules); ModulePlacer.ReserveVersionAreas(size, version, blockedModules); ModulePlacer.PlaceDataWords(qr, interleavedData, blockedModules); var maskVersion = ModulePlacer.MaskCode(qr, version, blockedModules, eccLevel); - GetFormatString(tempBitArray, eccLevel, maskVersion); + GetFormatString(tempBitArray, version, eccLevel, maskVersion); ModulePlacer.PlaceFormat(qr, tempBitArray, true); } @@ -348,19 +453,25 @@ QRCodeData PlaceModules() private static readonly BitArray _getFormatGenerator = new BitArray(new bool[] { true, false, true, false, false, true, true, false, true, true, true }); private static readonly BitArray _getFormatMask = new BitArray(new bool[] { true, false, true, false, true, false, false, false, false, false, true, false, false, true, false }); + private static readonly BitArray _getFormatMicroMask = new BitArray(new bool[] { true, false, false, false, true, false, false, false, true, false, false, false, true, false, true }); + /// /// Generates a BitArray containing the format string for a QR code based on the error correction level and mask pattern version. /// The format string includes the error correction level, mask pattern version, and error correction coding. /// - /// The to write to, or null to create a new one. + /// The to write to, or null to create a new one. + /// The version number of the QR Code (1-40, or -1 to -4 for Micro QR codes). /// The error correction level to be encoded in the format string. /// The mask pattern version to be encoded in the format string. /// A BitArray containing the 15-bit format string used in QR code generation. - private static void GetFormatString(BitArray fStrEcc, ECCLevel level, int maskVersion) + private static void GetFormatString(BitArray fStrEcc, int version, ECCLevel level, int maskVersion) { fStrEcc.Length = 15; fStrEcc.SetAll(false); - WriteEccLevelAndVersion(); + if (version < 0) + WriteMicroEccLevelAndVersion(); + else + WriteEccLevelAndVersion(); // Apply the format generator polynomial to add error correction to the format string. int index = 0; @@ -379,10 +490,13 @@ private static void GetFormatString(BitArray fStrEcc, ECCLevel level, int maskVe // Prefix the error correction bits with the ECC level and version number. fStrEcc.Length = 10 + 5; ShiftAwayFromBit0(fStrEcc, (10 - count) + 5); - WriteEccLevelAndVersion(); + if (version < 0) + WriteMicroEccLevelAndVersion(); + else + WriteEccLevelAndVersion(); // XOR the format string with a predefined mask to add robustness against errors. - fStrEcc.Xor(_getFormatMask); + fStrEcc.Xor(version < 0 ? _getFormatMicroMask : _getFormatMask); void WriteEccLevelAndVersion() { @@ -405,6 +519,49 @@ void WriteEccLevelAndVersion() // Insert the 3-bit mask version directly after the error correction level bits. DecToBin(maskVersion, 3, fStrEcc, 2); } + + void WriteMicroEccLevelAndVersion() + { + switch (version) + { + case -1: // M1 + break; + case -2: // M2 + fStrEcc[level == ECCLevel.L ? 2 : 1] = true; // 001 for L and 010 for M + break; + case -3: // M3 + if (level == ECCLevel.L) + { + fStrEcc[1] = true; // 011 for L + fStrEcc[2] = true; + } + else + fStrEcc[0] = true; // 100 for M + break; + default: // M4 + fStrEcc[0] = true; + if (level == ECCLevel.L) // 101 for L + fStrEcc[2] = true; + else if (level == ECCLevel.M) // 110 for M + fStrEcc[1] = true; + else // 111 for Q + { + fStrEcc[1] = true; + fStrEcc[2] = true; + } + break; + } + + // Insert the 2-bit mask version directly after the version / error correction level bits. + int microMaskVersion = maskVersion switch + { + 1 => 0, + 4 => 1, + 6 => 2, + _ => 3 + }; + DecToBin(microMaskVersion, 2, fStrEcc, 3); + } } #if !NETFRAMEWORK || NET45_OR_GREATER @@ -583,17 +740,52 @@ private static bool IsInRange(char c, char min, char max) => (uint)(c - min) <= (uint)(max - min); /// - /// Calculates the message polynomial from a bit array which represents the encoded data. + /// Converts a segment of a BitArray representing QR code data into a polynomial, + /// padding the final byte if necessary (for Micro QR variants like M1 or M3). /// - /// A polynomial representation of the message. - private static Polynom CalculateMessagePolynom(BitArray bitArray, int offset, int count) + /// The full bit array representing encoded QR code data. + /// Starting position in the bit array. + /// Total number of bits to convert into codewords. + /// A polynomial representing the message codewords. + private static Polynom CalculateMessagePolynom(BitArray bitArray, int offset, int bitCount) { - var messagePol = new Polynom(count /= 8); - for (var i = count - 1; i >= 0; i--) + // Calculate how many full 8-bit codewords are present + var fullBytes = bitCount / 8; + + // Determine if there is a remaining partial byte (e.g., 4 bits for Micro QR M1 and M3 versions) + var remainingBits = bitCount % 8; + + if (remainingBits > 0) { - messagePol.Add(new PolynomItem(BinToDec(bitArray, offset, 8), i)); + // Pad the last byte with zero bits to make it a full 8-bit codeword + var addlBits = 8 - remainingBits; + var minBitArrayLength = offset + bitCount + addlBits; + + // Extend BitArray length if needed to fit the padded bits + if (bitArray.Length < minBitArrayLength) + bitArray.Length = minBitArrayLength; + + // Pad the remaining bits with false (0) values + for (int i = 0; i < addlBits; i++) + bitArray[offset + bitCount + i] = false; + } + + // Total number of codewords (includes extra for partial byte if present) + var polynomLength = fullBytes + (remainingBits > 0 ? 1 : 0); + + // Initialize the polynomial + var messagePol = new Polynom(polynomLength); + + // Exponent for polynomial terms starts from highest degree + int exponent = polynomLength - 1; + + // Convert each 8-bit segment into a decimal value and add it to the polynomial + for (int i = 0; i < polynomLength; i++) + { + messagePol.Add(new PolynomItem(BinToDec(bitArray, offset, 8), exponent--)); offset += 8; } + return messagePol; } @@ -670,7 +862,33 @@ private static int DecToBin(int decNum, int bits, BitArray bitList, int index) private static int GetCountIndicatorLength(int version, EncodingMode encMode) { // Different versions and encoding modes require different lengths of bits to represent the character count efficiently - if (version < 10) + if (version == -1) + { + return 3; + } + else if (version == -2) + { + return encMode == EncodingMode.Numeric ? 4 : 3; + } + else if (version == -3) + { + if (encMode == EncodingMode.Numeric) + return 5; + else if (encMode == EncodingMode.Kanji) + return 3; + else + return 4; + } + else if (version == -4) + { + if (encMode == EncodingMode.Numeric) + return 6; + else if (encMode == EncodingMode.Kanji) + return 4; + else + return 5; + } + else if (version < 10) { if (encMode == EncodingMode.Numeric) return 10; diff --git a/QRCoder/QRCodeGenerator/AlignmentPatterns.cs b/QRCoder/QRCodeGenerator/AlignmentPatterns.cs index fe879623..deb1abb5 100644 --- a/QRCoder/QRCodeGenerator/AlignmentPatterns.cs +++ b/QRCoder/QRCodeGenerator/AlignmentPatterns.cs @@ -28,7 +28,7 @@ private static class AlignmentPatterns private static Dictionary CreateAlignmentPatternTable() { var alignmentPatternBaseValues = new int[] { 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 }; - var localAlignmentPatternTable = new Dictionary(40); + var localAlignmentPatternTable = new Dictionary(40 + 4); for (var i = 0; i < (7 * 40); i += 7) { @@ -56,6 +56,14 @@ private static Dictionary CreateAlignmentPatternTable() PatternPositions = points }); } + + // Micro QR codes do not have alignment patterns. + var emptyPointList = new List(); + localAlignmentPatternTable.Add(-1, new AlignmentPattern { Version = -1, PatternPositions = emptyPointList }); + localAlignmentPatternTable.Add(-2, new AlignmentPattern { Version = -2, PatternPositions = emptyPointList }); + localAlignmentPatternTable.Add(-3, new AlignmentPattern { Version = -3, PatternPositions = emptyPointList }); + localAlignmentPatternTable.Add(-4, new AlignmentPattern { Version = -4, PatternPositions = emptyPointList }); + return localAlignmentPatternTable; } } diff --git a/QRCoder/QRCodeGenerator/CapacityTables.cs b/QRCoder/QRCodeGenerator/CapacityTables.cs index 59f0d2c9..df885273 100644 --- a/QRCoder/QRCodeGenerator/CapacityTables.cs +++ b/QRCoder/QRCodeGenerator/CapacityTables.cs @@ -20,6 +20,7 @@ private static class CapacityTables /// The index in the capacity table corresponds to one less than the version number. /// private static readonly List _capacityTable = CreateCapacityTable(_capacityBaseValues); + private static readonly List _microCapacityTable = CreateMicroCapacityTable(); /// /// A table containing the error correction capacities and data codeword information for different combinations of QR code versions and error correction levels. @@ -29,7 +30,7 @@ private static class CapacityTables /// /// Retrieves the error correction information for a specific QR code version and error correction level. /// - /// The version of the QR code (1 to 40). + /// The version of the QR code (1 to 40, or -1 to -4 for M1 to M4). /// The desired error correction level (L, M, Q, or H). Do not supply . /// /// An object containing the total number of data codewords, ECC per block, @@ -44,25 +45,25 @@ public static ECCInfo GetEccInfo(int version, ECCLevel eccLevel) /// and encoding mode (Numeric, Alphanumeric, Byte, Kanji), indicating the maximum number of characters /// that can be stored in a QR code of the specified version under each configuration. /// - /// The version of the QR code (1 to 40). + /// The version of the QR code (1 to 40, or -1 to -4 for M1 to M4). /// /// A object containing data capacity details for all error correction levels /// and encoding modes for the specified version. /// public static VersionInfo GetVersionInfo(int version) - => _capacityTable[version - 1]; + => version < 0 ? _microCapacityTable[-version - 1] : _capacityTable[version - 1]; /// /// Retrieves the number of remainder bits required for a specific QR code version. /// Remainder bits are added to the final bit stream to ensure proper alignment with byte boundaries, /// as required by the QR code specification. /// - /// The version of the QR code (1 to 40). + /// The version of the QR code (1 to 40, or -1 to -4 for M1 to M4). /// /// The number of remainder bits (0 to 7) that must be appended to the encoded bit stream. /// public static int GetRemainderBits(int version) - => _remainderBits[version - 1]; + => version < 0 ? 0 : _remainderBits[version - 1]; /// /// Determines the minimum QR code version required to encode a given amount of data with a specific encoding mode and error correction level. @@ -77,6 +78,7 @@ public static int GetRemainderBits(int version) /// public static int CalculateMinimumVersion(int length, EncodingMode encMode, ECCLevel eccLevel) { + // only iterates through non-micro QR codes // capacity table is already sorted by version number ascending, so the smallest version that can hold the data is the first one found foreach (var x in _capacityTable) { @@ -99,6 +101,50 @@ public static int CalculateMinimumVersion(int length, EncodingMode encMode, ECCL throw new QRCoder.Exceptions.DataTooLongException(eccLevel.ToString(), encMode.ToString(), maxSizeByte); } + + /// + /// Determines the minimum Micro QR code version required to encode a given amount of data with a specific encoding mode and error correction level. + /// If no suitable version is found, it throws an exception indicating that the data length exceeds the maximum capacity for the given settings. + /// + /// The length of the data to be encoded. + /// The encoding mode (e.g., Numeric, Alphanumeric, Byte). + /// The error correction level (e.g., Default, Low, Medium, Quartile, High). + /// The minimum version of the QR code (-1 to -4) that can accommodate the given data and settings. + /// + /// Thrown when the data length exceeds the maximum capacity for the specified encoding mode and error correction level. + /// + public static int CalculateMinimumMicroVersion(int length, EncodingMode encMode, ECCLevel eccLevel) + { + // only iterates through non-micro QR codes + // capacity table is already sorted by version number ascending, so the smallest version that can hold the data is the first one found + foreach (var x in _microCapacityTable) + { + // find the requested ECC level and encoding mode in the capacity table + foreach (var y in x.Details) + { + // Use ECC level L for Micro QR Code versions 2, 3 and 4 when Default is specified + if (y.ErrorCorrectionLevel == eccLevel || (eccLevel == ECCLevel.Default && y.ErrorCorrectionLevel == ECCLevel.L)) + { + // Not all versions support all encoding modes, so check if the encoding mode is supported + if (y.CapacityDict.TryGetValue(encMode, out int maxLength) && maxLength >= length) + { + // if the capacity of the current version is enough, return the version number + return x.Version; + } + } + } + } + + // if no version was found, throw an exception + var maxSizeByte = _microCapacityTable + .SelectMany(x => x.Details) + .Where(y => (y.ErrorCorrectionLevel == eccLevel || (eccLevel == ECCLevel.Default && y.ErrorCorrectionLevel == ECCLevel.L)) + && y.CapacityDict.ContainsKey(encMode)) + .Max(y => y.CapacityDict[encMode]); + + throw new QRCoder.Exceptions.DataTooLongException(eccLevel.ToString(), encMode.ToString(), maxSizeByte); + } + /// /// Generates a table containing the error correction capacities and data codeword information for different QR code versions and error correction levels. /// This table is essential for determining how much data can be encoded in a QR code of a specific version and ECC level, @@ -107,7 +153,7 @@ public static int CalculateMinimumVersion(int length, EncodingMode encMode, ECCL /// A list of ECCInfo structures, each representing the ECC data and capacities for different combinations of QR code versions and ECC levels. private static List CreateCapacityECCTable(int[] capacityECCBaseValues) { - var localCapacityECCTable = new List(160); + var localCapacityECCTable = new List(160 + 8); for (var i = 0; i < (4 * 6 * 40); i += (4 * 6)) { localCapacityECCTable.AddRange(new[] @@ -153,6 +199,31 @@ private static List CreateCapacityECCTable(int[] capacityECCBaseValues) ) }); } + + localCapacityECCTable.AddRange(new ECCInfo[] + { + // Micro QR Code Version M1 - only supports ECCLevel.Default (none) + new ECCInfo( + version: -1, + errorCorrectionLevel: ECCLevel.Default, + totalDataCodewords: 3, + totalDataBits: 20, + eccPerBlock: 2), + + // Micro QR Code Version M2 + new ECCInfo(-2, ECCLevel.L, 5, 40, 5), + new ECCInfo(-2, ECCLevel.M, 4, 32, 6), + + // Micro QR Code Version M3 + new ECCInfo(-3, ECCLevel.L, 11, 84, 6), + new ECCInfo(-3, ECCLevel.M, 9, 68, 8), + + // Micro QR Code Version M4 + new ECCInfo(-4, ECCLevel.L, 16, 128, 8), + new ECCInfo(-4, ECCLevel.M, 14, 112, 10), + new ECCInfo(-4, ECCLevel.Q, 10, 80, 14), + }); + return localCapacityECCTable; } @@ -212,5 +283,98 @@ private static List CreateCapacityTable(int[] capacityBaseValues) } return localCapacityTable; } + + /// + private static List CreateMicroCapacityTable() + { + var tbl = new List(4); + + var m1details = new List(1) + { + new VersionInfoDetails( + ECCLevel.Default, // none + new Dictionary(1) { + { EncodingMode.Numeric, 5 }, + } + ) + }; + tbl.Add(new VersionInfo(-1, m1details)); + + var m2details = new List(2) + { + new VersionInfoDetails( + ECCLevel.L, + new Dictionary(2) { + { EncodingMode.Numeric, 10 }, + { EncodingMode.Alphanumeric, 6 }, + } + ), + new VersionInfoDetails( + ECCLevel.M, + new Dictionary(2) { + { EncodingMode.Numeric, 8 }, + { EncodingMode.Alphanumeric, 5 }, + } + ), + }; + tbl.Add(new VersionInfo(-2, m2details)); + + var m3details = new List(2) + { + new VersionInfoDetails( + ECCLevel.L, + new Dictionary(4) { + { EncodingMode.Numeric, 23 }, + { EncodingMode.Alphanumeric, 14 }, + { EncodingMode.Byte, 9 }, + { EncodingMode.Kanji, 6 }, + } + ), + new VersionInfoDetails( + ECCLevel.M, + new Dictionary(4) { + { EncodingMode.Numeric, 18 }, + { EncodingMode.Alphanumeric, 11 }, + { EncodingMode.Byte, 7 }, + { EncodingMode.Kanji, 4 }, + } + ), + }; + tbl.Add(new VersionInfo(-3, m3details)); + + var m4details = new List(3) + { + new VersionInfoDetails( + ECCLevel.L, + new Dictionary(4) { + { EncodingMode.Numeric, 35 }, + { EncodingMode.Alphanumeric, 21 }, + { EncodingMode.Byte, 15 }, + { EncodingMode.Kanji, 9 }, + } + ), + new VersionInfoDetails( + ECCLevel.M, + new Dictionary(4) { + { EncodingMode.Numeric, 30 }, + { EncodingMode.Alphanumeric, 18 }, + { EncodingMode.Byte, 13 }, + { EncodingMode.Kanji, 8 }, + } + ), + new VersionInfoDetails( + ECCLevel.Q, + new Dictionary(4) { + { EncodingMode.Numeric, 21 }, + { EncodingMode.Alphanumeric, 13 }, + { EncodingMode.Byte, 9 }, + { EncodingMode.Kanji, 5 }, + } + ), + }; + tbl.Add(new VersionInfo(-4, m4details)); + + return tbl; + } } } diff --git a/QRCoder/QRCodeGenerator/ECCInfo.cs b/QRCoder/QRCodeGenerator/ECCInfo.cs index 919288ca..9e8b787e 100644 --- a/QRCoder/QRCodeGenerator/ECCInfo.cs +++ b/QRCoder/QRCodeGenerator/ECCInfo.cs @@ -24,6 +24,7 @@ public ECCInfo(int version, ECCLevel errorCorrectionLevel, int totalDataCodeword Version = version; ErrorCorrectionLevel = errorCorrectionLevel; TotalDataCodewords = totalDataCodewords; + TotalDataBits = totalDataCodewords * 8; ECCPerBlock = eccPerBlock; BlocksInGroup1 = blocksInGroup1; CodewordsInGroup1 = codewordsInGroup1; @@ -31,6 +32,27 @@ public ECCInfo(int version, ECCLevel errorCorrectionLevel, int totalDataCodeword CodewordsInGroup2 = codewordsInGroup2; } + /// + /// Initializes a new instance of the ECCInfo struct with specified properties for Micro QR codes. + /// + /// The version number of the QR code. + /// The error correction level used in the QR code. + /// The total number of data codewords for this version and error correction level. + /// The total number of data bits for this version and error correction level. + /// The number of error correction codewords per block. + public ECCInfo(int version, ECCLevel errorCorrectionLevel, int totalDataCodewords, int totalDataBits, int eccPerBlock) + { + Version = version; + ErrorCorrectionLevel = errorCorrectionLevel; + TotalDataCodewords = totalDataCodewords; + TotalDataBits = totalDataBits; + ECCPerBlock = eccPerBlock; + BlocksInGroup1 = 1; + CodewordsInGroup1 = totalDataCodewords; + BlocksInGroup2 = 0; + CodewordsInGroup2 = 0; + } + /// /// Gets the version number of the QR code. /// @@ -46,6 +68,11 @@ public ECCInfo(int version, ECCLevel errorCorrectionLevel, int totalDataCodeword /// public int TotalDataCodewords { get; } + /// + /// Gets the total number of data codewords for this version and error correction level. + /// + public int TotalDataBits { get; } + /// /// Gets the number of error correction codewords per block. /// diff --git a/QRCoder/QRCodeGenerator/ModulePlacer.MaskPattern.cs b/QRCoder/QRCodeGenerator/ModulePlacer.MaskPattern.cs index 4b46dac6..3df023a3 100644 --- a/QRCoder/QRCodeGenerator/ModulePlacer.MaskPattern.cs +++ b/QRCoder/QRCodeGenerator/ModulePlacer.MaskPattern.cs @@ -78,6 +78,28 @@ public static bool Pattern7(int x, int y) public static bool Pattern8(int x, int y) => (((x + y) % 2) + ((x * y) % 3)) % 2 == 0; + /// + /// Calculates a penalty score for a Micro QR code to evaluate the effectiveness of a mask pattern. + /// A lower score indicates a QR code that is easier for decoders to read accurately. + /// + /// The QR code data structure to be evaluated. + /// The total penalty score of the QR code. + public static int ScoreMicro(QRCodeData qrCode) + { + int size = qrCode.ModuleMatrix.Count; + int sum1 = 0; + int sum2 = 0; + for (int i = 1; i < size; i++) + { + if (qrCode.ModuleMatrix[size - 1][i]) + sum1++; + if (qrCode.ModuleMatrix[i][size - 1]) + sum2++; + } + int total = sum1 < sum2 ? sum1 * 16 + sum2 : sum2 * 16 + sum1; + return -total; // negate so that lower is better + } + /// /// Calculates a penalty score for a QR code to evaluate the effectiveness of a mask pattern. /// A lower score indicates a QR code that is easier for decoders to read accurately. diff --git a/QRCoder/QRCodeGenerator/ModulePlacer.cs b/QRCoder/QRCodeGenerator/ModulePlacer.cs index f452e6b3..774d13aa 100644 --- a/QRCoder/QRCodeGenerator/ModulePlacer.cs +++ b/QRCoder/QRCodeGenerator/ModulePlacer.cs @@ -35,11 +35,15 @@ public static void PlaceVersion(QRCodeData qrCode, BitArray versionStr, bool off /// /// The QR code data structure to modify. /// The bit array containing the format information. + /// Specifies whether an offset should be applied. public static void PlaceFormat(QRCodeData qrCode, BitArray formatStr, bool offset) { + var isMicro = qrCode.Version < 0; // Negative versions indicate Micro QR codes. var offsetValue = offset ? 4 : 0; var size = qrCode.ModuleMatrix.Count - offsetValue - offsetValue; + // Standard QR Code Format Positions: + // // { x1, y1, x2, y2 } i // =============================== // { 8, 0, size - 1, 8 }, // 0 @@ -58,16 +62,53 @@ public static void PlaceFormat(QRCodeData qrCode, BitArray formatStr, bool offse // { 1, 8, 8, size - 2 }, // 13 // { 0, 8, 8, size - 1 } }; // 14 + // Micro QR Code Format Positions: + // + // i { x1, y1 } + // =============== + // 0 { 8, 1 } + // 1 { 8, 2 } + // 2 { 8, 3 } + // 3 { 8, 4 } + // 4 { 8, 5 } + // 5 { 8, 6 } + // 6 { 8, 7 } + // 7 { 8, 8 } + // 8 { 7, 8 } + // 9 { 6, 8 } + // 10 { 5, 8 } + // 11 { 4, 8 } + // 12 { 3, 8 } + // 13 { 2, 8 } + // 14 { 1, 8 } + + // The bit pattern is considered an entire 'word' and LSB goes in position 0 + // So, we need to reverse the order of the generated bit pattern, hence the (14 - i) below + for (var i = 0; i < 15; i++) { - // values computed to follow table above - var x1 = i < 8 ? 8 : i == 8 ? 7 : 14 - i; - var y1 = i < 6 ? i : i < 7 ? i + 1 : 8; - var x2 = i < 8 ? size - 1 - i : 8; - var y2 = i < 8 ? 8 : size - (15 - i); - - qrCode.ModuleMatrix[y1 + offsetValue][x1 + offsetValue] = formatStr[14 - i]; - qrCode.ModuleMatrix[y2 + offsetValue][x2 + offsetValue] = formatStr[14 - i]; + int x1, y1, x2, y2; + + if (isMicro) + { + // Micro QR format positions + x1 = i < 8 ? 8 : 14 - i + 1; + y1 = i < 8 ? i + 1 : 8; + + // Micro QR only uses one set of format positions, no duplication. + qrCode.ModuleMatrix[y1 + offsetValue][x1 + offsetValue] = formatStr[14 - i]; + } + else + { + // Standard QR format positions + x1 = i < 8 ? 8 : i == 8 ? 7 : 14 - i; + y1 = i < 6 ? i : i < 7 ? i + 1 : 8; + x2 = i < 8 ? size - 1 - i : 8; + y2 = i < 8 ? 8 : size - (15 - i); + + qrCode.ModuleMatrix[y1 + offsetValue][x1 + offsetValue] = formatStr[14 - i]; + qrCode.ModuleMatrix[y2 + offsetValue][x2 + offsetValue] = formatStr[14 - i]; + } } } @@ -98,6 +139,9 @@ public static int MaskCode(QRCodeData qrCode, int version, BlockedModules blocke var formatStr = new BitArray(15); for (var maskPattern = 0; maskPattern < 8; maskPattern++) { + if (version < 0 && (maskPattern == 0 || maskPattern == 2 || maskPattern == 3 || maskPattern == 5)) + continue; // Micro QR codes only support certain mask patterns. + var patternFunc = MaskPattern.Patterns[maskPattern]; // Reset the temporary QR code to the current state of the actual QR code. @@ -110,7 +154,7 @@ public static int MaskCode(QRCodeData qrCode, int version, BlockedModules blocke } // Place format information using the current mask pattern. - GetFormatString(formatStr, eccLevel, maskPattern); + GetFormatString(formatStr, version, eccLevel, maskPattern); ModulePlacer.PlaceFormat(qrTemp, formatStr, false); // Place version information if applicable. @@ -137,7 +181,7 @@ public static int MaskCode(QRCodeData qrCode, int version, BlockedModules blocke } } - var score = MaskPattern.Score(qrTemp); + var score = version < 0 ? MaskPattern.ScoreMicro(qrTemp) : MaskPattern.Score(qrTemp); // Select the pattern with the lowest score, indicating better QR code readability. if (patternScore > score) @@ -185,35 +229,23 @@ public static void PlaceDataWords(QRCodeData qrCode, BitArray data, BlockedModul // Loop from the rightmost column to the leftmost column, skipping one column each time. for (var x = size - 1; x >= 0; x -= 2) { - // Skip the timing pattern column at position 6. - if (x == 6) + // Skip the timing pattern column at position 6 (for normal QR codes only, not Micro QR codes). + if (qrCode.Version > 0 && x == 6) x = 5; // Loop through each row in the current column set. for (var yMod = 1; yMod <= size; yMod++) { - int y; // Actual y position to place data in the matrix. - // Determine the actual y position based on the current fill direction. - if (up) - { - y = size - yMod; // Calculate y for upward direction. - // Place data if within data length, current position is not blocked, and leftward column is in bounds. - if (index < count && !blockedModules.IsBlocked(x, y)) - qrCode.ModuleMatrix[y + 4][x + 4] = data[index++]; - if (index < count && x > 0 && !blockedModules.IsBlocked(x - 1, y)) - qrCode.ModuleMatrix[y + 4][x - 1 + 4] = data[index++]; - } - else - { - y = yMod - 1; // Calculate y for downward direction. - // Similar checks and data placement for the downward direction. - if (index < count && !blockedModules.IsBlocked(x, y)) - qrCode.ModuleMatrix[y + 4][x + 4] = data[index++]; - if (index < count && x > 0 && !blockedModules.IsBlocked(x - 1, y)) - qrCode.ModuleMatrix[y + 4][x - 1 + 4] = data[index++]; - } + int y = up ? size - yMod : yMod - 1; + + // Place data if within data length and current position is not blocked. + if (index < count && !blockedModules.IsBlocked(x, y)) + qrCode.ModuleMatrix[y + 4][x + 4] = data[index++]; + if (index < count && x > 0 && !blockedModules.IsBlocked(x - 1, y)) + qrCode.ModuleMatrix[y + 4][x - 1 + 4] = data[index++]; } + // Switch the fill direction after completing each column set. up = !up; } @@ -224,15 +256,21 @@ public static void PlaceDataWords(QRCodeData qrCode, BitArray data, BlockedModul /// /// The size of the QR code matrix. /// A list of rectangles representing areas that must not be overwritten. - public static void ReserveSeperatorAreas(int size, BlockedModules blockedModules) + public static void ReserveSeperatorAreas(int version, int size, BlockedModules blockedModules) { - // Block areas around the finder patterns, which are located near three corners of the QR code. + // Block areas around the top-left finder pattern blockedModules.Add(new Rectangle(7, 0, 1, 8)); // Vertical block near the top left finder pattern blockedModules.Add(new Rectangle(0, 7, 7, 1)); // Horizontal block near the top left finder pattern - blockedModules.Add(new Rectangle(0, size - 8, 8, 1)); // Horizontal block near the bottom left finder pattern - blockedModules.Add(new Rectangle(7, size - 7, 1, 7)); // Vertical block near the bottom left finder pattern - blockedModules.Add(new Rectangle(size - 8, 0, 1, 8)); // Vertical block near the top right finder pattern - blockedModules.Add(new Rectangle(size - 7, 7, 7, 1)); // Horizontal block near the top right finder pattern + + if (version > 0) // Non-micro QR codes have 3 finder patterns + { + // Block areas around the bottom-left finder pattern + blockedModules.Add(new Rectangle(0, size - 8, 8, 1)); // Horizontal block near the bottom left finder pattern + blockedModules.Add(new Rectangle(7, size - 7, 1, 7)); // Vertical block near the bottom left finder pattern + // Block areas around the top-right finder pattern + blockedModules.Add(new Rectangle(size - 8, 0, 1, 8)); // Vertical block near the top right finder pattern + blockedModules.Add(new Rectangle(size - 7, 7, 7, 1)); // Horizontal block near the top right finder pattern + } } /// @@ -243,6 +281,13 @@ public static void ReserveSeperatorAreas(int size, BlockedModules blockedModules /// A list of rectangles representing areas that must not be overwritten. public static void ReserveVersionAreas(int size, int version, BlockedModules blockedModules) { + if (version < 0) // Micro QR codes + { + blockedModules.Add(new Rectangle(0, 8, 9, 1)); + blockedModules.Add(new Rectangle(8, 0, 1, 8)); + return; + } + // Reserve areas near the timing patterns for version and format information. blockedModules.Add(new Rectangle(8, 0, 1, 6)); // Near the top timing pattern blockedModules.Add(new Rectangle(8, 7, 1, 1)); // Small square near the top left finder pattern @@ -267,6 +312,9 @@ public static void ReserveVersionAreas(int size, int version, BlockedModules blo /// A list of rectangles representing areas that must not be overwritten, updated to include the dark module. public static void PlaceDarkModule(QRCodeData qrCode, int version, BlockedModules blockedModules) { + // Micro QR codes do not have a dark module + if (version < 0) + return; // Place the dark module, which is always required to be black. qrCode.ModuleMatrix[4 * version + 9 + 4][8 + 4] = true; // Block the dark module area to prevent overwriting during further QR code generation steps. @@ -283,7 +331,8 @@ public static void PlaceFinderPatterns(QRCodeData qrCode, BlockedModules blocked var size = qrCode.ModuleMatrix.Count - 8; // Loop to place three finder patterns in the top-left, top-right, and bottom-left corners of the QR code. - for (var i = 0; i < 3; i++) + var count = qrCode.Version < 0 ? 1 : 3; // Micro QR codes have only one finder pattern. + for (var i = 0; i < count; i++) { // Calculate the x and y starting positions for each finder pattern based on the index. var locationX = i == 1 ? size - 7 : 0; // Place at top-right if i is 1, otherwise at left side (top or bottom). @@ -355,21 +404,41 @@ public static void PlaceAlignmentPatterns(QRCodeData qrCode, List alignme /// A list of rectangles representing areas that must not be overwritten. Updated with the areas occupied by timing patterns. public static void PlaceTimingPatterns(QRCodeData qrCode, BlockedModules blockedModules) { - var size = qrCode.ModuleMatrix.Count - 8; // Get the size of the QR code matrix. + // Get the size of the QR code matrix excluding padding. + var size = qrCode.ModuleMatrix.Count - 8; - // Place timing patterns starting from the 8th module to the size - 8 to avoid overlapping with finder patterns. - for (var i = 8; i < size - 8; i++) + if (qrCode.Version > 0) { - if (i % 2 == 0) // Place a dark module every other module to create the alternating pattern. + // Place timing patterns starting from the 8th module to the size - 8 to avoid overlapping with finder patterns. + for (var i = 8; i < size - 8; i++) { - qrCode.ModuleMatrix[6 + 4][i + 4] = true; // Horizontal timing pattern - qrCode.ModuleMatrix[i + 4][6 + 4] = true; // Vertical timing pattern + if (i % 2 == 0) // Place a dark module every other module to create the alternating pattern. + { + qrCode.ModuleMatrix[6 + 4][i + 4] = true; // Horizontal timing pattern + qrCode.ModuleMatrix[i + 4][6 + 4] = true; // Vertical timing pattern + } } + + // Add the areas occupied by the timing patterns to the list of blocked modules. + blockedModules.Add(new Rectangle(6, 8, 1, size - 16)); // Horizontal timing pattern area + blockedModules.Add(new Rectangle(8, 6, size - 16, 1)); // Vertical timing pattern area } + else // Micro QR codes + { + // Place timing patterns starting from the 8th module to avoid overlapping with finder patterns. + for (var i = 8; i < size; i++) + { + if (i % 2 == 0) // Place a dark module every other module to create the alternating pattern. + { + qrCode.ModuleMatrix[4][i + 4] = true; // Horizontal timing pattern + qrCode.ModuleMatrix[i + 4][4] = true; // Vertical timing pattern + } + } - // Add the areas occupied by the timing patterns to the list of blocked modules. - blockedModules.Add(new Rectangle(6, 8, 1, size - 16)); // Horizontal timing pattern area - blockedModules.Add(new Rectangle(8, 6, size - 16, 1)); // Vertical timing pattern area + // Add the areas occupied by the timing patterns to the list of blocked modules. + blockedModules.Add(new Rectangle(0, 8, 1, size - 8)); // Horizontal timing pattern area + blockedModules.Add(new Rectangle(8, 0, size - 8, 1)); // Vertical timing pattern area + } } } } diff --git a/QRCoder/QRCodeGenerator/Polynom.cs b/QRCoder/QRCodeGenerator/Polynom.cs index 6fb3c302..6cb6115d 100644 --- a/QRCoder/QRCodeGenerator/Polynom.cs +++ b/QRCoder/QRCodeGenerator/Polynom.cs @@ -154,8 +154,9 @@ public override string ToString() { var sb = new StringBuilder(); - foreach (var polyItem in _polyItems) + for (int i = 0; i < Count; i++) { + var polyItem = _polyItems[i]; sb.Append("a^" + polyItem.Coefficient + "*x^" + polyItem.Exponent + " + "); } diff --git a/QRCoderApiTests/net35+net40+net50+net50-windows+netstandard20/QRCoder.approved.txt b/QRCoderApiTests/net35+net40+net50+net50-windows+netstandard20/QRCoder.approved.txt index 0a75bc99..20c384af 100644 --- a/QRCoderApiTests/net35+net40+net50+net50-windows+netstandard20/QRCoder.approved.txt +++ b/QRCoderApiTests/net35+net40+net50+net50-windows+netstandard20/QRCoder.approved.txt @@ -894,6 +894,7 @@ namespace QRCoder public QRCoder.QRCodeData CreateQrCode(byte[] binaryData, QRCoder.QRCodeGenerator.ECCLevel eccLevel) { } public QRCoder.QRCodeData CreateQrCode(string plainText, QRCoder.QRCodeGenerator.ECCLevel eccLevel, bool forceUtf8 = false, bool utf8BOM = false, QRCoder.QRCodeGenerator.EciMode eciMode = 0, int requestedVersion = -1) { } public void Dispose() { } + public static QRCoder.QRCodeData GenerateMicroQrCode(string plainText, QRCoder.QRCodeGenerator.ECCLevel eccLevel = -1, int requestedVersion = 0) { } public static QRCoder.QRCodeData GenerateQrCode(QRCoder.PayloadGenerator.Payload payload) { } public static QRCoder.QRCodeData GenerateQrCode(QRCoder.PayloadGenerator.Payload payload, QRCoder.QRCodeGenerator.ECCLevel eccLevel) { } public static QRCoder.QRCodeData GenerateQrCode(byte[] binaryData, QRCoder.QRCodeGenerator.ECCLevel eccLevel) { } diff --git a/QRCoderApiTests/net60-windows/QRCoder.approved.txt b/QRCoderApiTests/net60-windows/QRCoder.approved.txt index 99b5f961..5c4386b0 100644 --- a/QRCoderApiTests/net60-windows/QRCoder.approved.txt +++ b/QRCoderApiTests/net60-windows/QRCoder.approved.txt @@ -902,6 +902,7 @@ namespace QRCoder public QRCoder.QRCodeData CreateQrCode(byte[] binaryData, QRCoder.QRCodeGenerator.ECCLevel eccLevel) { } public QRCoder.QRCodeData CreateQrCode(string plainText, QRCoder.QRCodeGenerator.ECCLevel eccLevel, bool forceUtf8 = false, bool utf8BOM = false, QRCoder.QRCodeGenerator.EciMode eciMode = 0, int requestedVersion = -1) { } public void Dispose() { } + public static QRCoder.QRCodeData GenerateMicroQrCode(string plainText, QRCoder.QRCodeGenerator.ECCLevel eccLevel = -1, int requestedVersion = 0) { } public static QRCoder.QRCodeData GenerateQrCode(QRCoder.PayloadGenerator.Payload payload) { } public static QRCoder.QRCodeData GenerateQrCode(QRCoder.PayloadGenerator.Payload payload, QRCoder.QRCodeGenerator.ECCLevel eccLevel) { } public static QRCoder.QRCodeData GenerateQrCode(byte[] binaryData, QRCoder.QRCodeGenerator.ECCLevel eccLevel) { } diff --git a/QRCoderApiTests/net60/QRCoder.approved.txt b/QRCoderApiTests/net60/QRCoder.approved.txt index 6d269d08..8ee10f23 100644 --- a/QRCoderApiTests/net60/QRCoder.approved.txt +++ b/QRCoderApiTests/net60/QRCoder.approved.txt @@ -836,6 +836,7 @@ namespace QRCoder public QRCoder.QRCodeData CreateQrCode(byte[] binaryData, QRCoder.QRCodeGenerator.ECCLevel eccLevel) { } public QRCoder.QRCodeData CreateQrCode(string plainText, QRCoder.QRCodeGenerator.ECCLevel eccLevel, bool forceUtf8 = false, bool utf8BOM = false, QRCoder.QRCodeGenerator.EciMode eciMode = 0, int requestedVersion = -1) { } public void Dispose() { } + public static QRCoder.QRCodeData GenerateMicroQrCode(string plainText, QRCoder.QRCodeGenerator.ECCLevel eccLevel = -1, int requestedVersion = 0) { } public static QRCoder.QRCodeData GenerateQrCode(QRCoder.PayloadGenerator.Payload payload) { } public static QRCoder.QRCodeData GenerateQrCode(QRCoder.PayloadGenerator.Payload payload, QRCoder.QRCodeGenerator.ECCLevel eccLevel) { } public static QRCoder.QRCodeData GenerateQrCode(byte[] binaryData, QRCoder.QRCodeGenerator.ECCLevel eccLevel) { } diff --git a/QRCoderApiTests/netstandard13/QRCoder.approved.txt b/QRCoderApiTests/netstandard13/QRCoder.approved.txt index 343903e1..4b1075b0 100644 --- a/QRCoderApiTests/netstandard13/QRCoder.approved.txt +++ b/QRCoderApiTests/netstandard13/QRCoder.approved.txt @@ -800,6 +800,7 @@ namespace QRCoder public QRCoder.QRCodeData CreateQrCode(byte[] binaryData, QRCoder.QRCodeGenerator.ECCLevel eccLevel) { } public QRCoder.QRCodeData CreateQrCode(string plainText, QRCoder.QRCodeGenerator.ECCLevel eccLevel, bool forceUtf8 = false, bool utf8BOM = false, QRCoder.QRCodeGenerator.EciMode eciMode = 0, int requestedVersion = -1) { } public void Dispose() { } + public static QRCoder.QRCodeData GenerateMicroQrCode(string plainText, QRCoder.QRCodeGenerator.ECCLevel eccLevel = -1, int requestedVersion = 0) { } public static QRCoder.QRCodeData GenerateQrCode(QRCoder.PayloadGenerator.Payload payload) { } public static QRCoder.QRCodeData GenerateQrCode(QRCoder.PayloadGenerator.Payload payload, QRCoder.QRCodeGenerator.ECCLevel eccLevel) { } public static QRCoder.QRCodeData GenerateQrCode(byte[] binaryData, QRCoder.QRCodeGenerator.ECCLevel eccLevel) { } diff --git a/QRCoderTests/QRGeneratorTests.cs b/QRCoderTests/QRGeneratorTests.cs index b2ec4441..2f5921cd 100644 --- a/QRCoderTests/QRGeneratorTests.cs +++ b/QRCoderTests/QRGeneratorTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; @@ -41,6 +42,22 @@ public void validate_antilogtable() } #if !NETFRAMEWORK // [Theory] is not supported in xunit < 2.0.0 + [Theory] + [InlineData("54321", ECCLevel.Default, "ZfnO93tpy9jjaACKXue2VsACXxY", 11)] //verified + [InlineData("00000000", ECCLevel.M, "lEBc3nKaK0UpMfenT5FTX02Zgfg", 13)] //verified + [InlineData("123456789", ECCLevel.L, "gCY4Cj1uLhI/0JjWG1F9kC4S1+I", 13)] //verified + [InlineData("abcd56789012345", ECCLevel.L, "kqcKfCCdu1VTjjtsmK4iBav9FTs", 17)] //verified + [InlineData("abc", ECCLevel.M, "334sxrtY5KkNZRGj1pBgb87/cFc", 15)] //reads fine, but unable to verify repeating pattern + public void validate_micro_qr_code(string input, ECCLevel eccLevel, string expectedHash, int expectedSize) + { + var qrData = QRCodeGenerator.GenerateMicroQrCode(input, eccLevel); + (qrData.ModuleMatrix.Count - 8).ShouldBe(expectedSize); // exclude padding + var result = string.Join("", qrData.ModuleMatrix.Select(x => x.ToBitString()).ToArray()); + var hash = System.Security.Cryptography.SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(result)); + var hashString = Convert.ToBase64String(hash); + hashString.TrimEnd('=').ShouldBe(expectedHash); + } + [Theory] // version 1 numeric [InlineData("1", "KWw84nkWZLMh5LqAJ/4s/4mW/08", 21)]