From 5692e951ddebfb2f0df4fcf6747defbedab1036e Mon Sep 17 00:00:00 2001 From: Nayuki Minase Date: Sat, 16 Apr 2016 03:53:58 +0000 Subject: [PATCH] Revamped QrCode.encodeSegments() to add parameters to make a much richer API, in all language versions; updated JavaScript demo script to handle new semantics. --- cpp/QrCode.cpp | 48 ++++++++-------- cpp/QrCode.hpp | 11 ++-- cpp/QrSegment.cpp | 17 ++++++ cpp/QrSegment.hpp | 4 ++ java/io/nayuki/qrcodegen/QrCode.java | 76 +++++++++++++++---------- java/io/nayuki/qrcodegen/QrSegment.java | 21 +++++++ javascript/qrcodegen-demo.js | 3 +- javascript/qrcodegen.js | 73 ++++++++++++++---------- python/qrcodegen.py | 65 +++++++++++++-------- 9 files changed, 205 insertions(+), 113 deletions(-) diff --git a/cpp/QrCode.cpp b/cpp/QrCode.cpp index 273a688..866c33a 100644 --- a/cpp/QrCode.cpp +++ b/cpp/QrCode.cpp @@ -55,34 +55,34 @@ qrcodegen::QrCode qrcodegen::QrCode::encodeBinary(const std::vector &da } -qrcodegen::QrCode qrcodegen::QrCode::encodeSegments(const std::vector &segs, const Ecc &ecl) { +qrcodegen::QrCode qrcodegen::QrCode::encodeSegments(const std::vector &segs, const Ecc &ecl, + int minVersion, int maxVersion, int mask, bool boostEcl) { + if (!(1 <= minVersion && minVersion <= maxVersion && maxVersion <= 40) || mask < -1 || mask > 7) + throw "Invalid value"; + // Find the minimal version number to use - int version, dataCapacityBits; - for (version = 1; ; version++) { // Increment until the data fits in the QR Code - if (version > 40) // All versions could not fit the given data - throw "Data too long"; - dataCapacityBits = getNumDataCodewords(version, ecl) * 8; // Number of data bits available - - // Calculate the total number of bits needed at this version number - // to encode all the segments (i.e. segment metadata and payloads) - int dataUsedBits = 0; - for (size_t i = 0; i < segs.size(); i++) { - const QrSegment &seg(segs.at(i)); - if (seg.numChars < 0) - throw "Assertion error"; - int ccbits = seg.mode.numCharCountBits(version); - if (seg.numChars >= (1 << ccbits)) { - // Segment length value doesn't fit in the length field's bit-width, so fail immediately - goto continueOuter; - } - dataUsedBits += 4 + ccbits + seg.bitLength; - } - if (dataUsedBits <= dataCapacityBits) + int version, dataUsedBits; + for (version = minVersion; ; version++) { + int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; // Number of data bits available + dataUsedBits = QrSegment::getTotalBits(segs, version); + if (dataUsedBits != -1 && dataUsedBits <= dataCapacityBits) break; // This version number is found to be suitable - continueOuter:; + if (version >= maxVersion) // All versions in the range could not fit the given data + throw "Data too long"; + } + if (dataUsedBits == -1) + throw "Assertion error"; + + // Increase the error correction level while the data still fits in the current version number + const Ecc *newEcl = &ecl; + if (boostEcl) { + if (dataUsedBits <= getNumDataCodewords(version, Ecc::MEDIUM ) * 8) newEcl = &Ecc::MEDIUM ; + if (dataUsedBits <= getNumDataCodewords(version, Ecc::QUARTILE) * 8) newEcl = &Ecc::QUARTILE; + if (dataUsedBits <= getNumDataCodewords(version, Ecc::HIGH ) * 8) newEcl = &Ecc::HIGH ; } // Create the data bit string by concatenating all segments + int dataCapacityBits = getNumDataCodewords(version, *newEcl) * 8; BitBuffer bb; for (size_t i = 0; i < segs.size(); i++) { const QrSegment &seg(segs.at(i)); @@ -102,7 +102,7 @@ qrcodegen::QrCode qrcodegen::QrCode::encodeSegments(const std::vector throw "Assertion error"; // Create the QR Code symbol - return QrCode(version, ecl, bb.getBytes(), -1); + return QrCode(version, *newEcl, bb.getBytes(), mask); } diff --git a/cpp/QrCode.hpp b/cpp/QrCode.hpp index 9463279..7302fd3 100644 --- a/cpp/QrCode.hpp +++ b/cpp/QrCode.hpp @@ -83,13 +83,14 @@ public: /* - * Returns a QR Code symbol representing the given data segments at the given error - * correction level. The smallest possible QR Code version is automatically chosen for the output. + * Returns a QR Code symbol representing the specified data segments with the specified encoding parameters. + * The smallest possible QR Code version within the specified range is automatically chosen for the output. * This function allows the user to create a custom sequence of segments that switches - * between modes (such as alphanumeric and binary) to encode text more efficiently. This - * function is considered to be lower level than simply encoding text or binary data. + * between modes (such as alphanumeric and binary) to encode text more efficiently. + * This function is considered to be lower level than simply encoding text or binary data. */ - static QrCode encodeSegments(const std::vector &segs, const Ecc &ecl); + static QrCode encodeSegments(const std::vector &segs, const Ecc &ecl, + int minVersion=1, int maxVersion=40, int mask=-1, bool boostEcl=true); // All optional parameters diff --git a/cpp/QrSegment.cpp b/cpp/QrSegment.cpp index 0c48908..2b76cf4 100644 --- a/cpp/QrSegment.cpp +++ b/cpp/QrSegment.cpp @@ -22,6 +22,7 @@ * Software. */ +#include #include "BitBuffer.hpp" #include "QrSegment.hpp" @@ -128,6 +129,22 @@ qrcodegen::QrSegment::QrSegment(const Mode &md, int numCh, const std::vector &segs, int version) { + if (version < 1 || version > 40) + throw "Version number out of range"; + int result = 0; + for (size_t i = 0; i < segs.size(); i++) { + const QrSegment &seg(segs.at(i)); + int ccbits = seg.mode.numCharCountBits(version); + // Fail if segment length value doesn't fit in the length field's bit-width + if (seg.numChars >= (1 << ccbits)) + return -1; + result += 4 + ccbits + seg.bitLength; + } + return result; +} + + bool qrcodegen::QrSegment::isAlphanumeric(const char *text) { for (; *text != '\0'; text++) { char c = *text; diff --git a/cpp/QrSegment.hpp b/cpp/QrSegment.hpp index 18f7a51..54682ee 100644 --- a/cpp/QrSegment.hpp +++ b/cpp/QrSegment.hpp @@ -151,6 +151,10 @@ public: QrSegment(const Mode &md, int numCh, const std::vector &b, int bitLen); + // Package-private helper function. + static int getTotalBits(const std::vector &segs, int version); + + /*---- Constant ----*/ private: diff --git a/java/io/nayuki/qrcodegen/QrCode.java b/java/io/nayuki/qrcodegen/QrCode.java index 3fc5581..2abdebe 100644 --- a/java/io/nayuki/qrcodegen/QrCode.java +++ b/java/io/nayuki/qrcodegen/QrCode.java @@ -76,49 +76,66 @@ public final class QrCode { /** - * Returns a QR Code symbol representing the specified data segments at the specified error - * correction level. The smallest possible QR Code version is automatically chosen for the output. + * Returns a QR Code symbol representing the specified data segments at the specified error correction + * level or higher. The smallest possible QR Code version is automatically chosen for the output. *

This function allows the user to create a custom sequence of segments that switches * between modes (such as alphanumeric and binary) to encode text more efficiently. This * function is considered to be lower level than simply encoding text or binary data.

* @param segs the segments to encode - * @param ecl the error correction level to use + * @param ecl the error correction level to use (will be boosted) * @return a QR Code representing the segments * @throws NullPointerException if the list of segments, a segment, or the error correction level is {@code null} - * @throws IllegalArgumentException if the data fails to fit in the largest version QR Code, which means it is too long + * @throws IllegalArgumentException if the data is too long to fit in the largest version QR Code at the ECL */ public static QrCode encodeSegments(List segs, Ecc ecl) { + return encodeSegments(segs, ecl, 1, 40, -1, true); + } + + + /** + * Returns a QR Code symbol representing the specified data segments with the specified encoding parameters. + * The smallest possible QR Code version within the specified range is automatically chosen for the output. + *

This function allows the user to create a custom sequence of segments that switches + * between modes (such as alphanumeric and binary) to encode text more efficiently. + * This function is considered to be lower level than simply encoding text or binary data.

+ * @param segs the segments to encode + * @param ecl the error correction level to use (may be boosted) + * @param minVersion the minimum allowed version of the QR symbol (at least 1) + * @param maxVersion the maximum allowed version of the QR symbol (at most 40) + * @param mask the mask pattern to use, which is either -1 for automatic choice or from 0 to 7 for fixed choice + * @param boostEcl increases the error correction level if it can be done without increasing the version number + * @return a QR Code representing the segments + * @throws NullPointerException if the list of segments, a segment, or the error correction level is {@code null} + * @throws IllegalArgumentException if 1 ≤ minVersion ≤ maxVersion ≤ 40 is violated, or if mask + * < −1 or mask > 7, or if the data is too long to fit in a QR Code at maxVersion at the ECL + */ + public static QrCode encodeSegments(List segs, Ecc ecl, int minVersion, int maxVersion, int mask, boolean boostEcl) { if (segs == null || ecl == null) throw new NullPointerException(); + if (!(1 <= minVersion && minVersion <= maxVersion && maxVersion <= 40) || mask < -1 || mask > 7) + throw new IllegalArgumentException("Invalid value"); // Find the minimal version number to use - int version, dataCapacityBits; - outer: - for (version = 1; ; version++) { // Increment until the data fits in the QR Code - if (version > 40) // All versions could not fit the given data - throw new IllegalArgumentException("Data too long"); - dataCapacityBits = getNumDataCodewords(version, ecl) * 8; // Number of data bits available - - // Calculate the total number of bits needed at this version number - // to encode all the segments (i.e. segment metadata and payloads) - int dataUsedBits = 0; - for (QrSegment seg : segs) { - if (seg == null) - throw new NullPointerException(); - if (seg.numChars < 0) - throw new AssertionError(); - int ccbits = seg.mode.numCharCountBits(version); - if (seg.numChars >= (1 << ccbits)) { - // Segment length value doesn't fit in the length field's bit-width, so fail immediately - continue outer; - } - dataUsedBits += 4 + ccbits + seg.bitLength; - } - if (dataUsedBits <= dataCapacityBits) + int version, dataUsedBits; + for (version = minVersion; ; version++) { + int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; // Number of data bits available + dataUsedBits = QrSegment.getTotalBits(segs, version); + if (dataUsedBits != -1 && dataUsedBits <= dataCapacityBits) break; // This version number is found to be suitable + if (version >= maxVersion) // All versions in the range could not fit the given data + throw new IllegalArgumentException("Data too long"); + } + if (dataUsedBits == -1) + throw new AssertionError(); + + // Increase the error correction level while the data still fits in the current version number + for (Ecc newEcl : Ecc.values()) { + if (boostEcl && dataUsedBits <= getNumDataCodewords(version, newEcl) * 8) + ecl = newEcl; } // Create the data bit string by concatenating all segments + int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; BitBuffer bb = new BitBuffer(); for (QrSegment seg : segs) { bb.appendBits(seg.mode.modeBits, 4); @@ -137,7 +154,7 @@ public final class QrCode { throw new AssertionError(); // Create the QR Code symbol - return new QrCode(version, ecl, bb.getBytes(), -1); + return new QrCode(version, ecl, bb.getBytes(), mask); } @@ -732,7 +749,8 @@ public final class QrCode { * Represents the error correction level used in a QR Code symbol. */ public enum Ecc { - // Constants declared in ascending order of error protection. + // These enum constants must be declared in ascending order of error protection, + // for the sake of the implicit ordinal() method and values() function. LOW(1), MEDIUM(0), QUARTILE(3), HIGH(2); // In the range 0 to 3 (unsigned 2-bit integer). diff --git a/java/io/nayuki/qrcodegen/QrSegment.java b/java/io/nayuki/qrcodegen/QrSegment.java index 8ebc224..1315a48 100644 --- a/java/io/nayuki/qrcodegen/QrSegment.java +++ b/java/io/nayuki/qrcodegen/QrSegment.java @@ -186,6 +186,27 @@ public final class QrSegment { } + // Package-private helper function. + static int getTotalBits(List segs, int version) { + if (segs == null) + throw new NullPointerException(); + if (version < 1 || version > 40) + throw new IllegalArgumentException("Version number out of range"); + + int result = 0; + for (QrSegment seg : segs) { + if (seg == null) + throw new NullPointerException(); + int ccbits = seg.mode.numCharCountBits(version); + // Fail if segment length value doesn't fit in the length field's bit-width + if (seg.numChars >= (1 << ccbits)) + return -1; + result += 4 + ccbits + seg.bitLength; + } + return result; + } + + /*---- Constants ----*/ /** Can test whether a string is encodable in numeric mode (such as by using {@link #makeNumeric(String)}). */ diff --git a/javascript/qrcodegen-demo.js b/javascript/qrcodegen-demo.js index e4a21b6..f5b304b 100644 --- a/javascript/qrcodegen-demo.js +++ b/javascript/qrcodegen-demo.js @@ -88,7 +88,8 @@ function redrawQrCode() { segs.forEach(function(seg) { databits += 4 + seg.getMode().numCharCountBits(qr.getVersion()) + seg.getBits().length; }); - stats += ", data bits = " + databits + "."; + stats += ", error correction = level " + "LMQH".charAt(qr.getErrorCorrectionLevel().ordinal) + ", "; + stats += "data bits = " + databits + "."; var elem = document.getElementById("statistics-output"); while (elem.firstChild != null) elem.removeChild(elem.firstChild); diff --git a/javascript/qrcodegen.js b/javascript/qrcodegen.js index 77da0c7..1aae9ee 100644 --- a/javascript/qrcodegen.js +++ b/javascript/qrcodegen.js @@ -29,7 +29,8 @@ * Module "qrcodegen". Public members inside this namespace: * - Function encodeText(str text, QrCode.Ecc ecl) -> QrCode * - Function encodeBinary(list data, QrCode.Ecc ecl) -> QrCode - * - Function encodeSegments(list segs, QrCode.Ecc ecl) -> QrCode + * - Function encodeSegments(list segs, QrCode.Ecc ecl, + * int minVersion=1, int maxVersion=40, mask=-1, boostEcl=true) -> QrCode * - Class QrCode: * - Constructor QrCode(QrCode qr, int mask) * - Constructor QrCode(list bytes, int mask, int version, QrCode.Ecc ecl) @@ -84,40 +85,39 @@ var qrcodegen = new function() { /* - * Returns a QR Code symbol representing the given data segments at the given error - * correction level. The smallest possible QR Code version is automatically chosen for the output. + * Returns a QR Code symbol representing the specified data segments with the specified encoding parameters. + * The smallest possible QR Code version within the specified range is automatically chosen for the output. * This function allows the user to create a custom sequence of segments that switches - * between modes (such as alphanumeric and binary) to encode text more efficiently. This - * function is considered to be lower level than simply encoding text or binary data. + * between modes (such as alphanumeric and binary) to encode text more efficiently. + * This function is considered to be lower level than simply encoding text or binary data. */ - this.encodeSegments = function(segs, ecl) { + this.encodeSegments = function(segs, ecl, minVersion, maxVersion, mask, boostEcl) { + if (minVersion == undefined) minVersion = 1; + if (maxVersion == undefined) maxVersion = 40; + if (mask == undefined) mask = -1; + if (boostEcl == undefined) boostEcl = true; + if (!(1 <= minVersion && minVersion <= maxVersion && maxVersion <= 40) || mask < -1 || mask > 7) + throw "Invalid value"; + // Find the minimal version number to use - var version, dataCapacityBits; - outer: - for (version = 1; ; version++) { // Increment until the data fits in the QR Code - if (version > 40) // All versions could not fit the given data - throw "Data too long"; - dataCapacityBits = QrCode.getNumDataCodewords(version, ecl) * 8; // Number of data bits available - - // Calculate the total number of bits needed at this version number - // to encode all the segments (i.e. segment metadata and payloads) - var dataUsedBits = 0; - for (var i = 0; i < segs.length; i++) { - var seg = segs[i]; - if (seg.numChars < 0) - throw "Assertion error"; - var ccbits = seg.getMode().numCharCountBits(version); - if (seg.getNumChars() >= (1 << ccbits)) { - // Segment length value doesn't fit in the length field's bit-width, so fail immediately - continue outer; - } - dataUsedBits += 4 + ccbits + seg.getBits().length; - } - if (dataUsedBits <= dataCapacityBits) + var version, dataUsedBits; + for (version = minVersion; ; version++) { + var dataCapacityBits = QrCode.getNumDataCodewords(version, ecl) * 8; // Number of data bits available + dataUsedBits = this.QrSegment.getTotalBits(segs, version); + if (dataUsedBits != null && dataUsedBits <= dataCapacityBits) break; // This version number is found to be suitable + if (version >= maxVersion) // All versions in the range could not fit the given data + throw "Data too long"; } + // Increase the error correction level while the data still fits in the current version number + [this.QrCode.Ecc.MEDIUM, this.QrCode.Ecc.QUARTILE, this.QrCode.Ecc.HIGH].forEach(function(newEcl) { + if (boostEcl && dataUsedBits <= QrCode.getNumDataCodewords(version, newEcl) * 8) + ecl = newEcl; + }); + // Create the data bit string by concatenating all segments + var dataCapacityBits = QrCode.getNumDataCodewords(version, ecl) * 8; var bb = new BitBuffer(); segs.forEach(function(seg) { bb.appendBits(seg.getMode().getModeBits(), 4); @@ -136,7 +136,7 @@ var qrcodegen = new function() { throw "Assertion error"; // Create the QR Code symbol - return new this.QrCode(bb.getBytes(), -1, version, ecl); + return new this.QrCode(bb.getBytes(), mask, version, ecl); }; @@ -775,6 +775,21 @@ var qrcodegen = new function() { return [this.makeBytes(toUtf8ByteArray(text))]; }; + // Package-private helper function. + this.QrSegment.getTotalBits = function(segs, version) { + if (version < 1 || version > 40) + throw "Version number out of range"; + var result = 0; + segs.forEach(function(seg) { + var ccbits = seg.getMode().numCharCountBits(version); + // Fail if segment length value doesn't fit in the length field's bit-width + if (seg.getNumChars() >= (1 << ccbits)) + return null; + result += 4 + ccbits + seg.getBits().length; + }); + return result; + }; + /*-- Constants --*/ var QrSegment = {}; // Private object to assign properties to diff --git a/python/qrcodegen.py b/python/qrcodegen.py index 1aefcda..ab7f467 100644 --- a/python/qrcodegen.py +++ b/python/qrcodegen.py @@ -29,7 +29,8 @@ import itertools, re, sys Public members inside this module "qrcodegen": - Function encode_text(str text, QrCode.Ecc ecl) -> QrCode - Function encode_binary(bytes data, QrCode.Ecc ecl) -> QrCode -- Function encode_segments(list segs, QrCode.Ecc ecl) -> QrCode +- Function encode_segments(list segs, QrCode.Ecc ecl, + int minversion=1, int maxversion=40, mask=-1, boostecl=true) -> QrCode - Class QrCode: - Constructor QrCode(QrCode qr, int mask) - Constructor QrCode(bytes bytes, int mask, int version, QrCode.Ecc ecl) @@ -77,35 +78,34 @@ def encode_binary(data, ecl): return QrCode.encode_segments([QrSegment.make_bytes(data)], ecl) -def encode_segments(segs, ecl): - """Returns a QR Code symbol representing the given data segments at the given error - correction level. The smallest possible QR Code version is automatically chosen for the output. +def encode_segments(segs, ecl, minversion=1, maxversion=40, mask=-1, boostecl=True): + """Returns a QR Code symbol representing the specified data segments with the specified encoding parameters. + The smallest possible QR Code version within the specified range is automatically chosen for the output. This function allows the user to create a custom sequence of segments that switches - between modes (such as alphanumeric and binary) to encode text more efficiently. This - function is considered to be lower level than simply encoding text or binary data.""" + between modes (such as alphanumeric and binary) to encode text more efficiently. + This function is considered to be lower level than simply encoding text or binary data.""" + + if not 1 <= minversion <= maxversion <= 40 or not -1 <= mask <= 7: + raise ValueError("Invalid value") # Find the minimal version number to use - for version in itertools.count(1): # Increment until the data fits in the QR Code - if version > 40: # All versions could not fit the given data - raise ValueError("Data too long") + for version in range(minversion, maxversion + 1): datacapacitybits = QrCode._get_num_data_codewords(version, ecl) * 8 # Number of data bits available - - # Calculate the total number of bits needed at this version number - # to encode all the segments (i.e. segment metadata and payloads) - datausedbits = 0 - for seg in segs: - if seg.get_num_chars() < 0: - raise AssertionError() - ccbits = seg.get_mode().num_char_count_bits(version) - if seg.get_num_chars() >= (1 << ccbits): - # Segment length value doesn't fit in the length field's bit-width, so fail immediately - break - datausedbits += 4 + ccbits + len(seg.get_bits()) - else: # If the loop above did not break - if datausedbits <= datacapacitybits: - break # This version number is found to be suitable + datausedbits = QrSegment.get_total_bits(segs, version) + if datausedbits is not None and datausedbits <= datacapacitybits: + break # This version number is found to be suitable + if version >= maxversion: # All versions in the range could not fit the given data + raise ValueError("Data too long") + if datausedbits is None: + raise AssertionError() + + # Increase the error correction level while the data still fits in the current version number + for newecl in (QrCode.Ecc.MEDIUM, QrCode.Ecc.QUARTILE, QrCode.Ecc.HIGH): + if boostecl and datausedbits <= QrCode._get_num_data_codewords(version, newecl) * 8: + ecl = newecl # Create the data bit string by concatenating all segments + datacapacitybits = QrCode._get_num_data_codewords(version, ecl) * 8 bb = _BitBuffer() for seg in segs: bb.append_bits(seg.get_mode().get_mode_bits(), 4) @@ -124,7 +124,7 @@ def encode_segments(segs, ecl): assert bb.bit_length() % 8 == 0 # Create the QR Code symbol - return QrCode(datacodewords=bb.get_bytes(), mask=-1, version=version, errcorlvl=ecl) + return QrCode(None, bb.get_bytes(), mask, version, ecl) @@ -686,6 +686,21 @@ class QrSegment(object): return list(self._bitdata) # Defensive copy + # Package-private helper function. + @staticmethod + def get_total_bits(segs, version): + if not 1 <= version <= 40: + raise ValueError("Version number out of range") + result = 0 + for seg in segs: + ccbits = seg.get_mode().num_char_count_bits(version) + # Fail if segment length value doesn't fit in the length field's bit-width + if seg.get_num_chars() >= (1 << ccbits): + return None + result += 4 + ccbits + len(seg.get_bits()) + return result + + # -- Constants -- # Can test whether a string is encodable in numeric mode (such as by using make_numeric())