1 /* 2 * Copyright 2013 ZXing authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.zxing.aztec.encoder; 18 19 import com.google.zxing.common.BitArray; 20 import com.google.zxing.common.BitMatrix; 21 import com.google.zxing.common.reedsolomon.GenericGF; 22 import com.google.zxing.common.reedsolomon.ReedSolomonEncoder; 23 24 import java.nio.charset.Charset; 25 import java.nio.charset.StandardCharsets; 26 27 /** 28 * Generates Aztec 2D barcodes. 29 * 30 * @author Rustam Abdullaev 31 */ 32 public final class Encoder { 33 34 public static final int DEFAULT_EC_PERCENT = 33; // default minimal percentage of error check words 35 public static final int DEFAULT_AZTEC_LAYERS = 0; 36 private static final int MAX_NB_BITS = 32; 37 private static final int MAX_NB_BITS_COMPACT = 4; 38 39 private static final int[] WORD_SIZE = { 40 4, 6, 6, 8, 8, 8, 8, 8, 8, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 41 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 42 }; 43 Encoder()44 private Encoder() { 45 } 46 47 /** 48 * Encodes the given string content as an Aztec symbol (without ECI code) 49 * 50 * @param data input data string; must be encodable as ISO/IEC 8859-1 (Latin-1) 51 * @return Aztec symbol matrix with metadata 52 */ encode(String data)53 public static AztecCode encode(String data) { 54 return encode(data.getBytes(StandardCharsets.ISO_8859_1)); 55 } 56 57 /** 58 * Encodes the given string content as an Aztec symbol (without ECI code) 59 * 60 * @param data input data string; must be encodable as ISO/IEC 8859-1 (Latin-1) 61 * @param minECCPercent minimal percentage of error check words (According to ISO/IEC 24778:2008, 62 * a minimum of 23% + 3 words is recommended) 63 * @param userSpecifiedLayers if non-zero, a user-specified value for the number of layers 64 * @return Aztec symbol matrix with metadata 65 */ encode(String data, int minECCPercent, int userSpecifiedLayers)66 public static AztecCode encode(String data, int minECCPercent, int userSpecifiedLayers) { 67 return encode(data.getBytes(StandardCharsets.ISO_8859_1), minECCPercent, userSpecifiedLayers, null); 68 } 69 70 /** 71 * Encodes the given string content as an Aztec symbol 72 * 73 * @param data input data string 74 * @param minECCPercent minimal percentage of error check words (According to ISO/IEC 24778:2008, 75 * a minimum of 23% + 3 words is recommended) 76 * @param userSpecifiedLayers if non-zero, a user-specified value for the number of layers 77 * @param charset character set in which to encode string using ECI; if null, no ECI code 78 * will be inserted, and the string must be encodable as ISO/IEC 8859-1 79 * (Latin-1), the default encoding of the symbol. 80 * @return Aztec symbol matrix with metadata 81 */ encode(String data, int minECCPercent, int userSpecifiedLayers, Charset charset)82 public static AztecCode encode(String data, int minECCPercent, int userSpecifiedLayers, Charset charset) { 83 byte[] bytes = data.getBytes(null != charset ? charset : StandardCharsets.ISO_8859_1); 84 return encode(bytes, minECCPercent, userSpecifiedLayers, charset); 85 } 86 87 /** 88 * Encodes the given binary content as an Aztec symbol (without ECI code) 89 * 90 * @param data input data string 91 * @return Aztec symbol matrix with metadata 92 */ encode(byte[] data)93 public static AztecCode encode(byte[] data) { 94 return encode(data, DEFAULT_EC_PERCENT, DEFAULT_AZTEC_LAYERS, null); 95 } 96 97 /** 98 * Encodes the given binary content as an Aztec symbol (without ECI code) 99 * 100 * @param data input data string 101 * @param minECCPercent minimal percentage of error check words (According to ISO/IEC 24778:2008, 102 * a minimum of 23% + 3 words is recommended) 103 * @param userSpecifiedLayers if non-zero, a user-specified value for the number of layers 104 * @return Aztec symbol matrix with metadata 105 */ encode(byte[] data, int minECCPercent, int userSpecifiedLayers)106 public static AztecCode encode(byte[] data, int minECCPercent, int userSpecifiedLayers) { 107 return encode(data, minECCPercent, userSpecifiedLayers, null); 108 } 109 110 /** 111 * Encodes the given binary content as an Aztec symbol 112 * 113 * @param data input data string 114 * @param minECCPercent minimal percentage of error check words (According to ISO/IEC 24778:2008, 115 * a minimum of 23% + 3 words is recommended) 116 * @param userSpecifiedLayers if non-zero, a user-specified value for the number of layers 117 * @param charset character set to mark using ECI; if null, no ECI code will be inserted, and the 118 * default encoding of ISO/IEC 8859-1 will be assuming by readers. 119 * @return Aztec symbol matrix with metadata 120 */ encode(byte[] data, int minECCPercent, int userSpecifiedLayers, Charset charset)121 public static AztecCode encode(byte[] data, int minECCPercent, int userSpecifiedLayers, Charset charset) { 122 // High-level encode 123 BitArray bits = new HighLevelEncoder(data, charset).encode(); 124 125 // stuff bits and choose symbol size 126 int eccBits = bits.getSize() * minECCPercent / 100 + 11; 127 int totalSizeBits = bits.getSize() + eccBits; 128 boolean compact; 129 int layers; 130 int totalBitsInLayer; 131 int wordSize; 132 BitArray stuffedBits; 133 if (userSpecifiedLayers != DEFAULT_AZTEC_LAYERS) { 134 compact = userSpecifiedLayers < 0; 135 layers = Math.abs(userSpecifiedLayers); 136 if (layers > (compact ? MAX_NB_BITS_COMPACT : MAX_NB_BITS)) { 137 throw new IllegalArgumentException( 138 String.format("Illegal value %s for layers", userSpecifiedLayers)); 139 } 140 totalBitsInLayer = totalBitsInLayer(layers, compact); 141 wordSize = WORD_SIZE[layers]; 142 int usableBitsInLayers = totalBitsInLayer - (totalBitsInLayer % wordSize); 143 stuffedBits = stuffBits(bits, wordSize); 144 if (stuffedBits.getSize() + eccBits > usableBitsInLayers) { 145 throw new IllegalArgumentException("Data to large for user specified layer"); 146 } 147 if (compact && stuffedBits.getSize() > wordSize * 64) { 148 // Compact format only allows 64 data words, though C4 can hold more words than that 149 throw new IllegalArgumentException("Data to large for user specified layer"); 150 } 151 } else { 152 wordSize = 0; 153 stuffedBits = null; 154 // We look at the possible table sizes in the order Compact1, Compact2, Compact3, 155 // Compact4, Normal4,... Normal(i) for i < 4 isn't typically used since Compact(i+1) 156 // is the same size, but has more data. 157 for (int i = 0; ; i++) { 158 if (i > MAX_NB_BITS) { 159 throw new IllegalArgumentException("Data too large for an Aztec code"); 160 } 161 compact = i <= 3; 162 layers = compact ? i + 1 : i; 163 totalBitsInLayer = totalBitsInLayer(layers, compact); 164 if (totalSizeBits > totalBitsInLayer) { 165 continue; 166 } 167 // [Re]stuff the bits if this is the first opportunity, or if the 168 // wordSize has changed 169 if (stuffedBits == null || wordSize != WORD_SIZE[layers]) { 170 wordSize = WORD_SIZE[layers]; 171 stuffedBits = stuffBits(bits, wordSize); 172 } 173 int usableBitsInLayers = totalBitsInLayer - (totalBitsInLayer % wordSize); 174 if (compact && stuffedBits.getSize() > wordSize * 64) { 175 // Compact format only allows 64 data words, though C4 can hold more words than that 176 continue; 177 } 178 if (stuffedBits.getSize() + eccBits <= usableBitsInLayers) { 179 break; 180 } 181 } 182 } 183 BitArray messageBits = generateCheckWords(stuffedBits, totalBitsInLayer, wordSize); 184 185 // generate mode message 186 int messageSizeInWords = stuffedBits.getSize() / wordSize; 187 BitArray modeMessage = generateModeMessage(compact, layers, messageSizeInWords); 188 189 // allocate symbol 190 int baseMatrixSize = (compact ? 11 : 14) + layers * 4; // not including alignment lines 191 int[] alignmentMap = new int[baseMatrixSize]; 192 int matrixSize; 193 if (compact) { 194 // no alignment marks in compact mode, alignmentMap is a no-op 195 matrixSize = baseMatrixSize; 196 for (int i = 0; i < alignmentMap.length; i++) { 197 alignmentMap[i] = i; 198 } 199 } else { 200 matrixSize = baseMatrixSize + 1 + 2 * ((baseMatrixSize / 2 - 1) / 15); 201 int origCenter = baseMatrixSize / 2; 202 int center = matrixSize / 2; 203 for (int i = 0; i < origCenter; i++) { 204 int newOffset = i + i / 15; 205 alignmentMap[origCenter - i - 1] = center - newOffset - 1; 206 alignmentMap[origCenter + i] = center + newOffset + 1; 207 } 208 } 209 BitMatrix matrix = new BitMatrix(matrixSize); 210 211 // draw data bits 212 for (int i = 0, rowOffset = 0; i < layers; i++) { 213 int rowSize = (layers - i) * 4 + (compact ? 9 : 12); 214 for (int j = 0; j < rowSize; j++) { 215 int columnOffset = j * 2; 216 for (int k = 0; k < 2; k++) { 217 if (messageBits.get(rowOffset + columnOffset + k)) { 218 matrix.set(alignmentMap[i * 2 + k], alignmentMap[i * 2 + j]); 219 } 220 if (messageBits.get(rowOffset + rowSize * 2 + columnOffset + k)) { 221 matrix.set(alignmentMap[i * 2 + j], alignmentMap[baseMatrixSize - 1 - i * 2 - k]); 222 } 223 if (messageBits.get(rowOffset + rowSize * 4 + columnOffset + k)) { 224 matrix.set(alignmentMap[baseMatrixSize - 1 - i * 2 - k], alignmentMap[baseMatrixSize - 1 - i * 2 - j]); 225 } 226 if (messageBits.get(rowOffset + rowSize * 6 + columnOffset + k)) { 227 matrix.set(alignmentMap[baseMatrixSize - 1 - i * 2 - j], alignmentMap[i * 2 + k]); 228 } 229 } 230 } 231 rowOffset += rowSize * 8; 232 } 233 234 // draw mode message 235 drawModeMessage(matrix, compact, matrixSize, modeMessage); 236 237 // draw alignment marks 238 if (compact) { 239 drawBullsEye(matrix, matrixSize / 2, 5); 240 } else { 241 drawBullsEye(matrix, matrixSize / 2, 7); 242 for (int i = 0, j = 0; i < baseMatrixSize / 2 - 1; i += 15, j += 16) { 243 for (int k = (matrixSize / 2) & 1; k < matrixSize; k += 2) { 244 matrix.set(matrixSize / 2 - j, k); 245 matrix.set(matrixSize / 2 + j, k); 246 matrix.set(k, matrixSize / 2 - j); 247 matrix.set(k, matrixSize / 2 + j); 248 } 249 } 250 } 251 252 AztecCode aztec = new AztecCode(); 253 aztec.setCompact(compact); 254 aztec.setSize(matrixSize); 255 aztec.setLayers(layers); 256 aztec.setCodeWords(messageSizeInWords); 257 aztec.setMatrix(matrix); 258 return aztec; 259 } 260 drawBullsEye(BitMatrix matrix, int center, int size)261 private static void drawBullsEye(BitMatrix matrix, int center, int size) { 262 for (int i = 0; i < size; i += 2) { 263 for (int j = center - i; j <= center + i; j++) { 264 matrix.set(j, center - i); 265 matrix.set(j, center + i); 266 matrix.set(center - i, j); 267 matrix.set(center + i, j); 268 } 269 } 270 matrix.set(center - size, center - size); 271 matrix.set(center - size + 1, center - size); 272 matrix.set(center - size, center - size + 1); 273 matrix.set(center + size, center - size); 274 matrix.set(center + size, center - size + 1); 275 matrix.set(center + size, center + size - 1); 276 } 277 generateModeMessage(boolean compact, int layers, int messageSizeInWords)278 static BitArray generateModeMessage(boolean compact, int layers, int messageSizeInWords) { 279 BitArray modeMessage = new BitArray(); 280 if (compact) { 281 modeMessage.appendBits(layers - 1, 2); 282 modeMessage.appendBits(messageSizeInWords - 1, 6); 283 modeMessage = generateCheckWords(modeMessage, 28, 4); 284 } else { 285 modeMessage.appendBits(layers - 1, 5); 286 modeMessage.appendBits(messageSizeInWords - 1, 11); 287 modeMessage = generateCheckWords(modeMessage, 40, 4); 288 } 289 return modeMessage; 290 } 291 drawModeMessage(BitMatrix matrix, boolean compact, int matrixSize, BitArray modeMessage)292 private static void drawModeMessage(BitMatrix matrix, boolean compact, int matrixSize, BitArray modeMessage) { 293 int center = matrixSize / 2; 294 if (compact) { 295 for (int i = 0; i < 7; i++) { 296 int offset = center - 3 + i; 297 if (modeMessage.get(i)) { 298 matrix.set(offset, center - 5); 299 } 300 if (modeMessage.get(i + 7)) { 301 matrix.set(center + 5, offset); 302 } 303 if (modeMessage.get(20 - i)) { 304 matrix.set(offset, center + 5); 305 } 306 if (modeMessage.get(27 - i)) { 307 matrix.set(center - 5, offset); 308 } 309 } 310 } else { 311 for (int i = 0; i < 10; i++) { 312 int offset = center - 5 + i + i / 5; 313 if (modeMessage.get(i)) { 314 matrix.set(offset, center - 7); 315 } 316 if (modeMessage.get(i + 10)) { 317 matrix.set(center + 7, offset); 318 } 319 if (modeMessage.get(29 - i)) { 320 matrix.set(offset, center + 7); 321 } 322 if (modeMessage.get(39 - i)) { 323 matrix.set(center - 7, offset); 324 } 325 } 326 } 327 } 328 generateCheckWords(BitArray bitArray, int totalBits, int wordSize)329 private static BitArray generateCheckWords(BitArray bitArray, int totalBits, int wordSize) { 330 // bitArray is guaranteed to be a multiple of the wordSize, so no padding needed 331 int messageSizeInWords = bitArray.getSize() / wordSize; 332 ReedSolomonEncoder rs = new ReedSolomonEncoder(getGF(wordSize)); 333 int totalWords = totalBits / wordSize; 334 int[] messageWords = bitsToWords(bitArray, wordSize, totalWords); 335 rs.encode(messageWords, totalWords - messageSizeInWords); 336 int startPad = totalBits % wordSize; 337 BitArray messageBits = new BitArray(); 338 messageBits.appendBits(0, startPad); 339 for (int messageWord : messageWords) { 340 messageBits.appendBits(messageWord, wordSize); 341 } 342 return messageBits; 343 } 344 bitsToWords(BitArray stuffedBits, int wordSize, int totalWords)345 private static int[] bitsToWords(BitArray stuffedBits, int wordSize, int totalWords) { 346 int[] message = new int[totalWords]; 347 int i; 348 int n; 349 for (i = 0, n = stuffedBits.getSize() / wordSize; i < n; i++) { 350 int value = 0; 351 for (int j = 0; j < wordSize; j++) { 352 value |= stuffedBits.get(i * wordSize + j) ? (1 << wordSize - j - 1) : 0; 353 } 354 message[i] = value; 355 } 356 return message; 357 } 358 getGF(int wordSize)359 private static GenericGF getGF(int wordSize) { 360 switch (wordSize) { 361 case 4: 362 return GenericGF.AZTEC_PARAM; 363 case 6: 364 return GenericGF.AZTEC_DATA_6; 365 case 8: 366 return GenericGF.AZTEC_DATA_8; 367 case 10: 368 return GenericGF.AZTEC_DATA_10; 369 case 12: 370 return GenericGF.AZTEC_DATA_12; 371 default: 372 throw new IllegalArgumentException("Unsupported word size " + wordSize); 373 } 374 } 375 stuffBits(BitArray bits, int wordSize)376 static BitArray stuffBits(BitArray bits, int wordSize) { 377 BitArray out = new BitArray(); 378 379 int n = bits.getSize(); 380 int mask = (1 << wordSize) - 2; 381 for (int i = 0; i < n; i += wordSize) { 382 int word = 0; 383 for (int j = 0; j < wordSize; j++) { 384 if (i + j >= n || bits.get(i + j)) { 385 word |= 1 << (wordSize - 1 - j); 386 } 387 } 388 if ((word & mask) == mask) { 389 out.appendBits(word & mask, wordSize); 390 i--; 391 } else if ((word & mask) == 0) { 392 out.appendBits(word | 1, wordSize); 393 i--; 394 } else { 395 out.appendBits(word, wordSize); 396 } 397 } 398 return out; 399 } 400 totalBitsInLayer(int layers, boolean compact)401 private static int totalBitsInLayer(int layers, boolean compact) { 402 return ((compact ? 88 : 112) + 16 * layers) * layers; 403 } 404 } 405