Files
aistudio-wpf-diagram/zxing.core/xx/oned/MSIReader.cs
2021-07-23 09:42:22 +08:00

356 lines
12 KiB
C#

/*
* Copyright 2013 ZXing.Net authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Text;
using ZXing.Common;
namespace ZXing.OneD
{
/// <summary>
/// Decodes MSI barcodes.
/// </summary>
public sealed class MSIReader : OneDReader
{
internal static String ALPHABET_STRING = "0123456789";
private static readonly char[] ALPHABET = ALPHABET_STRING.ToCharArray();
/// <summary>
/// These represent the encodings of characters, as patterns of wide and narrow bars.
/// The 9 least-significant bits of each int correspond to the pattern of wide and narrow,
/// with 1s representing "wide" and 0s representing narrow.
/// </summary>
internal static int[] CHARACTER_ENCODINGS = {
0x924, 0x926, 0x934, 0x936, 0x9A4, 0x9A6, 0x9B4, 0x9B6, 0xD24, 0xD26 // 0-9
};
private const int START_ENCODING = 0x06;
private const int END_ENCODING = 0x09;
private readonly bool usingCheckDigit;
private readonly StringBuilder decodeRowResult;
private readonly int[] counters;
private int averageCounterWidth;
/// <summary>
/// Creates a reader that assumes all encoded data is data, and does not treat the final
/// character as a check digit.
/// </summary>
public MSIReader()
: this(false)
{
}
/// <summary>
/// Creates a reader that can be configured to check the last character as a check digit,
/// </summary>
/// <param name="usingCheckDigit">if true, treat the last data character as a check digit, not
/// data, and verify that the checksum passes.</param>
public MSIReader(bool usingCheckDigit)
{
this.usingCheckDigit = usingCheckDigit;
decodeRowResult = new StringBuilder(20);
counters = new int[8];
}
/// <summary>
/// <p>Attempts to decode a one-dimensional barcode format given a single row of
/// an image.</p>
/// </summary>
/// <param name="rowNumber">row number from top of the row</param>
/// <param name="row">the black/white pixel data of the row</param>
/// <param name="hints">decode hints</param>
/// <returns><see cref="Result"/>containing encoded string and start/end of barcode</returns>
override public Result decodeRow(int rowNumber, BitArray row, IDictionary<DecodeHintType, object> hints)
{
for (var index = 0; index < counters.Length; index++)
counters[index] = 0;
decodeRowResult.Length = 0;
int[] start = findStartPattern(row, counters);
if (start == null)
return null;
// Read off white space
int nextStart = row.getNextSet(start[1]);
char decodedChar;
int lastStart = nextStart;
int pattern;
do
{
if (!recordPattern(row, nextStart, counters, 8))
{
// not enough bars for a number but perhaps enough for the end pattern
var endPattern = findEndPattern(row, nextStart, counters);
if (endPattern == null)
return null;
lastStart = nextStart;
nextStart = endPattern[1];
break;
}
pattern = toPattern(counters, 8);
if (!patternToChar(pattern, out decodedChar))
{
// pattern doesn't result in an encoded number
// but it could be the end pattern followed by some black areas
var endPattern = findEndPattern(row, nextStart, counters);
if (endPattern == null)
return null;
lastStart = nextStart;
nextStart = endPattern[1];
break;
}
decodeRowResult.Append(decodedChar);
lastStart = nextStart;
foreach (int counter in counters)
{
nextStart += counter;
}
// Read off white space
nextStart = row.getNextSet(nextStart);
} while (decodedChar != '*');
// at least 3 digits to prevent false positives within other kind
// of codes like PDF417
if (decodeRowResult.Length < 3)
{
return null;
}
var rawBytes = Encoding.UTF8.GetBytes(decodeRowResult.ToString());
var resultString = decodeRowResult.ToString();
if (usingCheckDigit)
{
var resultStringWithoutChecksum = resultString.Substring(0, resultString.Length - 1);
int checkSum = CalculateChecksumLuhn(resultStringWithoutChecksum);
if ((char)(checkSum + 48) != resultString[resultStringWithoutChecksum.Length])
{
return null;
}
}
float left = (float)(start[1] + start[0]) / 2.0f;
float right = (float)(nextStart + lastStart) / 2.0f;
var resultPointCallback = hints == null || !hints.ContainsKey(DecodeHintType.NEED_RESULT_POINT_CALLBACK)
? null
: (ResultPointCallback)hints[DecodeHintType.NEED_RESULT_POINT_CALLBACK];
if (resultPointCallback != null)
{
resultPointCallback(new ResultPoint(left, rowNumber));
resultPointCallback(new ResultPoint(right, rowNumber));
}
return new Result(
resultString,
rawBytes,
new[]
{
new ResultPoint(left, rowNumber),
new ResultPoint(right, rowNumber)
},
BarcodeFormat.MSI);
}
private int[] findStartPattern(BitArray row, int[] counters)
{
const int patternLength = 2;
int width = row.Size;
int rowOffset = row.getNextSet(0);
int counterPosition = 0;
int patternStart = rowOffset;
bool isWhite = false;
counters[0] = 0;
counters[1] = 0;
for (int i = rowOffset; i < width; i++)
{
if (row[i] ^ isWhite)
{
counters[counterPosition]++;
}
else
{
if (counterPosition == patternLength - 1)
{
// narrow and wide areas should be as near as possible to factor 2
// lets say we will check 1.5 <= factor <= 5
var factorNarrowToWide = ((float)counters[0]) / ((float)counters[1]);
if (factorNarrowToWide >= 1.5 && factorNarrowToWide <= 5)
{
calculateAverageCounterWidth(counters, patternLength);
if (toPattern(counters, patternLength) == START_ENCODING)
{
// Look for whitespace before start pattern, >= 50% of width of start pattern
if (row.isRange(Math.Max(0, patternStart - ((i - patternStart) >> 1)), patternStart, false))
{
return new int[] {patternStart, i};
}
}
}
patternStart += counters[0] + counters[1];
Array.Copy(counters, 2, counters, 0, patternLength - 2);
counters[patternLength - 2] = 0;
counters[patternLength - 1] = 0;
counterPosition--;
}
else
{
counterPosition++;
}
counters[counterPosition] = 1;
isWhite = !isWhite;
}
}
return null;
}
private int[] findEndPattern(BitArray row, int rowOffset, int[] counters)
{
const int patternLength = 3;
int width = row.Size;
int counterPosition = 0;
int patternStart = rowOffset;
bool isWhite = false;
counters[0] = 0;
counters[1] = 0;
counters[2] = 0;
for (int i = rowOffset; i < width; i++)
{
if (row[i] ^ isWhite)
{
counters[counterPosition]++;
}
else
{
if (counterPosition == patternLength - 1)
{
var factorNarrowToWide = ((float)counters[1]) / ((float)counters[0]);
if (factorNarrowToWide >= 1.5 && factorNarrowToWide <= 5)
{
if (toPattern(counters, patternLength) == END_ENCODING)
{
// Look for whitespace after end pattern, >= 50% of width of end pattern
var minEndOfWhite = Math.Min(row.Size - 1, i + ((i - patternStart) >> 1));
if (row.isRange(i, minEndOfWhite, false))
{
return new int[] {patternStart, i};
}
}
}
return null;
}
counterPosition++;
counters[counterPosition] = 1;
isWhite = !isWhite;
}
}
return null;
}
private void calculateAverageCounterWidth(int[] counters, int patternLength)
{
// look for the minimum and the maximum width of the bars
// there are only two sizes for MSI barcodes
// all numbers are encoded as a chain of the pattern 100 and 110
// the complete pattern of one number always starts with 1 or 11 (black bar(s))
int minCounter = Int32.MaxValue;
int maxCounter = 0;
for (var index = 0; index < patternLength; index++)
{
var counter = counters[index];
if (counter < minCounter)
{
minCounter = counter;
}
if (counter > maxCounter)
{
maxCounter = counter;
}
}
// calculate the average of the minimum and maximum width
// using some bit shift to get a higher resolution without floating point arithmetic
averageCounterWidth = ((maxCounter << 8) + (minCounter << 8)) / 2;
}
private int toPattern(int[] counters, int patternLength)
{
// calculating the encoded value from the pattern
int pattern = 0;
int bit = 1;
int doubleBit = 3;
for (var index = 0; index < patternLength; index++)
{
var counter = counters[index];
if ((counter << 8) < averageCounterWidth)
{
pattern = (pattern << 1) | bit;
}
else
{
pattern = (pattern << 2) | doubleBit;
}
bit = bit ^ 1;
doubleBit = doubleBit ^ 3;
}
return pattern;
}
private static bool patternToChar(int pattern, out char c)
{
for (int i = 0; i < CHARACTER_ENCODINGS.Length; i++)
{
if (CHARACTER_ENCODINGS[i] == pattern)
{
c = ALPHABET[i];
return true;
}
}
c = '*';
return false;
}
private static readonly int[] doubleAndCrossSum = new [] { 0, 2, 4, 6, 8, 1, 3, 5, 7, 9 };
private static int CalculateChecksumLuhn(string number)
{
var checksum = 0;
for (var index = number.Length - 2; index >= 0; index -= 2)
{
var digit = number[index] - 48;
checksum += digit;
}
for (var index = number.Length - 1; index >= 0; index -= 2)
{
var digit = doubleAndCrossSum[number[index] - 48];
checksum += digit;
}
return (10 - (checksum % 10)) % 10;
}
}
}