Add ability to specify a QR code version hint. (#637)

* Add ability to specify a QR code version hint.

* Checkstyle.
This commit is contained in:
germinator 2016-08-08 03:22:35 -07:00 committed by Sean Owen
parent f6a1e20e1b
commit d60c0f14bd
3 changed files with 84 additions and 21 deletions

View file

@ -95,4 +95,10 @@ public enum EncodeHintType {
* (Type {@link Integer}, or {@link String} representation of the integer value). * (Type {@link Integer}, or {@link String} representation of the integer value).
*/ */
AZTEC_LAYERS, AZTEC_LAYERS,
/**
* Specifies the exact version of QR code to be encoded. An integer. If the data specified
* cannot fit within the required version, a WriterException will be thrown.
*/
QR_VERSION
} }

View file

@ -106,21 +106,18 @@ public final class Encoder {
BitArray dataBits = new BitArray(); BitArray dataBits = new BitArray();
appendBytes(content, mode, dataBits, encoding); appendBytes(content, mode, dataBits, encoding);
// Hard part: need to know version to know how many bits length takes. But need to know how many Version version = null;
// bits it takes to know version. First we take a guess at version by assuming version will be if (hints != null && hints.containsKey(EncodeHintType.QR_VERSION)) {
// the minimum, 1: Version requestedVersion = Version.getVersionForNumber((Integer) hints.get(EncodeHintType.QR_VERSION));
int bitsNeeded = calculateBitsNeeded(mode, headerBits, dataBits, requestedVersion);
int provisionalBitsNeeded = headerBits.getSize() if (willFit(bitsNeeded, requestedVersion, ecLevel)) {
+ mode.getCharacterCountBits(Version.getVersionForNumber(1)) version = requestedVersion;
+ dataBits.getSize(); } else {
Version provisionalVersion = chooseVersion(provisionalBitsNeeded, ecLevel); throw new WriterException("Data too big for requested version");
}
// Use that guess to calculate the right version. I am still not sure this works in 100% of cases. } else {
version = recommendVersion(ecLevel, mode, headerBits, dataBits);
int bitsNeeded = headerBits.getSize() }
+ mode.getCharacterCountBits(provisionalVersion)
+ dataBits.getSize();
Version version = chooseVersion(bitsNeeded, ecLevel);
BitArray headerAndDataBits = new BitArray(); BitArray headerAndDataBits = new BitArray();
headerAndDataBits.appendBitArray(headerBits); headerAndDataBits.appendBitArray(headerBits);
@ -161,6 +158,36 @@ public final class Encoder {
return qrCode; return qrCode;
} }
/**
* Decides the smallest version of QR code that will contain all of the provided data.
* @throws WriterException if the data cannot fit in any version
*/
private static Version recommendVersion(ErrorCorrectionLevel ecLevel,
Mode mode,
BitArray headerBits,
BitArray dataBits) throws WriterException {
// Hard part: need to know version to know how many bits length takes. But need to know how many
// bits it takes to know version. First we take a guess at version by assuming version will be
// the minimum, 1:
int provisionalBitsNeeded = calculateBitsNeeded(mode, headerBits, dataBits, Version.getVersionForNumber(1));
Version provisionalVersion = chooseVersion(provisionalBitsNeeded, ecLevel);
// Use that guess to calculate the right version. I am still not sure this works in 100% of cases.
int bitsNeeded = calculateBitsNeeded(mode, headerBits, dataBits, provisionalVersion);
Version version = chooseVersion(bitsNeeded, ecLevel);
return version;
}
private static int calculateBitsNeeded(Mode mode,
BitArray headerBits,
BitArray dataBits,
Version version) {
int bitsNeeded = headerBits.getSize()
+ mode.getCharacterCountBits(version)
+ dataBits.getSize();
return bitsNeeded;
}
/** /**
* @return the code point of the table used in alphanumeric mode or * @return the code point of the table used in alphanumeric mode or
* -1 if there is no corresponding code in the table. * -1 if there is no corresponding code in the table.
@ -246,9 +273,21 @@ public final class Encoder {
} }
private static Version chooseVersion(int numInputBits, ErrorCorrectionLevel ecLevel) throws WriterException { private static Version chooseVersion(int numInputBits, ErrorCorrectionLevel ecLevel) throws WriterException {
// In the following comments, we use numbers of Version 7-H.
for (int versionNum = 1; versionNum <= 40; versionNum++) { for (int versionNum = 1; versionNum <= 40; versionNum++) {
Version version = Version.getVersionForNumber(versionNum); Version version = Version.getVersionForNumber(versionNum);
if (willFit(numInputBits, version, ecLevel)) {
return version;
}
}
throw new WriterException("Data too big");
}
/**
* @return true if the number of input bits will fit in a code with the specified version and
* error correction level.
*/
private static boolean willFit(int numInputBits, Version version, ErrorCorrectionLevel ecLevel) {
// In the following comments, we use numbers of Version 7-H.
// numBytes = 196 // numBytes = 196
int numBytes = version.getTotalCodewords(); int numBytes = version.getTotalCodewords();
// getNumECBytes = 130 // getNumECBytes = 130
@ -257,11 +296,7 @@ public final class Encoder {
// getNumDataBytes = 196 - 130 = 66 // getNumDataBytes = 196 - 130 = 66
int numDataBytes = numBytes - numEcBytes; int numDataBytes = numBytes - numEcBytes;
int totalInputBytes = (numInputBits + 7) / 8; int totalInputBytes = (numInputBits + 7) / 8;
if (numDataBytes >= totalInputBytes) { return numDataBytes >= totalInputBytes;
return version;
}
}
throw new WriterException("Data too big");
} }
/** /**

View file

@ -22,11 +22,13 @@ import com.google.zxing.common.BitArray;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.google.zxing.qrcode.decoder.Mode; import com.google.zxing.qrcode.decoder.Mode;
import com.google.zxing.qrcode.decoder.Version; import com.google.zxing.qrcode.decoder.Version;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
@ -126,6 +128,26 @@ public final class EncoderTestCase extends Assert {
">>\n"; ">>\n";
assertEquals(expected, qrCode.toString()); assertEquals(expected, qrCode.toString());
} }
@Test
public void testEncodeWithVersion() throws WriterException {
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.QR_VERSION, 7);
QRCode qrCode = Encoder.encode("ABCDEF", ErrorCorrectionLevel.H, hints);
assertTrue(qrCode.toString().contains(" version: 7\n"));
}
@Test
public void testEncodeWithVersionTooSmall() throws WriterException {
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.QR_VERSION, 3);
try {
Encoder.encode("THISMESSAGEISTOOLONGFORAQRCODEVERSION3", ErrorCorrectionLevel.H, hints);
fail();
} catch (WriterException e) {
assertEquals("Data too big for requested version", e.getMessage());
}
}
@Test @Test
public void testSimpleUTF8ECI() throws WriterException { public void testSimpleUTF8ECI() throws WriterException {