1 /* 2 * Fast QR Code generator library 3 * 4 * Copyright (c) Project Nayuki. (MIT License) 5 * https://www.nayuki.io/page/fast-qr-code-generator-library 6 * 7 * Permission is hereby granted, free of charge, to any person obtaining a copy of 8 * this software and associated documentation files (the "Software"), to deal in 9 * the Software without restriction, including without limitation the rights to 10 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11 * the Software, and to permit persons to whom the Software is furnished to do so, 12 * subject to the following conditions: 13 * - The above copyright notice and this permission notice shall be included in 14 * all copies or substantial portions of the Software. 15 * - The Software is provided "as is", without warranty of any kind, express or 16 * implied, including but not limited to the warranties of merchantability, 17 * fitness for a particular purpose and noninfringement. In no event shall the 18 * authors or copyright holders be liable for any claim, damages or other 19 * liability, whether in an action of contract, tort or otherwise, arising from, 20 * out of or in connection with the Software or the use or other dealings in the 21 * Software. 22 */ 23 24 package io.nayuki.fastqrcodegen; 25 26 import java.nio.charset.StandardCharsets; 27 import java.util.ArrayList; 28 import java.util.Arrays; 29 import java.util.List; 30 import java.util.Objects; 31 32 33 /** 34 * A segment of character/binary/control data in a QR Code symbol. 35 * Instances of this class are immutable. 36 * <p>The mid-level way to create a segment is to take the payload data and call a 37 * static factory function such as {@link QrSegment#makeNumeric(String)}. The low-level 38 * way to create a segment is to custom-make the bit buffer and call the {@link 39 * QrSegment#QrSegment(Mode,int,int[],int) constructor} with appropriate values.</p> 40 * <p>This segment class imposes no length restrictions, but QR Codes have restrictions. 41 * Even in the most favorable conditions, a QR Code can only hold 7089 characters of data. 42 * Any segment longer than this is meaningless for the purpose of generating QR Codes. 43 * This class can represent kanji mode segments, but provides no help in encoding them 44 * - see {@link QrSegmentAdvanced} for full kanji support.</p> 45 */ 46 public final class QrSegment { 47 48 /*---- Static factory functions (mid level) ----*/ 49 50 /** 51 * Returns a segment representing the specified binary data 52 * encoded in byte mode. All input byte arrays are acceptable. 53 * <p>Any text string can be converted to UTF-8 bytes ({@code 54 * s.getBytes(StandardCharsets.UTF_8)}) and encoded as a byte mode segment.</p> 55 * @param data the binary data (not {@code null}) 56 * @return a segment (not {@code null}) containing the data 57 * @throws NullPointerException if the array is {@code null} 58 */ makeBytes(byte[] data)59 public static QrSegment makeBytes(byte[] data) { 60 Objects.requireNonNull(data); 61 if (data.length * 8L > Integer.MAX_VALUE) 62 throw new IllegalArgumentException("Data too long"); 63 int[] bits = new int[(data.length + 3) / 4]; 64 for (int i = 0; i < data.length; i++) 65 bits[i >>> 2] |= (data[i] & 0xFF) << (~i << 3); 66 return new QrSegment(Mode.BYTE, data.length, bits, data.length * 8); 67 } 68 69 70 /** 71 * Returns a segment representing the specified string of decimal digits encoded in numeric mode. 72 * @param digits the text (not {@code null}), with only digits from 0 to 9 allowed 73 * @return a segment (not {@code null}) containing the text 74 * @throws NullPointerException if the string is {@code null} 75 * @throws IllegalArgumentException if the string contains non-digit characters 76 */ makeNumeric(String digits)77 public static QrSegment makeNumeric(String digits) { 78 Objects.requireNonNull(digits); 79 BitBuffer bb = new BitBuffer(); 80 int accumData = 0; 81 int accumCount = 0; 82 for (int i = 0; i < digits.length(); i++) { 83 char c = digits.charAt(i); 84 if (c < '0' || c > '9') 85 throw new IllegalArgumentException("String contains non-numeric characters"); 86 accumData = accumData * 10 + (c - '0'); 87 accumCount++; 88 if (accumCount == 3) { 89 bb.appendBits(accumData, 10); 90 accumData = 0; 91 accumCount = 0; 92 } 93 } 94 if (accumCount > 0) // 1 or 2 digits remaining 95 bb.appendBits(accumData, accumCount * 3 + 1); 96 return new QrSegment(Mode.NUMERIC, digits.length(), bb.data, bb.bitLength); 97 } 98 99 100 /** 101 * Returns a segment representing the specified text string encoded in alphanumeric mode. 102 * The characters allowed are: 0 to 9, A to Z (uppercase only), space, 103 * dollar, percent, asterisk, plus, hyphen, period, slash, colon. 104 * @param text the text (not {@code null}), with only certain characters allowed 105 * @return a segment (not {@code null}) containing the text 106 * @throws NullPointerException if the string is {@code null} 107 * @throws IllegalArgumentException if the string contains non-encodable characters 108 */ makeAlphanumeric(String text)109 public static QrSegment makeAlphanumeric(String text) { 110 Objects.requireNonNull(text); 111 BitBuffer bb = new BitBuffer(); 112 int accumData = 0; 113 int accumCount = 0; 114 for (int i = 0; i < text.length(); i++) { 115 char c = text.charAt(i); 116 if (c >= ALPHANUMERIC_MAP.length || ALPHANUMERIC_MAP[c] == -1) 117 throw new IllegalArgumentException("String contains unencodable characters in alphanumeric mode"); 118 accumData = accumData * 45 + ALPHANUMERIC_MAP[c]; 119 accumCount++; 120 if (accumCount == 2) { 121 bb.appendBits(accumData, 11); 122 accumData = 0; 123 accumCount = 0; 124 } 125 } 126 if (accumCount > 0) // 1 character remaining 127 bb.appendBits(accumData, 6); 128 return new QrSegment(Mode.ALPHANUMERIC, text.length(), bb.data, bb.bitLength); 129 } 130 131 132 /** 133 * Returns a list of zero or more segments to represent the specified Unicode text string. 134 * The result may use various segment modes and switch modes to optimize the length of the bit stream. 135 * @param text the text to be encoded, which can be any Unicode string 136 * @return a new mutable list (not {@code null}) of segments (not {@code null}) containing the text 137 * @throws NullPointerException if the text is {@code null} 138 */ makeSegments(String text)139 public static List<QrSegment> makeSegments(String text) { 140 Objects.requireNonNull(text); 141 142 // Select the most efficient segment encoding automatically 143 List<QrSegment> result = new ArrayList<>(); 144 if (text.equals("")); // Leave result empty 145 else if (isNumeric(text)) 146 result.add(makeNumeric(text)); 147 else if (isAlphanumeric(text)) 148 result.add(makeAlphanumeric(text)); 149 else 150 result.add(makeBytes(text.getBytes(StandardCharsets.UTF_8))); 151 return result; 152 } 153 154 155 /** 156 * Returns a segment representing an Extended Channel Interpretation 157 * (ECI) designator with the specified assignment value. 158 * @param assignVal the ECI assignment number (see the AIM ECI specification) 159 * @return a segment (not {@code null}) containing the data 160 * @throws IllegalArgumentException if the value is outside the range [0, 10<sup>6</sup>) 161 */ makeEci(int assignVal)162 public static QrSegment makeEci(int assignVal) { 163 BitBuffer bb = new BitBuffer(); 164 if (assignVal < 0) 165 throw new IllegalArgumentException("ECI assignment value out of range"); 166 else if (assignVal < (1 << 7)) 167 bb.appendBits(assignVal, 8); 168 else if (assignVal < (1 << 14)) { 169 bb.appendBits(2, 2); 170 bb.appendBits(assignVal, 14); 171 } else if (assignVal < 1_000_000) { 172 bb.appendBits(6, 3); 173 bb.appendBits(assignVal, 21); 174 } else 175 throw new IllegalArgumentException("ECI assignment value out of range"); 176 return new QrSegment(Mode.ECI, 0, bb.data, bb.bitLength); 177 } 178 179 180 /** 181 * Tests whether the specified string can be encoded as a segment in numeric mode. 182 * A string is encodable iff each character is in the range 0 to 9. 183 * @param text the string to test for encodability (not {@code null}) 184 * @return {@code true} iff each character is in the range 0 to 9. 185 * @throws NullPointerException if the string is {@code null} 186 * @see #makeNumeric(String) 187 */ isNumeric(String text)188 public static boolean isNumeric(String text) { 189 for (int i = 0; i < text.length(); i++) { 190 char c = text.charAt(i); 191 if (c < '0' || c > '9') 192 return false; 193 } 194 return true; 195 } 196 197 198 /** 199 * Tests whether the specified string can be encoded as a segment in alphanumeric mode. 200 * A string is encodable iff each character is in the following set: 0 to 9, A to Z 201 * (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon. 202 * @param text the string to test for encodability (not {@code null}) 203 * @return {@code true} iff each character is in the alphanumeric mode character set 204 * @throws NullPointerException if the string is {@code null} 205 * @see #makeAlphanumeric(String) 206 */ isAlphanumeric(String text)207 public static boolean isAlphanumeric(String text) { 208 for (int i = 0; i < text.length(); i++) { 209 char c = text.charAt(i); 210 if (c >= ALPHANUMERIC_MAP.length || ALPHANUMERIC_MAP[c] == -1) 211 return false; 212 } 213 return true; 214 } 215 216 217 218 /*---- Instance fields ----*/ 219 220 /** The mode indicator of this segment. Not {@code null}. */ 221 public final Mode mode; 222 223 /** The length of this segment's unencoded data. Measured in characters for 224 * numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode. 225 * Always zero or positive. Not the same as the data's bit length. */ 226 public final int numChars; 227 228 // The data bits of this segment. Not null. 229 final int[] data; 230 231 // Requires 0 <= bitLength <= data.length * 32. 232 final int bitLength; 233 234 235 /*---- Constructor (low level) ----*/ 236 237 /** 238 * Constructs a QR Code segment with the specified attributes and data. 239 * The character count (numCh) must agree with the mode and the bit buffer length, 240 * but the constraint isn't checked. The specified bit buffer is cloned and stored. 241 * @param md the mode (not {@code null}) 242 * @param numCh the data length in characters or bytes, which is non-negative 243 * @param data the data bits (not {@code null}) 244 * @param bitLen the number of valid prefix bits in the data array 245 * @throws NullPointerException if the mode or data is {@code null} 246 * @throws IllegalArgumentException if the character count is negative 247 */ QrSegment(Mode md, int numCh, int[] data, int bitLen)248 public QrSegment(Mode md, int numCh, int[] data, int bitLen) { 249 mode = Objects.requireNonNull(md); 250 this.data = Objects.requireNonNull(data); 251 if (numCh < 0 || bitLen < 0 || bitLen > data.length * 32L) 252 throw new IllegalArgumentException("Invalid value"); 253 numChars = numCh; 254 bitLength = bitLen; 255 } 256 257 258 // Calculates the number of bits needed to encode the given segments at the given version. 259 // Returns a non-negative number if successful. Otherwise returns -1 if a segment has too 260 // many characters to fit its length field, or the total bits exceeds Integer.MAX_VALUE. getTotalBits(List<QrSegment> segs, int version)261 static int getTotalBits(List<QrSegment> segs, int version) { 262 Objects.requireNonNull(segs); 263 long result = 0; 264 for (QrSegment seg : segs) { 265 Objects.requireNonNull(seg); 266 int ccbits = seg.mode.numCharCountBits(version); 267 if (seg.numChars >= (1 << ccbits)) 268 return -1; // The segment's length doesn't fit the field's bit width 269 result += 4L + ccbits + seg.bitLength; 270 if (result > Integer.MAX_VALUE) 271 return -1; // The sum will overflow an int type 272 } 273 return (int)result; 274 } 275 276 277 /*---- Constants ----*/ 278 279 static final int[] ALPHANUMERIC_MAP; 280 281 static { 282 final String ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"; 283 int maxCh = -1; 284 for (int i = 0; i < ALPHANUMERIC_CHARSET.length(); i++) 285 maxCh = Math.max(ALPHANUMERIC_CHARSET.charAt(i), maxCh); 286 ALPHANUMERIC_MAP = new int[maxCh + 1]; Arrays.fill(ALPHANUMERIC_MAP, -1)287 Arrays.fill(ALPHANUMERIC_MAP, -1); 288 for (int i = 0; i < ALPHANUMERIC_CHARSET.length(); i++) 289 ALPHANUMERIC_MAP[ALPHANUMERIC_CHARSET.charAt(i)] = i; 290 } 291 292 293 294 /*---- Public helper enumeration ----*/ 295 296 /** 297 * Describes how a segment's data bits are interpreted. 298 */ 299 public enum Mode { 300 301 /*-- Constants --*/ 302 303 NUMERIC (0x1, 10, 12, 14), 304 ALPHANUMERIC(0x2, 9, 11, 13), 305 BYTE (0x4, 8, 16, 16), 306 KANJI (0x8, 8, 10, 12), 307 ECI (0x7, 0, 0, 0); 308 309 310 /*-- Fields --*/ 311 312 // The mode indicator bits, which is a uint4 value (range 0 to 15). 313 final int modeBits; 314 315 // Number of character count bits for three different version ranges. 316 private final int[] numBitsCharCount; 317 318 319 /*-- Constructor --*/ 320 Mode(int mode, int... ccbits)321 private Mode(int mode, int... ccbits) { 322 modeBits = mode; 323 numBitsCharCount = ccbits; 324 } 325 326 327 /*-- Method --*/ 328 329 // Returns the bit width of the character count field for a segment in this mode 330 // in a QR Code at the given version number. The result is in the range [0, 16]. numCharCountBits(int ver)331 int numCharCountBits(int ver) { 332 assert QrCode.MIN_VERSION <= ver && ver <= QrCode.MAX_VERSION; 333 return numBitsCharCount[(ver + 7) / 17]; 334 } 335 336 } 337 338 } 339