mirror of
https://github.com/zxing/zxing.git
synced 2025-03-05 20:48:51 -08:00
Now support scanning for 2-digit as well as 5-digit UPC/EAN extension codes
git-svn-id: https://zxing.googlecode.com/svn/trunk@2287 59b500cc-1b3d-0410-9834-0bbf25fbcc57
This commit is contained in:
parent
1b90db519e
commit
420ec722fb
112
core/src/com/google/zxing/oned/UPCEANExtension2Support.java
Normal file
112
core/src/com/google/zxing/oned/UPCEANExtension2Support.java
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2012 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.zxing.oned;
|
||||||
|
|
||||||
|
import com.google.zxing.BarcodeFormat;
|
||||||
|
import com.google.zxing.NotFoundException;
|
||||||
|
import com.google.zxing.Result;
|
||||||
|
import com.google.zxing.ResultMetadataType;
|
||||||
|
import com.google.zxing.ResultPoint;
|
||||||
|
import com.google.zxing.common.BitArray;
|
||||||
|
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see UPCEANExtension5Support
|
||||||
|
*/
|
||||||
|
final class UPCEANExtension2Support {
|
||||||
|
|
||||||
|
private final int[] decodeMiddleCounters = new int[4];
|
||||||
|
private final StringBuilder decodeRowStringBuffer = new StringBuilder();
|
||||||
|
|
||||||
|
Result decodeRow(int rowNumber, BitArray row, int[] extensionStartRange) throws NotFoundException {
|
||||||
|
|
||||||
|
StringBuilder result = decodeRowStringBuffer;
|
||||||
|
result.setLength(0);
|
||||||
|
int end = decodeMiddle(row, extensionStartRange, result);
|
||||||
|
|
||||||
|
String resultString = result.toString();
|
||||||
|
Map<ResultMetadataType,Object> extensionData = parseExtensionString(resultString);
|
||||||
|
|
||||||
|
Result extensionResult =
|
||||||
|
new Result(resultString,
|
||||||
|
null,
|
||||||
|
new ResultPoint[] {
|
||||||
|
new ResultPoint((extensionStartRange[0] + extensionStartRange[1]) / 2.0f, (float) rowNumber),
|
||||||
|
new ResultPoint((float) end, (float) rowNumber),
|
||||||
|
},
|
||||||
|
BarcodeFormat.UPC_EAN_EXTENSION);
|
||||||
|
if (extensionData != null) {
|
||||||
|
extensionResult.putAllMetadata(extensionData);
|
||||||
|
}
|
||||||
|
return extensionResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
int decodeMiddle(BitArray row, int[] startRange, StringBuilder resultString) throws NotFoundException {
|
||||||
|
int[] counters = decodeMiddleCounters;
|
||||||
|
counters[0] = 0;
|
||||||
|
counters[1] = 0;
|
||||||
|
counters[2] = 0;
|
||||||
|
counters[3] = 0;
|
||||||
|
int end = row.getSize();
|
||||||
|
int rowOffset = startRange[1];
|
||||||
|
|
||||||
|
int checkParity = 0;
|
||||||
|
|
||||||
|
for (int x = 0; x < 2 && rowOffset < end; x++) {
|
||||||
|
int bestMatch = UPCEANReader.decodeDigit(row, counters, rowOffset, UPCEANReader.L_AND_G_PATTERNS);
|
||||||
|
resultString.append((char) ('0' + bestMatch % 10));
|
||||||
|
for (int counter : counters) {
|
||||||
|
rowOffset += counter;
|
||||||
|
}
|
||||||
|
if (bestMatch >= 10) {
|
||||||
|
checkParity |= 1 << (1 - x);
|
||||||
|
}
|
||||||
|
if (x != 1) {
|
||||||
|
// Read off separator if not last
|
||||||
|
rowOffset = row.getNextSet(rowOffset);
|
||||||
|
rowOffset = row.getNextUnset(rowOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultString.length() != 2) {
|
||||||
|
throw NotFoundException.getNotFoundInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Integer.parseInt(resultString.toString()) % 4 != checkParity) {
|
||||||
|
throw NotFoundException.getNotFoundInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param raw raw content of extension
|
||||||
|
* @return formatted interpretation of raw content as a {@link Map} mapping
|
||||||
|
* one {@link ResultMetadataType} to appropriate value, or {@code null} if not known
|
||||||
|
*/
|
||||||
|
private static Map<ResultMetadataType,Object> parseExtensionString(String raw) {
|
||||||
|
if (raw.length() != 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Map<ResultMetadataType,Object> result = new EnumMap<ResultMetadataType,Object>(ResultMetadataType.class);
|
||||||
|
result.put(ResultMetadataType.ISSUE_NUMBER, Integer.valueOf(raw));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
181
core/src/com/google/zxing/oned/UPCEANExtension5Support.java
Normal file
181
core/src/com/google/zxing/oned/UPCEANExtension5Support.java
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.zxing.oned;
|
||||||
|
|
||||||
|
import com.google.zxing.BarcodeFormat;
|
||||||
|
import com.google.zxing.NotFoundException;
|
||||||
|
import com.google.zxing.Result;
|
||||||
|
import com.google.zxing.ResultMetadataType;
|
||||||
|
import com.google.zxing.ResultPoint;
|
||||||
|
import com.google.zxing.common.BitArray;
|
||||||
|
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see UPCEANExtension2Support
|
||||||
|
*/
|
||||||
|
final class UPCEANExtension5Support {
|
||||||
|
|
||||||
|
private static final int[] CHECK_DIGIT_ENCODINGS = {
|
||||||
|
0x18, 0x14, 0x12, 0x11, 0x0C, 0x06, 0x03, 0x0A, 0x09, 0x05
|
||||||
|
};
|
||||||
|
|
||||||
|
private final int[] decodeMiddleCounters = new int[4];
|
||||||
|
private final StringBuilder decodeRowStringBuffer = new StringBuilder();
|
||||||
|
|
||||||
|
Result decodeRow(int rowNumber, BitArray row, int[] extensionStartRange) throws NotFoundException {
|
||||||
|
|
||||||
|
StringBuilder result = decodeRowStringBuffer;
|
||||||
|
result.setLength(0);
|
||||||
|
int end = decodeMiddle(row, extensionStartRange, result);
|
||||||
|
|
||||||
|
String resultString = result.toString();
|
||||||
|
Map<ResultMetadataType,Object> extensionData = parseExtensionString(resultString);
|
||||||
|
|
||||||
|
Result extensionResult =
|
||||||
|
new Result(resultString,
|
||||||
|
null,
|
||||||
|
new ResultPoint[] {
|
||||||
|
new ResultPoint((extensionStartRange[0] + extensionStartRange[1]) / 2.0f, (float) rowNumber),
|
||||||
|
new ResultPoint((float) end, (float) rowNumber),
|
||||||
|
},
|
||||||
|
BarcodeFormat.UPC_EAN_EXTENSION);
|
||||||
|
if (extensionData != null) {
|
||||||
|
extensionResult.putAllMetadata(extensionData);
|
||||||
|
}
|
||||||
|
return extensionResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
int decodeMiddle(BitArray row, int[] startRange, StringBuilder resultString) throws NotFoundException {
|
||||||
|
int[] counters = decodeMiddleCounters;
|
||||||
|
counters[0] = 0;
|
||||||
|
counters[1] = 0;
|
||||||
|
counters[2] = 0;
|
||||||
|
counters[3] = 0;
|
||||||
|
int end = row.getSize();
|
||||||
|
int rowOffset = startRange[1];
|
||||||
|
|
||||||
|
int lgPatternFound = 0;
|
||||||
|
|
||||||
|
for (int x = 0; x < 5 && rowOffset < end; x++) {
|
||||||
|
int bestMatch = UPCEANReader.decodeDigit(row, counters, rowOffset, UPCEANReader.L_AND_G_PATTERNS);
|
||||||
|
resultString.append((char) ('0' + bestMatch % 10));
|
||||||
|
for (int counter : counters) {
|
||||||
|
rowOffset += counter;
|
||||||
|
}
|
||||||
|
if (bestMatch >= 10) {
|
||||||
|
lgPatternFound |= 1 << (4 - x);
|
||||||
|
}
|
||||||
|
if (x != 4) {
|
||||||
|
// Read off separator if not last
|
||||||
|
rowOffset = row.getNextSet(rowOffset);
|
||||||
|
rowOffset = row.getNextUnset(rowOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultString.length() != 5) {
|
||||||
|
throw NotFoundException.getNotFoundInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
int checkDigit = determineCheckDigit(lgPatternFound);
|
||||||
|
if (extensionChecksum(resultString.toString()) != checkDigit) {
|
||||||
|
throw NotFoundException.getNotFoundInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int extensionChecksum(CharSequence s) {
|
||||||
|
int length = s.length();
|
||||||
|
int sum = 0;
|
||||||
|
for (int i = length - 2; i >= 0; i -= 2) {
|
||||||
|
sum += (int) s.charAt(i) - (int) '0';
|
||||||
|
}
|
||||||
|
sum *= 3;
|
||||||
|
for (int i = length - 1; i >= 0; i -= 2) {
|
||||||
|
sum += (int) s.charAt(i) - (int) '0';
|
||||||
|
}
|
||||||
|
sum *= 3;
|
||||||
|
return sum % 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int determineCheckDigit(int lgPatternFound)
|
||||||
|
throws NotFoundException {
|
||||||
|
for (int d = 0; d < 10; d++) {
|
||||||
|
if (lgPatternFound == CHECK_DIGIT_ENCODINGS[d]) {
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NotFoundException.getNotFoundInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param raw raw content of extension
|
||||||
|
* @return formatted interpretation of raw content as a {@link Map} mapping
|
||||||
|
* one {@link ResultMetadataType} to appropriate value, or {@code null} if not known
|
||||||
|
*/
|
||||||
|
private static Map<ResultMetadataType,Object> parseExtensionString(String raw) {
|
||||||
|
if (raw.length() != 5) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Object value = parseExtension5String(raw);
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Map<ResultMetadataType,Object> result = new EnumMap<ResultMetadataType,Object>(ResultMetadataType.class);
|
||||||
|
result.put(ResultMetadataType.SUGGESTED_PRICE, value);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String parseExtension5String(String raw) {
|
||||||
|
String currency;
|
||||||
|
switch (raw.charAt(0)) {
|
||||||
|
case '0':
|
||||||
|
currency = "£";
|
||||||
|
break;
|
||||||
|
case '5':
|
||||||
|
currency = "$";
|
||||||
|
break;
|
||||||
|
case '9':
|
||||||
|
// Reference: http://www.jollytech.com
|
||||||
|
if ("90000".equals(raw)) {
|
||||||
|
// No suggested retail price
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ("99991".equals(raw)) {
|
||||||
|
// Complementary
|
||||||
|
return "0.00";
|
||||||
|
}
|
||||||
|
if ("99990".equals(raw)) {
|
||||||
|
return "Used";
|
||||||
|
}
|
||||||
|
// Otherwise... unknown currency?
|
||||||
|
currency = "";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
currency = "";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
int rawAmount = Integer.parseInt(raw.substring(1));
|
||||||
|
String unitsString = String.valueOf(rawAmount / 100);
|
||||||
|
int hundredths = rawAmount % 100;
|
||||||
|
String hundredthsString = hundredths < 10 ? "0" + hundredths : String.valueOf(hundredths);
|
||||||
|
return currency + unitsString + '.' + hundredthsString;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -16,182 +16,25 @@
|
||||||
|
|
||||||
package com.google.zxing.oned;
|
package com.google.zxing.oned;
|
||||||
|
|
||||||
import com.google.zxing.BarcodeFormat;
|
|
||||||
import com.google.zxing.NotFoundException;
|
import com.google.zxing.NotFoundException;
|
||||||
|
import com.google.zxing.ReaderException;
|
||||||
import com.google.zxing.Result;
|
import com.google.zxing.Result;
|
||||||
import com.google.zxing.ResultMetadataType;
|
|
||||||
import com.google.zxing.ResultPoint;
|
|
||||||
import com.google.zxing.common.BitArray;
|
import com.google.zxing.common.BitArray;
|
||||||
|
|
||||||
import java.util.EnumMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
final class UPCEANExtensionSupport {
|
final class UPCEANExtensionSupport {
|
||||||
|
|
||||||
private static final int[] EXTENSION_START_PATTERN = {1,1,2};
|
private static final int[] EXTENSION_START_PATTERN = {1,1,2};
|
||||||
private static final int[] CHECK_DIGIT_ENCODINGS = {
|
|
||||||
0x18, 0x14, 0x12, 0x11, 0x0C, 0x06, 0x03, 0x0A, 0x09, 0x05
|
|
||||||
};
|
|
||||||
|
|
||||||
private final int[] decodeMiddleCounters = new int[4];
|
private final UPCEANExtension2Support twoSupport = new UPCEANExtension2Support();
|
||||||
private final StringBuilder decodeRowStringBuffer = new StringBuilder();
|
private final UPCEANExtension5Support fiveSupport = new UPCEANExtension5Support();
|
||||||
|
|
||||||
Result decodeRow(int rowNumber, BitArray row, int rowOffset) throws NotFoundException {
|
Result decodeRow(int rowNumber, BitArray row, int rowOffset) throws NotFoundException {
|
||||||
|
|
||||||
int[] extensionStartRange = UPCEANReader.findGuardPattern(row, rowOffset, false, EXTENSION_START_PATTERN);
|
int[] extensionStartRange = UPCEANReader.findGuardPattern(row, rowOffset, false, EXTENSION_START_PATTERN);
|
||||||
|
try {
|
||||||
StringBuilder result = decodeRowStringBuffer;
|
return fiveSupport.decodeRow(rowNumber, row, extensionStartRange);
|
||||||
result.setLength(0);
|
} catch (ReaderException re) {
|
||||||
int end = decodeMiddle(row, extensionStartRange, result);
|
return twoSupport.decodeRow(rowNumber, row, extensionStartRange);
|
||||||
|
|
||||||
String resultString = result.toString();
|
|
||||||
Map<ResultMetadataType,Object> extensionData = parseExtensionString(resultString);
|
|
||||||
|
|
||||||
Result extensionResult =
|
|
||||||
new Result(resultString,
|
|
||||||
null,
|
|
||||||
new ResultPoint[] {
|
|
||||||
new ResultPoint((extensionStartRange[0] + extensionStartRange[1]) / 2.0f, (float) rowNumber),
|
|
||||||
new ResultPoint((float) end, (float) rowNumber),
|
|
||||||
},
|
|
||||||
BarcodeFormat.UPC_EAN_EXTENSION);
|
|
||||||
if (extensionData != null) {
|
|
||||||
extensionResult.putAllMetadata(extensionData);
|
|
||||||
}
|
}
|
||||||
return extensionResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
int decodeMiddle(BitArray row,
|
|
||||||
int[] startRange,
|
|
||||||
StringBuilder resultString) throws NotFoundException {
|
|
||||||
int[] counters = decodeMiddleCounters;
|
|
||||||
counters[0] = 0;
|
|
||||||
counters[1] = 0;
|
|
||||||
counters[2] = 0;
|
|
||||||
counters[3] = 0;
|
|
||||||
int end = row.getSize();
|
|
||||||
int rowOffset = startRange[1];
|
|
||||||
|
|
||||||
int lgPatternFound = 0;
|
|
||||||
|
|
||||||
for (int x = 0; x < 5 && rowOffset < end; x++) {
|
|
||||||
int bestMatch = UPCEANReader.decodeDigit(row, counters, rowOffset, UPCEANReader.L_AND_G_PATTERNS);
|
|
||||||
resultString.append((char) ('0' + bestMatch % 10));
|
|
||||||
for (int counter : counters) {
|
|
||||||
rowOffset += counter;
|
|
||||||
}
|
|
||||||
if (bestMatch >= 10) {
|
|
||||||
lgPatternFound |= 1 << (4 - x);
|
|
||||||
}
|
|
||||||
if (x != 4) {
|
|
||||||
// Read off separator if not last
|
|
||||||
rowOffset = row.getNextSet(rowOffset);
|
|
||||||
rowOffset = row.getNextUnset(rowOffset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resultString.length() != 5) {
|
|
||||||
throw NotFoundException.getNotFoundInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
int checkDigit = determineCheckDigit(lgPatternFound);
|
|
||||||
if (extensionChecksum(resultString.toString()) != checkDigit) {
|
|
||||||
throw NotFoundException.getNotFoundInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
return rowOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int extensionChecksum(CharSequence s) {
|
|
||||||
int length = s.length();
|
|
||||||
int sum = 0;
|
|
||||||
for (int i = length - 2; i >= 0; i -= 2) {
|
|
||||||
sum += (int) s.charAt(i) - (int) '0';
|
|
||||||
}
|
|
||||||
sum *= 3;
|
|
||||||
for (int i = length - 1; i >= 0; i -= 2) {
|
|
||||||
sum += (int) s.charAt(i) - (int) '0';
|
|
||||||
}
|
|
||||||
sum *= 3;
|
|
||||||
return sum % 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int determineCheckDigit(int lgPatternFound)
|
|
||||||
throws NotFoundException {
|
|
||||||
for (int d = 0; d < 10; d++) {
|
|
||||||
if (lgPatternFound == CHECK_DIGIT_ENCODINGS[d]) {
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw NotFoundException.getNotFoundInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param raw raw content of extension
|
|
||||||
* @return formatted interpretation of raw content as a {@link Map} mapping
|
|
||||||
* one {@link ResultMetadataType} to appropriate value, or {@code null} if not known
|
|
||||||
*/
|
|
||||||
private static Map<ResultMetadataType,Object> parseExtensionString(String raw) {
|
|
||||||
ResultMetadataType type;
|
|
||||||
Object value;
|
|
||||||
switch (raw.length()) {
|
|
||||||
case 2:
|
|
||||||
type = ResultMetadataType.ISSUE_NUMBER;
|
|
||||||
value = parseExtension2String(raw);
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
type = ResultMetadataType.SUGGESTED_PRICE;
|
|
||||||
value = parseExtension5String(raw);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Map<ResultMetadataType,Object> result = new EnumMap<ResultMetadataType,Object>(ResultMetadataType.class);
|
|
||||||
result.put(type, value);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Integer parseExtension2String(String raw) {
|
|
||||||
return Integer.valueOf(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String parseExtension5String(String raw) {
|
|
||||||
String currency;
|
|
||||||
switch (raw.charAt(0)) {
|
|
||||||
case '0':
|
|
||||||
currency = "£";
|
|
||||||
break;
|
|
||||||
case '5':
|
|
||||||
currency = "$";
|
|
||||||
break;
|
|
||||||
case '9':
|
|
||||||
// Reference: http://www.jollytech.com
|
|
||||||
if ("90000".equals(raw)) {
|
|
||||||
// No suggested retail price
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if ("99991".equals(raw)) {
|
|
||||||
// Complementary
|
|
||||||
return "0.00";
|
|
||||||
}
|
|
||||||
if ("99990".equals(raw)) {
|
|
||||||
return "Used";
|
|
||||||
}
|
|
||||||
// Otherwise... unknown currency?
|
|
||||||
currency = "";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
currency = "";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
int rawAmount = Integer.parseInt(raw.substring(1));
|
|
||||||
String unitsString = String.valueOf(rawAmount / 100);
|
|
||||||
int hundredths = rawAmount % 100;
|
|
||||||
String hundredthsString = hundredths < 10 ? "0" + hundredths : String.valueOf(hundredths);
|
|
||||||
return currency + unitsString + '.' + hundredthsString;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue