com.google.zxing.oned.rss.expanded.RSSExpandedReader Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core Show documentation
Show all versions of core Show documentation
Core barcode encoding/decoding library
/*
* Copyright (C) 2010 ZXing 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.
*/
/*
* These authors would like to acknowledge the Spanish Ministry of Industry,
* Tourism and Trade, for the support in the project TSI020301-2008-2
* "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled
* Mobile Dynamic Environments", led by Treelogic
* ( http://www.treelogic.com/ ):
*
* http://www.piramidepse.com/
*/
package com.google.zxing.oned.rss.expanded;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.DecodeHintType;
import com.google.zxing.FormatException;
import com.google.zxing.NotFoundException;
import com.google.zxing.Result;
import com.google.zxing.ResultPoint;
import com.google.zxing.common.BitArray;
import com.google.zxing.common.detector.MathUtils;
import com.google.zxing.oned.rss.AbstractRSSReader;
import com.google.zxing.oned.rss.DataCharacter;
import com.google.zxing.oned.rss.FinderPattern;
import com.google.zxing.oned.rss.RSSUtils;
import com.google.zxing.oned.rss.expanded.decoders.AbstractExpandedDecoder;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Collections;
/**
* @author Pablo Orduña, University of Deusto ([email protected])
* @author Eduardo Castillejo, University of Deusto ([email protected])
*/
public final class RSSExpandedReader extends AbstractRSSReader {
private static final int[] SYMBOL_WIDEST = {7, 5, 4, 3, 1};
private static final int[] EVEN_TOTAL_SUBSET = {4, 20, 52, 104, 204};
private static final int[] GSUM = {0, 348, 1388, 2948, 3988};
private static final int[][] FINDER_PATTERNS = {
{1,8,4,1}, // A
{3,6,4,1}, // B
{3,4,6,1}, // C
{3,2,8,1}, // D
{2,6,5,1}, // E
{2,2,9,1} // F
};
private static final int[][] WEIGHTS = {
{ 1, 3, 9, 27, 81, 32, 96, 77},
{ 20, 60, 180, 118, 143, 7, 21, 63},
{189, 145, 13, 39, 117, 140, 209, 205},
{193, 157, 49, 147, 19, 57, 171, 91},
{ 62, 186, 136, 197, 169, 85, 44, 132},
{185, 133, 188, 142, 4, 12, 36, 108},
{113, 128, 173, 97, 80, 29, 87, 50},
{150, 28, 84, 41, 123, 158, 52, 156},
{ 46, 138, 203, 187, 139, 206, 196, 166},
{ 76, 17, 51, 153, 37, 111, 122, 155},
{ 43, 129, 176, 106, 107, 110, 119, 146},
{ 16, 48, 144, 10, 30, 90, 59, 177},
{109, 116, 137, 200, 178, 112, 125, 164},
{ 70, 210, 208, 202, 184, 130, 179, 115},
{134, 191, 151, 31, 93, 68, 204, 190},
{148, 22, 66, 198, 172, 94, 71, 2},
{ 6, 18, 54, 162, 64, 192,154, 40},
{120, 149, 25, 75, 14, 42,126, 167},
{ 79, 26, 78, 23, 69, 207,199, 175},
{103, 98, 83, 38, 114, 131, 182, 124},
{161, 61, 183, 127, 170, 88, 53, 159},
{ 55, 165, 73, 8, 24, 72, 5, 15},
{ 45, 135, 194, 160, 58, 174, 100, 89}
};
private static final int FINDER_PAT_A = 0;
private static final int FINDER_PAT_B = 1;
private static final int FINDER_PAT_C = 2;
private static final int FINDER_PAT_D = 3;
private static final int FINDER_PAT_E = 4;
private static final int FINDER_PAT_F = 5;
private static final int[][] FINDER_PATTERN_SEQUENCES = {
{ FINDER_PAT_A, FINDER_PAT_A },
{ FINDER_PAT_A, FINDER_PAT_B, FINDER_PAT_B },
{ FINDER_PAT_A, FINDER_PAT_C, FINDER_PAT_B, FINDER_PAT_D },
{ FINDER_PAT_A, FINDER_PAT_E, FINDER_PAT_B, FINDER_PAT_D, FINDER_PAT_C },
{ FINDER_PAT_A, FINDER_PAT_E, FINDER_PAT_B, FINDER_PAT_D, FINDER_PAT_D, FINDER_PAT_F },
{ FINDER_PAT_A, FINDER_PAT_E, FINDER_PAT_B, FINDER_PAT_D, FINDER_PAT_E, FINDER_PAT_F, FINDER_PAT_F },
{ FINDER_PAT_A, FINDER_PAT_A, FINDER_PAT_B, FINDER_PAT_B, FINDER_PAT_C, FINDER_PAT_C, FINDER_PAT_D, FINDER_PAT_D },
{ FINDER_PAT_A, FINDER_PAT_A, FINDER_PAT_B, FINDER_PAT_B, FINDER_PAT_C, FINDER_PAT_C, FINDER_PAT_D, FINDER_PAT_E, FINDER_PAT_E },
{ FINDER_PAT_A, FINDER_PAT_A, FINDER_PAT_B, FINDER_PAT_B, FINDER_PAT_C, FINDER_PAT_C, FINDER_PAT_D, FINDER_PAT_E, FINDER_PAT_F, FINDER_PAT_F },
{ FINDER_PAT_A, FINDER_PAT_A, FINDER_PAT_B, FINDER_PAT_B, FINDER_PAT_C, FINDER_PAT_D, FINDER_PAT_D, FINDER_PAT_E, FINDER_PAT_E, FINDER_PAT_F, FINDER_PAT_F },
};
private static final int MAX_PAIRS = 11;
private final List pairs = new ArrayList<>(MAX_PAIRS);
private final List rows = new ArrayList<>();
private final int [] startEnd = new int[2];
private boolean startFromEven;
@Override
public Result decodeRow(int rowNumber,
BitArray row,
Map hints) throws NotFoundException, FormatException {
// Rows can start with even pattern in case in prev rows there where odd number of patters.
// So lets try twice
this.pairs.clear();
this.startFromEven = false;
try {
return constructResult(decodeRow2pairs(rowNumber, row));
} catch (NotFoundException e) {
// OK
}
this.pairs.clear();
this.startFromEven = true;
return constructResult(decodeRow2pairs(rowNumber, row));
}
@Override
public void reset() {
this.pairs.clear();
this.rows.clear();
}
// Not private for testing
List decodeRow2pairs(int rowNumber, BitArray row) throws NotFoundException {
boolean done = false;
while (!done) {
try {
this.pairs.add(retrieveNextPair(row, this.pairs, rowNumber));
} catch (NotFoundException nfe) {
if (this.pairs.isEmpty()) {
throw nfe;
}
// exit this loop when retrieveNextPair() fails and throws
done = true;
}
}
// TODO: verify sequence of finder patterns as in checkPairSequence()
if (checkChecksum()) {
return this.pairs;
}
boolean tryStackedDecode = !this.rows.isEmpty();
storeRow(rowNumber); // TODO: deal with reversed rows
if (tryStackedDecode) {
// When the image is 180-rotated, then rows are sorted in wrong direction.
// Try twice with both the directions.
List ps = checkRows(false);
if (ps != null) {
return ps;
}
ps = checkRows(true);
if (ps != null) {
return ps;
}
}
throw NotFoundException.getNotFoundInstance();
}
private List checkRows(boolean reverse) {
// Limit number of rows we are checking
// We use recursive algorithm with pure complexity and don't want it to take forever
// Stacked barcode can have up to 11 rows, so 25 seems reasonable enough
if (this.rows.size() > 25) {
this.rows.clear(); // We will never have a chance to get result, so clear it
return null;
}
this.pairs.clear();
if (reverse) {
Collections.reverse(this.rows);
}
List ps = null;
try {
ps = checkRows(new ArrayList<>(), 0);
} catch (NotFoundException e) {
// OK
}
if (reverse) {
Collections.reverse(this.rows);
}
return ps;
}
// Try to construct a valid rows sequence
// Recursion is used to implement backtracking
private List checkRows(List collectedRows, int currentRow) throws NotFoundException {
for (int i = currentRow; i < rows.size(); i++) {
ExpandedRow row = rows.get(i);
this.pairs.clear();
for (ExpandedRow collectedRow : collectedRows) {
this.pairs.addAll(collectedRow.getPairs());
}
this.pairs.addAll(row.getPairs());
if (isValidSequence(this.pairs)) {
if (checkChecksum()) {
return this.pairs;
}
List rs = new ArrayList<>(collectedRows);
rs.add(row);
try {
// Recursion: try to add more rows
return checkRows(rs, i + 1);
} catch (NotFoundException e) {
// We failed, try the next candidate
}
}
}
throw NotFoundException.getNotFoundInstance();
}
// Whether the pairs form a valid find pattern sequence,
// either complete or a prefix
private static boolean isValidSequence(List pairs) {
for (int[] sequence : FINDER_PATTERN_SEQUENCES) {
if (pairs.size() <= sequence.length) {
boolean stop = true;
for (int j = 0; j < pairs.size(); j++) {
if (pairs.get(j).getFinderPattern().getValue() != sequence[j]) {
stop = false;
break;
}
}
if (stop) {
return true;
}
}
}
return false;
}
private void storeRow(int rowNumber) {
// Discard if duplicate above or below; otherwise insert in order by row number.
int insertPos = 0;
boolean prevIsSame = false;
boolean nextIsSame = false;
while (insertPos < this.rows.size()) {
ExpandedRow erow = this.rows.get(insertPos);
if (erow.getRowNumber() > rowNumber) {
nextIsSame = erow.isEquivalent(this.pairs);
break;
}
prevIsSame = erow.isEquivalent(this.pairs);
insertPos++;
}
if (nextIsSame || prevIsSame) {
return;
}
// When the row was partially decoded (e.g. 2 pairs found instead of 3),
// it will prevent us from detecting the barcode.
// Try to merge partial rows
// Check whether the row is part of an already detected row
if (isPartialRow(this.pairs, this.rows)) {
return;
}
this.rows.add(insertPos, new ExpandedRow(this.pairs, rowNumber, false));
removePartialRows(this.pairs, this.rows);
}
// Remove all the rows that contains only specified pairs
private static void removePartialRows(Collection pairs, Collection rows) {
for (Iterator iterator = rows.iterator(); iterator.hasNext();) {
ExpandedRow r = iterator.next();
if (r.getPairs().size() != pairs.size()) {
boolean allFound = true;
for (ExpandedPair p : r.getPairs()) {
if (!pairs.contains(p)) {
allFound = false;
break;
}
}
if (allFound) {
// 'pairs' contains all the pairs from the row 'r'
iterator.remove();
}
}
}
}
// Returns true when one of the rows already contains all the pairs
private static boolean isPartialRow(Iterable pairs, Iterable rows) {
for (ExpandedRow r : rows) {
boolean allFound = true;
for (ExpandedPair p : pairs) {
boolean found = false;
for (ExpandedPair pp : r.getPairs()) {
if (p.equals(pp)) {
found = true;
break;
}
}
if (!found) {
allFound = false;
break;
}
}
if (allFound) {
// the row 'r' contain all the pairs from 'pairs'
return true;
}
}
return false;
}
// Only used for unit testing
List getRows() {
return this.rows;
}
// Not private for unit testing
static Result constructResult(List pairs) throws NotFoundException, FormatException {
BitArray binary = BitArrayBuilder.buildBitArray(pairs);
AbstractExpandedDecoder decoder = AbstractExpandedDecoder.createDecoder(binary);
String resultingString = decoder.parseInformation();
ResultPoint[] firstPoints = pairs.get(0).getFinderPattern().getResultPoints();
ResultPoint[] lastPoints = pairs.get(pairs.size() - 1).getFinderPattern().getResultPoints();
return new Result(
resultingString,
null,
new ResultPoint[]{firstPoints[0], firstPoints[1], lastPoints[0], lastPoints[1]},
BarcodeFormat.RSS_EXPANDED
);
}
private boolean checkChecksum() {
ExpandedPair firstPair = this.pairs.get(0);
DataCharacter checkCharacter = firstPair.getLeftChar();
DataCharacter firstCharacter = firstPair.getRightChar();
if (firstCharacter == null) {
return false;
}
int checksum = firstCharacter.getChecksumPortion();
int s = 2;
for (int i = 1; i < this.pairs.size(); ++i) {
ExpandedPair currentPair = this.pairs.get(i);
checksum += currentPair.getLeftChar().getChecksumPortion();
s++;
DataCharacter currentRightChar = currentPair.getRightChar();
if (currentRightChar != null) {
checksum += currentRightChar.getChecksumPortion();
s++;
}
}
checksum %= 211;
int checkCharacterValue = 211 * (s - 4) + checksum;
return checkCharacterValue == checkCharacter.getValue();
}
private static int getNextSecondBar(BitArray row, int initialPos) {
int currentPos;
if (row.get(initialPos)) {
currentPos = row.getNextUnset(initialPos);
currentPos = row.getNextSet(currentPos);
} else {
currentPos = row.getNextSet(initialPos);
currentPos = row.getNextUnset(currentPos);
}
return currentPos;
}
// not private for testing
ExpandedPair retrieveNextPair(BitArray row, List previousPairs, int rowNumber)
throws NotFoundException {
boolean isOddPattern = previousPairs.size() % 2 == 0;
if (startFromEven) {
isOddPattern = !isOddPattern;
}
FinderPattern pattern;
boolean keepFinding = true;
int forcedOffset = -1;
do {
this.findNextPair(row, previousPairs, forcedOffset);
pattern = parseFoundFinderPattern(row, rowNumber, isOddPattern);
if (pattern == null) {
forcedOffset = getNextSecondBar(row, this.startEnd[0]);
} else {
keepFinding = false;
}
} while (keepFinding);
// When stacked symbol is split over multiple rows, there's no way to guess if this pair can be last or not.
// boolean mayBeLast = checkPairSequence(previousPairs, pattern);
DataCharacter leftChar = this.decodeDataCharacter(row, pattern, isOddPattern, true);
if (!previousPairs.isEmpty() && previousPairs.get(previousPairs.size() - 1).mustBeLast()) {
throw NotFoundException.getNotFoundInstance();
}
DataCharacter rightChar;
try {
rightChar = this.decodeDataCharacter(row, pattern, isOddPattern, false);
} catch (NotFoundException ignored) {
rightChar = null;
}
return new ExpandedPair(leftChar, rightChar, pattern);
}
private void findNextPair(BitArray row, List previousPairs, int forcedOffset)
throws NotFoundException {
int[] counters = this.getDecodeFinderCounters();
counters[0] = 0;
counters[1] = 0;
counters[2] = 0;
counters[3] = 0;
int width = row.getSize();
int rowOffset;
if (forcedOffset >= 0) {
rowOffset = forcedOffset;
} else if (previousPairs.isEmpty()) {
rowOffset = 0;
} else {
ExpandedPair lastPair = previousPairs.get(previousPairs.size() - 1);
rowOffset = lastPair.getFinderPattern().getStartEnd()[1];
}
boolean searchingEvenPair = previousPairs.size() % 2 != 0;
if (startFromEven) {
searchingEvenPair = !searchingEvenPair;
}
boolean isWhite = false;
while (rowOffset < width) {
isWhite = !row.get(rowOffset);
if (!isWhite) {
break;
}
rowOffset++;
}
int counterPosition = 0;
int patternStart = rowOffset;
for (int x = rowOffset; x < width; x++) {
if (row.get(x) != isWhite) {
counters[counterPosition]++;
} else {
if (counterPosition == 3) {
if (searchingEvenPair) {
reverseCounters(counters);
}
if (isFinderPattern(counters)) {
this.startEnd[0] = patternStart;
this.startEnd[1] = x;
return;
}
if (searchingEvenPair) {
reverseCounters(counters);
}
patternStart += counters[0] + counters[1];
counters[0] = counters[2];
counters[1] = counters[3];
counters[2] = 0;
counters[3] = 0;
counterPosition--;
} else {
counterPosition++;
}
counters[counterPosition] = 1;
isWhite = !isWhite;
}
}
throw NotFoundException.getNotFoundInstance();
}
private static void reverseCounters(int [] counters) {
int length = counters.length;
for (int i = 0; i < length / 2; ++i) {
int tmp = counters[i];
counters[i] = counters[length - i - 1];
counters[length - i - 1] = tmp;
}
}
private FinderPattern parseFoundFinderPattern(BitArray row, int rowNumber, boolean oddPattern) {
// Actually we found elements 2-5.
int firstCounter;
int start;
int end;
if (oddPattern) {
// If pattern number is odd, we need to locate element 1 *before* the current block.
int firstElementStart = this.startEnd[0] - 1;
// Locate element 1
while (firstElementStart >= 0 && !row.get(firstElementStart)) {
firstElementStart--;
}
firstElementStart++;
firstCounter = this.startEnd[0] - firstElementStart;
start = firstElementStart;
end = this.startEnd[1];
} else {
// If pattern number is even, the pattern is reversed, so we need to locate element 1 *after* the current block.
start = this.startEnd[0];
end = row.getNextUnset(this.startEnd[1] + 1);
firstCounter = end - this.startEnd[1];
}
// Make 'counters' hold 1-4
int [] counters = this.getDecodeFinderCounters();
System.arraycopy(counters, 0, counters, 1, counters.length - 1);
counters[0] = firstCounter;
int value;
try {
value = parseFinderValue(counters, FINDER_PATTERNS);
} catch (NotFoundException ignored) {
return null;
}
return new FinderPattern(value, new int[] {start, end}, start, end, rowNumber);
}
DataCharacter decodeDataCharacter(BitArray row,
FinderPattern pattern,
boolean isOddPattern,
boolean leftChar) throws NotFoundException {
int[] counters = this.getDataCharacterCounters();
Arrays.fill(counters, 0);
if (leftChar) {
recordPatternInReverse(row, pattern.getStartEnd()[0], counters);
} else {
recordPattern(row, pattern.getStartEnd()[1], counters);
// reverse it
for (int i = 0, j = counters.length - 1; i < j; i++, j--) {
int temp = counters[i];
counters[i] = counters[j];
counters[j] = temp;
}
} //counters[] has the pixels of the module
int numModules = 17; //left and right data characters have all the same length
float elementWidth = MathUtils.sum(counters) / (float) numModules;
// Sanity check: element width for pattern and the character should match
float expectedElementWidth = (pattern.getStartEnd()[1] - pattern.getStartEnd()[0]) / 15.0f;
if (Math.abs(elementWidth - expectedElementWidth) / expectedElementWidth > 0.3f) {
throw NotFoundException.getNotFoundInstance();
}
int[] oddCounts = this.getOddCounts();
int[] evenCounts = this.getEvenCounts();
float[] oddRoundingErrors = this.getOddRoundingErrors();
float[] evenRoundingErrors = this.getEvenRoundingErrors();
for (int i = 0; i < counters.length; i++) {
float value = 1.0f * counters[i] / elementWidth;
int count = (int) (value + 0.5f); // Round
if (count < 1) {
if (value < 0.3f) {
throw NotFoundException.getNotFoundInstance();
}
count = 1;
} else if (count > 8) {
if (value > 8.7f) {
throw NotFoundException.getNotFoundInstance();
}
count = 8;
}
int offset = i / 2;
if ((i & 0x01) == 0) {
oddCounts[offset] = count;
oddRoundingErrors[offset] = value - count;
} else {
evenCounts[offset] = count;
evenRoundingErrors[offset] = value - count;
}
}
adjustOddEvenCounts(numModules);
int weightRowNumber = 4 * pattern.getValue() + (isOddPattern ? 0 : 2) + (leftChar ? 0 : 1) - 1;
int oddSum = 0;
int oddChecksumPortion = 0;
for (int i = oddCounts.length - 1; i >= 0; i--) {
if (isNotA1left(pattern, isOddPattern, leftChar)) {
int weight = WEIGHTS[weightRowNumber][2 * i];
oddChecksumPortion += oddCounts[i] * weight;
}
oddSum += oddCounts[i];
}
int evenChecksumPortion = 0;
for (int i = evenCounts.length - 1; i >= 0; i--) {
if (isNotA1left(pattern, isOddPattern, leftChar)) {
int weight = WEIGHTS[weightRowNumber][2 * i + 1];
evenChecksumPortion += evenCounts[i] * weight;
}
}
int checksumPortion = oddChecksumPortion + evenChecksumPortion;
if ((oddSum & 0x01) != 0 || oddSum > 13 || oddSum < 4) {
throw NotFoundException.getNotFoundInstance();
}
int group = (13 - oddSum) / 2;
int oddWidest = SYMBOL_WIDEST[group];
int evenWidest = 9 - oddWidest;
int vOdd = RSSUtils.getRSSvalue(oddCounts, oddWidest, true);
int vEven = RSSUtils.getRSSvalue(evenCounts, evenWidest, false);
int tEven = EVEN_TOTAL_SUBSET[group];
int gSum = GSUM[group];
int value = vOdd * tEven + vEven + gSum;
return new DataCharacter(value, checksumPortion);
}
private static boolean isNotA1left(FinderPattern pattern, boolean isOddPattern, boolean leftChar) {
// A1: pattern.getValue is 0 (A), and it's an oddPattern, and it is a left char
return !(pattern.getValue() == 0 && isOddPattern && leftChar);
}
private void adjustOddEvenCounts(int numModules) throws NotFoundException {
int oddSum = MathUtils.sum(this.getOddCounts());
int evenSum = MathUtils.sum(this.getEvenCounts());
boolean incrementOdd = false;
boolean decrementOdd = false;
if (oddSum > 13) {
decrementOdd = true;
} else if (oddSum < 4) {
incrementOdd = true;
}
boolean incrementEven = false;
boolean decrementEven = false;
if (evenSum > 13) {
decrementEven = true;
} else if (evenSum < 4) {
incrementEven = true;
}
int mismatch = oddSum + evenSum - numModules;
boolean oddParityBad = (oddSum & 0x01) == 1;
boolean evenParityBad = (evenSum & 0x01) == 0;
switch (mismatch) {
case 1:
if (oddParityBad) {
if (evenParityBad) {
throw NotFoundException.getNotFoundInstance();
}
decrementOdd = true;
} else {
if (!evenParityBad) {
throw NotFoundException.getNotFoundInstance();
}
decrementEven = true;
}
break;
case -1:
if (oddParityBad) {
if (evenParityBad) {
throw NotFoundException.getNotFoundInstance();
}
incrementOdd = true;
} else {
if (!evenParityBad) {
throw NotFoundException.getNotFoundInstance();
}
incrementEven = true;
}
break;
case 0:
if (oddParityBad) {
if (!evenParityBad) {
throw NotFoundException.getNotFoundInstance();
}
// Both bad
if (oddSum < evenSum) {
incrementOdd = true;
decrementEven = true;
} else {
decrementOdd = true;
incrementEven = true;
}
} else {
if (evenParityBad) {
throw NotFoundException.getNotFoundInstance();
}
// Nothing to do!
}
break;
default:
throw NotFoundException.getNotFoundInstance();
}
if (incrementOdd) {
if (decrementOdd) {
throw NotFoundException.getNotFoundInstance();
}
increment(this.getOddCounts(), this.getOddRoundingErrors());
}
if (decrementOdd) {
decrement(this.getOddCounts(), this.getOddRoundingErrors());
}
if (incrementEven) {
if (decrementEven) {
throw NotFoundException.getNotFoundInstance();
}
increment(this.getEvenCounts(), this.getOddRoundingErrors());
}
if (decrementEven) {
decrement(this.getEvenCounts(), this.getEvenRoundingErrors());
}
}
}