/* * Copyright 2013 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 totalcross.zxing.aztec.encoder; import java.util.Arrays; import totalcross.zxing.common.BitArray; import totalcross.zxing.common.BitMatrix; import totalcross.zxing.common.reedsolomon.GenericGF; import totalcross.zxing.common.reedsolomon.ReedSolomonEncoder; /** * Generates Aztec 2D barcodes. * * @author Rustam Abdullaev */ public final class Encoder { public static final int DEFAULT_EC_PERCENT = 33; // default minimal percentage of error check words private static final int TABLE_UPPER = 0; // 5 bits private static final int TABLE_LOWER = 1; // 5 bits private static final int TABLE_DIGIT = 2; // 4 bits private static final int TABLE_MIXED = 3; // 5 bits private static final int TABLE_PUNCT = 4; // 5 bits private static final int TABLE_BINARY = 5; // 8 bits private static final int[][] CHAR_MAP = new int[5][256]; // reverse mapping ASCII -> table offset, per table private static final int[][] SHIFT_TABLE = new int[6][6]; // mode shift codes, per table private static final int[][] LATCH_TABLE = new int[6][6]; // mode latch codes, per table private static final int[] NB_BITS; // total bits per compact symbol for a given number of layers private static final int[] NB_BITS_COMPACT; // total bits per full symbol for a given number of layers static { CHAR_MAP[TABLE_UPPER][' '] = 1; for (int c = 'A'; c <= 'Z'; c++) { CHAR_MAP[TABLE_UPPER][c] = c - 'A' + 2; } CHAR_MAP[TABLE_LOWER][' '] = 1; for (int c = 'a'; c <= 'z'; c++) { CHAR_MAP[TABLE_LOWER][c] = c - 'a' + 2; } CHAR_MAP[TABLE_DIGIT][' '] = 1; for (int c = '0'; c <= '9'; c++) { CHAR_MAP[TABLE_DIGIT][c] = c - '0' + 2; } CHAR_MAP[TABLE_DIGIT][','] = 12; CHAR_MAP[TABLE_DIGIT]['.'] = 13; int[] mixedTable = { '\0', ' ', '\1', '\2', '\3', '\4', '\5', '\6', '\7', '\b', '\t', '\n', '\13', '\f', '\r', '\33', '\34', '\35', '\36', '\37', '@', '\\', '^', '_', '`', '|', '~', '\177' }; for (int i = 0; i < mixedTable.length; i++) { CHAR_MAP[TABLE_MIXED][mixedTable[i]] = i; } int[] punctTable = { '\0', '\r', '\0', '\0', '\0', '\0', '!', '\'', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '[', ']', '{', '}' }; for (int i = 0; i < punctTable.length; i++) { if (punctTable[i] > 0) { CHAR_MAP[TABLE_PUNCT][punctTable[i]] = i; } } for (int[] table : SHIFT_TABLE) { Arrays.fill(table, -1); } for (int[] table : LATCH_TABLE) { Arrays.fill(table, -1); } SHIFT_TABLE[TABLE_UPPER][TABLE_PUNCT] = 0; LATCH_TABLE[TABLE_UPPER][TABLE_LOWER] = 28; LATCH_TABLE[TABLE_UPPER][TABLE_MIXED] = 29; LATCH_TABLE[TABLE_UPPER][TABLE_DIGIT] = 30; SHIFT_TABLE[TABLE_UPPER][TABLE_BINARY] = 31; SHIFT_TABLE[TABLE_LOWER][TABLE_PUNCT] = 0; SHIFT_TABLE[TABLE_LOWER][TABLE_UPPER] = 28; LATCH_TABLE[TABLE_LOWER][TABLE_MIXED] = 29; LATCH_TABLE[TABLE_LOWER][TABLE_DIGIT] = 30; SHIFT_TABLE[TABLE_LOWER][TABLE_BINARY] = 31; SHIFT_TABLE[TABLE_MIXED][TABLE_PUNCT] = 0; LATCH_TABLE[TABLE_MIXED][TABLE_LOWER] = 28; LATCH_TABLE[TABLE_MIXED][TABLE_UPPER] = 29; LATCH_TABLE[TABLE_MIXED][TABLE_PUNCT] = 30; SHIFT_TABLE[TABLE_MIXED][TABLE_BINARY] = 31; LATCH_TABLE[TABLE_PUNCT][TABLE_UPPER] = 31; SHIFT_TABLE[TABLE_DIGIT][TABLE_PUNCT] = 0; LATCH_TABLE[TABLE_DIGIT][TABLE_UPPER] = 30; SHIFT_TABLE[TABLE_DIGIT][TABLE_UPPER] = 31; NB_BITS_COMPACT = new int[5]; for (int i = 1; i < NB_BITS_COMPACT.length; i++) { NB_BITS_COMPACT[i] = (88 + 16 * i) * i; } NB_BITS = new int[33]; for (int i = 1; i < NB_BITS.length; i++) { NB_BITS[i] = (112 + 16 * i) * i; } } private static final int[] WORD_SIZE = { 4, 6, 6, 8, 8, 8, 8, 8, 8, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 }; private Encoder() { } /** * Encodes the given binary content as an Aztec symbol * * @param data input data string * @return Aztec symbol matrix with metadata */ public static AztecCode encode(byte[] data) { return encode(data, DEFAULT_EC_PERCENT); } /** * Encodes the given binary content as an Aztec symbol * * @param data input data string * @param minECCPercent minimal percentange of error check words (According to ISO/IEC 24778:2008, * a minimum of 23% + 3 words is recommended) * @return Aztec symbol matrix with metadata */ public static AztecCode encode(byte[] data, int minECCPercent) { // High-level encode BitArray bits = highLevelEncode(data); // stuff bits and choose symbol size int eccBits = bits.getSize() * minECCPercent / 100 + 11; int totalSizeBits = bits.getSize() + eccBits; int layers; int wordSize = 0; int totalSymbolBits = 0; BitArray stuffedBits = null; for (layers = 1; layers < NB_BITS_COMPACT.length; layers++) { if (NB_BITS_COMPACT[layers] >= totalSizeBits) { if (wordSize != WORD_SIZE[layers]) { wordSize = WORD_SIZE[layers]; stuffedBits = stuffBits(bits, wordSize); } totalSymbolBits = NB_BITS_COMPACT[layers]; if (stuffedBits.getSize() + eccBits <= NB_BITS_COMPACT[layers]) { break; } } } boolean compact = true; if (layers == NB_BITS_COMPACT.length) { compact = false; for (layers = 1; layers < NB_BITS.length; layers++) { if (NB_BITS[layers] >= totalSizeBits) { if (wordSize != WORD_SIZE[layers]) { wordSize = WORD_SIZE[layers]; stuffedBits = stuffBits(bits, wordSize); } totalSymbolBits = NB_BITS[layers]; if (stuffedBits.getSize() + eccBits <= NB_BITS[layers]) { break; } } } } if (layers == NB_BITS.length) { throw new IllegalArgumentException("Data too large for an Aztec code"); } // pad the end int messageSizeInWords = (stuffedBits.getSize() + wordSize - 1) / wordSize; // This seems to be redundant? /* for (int i = messageSizeInWords * wordSize - stuffedBits.getSize(); i > 0; i--) { stuffedBits.appendBit(true); } */ // generate check words ReedSolomonEncoder rs = new ReedSolomonEncoder(getGF(wordSize)); int totalSizeInFullWords = totalSymbolBits / wordSize; int[] messageWords = bitsToWords(stuffedBits, wordSize, totalSizeInFullWords); rs.encode(messageWords, totalSizeInFullWords - messageSizeInWords); // convert to bit array and pad in the beginning int startPad = totalSymbolBits % wordSize; BitArray messageBits = new BitArray(); messageBits.appendBits(0, startPad); for (int messageWord : messageWords) { messageBits.appendBits(messageWord, wordSize); } // generate mode message BitArray modeMessage = generateModeMessage(compact, layers, messageSizeInWords); // allocate symbol int baseMatrixSize = compact ? 11 + layers * 4 : 14 + layers * 4; // not including alignment lines int[] alignmentMap = new int[baseMatrixSize]; int matrixSize; if (compact) { // no alignment marks in compact mode, alignmentMap is a no-op matrixSize = baseMatrixSize; for (int i = 0; i < alignmentMap.length; i++) { alignmentMap[i] = i; } } else { matrixSize = baseMatrixSize + 1 + 2 * ((baseMatrixSize / 2 - 1) / 15); int origCenter = baseMatrixSize / 2; int center = matrixSize / 2; for (int i = 0; i < origCenter; i++) { int newOffset = i + i / 15; alignmentMap[origCenter - i - 1] = center - newOffset - 1; alignmentMap[origCenter + i] = center + newOffset + 1; } } BitMatrix matrix = new BitMatrix(matrixSize); // draw mode and data bits for (int i = 0, rowOffset = 0; i < layers; i++) { int rowSize = compact ? (layers - i) * 4 + 9 : (layers - i) * 4 + 12; for (int j = 0; j < rowSize; j++) { int columnOffset = j * 2; for (int k = 0; k < 2; k++) { if (messageBits.get(rowOffset + columnOffset + k)) { matrix.set(alignmentMap[i * 2 + k], alignmentMap[i * 2 + j]); } if (messageBits.get(rowOffset + rowSize * 2 + columnOffset + k)) { matrix.set(alignmentMap[i * 2 + j], alignmentMap[baseMatrixSize - 1 - i * 2 - k]); } if (messageBits.get(rowOffset + rowSize * 4 + columnOffset + k)) { matrix.set(alignmentMap[baseMatrixSize - 1 - i * 2 - k], alignmentMap[baseMatrixSize - 1 - i * 2 - j]); } if (messageBits.get(rowOffset + rowSize * 6 + columnOffset + k)) { matrix.set(alignmentMap[baseMatrixSize - 1 - i * 2 - j], alignmentMap[i * 2 + k]); } } } rowOffset += rowSize * 8; } drawModeMessage(matrix, compact, matrixSize, modeMessage); // draw alignment marks if (compact) { drawBullsEye(matrix, matrixSize / 2, 5); } else { drawBullsEye(matrix, matrixSize / 2, 7); for (int i = 0, j = 0; i < baseMatrixSize / 2 - 1; i += 15, j += 16) { for (int k = (matrixSize / 2) & 1; k < matrixSize; k += 2) { matrix.set(matrixSize / 2 - j, k); matrix.set(matrixSize / 2 + j, k); matrix.set(k, matrixSize / 2 - j); matrix.set(k, matrixSize / 2 + j); } } } AztecCode aztec = new AztecCode(); aztec.setCompact(compact); aztec.setSize(matrixSize); aztec.setLayers(layers); aztec.setCodeWords(messageSizeInWords); aztec.setMatrix(matrix); return aztec; } static void drawBullsEye(BitMatrix matrix, int center, int size) { for (int i = 0; i < size; i += 2) { for (int j = center - i; j <= center + i; j++) { matrix.set(j, center - i); matrix.set(j, center + i); matrix.set(center - i, j); matrix.set(center + i, j); } } matrix.set(center - size, center - size); matrix.set(center - size + 1, center - size); matrix.set(center - size, center - size + 1); matrix.set(center + size, center - size); matrix.set(center + size, center - size + 1); matrix.set(center + size, center + size - 1); } static BitArray generateModeMessage(boolean compact, int layers, int messageSizeInWords) { BitArray modeMessage = new BitArray(); if (compact) { modeMessage.appendBits(layers - 1, 2); modeMessage.appendBits(messageSizeInWords - 1, 6); modeMessage = generateCheckWords(modeMessage, 28, 4); } else { modeMessage.appendBits(layers - 1, 5); modeMessage.appendBits(messageSizeInWords - 1, 11); modeMessage = generateCheckWords(modeMessage, 40, 4); } return modeMessage; } static void drawModeMessage(BitMatrix matrix, boolean compact, int matrixSize, BitArray modeMessage) { if (compact) { for (int i = 0; i < 7; i++) { if (modeMessage.get(i)) { matrix.set(matrixSize / 2 - 3 + i, matrixSize / 2 - 5); } if (modeMessage.get(i + 7)) { matrix.set(matrixSize / 2 + 5, matrixSize / 2 - 3 + i); } if (modeMessage.get(20 - i)) { matrix.set(matrixSize / 2 - 3 + i, matrixSize / 2 + 5); } if (modeMessage.get(27 - i)) { matrix.set(matrixSize / 2 - 5, matrixSize / 2 - 3 + i); } } } else { for (int i = 0; i < 10; i++) { if (modeMessage.get(i)) { matrix.set(matrixSize / 2 - 5 + i + i / 5, matrixSize / 2 - 7); } if (modeMessage.get(i + 10)) { matrix.set(matrixSize / 2 + 7, matrixSize / 2 - 5 + i + i / 5); } if (modeMessage.get(29 - i)) { matrix.set(matrixSize / 2 - 5 + i + i / 5, matrixSize / 2 + 7); } if (modeMessage.get(39 - i)) { matrix.set(matrixSize / 2 - 7, matrixSize / 2 - 5 + i + i / 5); } } } } static BitArray generateCheckWords(BitArray stuffedBits, int totalSymbolBits, int wordSize) { int messageSizeInWords = (stuffedBits.getSize() + wordSize - 1) / wordSize; for (int i = messageSizeInWords * wordSize - stuffedBits.getSize(); i > 0; i--) { stuffedBits.appendBit(true); } ReedSolomonEncoder rs = new ReedSolomonEncoder(getGF(wordSize)); int totalSizeInFullWords = totalSymbolBits / wordSize; int[] messageWords = bitsToWords(stuffedBits, wordSize, totalSizeInFullWords); rs.encode(messageWords, totalSizeInFullWords - messageSizeInWords); int startPad = totalSymbolBits % wordSize; BitArray messageBits = new BitArray(); messageBits.appendBits(0, startPad); for (int messageWord : messageWords) { messageBits.appendBits(messageWord, wordSize); } return messageBits; } static int[] bitsToWords(BitArray stuffedBits, int wordSize, int totalWords) { int[] message = new int[totalWords]; int i; int n; for (i = 0, n = stuffedBits.getSize() / wordSize; i < n; i++) { int value = 0; for (int j = 0; j < wordSize; j++) { value |= stuffedBits.get(i * wordSize + j) ? (1 << wordSize - j - 1) : 0; } message[i] = value; } return message; } static GenericGF getGF(int wordSize) { switch (wordSize) { case 4: return GenericGF.AZTEC_PARAM; case 6: return GenericGF.AZTEC_DATA_6; case 8: return GenericGF.AZTEC_DATA_8; case 10: return GenericGF.AZTEC_DATA_10; case 12: return GenericGF.AZTEC_DATA_12; default: return null; } } static BitArray stuffBits(BitArray bits, int wordSize) { BitArray out = new BitArray(); // 1. stuff the bits int n = bits.getSize(); int mask = (1 << wordSize) - 2; for (int i = 0; i < n; i += wordSize) { int word = 0; for (int j = 0; j < wordSize; j++) { if (i + j >= n || bits.get(i + j)) { word |= 1 << (wordSize - 1 - j); } } if ((word & mask) == mask) { out.appendBits(word & mask, wordSize); i--; } else if ((word & mask) == 0) { out.appendBits(word | 1, wordSize); i--; } else { out.appendBits(word, wordSize); } } // 2. pad last word to wordSize // This seems to be redundant? /* n = out.getSize(); int remainder = n % wordSize; if (remainder != 0) { int j = 1; for (int i = 0; i < remainder; i++) { if (!out.get(n - 1 - i)) { j = 0; } } for (int i = remainder; i < wordSize - 1; i++) { out.appendBit(true); } out.appendBit(j == 0); } */ return out; } static BitArray highLevelEncode(byte[] data) { BitArray bits = new BitArray(); int mode = TABLE_UPPER; int[] idx = new int[5]; int[] idxnext = new int[5]; for (int i = 0; i < data.length; i++) { int c = data[i] & 0xFF; int next = i < data.length - 1 ? data[i + 1] & 0xFF : 0; int punctWord = 0; // special case: double-character codes if (c == '\r' && next == '\n') { punctWord = 2; } else if (c == '.' && next == ' ') { punctWord = 3; } else if (c == ',' && next == ' ') { punctWord = 4; } else if (c == ':' && next == ' ') { punctWord = 5; } if (punctWord > 0) { if (mode == TABLE_PUNCT) { outputWord(bits, TABLE_PUNCT, punctWord); i++; continue; } else if (SHIFT_TABLE[mode][TABLE_PUNCT] >= 0) { outputWord(bits, mode, SHIFT_TABLE[mode][TABLE_PUNCT]); outputWord(bits, TABLE_PUNCT, punctWord); i++; continue; } else if (LATCH_TABLE[mode][TABLE_PUNCT] >= 0) { outputWord(bits, mode, LATCH_TABLE[mode][TABLE_PUNCT]); outputWord(bits, TABLE_PUNCT, punctWord); mode = TABLE_PUNCT; i++; continue; } } // find the best matching table, taking current mode and next character into account int firstMatch = -1; int shiftMode = -1; int latchMode = -1; int j; for (j = 0; j < TABLE_BINARY; j++) { idx[j] = CHAR_MAP[j][c]; if (idx[j] > 0 && firstMatch < 0) { firstMatch = j; } if (shiftMode < 0 && idx[j] > 0 && SHIFT_TABLE[mode][j] >= 0) { shiftMode = j; } idxnext[j] = CHAR_MAP[j][next]; if (latchMode < 0 && idx[j] > 0 && (next == 0 || idxnext[j] > 0) && LATCH_TABLE[mode][j] >= 0) { latchMode = j; } } if (shiftMode < 0 && latchMode < 0) { for (j = 0; j < TABLE_BINARY; j++) { if (idx[j] > 0 && LATCH_TABLE[mode][j] >= 0) { latchMode = j; break; } } } if (idx[mode] > 0) { // found character in current table - stay in current table outputWord(bits, mode, idx[mode]); } else { if (latchMode >= 0) { // latch into mode latchMode outputWord(bits, mode, LATCH_TABLE[mode][latchMode]); outputWord(bits, latchMode, idx[latchMode]); mode = latchMode; } else if (shiftMode >= 0) { // shift into shiftMode outputWord(bits, mode, SHIFT_TABLE[mode][shiftMode]); outputWord(bits, shiftMode, idx[shiftMode]); } else { if (firstMatch >= 0) { // can't switch into this mode from current mode - switch in two steps if (mode == TABLE_PUNCT) { outputWord(bits, TABLE_PUNCT, LATCH_TABLE[TABLE_PUNCT][TABLE_UPPER]); mode = TABLE_UPPER; i--; continue; } else if (mode == TABLE_DIGIT) { outputWord(bits, TABLE_DIGIT, LATCH_TABLE[TABLE_DIGIT][TABLE_UPPER]); mode = TABLE_UPPER; i--; continue; } } // use binary table // find the binary string length int k; int lookahead; for (k = i + 1, lookahead = 0; k < data.length; k++) { next = data[k] & 0xFF; boolean binary = true; for (j = 0; j < TABLE_BINARY; j++) { if (CHAR_MAP[j][next] > 0) { binary = false; break; } } if (binary) { lookahead = 0; } else { // skip over single character in between binary bytes if (lookahead >= 1) { k -= lookahead; break; } lookahead++; } } k -= i; // switch into binary table switch (mode) { case TABLE_UPPER: case TABLE_LOWER: case TABLE_MIXED: outputWord(bits, mode, SHIFT_TABLE[mode][TABLE_BINARY]); break; case TABLE_DIGIT: outputWord(bits, mode, LATCH_TABLE[mode][TABLE_UPPER]); mode = TABLE_UPPER; outputWord(bits, mode, SHIFT_TABLE[mode][TABLE_BINARY]); break; case TABLE_PUNCT: outputWord(bits, mode, LATCH_TABLE[mode][TABLE_UPPER]); mode = TABLE_UPPER; outputWord(bits, mode, SHIFT_TABLE[mode][TABLE_BINARY]); break; } if (k >= 32 && k < 63) { // optimization: split one long form into two short forms, saves 1 bit k = 31; } if (k > 542) { // maximum encodable binary length in long form is 511 + 31 k = 542; } if (k < 32) { bits.appendBits(k, 5); } else { bits.appendBits(k - 31, 16); } for (; k > 0; k--, i++) { bits.appendBits(data[i], 8); } i--; } } } return bits; } static void outputWord(BitArray bits, int mode, int value) { if (mode == TABLE_DIGIT) { bits.appendBits(value, 4); } else if (mode < TABLE_BINARY) { bits.appendBits(value, 5); } else { bits.appendBits(value, 8); } } }