OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Blame | Last modification | View Log | RSS feed

/*
 * Copyright 2014 Robin Stuart
 *
 * 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 uk.org.okapibarcode.backend;

import static uk.org.okapibarcode.util.Arrays.positionOf;

import java.io.UnsupportedEncodingException;

/**
 * Implements Micro QR Code According to ISO/IEC 18004:2006 <br>
 * A miniature version of the QR Code symbol for short messages. QR Code symbols can encode
 * characters in the Latin-1 set and Kanji characters which are members of the Shift-JIS encoding
 * scheme.
 *
 * @author <a href="mailto:rstuart114@gmail.com">Robin Stuart</a>
 */
public class MicroQrCode extends Symbol {

    public enum EccMode {
        L, M, Q, H
    }

    private enum qrMode {
        NULL, KANJI, BINARY, ALPHANUM, NUMERIC
    }

    /* Table 5 - Encoding/Decoding table for Alphanumeric mode */
    private static final char[] RHODIUM = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
            'V', 'W', 'X', 'Y', 'Z', ' ', '$', '%', '*', '+', '-', '.', '/', ':' };

    private static final int[] QR_ANNEX_C1 = {
            /* Micro QR Code format information */
            0x4445, 0x4172, 0x4e2b, 0x4b1c, 0x55ae, 0x5099, 0x5fc0, 0x5af7, 0x6793, 0x62a4, 0x6dfd, 0x68ca, 0x7678, 0x734f, 0x7c16, 0x7921, 0x06de, 0x03e9, 0x0cb0, 0x0987, 0x1735, 0x1202, 0x1d5b,
            0x186c, 0x2508, 0x203f, 0x2f66, 0x2a51, 0x34e3, 0x31d4, 0x3e8d, 0x3bba };

    private static final int[] MICRO_QR_SIZES = { 11, 13, 15, 17 };

    // user-specified values and settings

    private int preferredVersion;
    private EccMode preferredEccLevel = EccMode.L;

    // internal state calculated when setContent() is called

    private qrMode[] inputMode;
    private StringBuilder binary;
    private final int[] binaryCount = new int[4];
    private int[] grid;
    private int[] eval;

    /**
     * <p>
     * Sets the preferred symbol size. This value may be ignored if the data string is too large to
     * fit into the specified symbol. Input values correspond to symbol sizes as shown in the
     * following table.
     *
     * <table summary="Range of Micro QR symbol sizes">
     * <tbody>
     * <tr>
     * <th>Input</th>
     * <th>Version</th>
     * <th>Symbol Size</th>
     * </tr>
     * <tr>
     * <td>1</td>
     * <td>M1</td>
     * <td>11 x 11</td>
     * </tr>
     * <tr>
     * <td>2</td>
     * <td>M2</td>
     * <td>13 x 13</td>
     * </tr>
     * <tr>
     * <td>3</td>
     * <td>M3</td>
     * <td>15 x 15</td>
     * </tr>
     * <tr>
     * <td>4</td>
     * <td>M4</td>
     * <td>17 x 17</td>
     * </tr>
     * </tbody>
     * </table>
     *
     * @param version symbol size
     */
    public void setPreferredVersion(final int version) {
        if (version < 0 || version > 4) { // TODO: min 1
            throw new IllegalArgumentException("Invalid version: " + version);
        }
        this.preferredVersion = version;
    }

    /**
     * Returns the preferred symbol size.
     *
     * @return the preferred symbol size
     * @see #setPreferredVersion(int)
     */
    public int getPreferredVersion() {
        return this.preferredVersion;
    }

    /**
     * <p>
     * Set the amount of symbol space allocated to error correction. Levels are predefined according
     * to the following table:
     *
     * <table summary="Micro QR Error correction levels">
     * <tbody>
     * <tr>
     * <th>ECC Level</th>
     * <th>Error Correction Capacity</th>
     * <th>Recovery Capacity</th>
     * </tr>
     * <tr>
     * <td>L (default)</td>
     * <td>Approx 20% of symbol</td>
     * <td>Approx 7%</td>
     * </tr>
     * <tr>
     * <td>M</td>
     * <td>Approx 37% of symbol</td>
     * <td>Approx 15%</td>
     * </tr>
     * <tr>
     * <td>Q</td>
     * <td>Approx 55% of symbol</td>
     * <td>Approx 25%</td>
     * </tr>
     * <tr>
     * <td>H</td>
     * <td>Approx 65% of symbol</td>
     * <td>Approx 30%</td>
     * </tr>
     * </tbody>
     * </table>
     *
     * @param eccMode error correction level
     */
    public void setEccMode(final EccMode eccMode) {
        this.preferredEccLevel = eccMode;
    }

    /**
     * Returns the preferred ECC mode (error correction level).
     *
     * @return the preferred ECC mode
     * @see #setEccMode(EccMode)
     */
    public EccMode getEccMode() {
        return this.preferredEccLevel;
    }

    @Override
    protected void encode() {
        int i, j, size;
        final boolean[] version_valid = new boolean[4];
        int n_count, a_count;
        EccMode ecc_level;
        int version, autoversion;
        int bitmask;
        int format, format_full;
        final StringBuilder bin = new StringBuilder();
        boolean byteModeUsed;
        boolean alphanumModeUsed;
        boolean kanjiModeUsed;

        if (this.content.length() > 35) {
            throw new OkapiException("Input data too long");
        }

        inputCharCheck();

        for (i = 0; i < 4; i++) {
            version_valid[i] = true;
        }

        this.inputMode = new qrMode[40];
        selectEncodingMode();

        n_count = 0;
        a_count = 0;
        for (i = 0; i < this.content.length(); i++) {
            if (this.content.charAt(i) >= '0' && this.content.charAt(i) <= '9') {
                n_count++;
            }
            if (isAlphanumeric(this.content.charAt(i))) {
                a_count++;
            }
        }

        if (a_count == this.content.length()) {
            /* All data can be encoded in Alphanumeric mode */
            for (i = 0; i < this.content.length(); i++) {
                this.inputMode[i] = qrMode.ALPHANUM;
            }
        }

        if (n_count == this.content.length()) {
            /* All data can be encoded in Numeric mode */
            for (i = 0; i < this.content.length(); i++) {
                this.inputMode[i] = qrMode.NUMERIC;
            }
        }

        byteModeUsed = false;
        alphanumModeUsed = false;
        kanjiModeUsed = false;

        for (i = 0; i < this.content.length(); i++) {
            if (this.inputMode[i] == qrMode.BINARY) {
                byteModeUsed = true;
            }

            if (this.inputMode[i] == qrMode.ALPHANUM) {
                alphanumModeUsed = true;
            }

            if (this.inputMode[i] == qrMode.KANJI) {
                kanjiModeUsed = true;
            }
        }

        getBinaryLength();

        /* Eliminate possible versions depending on type of content */
        if (byteModeUsed) {
            version_valid[0] = false;
            version_valid[1] = false;
        }

        if (alphanumModeUsed) {
            version_valid[0] = false;
        }

        if (kanjiModeUsed) {
            version_valid[0] = false;
            version_valid[1] = false;
        }

        /* Eliminate possible versions depending on length of binary data */
        if (this.binaryCount[0] > 20) {
            version_valid[0] = false;
        }
        if (this.binaryCount[1] > 40) {
            version_valid[1] = false;
        }
        if (this.binaryCount[2] > 84) {
            version_valid[2] = false;
        }
        if (this.binaryCount[3] > 128) {
            throw new OkapiException("Input data too long");
        }

        /* Eliminate possible versions depending on error correction level specified */
        ecc_level = this.preferredEccLevel;

        if (ecc_level == EccMode.H) {
            throw new OkapiException("Error correction level H not available");
        }

        if (ecc_level == EccMode.Q) {
            version_valid[0] = false;
            version_valid[1] = false;
            version_valid[2] = false;
            if (this.binaryCount[3] > 80) {
                throw new OkapiException("Input data too long");
            }
        }

        if (ecc_level == EccMode.M) {
            version_valid[0] = false;
            if (this.binaryCount[1] > 32) {
                version_valid[1] = false;
            }
            if (this.binaryCount[2] > 68) {
                version_valid[2] = false;
            }
            if (this.binaryCount[3] > 112) {
                throw new OkapiException("Input data too long");
            }
        }

        autoversion = 3;
        if (version_valid[2]) {
            autoversion = 2;
        }
        if (version_valid[1]) {
            autoversion = 1;
        }
        if (version_valid[0]) {
            autoversion = 0;
        }

        version = autoversion;
        /* Get version from user */
        if (this.preferredVersion >= 1 && this.preferredVersion <= 4) {
            if (this.preferredVersion - 1 >= autoversion) {
                version = this.preferredVersion - 1;
            }
        }

        /* If there is enough unused space then increase the error correction level */
        if (version == 3) {
            if (this.binaryCount[3] <= 112) {
                ecc_level = EccMode.M;
            }
            if (this.binaryCount[3] <= 80) {
                ecc_level = EccMode.Q;
            }
        }

        if (version == 2 && this.binaryCount[2] <= 68) {
            ecc_level = EccMode.M;
        }

        if (version == 1 && this.binaryCount[1] <= 32) {
            ecc_level = EccMode.M;
        }

        this.binary = new StringBuilder();
        generateBinary(version);
        if (this.binary.length() > 128) {
            throw new OkapiException("Input data too long");
        }

        switch (version) {
        case 0:
            generateM1Symbol();
            infoLine("Version: M1");
            break;
        case 1:
            generateM2Symbol(ecc_level);
            infoLine("Version: M2");
            infoLine("ECC Level: " + levelToLetter(ecc_level));
            break;
        case 2:
            generateM3Symbol(ecc_level);
            infoLine("Version: M3");
            infoLine("ECC Level: " + levelToLetter(ecc_level));
            break;
        case 3:
            generateM4Symbol(ecc_level);
            infoLine("Version: M4");
            infoLine("ECC Level: " + levelToLetter(ecc_level));
            break;
        }

        size = MICRO_QR_SIZES[version];

        this.grid = new int[size * size];

        for (i = 0; i < size; i++) {
            for (j = 0; j < size; j++) {
                this.grid[i * size + j] = 0;
            }
        }

        setupBitGrid(size);
        populateBitGrid(size);
        bitmask = applyBitmask(size);

        infoLine("Mask Pattern: " + Integer.toBinaryString(bitmask));

        /* Add format data */
        format = 0;
        switch (version) {
        case 1:
            switch (ecc_level) {
            case L:
                format = 1;
                break;
            case M:
                format = 2;
                break;
            }
            break;
        case 2:
            switch (ecc_level) {
            case L:
                format = 3;
                break;
            case M:
                format = 4;
                break;
            }
            break;
        case 3:
            switch (ecc_level) {
            case L:
                format = 5;
                break;
            case M:
                format = 6;
                break;
            case Q:
                format = 7;
                break;
            }
            break;
        }

        format_full = QR_ANNEX_C1[(format << 2) + bitmask];

        if ((format_full & 0x4000) != 0) {
            this.grid[8 * size + 1] += 0x01;
        }
        if ((format_full & 0x2000) != 0) {
            this.grid[8 * size + 2] += 0x01;
        }
        if ((format_full & 0x1000) != 0) {
            this.grid[8 * size + 3] += 0x01;
        }
        if ((format_full & 0x800) != 0) {
            this.grid[8 * size + 4] += 0x01;
        }
        if ((format_full & 0x400) != 0) {
            this.grid[8 * size + 5] += 0x01;
        }
        if ((format_full & 0x200) != 0) {
            this.grid[8 * size + 6] += 0x01;
        }
        if ((format_full & 0x100) != 0) {
            this.grid[8 * size + 7] += 0x01;
        }
        if ((format_full & 0x80) != 0) {
            this.grid[8 * size + 8] += 0x01;
        }
        if ((format_full & 0x40) != 0) {
            this.grid[7 * size + 8] += 0x01;
        }
        if ((format_full & 0x20) != 0) {
            this.grid[6 * size + 8] += 0x01;
        }
        if ((format_full & 0x10) != 0) {
            this.grid[5 * size + 8] += 0x01;
        }
        if ((format_full & 0x08) != 0) {
            this.grid[4 * size + 8] += 0x01;
        }
        if ((format_full & 0x04) != 0) {
            this.grid[3 * size + 8] += 0x01;
        }
        if ((format_full & 0x02) != 0) {
            this.grid[2 * size + 8] += 0x01;
        }
        if ((format_full & 0x01) != 0) {
            this.grid[1 * size + 8] += 0x01;
        }

        this.readable = "";
        this.pattern = new String[size];
        this.row_count = size;
        this.row_height = new int[size];
        for (i = 0; i < size; i++) {
            bin.setLength(0);
            for (j = 0; j < size; j++) {
                if ((this.grid[i * size + j] & 0x01) != 0) {
                    bin.append('1');
                } else {
                    bin.append('0');
                }
            }
            this.pattern[i] = bin2pat(bin);
            this.row_height[i] = 1;
        }
    }

    private void inputCharCheck() {
        int qmarkBefore, qmarkAfter;
        int i;
        byte[] temp;

        /* Check that input includes valid characters */

        if (this.content.matches("[\u0000-\u00FF]+")) {
            /* All characters in ISO 8859-1 */
            return;
        }

        /* Otherwise check for Shift-JIS characters */
        qmarkBefore = 0;
        for (i = 0; i < this.content.length(); i++) {
            if (this.content.charAt(i) == '?') {
                qmarkBefore++;
            }
        }

        try {
            temp = this.content.getBytes("SJIS");
        } catch (final UnsupportedEncodingException e) {
            throw new OkapiException("Character encoding error");
        }

        qmarkAfter = 0;
        for (i = 0; i < temp.length; i++) {
            if (temp[i] == '?') {
                qmarkAfter++;
            }
        }

        /* If these values are the same, conversion was successful */
        if (qmarkBefore != qmarkAfter) {
            throw new OkapiException("Invalid characters in input data");
        }
    }

    private char levelToLetter(final EccMode ecc_mode) {
        switch (ecc_mode) {
        case L:
            return 'L';
        case M:
            return 'M';
        case Q:
            return 'Q';
        case H:
            return 'H';
        default:
            return ' ';
        }
    }

    private void selectEncodingMode() {
        int i, j;
        int mlen;
        final int length = this.content.length();

        for (i = 0; i < length; i++) {
            if (this.content.charAt(i) > 0xff) {
                this.inputMode[i] = qrMode.KANJI;
            } else {
                this.inputMode[i] = qrMode.BINARY;
                if (isAlphanumeric(this.content.charAt(i))) {
                    this.inputMode[i] = qrMode.ALPHANUM;
                }
                if (this.content.charAt(i) >= '0' && this.content.charAt(i) <= '9') {
                    this.inputMode[i] = qrMode.NUMERIC;
                }
            }
        }

        /* If less than 6 numeric digits together then don't use numeric mode */
        for (i = 0; i < length; i++) {
            if (this.inputMode[i] == qrMode.NUMERIC) {
                if (i != 0 && this.inputMode[i - 1] != qrMode.NUMERIC || i == 0) {
                    mlen = 0;
                    while (mlen + i < length && this.inputMode[mlen + i] == qrMode.NUMERIC) {
                        mlen++;
                    }
                    if (mlen < 6) {
                        for (j = 0; j < mlen; j++) {
                            this.inputMode[i + j] = qrMode.ALPHANUM;
                        }
                    }
                }
            }
        }

        /* If less than 4 alphanumeric characters together then don't use alphanumeric mode */
        for (i = 0; i < length; i++) {
            if (this.inputMode[i] == qrMode.ALPHANUM) {
                if (i != 0 && this.inputMode[i - 1] != qrMode.ALPHANUM || i == 0) {
                    mlen = 0;
                    while (mlen + i < length && this.inputMode[mlen + i] == qrMode.ALPHANUM) {
                        mlen++;
                    }
                    if (mlen < 6) {
                        for (j = 0; j < mlen; j++) {
                            this.inputMode[i + j] = qrMode.BINARY;
                        }
                    }
                }
            }
        }
    }

    private boolean isAlphanumeric(final char cglyph) {
        /* Returns true if input glyph is in the Alphanumeric set */
        boolean retval = false;

        if (cglyph >= '0' && cglyph <= '9') {
            retval = true;
        }
        if (cglyph >= 'A' && cglyph <= 'Z') {
            retval = true;
        }
        switch (cglyph) {
        case ' ':
        case '$':
        case '%':
        case '*':
        case '+':
        case '-':
        case '.':
        case '/':
        case ':':
            retval = true;
            break;
        }

        return retval;
    }

    private String toBinary(final int data, int h) {
        final StringBuilder binary = new StringBuilder();
        for (; h != 0; h >>= 1) {
            if ((data & h) != 0) {
                binary.append('1');
            } else {
                binary.append('0');
            }
        }
        return binary.toString();
    }

    private void getBinaryLength() {
        int i;
        qrMode currentMode = qrMode.NULL;
        int blockLength;

        /* Always include a terminator */
        for (i = 0; i < 4; i++) {
            this.binaryCount[i] = 0;
        }

        for (i = 0; i < this.content.length(); i++) {
            if (currentMode != this.inputMode[i]) {

                blockLength = 0;
                do {
                    blockLength++;
                } while (i + blockLength < this.content.length() && this.inputMode[i + blockLength] == this.inputMode[i]);

                switch (this.inputMode[i]) {
                case KANJI:
                    this.binaryCount[2] += 5 + blockLength * 13;
                    this.binaryCount[3] += 7 + blockLength * 13;

                    break;
                case BINARY:
                    this.binaryCount[2] += 6 + blockLength * 8;
                    this.binaryCount[3] += 8 + blockLength * 8;
                    break;
                case ALPHANUM:
                    int alphaLength;

                    if (blockLength % 2 == 1) {
                        /* Odd length block */
                        alphaLength = (blockLength - 1) / 2 * 11;
                        alphaLength += 6;
                    } else {
                        /* Even length block */
                        alphaLength = blockLength / 2 * 11;
                    }

                    this.binaryCount[1] += 4 + alphaLength;
                    this.binaryCount[2] += 6 + alphaLength;
                    this.binaryCount[3] += 8 + alphaLength;
                    break;
                case NUMERIC:
                    int numLength;

                    switch (blockLength % 3) {
                    case 1:
                        /* one digit left over */
                        numLength = (blockLength - 1) / 3 * 10;
                        numLength += 4;
                        break;
                    case 2:
                        /* two digits left over */
                        numLength = (blockLength - 2) / 3 * 10;
                        numLength += 7;
                        break;
                    default:
                        /* blockLength is a multiple of 3 */
                        numLength = blockLength / 3 * 10;
                        break;
                    }

                    this.binaryCount[0] += 3 + numLength;
                    this.binaryCount[1] += 5 + numLength;
                    this.binaryCount[2] += 7 + numLength;
                    this.binaryCount[3] += 9 + numLength;
                    break;
                }
                currentMode = this.inputMode[i];
            }
        }

        /* Add terminator */
        if (this.binaryCount[1] < 37) {
            this.binaryCount[1] += 5;
        }

        if (this.binaryCount[2] < 81) {
            this.binaryCount[2] += 7;
        }

        if (this.binaryCount[3] < 125) {
            this.binaryCount[3] += 9;
        }
    }

    private void generateBinary(final int version) {
        int position = 0;
        int blockLength, i;
        qrMode data_block;
        int msb, lsb, prod, jis;
        String oneChar;
        byte[] jisBytes;
        int count, first, second, third;

        info("Encoding: ");

        do {
            data_block = this.inputMode[position];
            blockLength = 0;
            do {
                blockLength++;
            } while (blockLength + position < this.content.length() && this.inputMode[position + blockLength] == data_block);

            switch (data_block) {
            case KANJI:
                /* Kanji mode */
                /* Mode indicator */
                switch (version) {
                case 2:
                    this.binary.append("11");
                    break;
                case 3:
                    this.binary.append("011");
                    break;
                }

                /* Character count indicator */
                this.binary.append(toBinary(blockLength, 1 << version)); /* version = 2..3 */

                info("KANJ (" + blockLength + ") ");

                /* Character representation */
                for (i = 0; i < blockLength; i++) {
                    oneChar = "";
                    oneChar += this.content.charAt(position + i);

                    /* Convert Unicode input to Shift-JIS */
                    try {
                        jisBytes = oneChar.getBytes("SJIS");
                    } catch (final UnsupportedEncodingException e) {
                        throw new OkapiException("Character encoding error");
                    }

                    jis = (jisBytes[0] & 0xFF) << 8;
                    if (jisBytes.length > 1) {
                        jis += jisBytes[1] & 0xFF;
                    }

                    if (jis > 0x9fff) {
                        jis -= 0xc140;
                    } else {
                        jis -= 0x8140;
                    }
                    msb = (jis & 0xff00) >> 8;
                    lsb = jis & 0xff;
                    prod = msb * 0xc0 + lsb;

                    this.binary.append(toBinary(prod, 0x1000));

                    infoSpace(prod);
                }

                break;
            case BINARY:
                /* Byte mode */
                /* Mode indicator */
                switch (version) {
                case 2:
                    this.binary.append("10");
                    break;
                case 3:
                    this.binary.append("010");
                    break;
                }

                /* Character count indicator */
                this.binary.append(toBinary(blockLength, 2 << version)); /* version = 2..3 */

                info("BYTE (" + blockLength + ") ");

                /* Character representation */
                for (i = 0; i < blockLength; i++) {
                    final int lbyte = this.content.charAt(position + i);
                    this.binary.append(toBinary(lbyte, 0x80));
                    infoSpace(lbyte);
                }

                break;
            case ALPHANUM:
                /* Alphanumeric mode */
                /* Mode indicator */
                switch (version) {
                case 1:
                    this.binary.append("1");
                    break;
                case 2:
                    this.binary.append("01");
                    break;
                case 3:
                    this.binary.append("001");
                    break;
                }

                /* Character count indicator */
                this.binary.append(toBinary(blockLength, 2 << version)); /* version = 1..3 */

                info("ALPH (" + blockLength + ") ");

                /* Character representation */
                i = 0;
                while (i < blockLength) {
                    first = positionOf(this.content.charAt(position + i), RHODIUM);
                    count = 1;
                    prod = first;

                    if (i + 1 < blockLength) {
                        if (this.inputMode[position + i + 1] == qrMode.ALPHANUM) {
                            second = positionOf(this.content.charAt(position + i + 1), RHODIUM);
                            count = 2;
                            prod = first * 45 + second;
                        }
                    }

                    this.binary.append(toBinary(prod, 1 << 5 * count)); /* count = 1..2 */

                    infoSpace(prod);

                    i += 2;
                }

                break;
            case NUMERIC:
                /* Numeric mode */
                /* Mode indicator */
                switch (version) {
                case 1:
                    this.binary.append("0");
                    break;
                case 2:
                    this.binary.append("00");
                    break;
                case 3:
                    this.binary.append("000");
                    break;
                }

                /* Character count indicator */
                this.binary.append(toBinary(blockLength, 4 << version)); /* version = 0..3 */

                info("NUMB (" + blockLength + ") ");

                /* Character representation */
                i = 0;
                while (i < blockLength) {
                    first = Character.getNumericValue(this.content.charAt(position + i));
                    count = 1;
                    prod = first;

                    if (i + 1 < blockLength) {
                        if (this.inputMode[position + i + 1] == qrMode.NUMERIC) {
                            second = Character.getNumericValue(this.content.charAt(position + i + 1));
                            count = 2;
                            prod = prod * 10 + second;
                        }
                    }

                    if (i + 2 < blockLength) {
                        if (this.inputMode[position + i + 2] == qrMode.NUMERIC) {
                            third = Character.getNumericValue(this.content.charAt(position + i + 2));
                            count = 3;
                            prod = prod * 10 + third;
                        }
                    }

                    this.binary.append(toBinary(prod, 1 << 3 * count)); /* count = 1..3 */

                    infoSpace(prod);

                    i += 3;
                }
                break;
            }

            position += blockLength;
        } while (position < this.content.length() - 1);

        /* Add terminator */
        switch (version) {
        case 0:
            this.binary.append("000");
            break;
        case 1:
            if (this.binary.length() < 37) {
                this.binary.append("00000");
            }
            break;
        case 2:
            if (this.binary.length() < 81) {
                this.binary.append("0000000");
            }
            break;
        case 3:
            if (this.binary.length() < 125) {
                this.binary.append("000000000");
            }
            break;
        }

        infoLine();
    }

    private void generateM1Symbol() {
        int i, latch;
        int bits_total, bits_left, remainder;
        int data_codewords, ecc_codewords;
        final int[] data_blocks = new int[4];
        final int[] ecc_blocks = new int[3];
        final ReedSolomon rs = new ReedSolomon();

        bits_total = 20;
        latch = 0;

        /* Manage last (4-bit) block */
        bits_left = bits_total - this.binary.length();
        if (bits_left <= 4) {
            for (i = 0; i < bits_left; i++) {
                this.binary.append("0");
            }
            latch = 1;
        }

        if (latch == 0) {
            /* Complete current byte */
            remainder = 8 - this.binary.length() % 8;
            if (remainder == 8) {
                remainder = 0;
            }
            for (i = 0; i < remainder; i++) {
                this.binary.append("0");
            }

            /* Add padding */
            bits_left = bits_total - this.binary.length();
            if (bits_left > 4) {
                remainder = (bits_left - 4) / 8;
                for (i = 0; i < remainder; i++) {
                    if ((i & 1) != 0) {
                        this.binary.append("00010001");
                    } else {
                        this.binary.append("11101100");
                    }
                }
            }
            this.binary.append("0000");
        }

        data_codewords = 3;
        ecc_codewords = 2;

        /* Copy data into codewords */
        for (i = 0; i < data_codewords - 1; i++) {
            data_blocks[i] = 0;
            if (this.binary.charAt(i * 8) == '1') {
                data_blocks[i] += 0x80;
            }
            if (this.binary.charAt(i * 8 + 1) == '1') {
                data_blocks[i] += 0x40;
            }
            if (this.binary.charAt(i * 8 + 2) == '1') {
                data_blocks[i] += 0x20;
            }
            if (this.binary.charAt(i * 8 + 3) == '1') {
                data_blocks[i] += 0x10;
            }
            if (this.binary.charAt(i * 8 + 4) == '1') {
                data_blocks[i] += 0x08;
            }
            if (this.binary.charAt(i * 8 + 5) == '1') {
                data_blocks[i] += 0x04;
            }
            if (this.binary.charAt(i * 8 + 6) == '1') {
                data_blocks[i] += 0x02;
            }
            if (this.binary.charAt(i * 8 + 7) == '1') {
                data_blocks[i] += 0x01;
            }
        }
        data_blocks[2] = 0;
        if (this.binary.charAt(16) == '1') {
            data_blocks[2] += 0x08;
        }
        if (this.binary.charAt(17) == '1') {
            data_blocks[2] += 0x04;
        }
        if (this.binary.charAt(18) == '1') {
            data_blocks[2] += 0x02;
        }
        if (this.binary.charAt(19) == '1') {
            data_blocks[2] += 0x01;
        }

        info("Codewords: ");
        for (i = 0; i < data_codewords; i++) {
            infoSpace(data_blocks[i]);
        }
        infoLine();

        /* Calculate Reed-Solomon error codewords */
        rs.init_gf(0x11d);
        rs.init_code(ecc_codewords, 0);
        rs.encode(data_codewords, data_blocks);
        for (i = 0; i < ecc_codewords; i++) {
            ecc_blocks[i] = rs.getResult(i);
        }

        /* Add Reed-Solomon codewords to binary data */
        for (i = 0; i < ecc_codewords; i++) {
            this.binary.append(toBinary(ecc_blocks[ecc_codewords - i - 1], 0x80));
        }
    }

    private void generateM2Symbol(final EccMode ecc_mode) {
        int i;
        int bits_total, bits_left, remainder;
        int data_codewords, ecc_codewords;
        final int[] data_blocks = new int[6];
        final int[] ecc_blocks = new int[7];
        final ReedSolomon rs = new ReedSolomon();

        bits_total = 40; // ecc_mode == EccMode.L
        if (ecc_mode == EccMode.M) {
            bits_total = 32;
        }

        /* Complete current byte */
        remainder = 8 - this.binary.length() % 8;
        if (remainder == 8) {
            remainder = 0;
        }
        for (i = 0; i < remainder; i++) {
            this.binary.append("0");
        }

        /* Add padding */
        bits_left = bits_total - this.binary.length();
        remainder = bits_left / 8;
        for (i = 0; i < remainder; i++) {
            if ((i & 1) != 0) {
                this.binary.append("00010001");
            } else {
                this.binary.append("11101100");
            }
        }

        data_codewords = 5;
        ecc_codewords = 5; // ecc_mode == EccMode.L
        if (ecc_mode == EccMode.M) {
            data_codewords = 4;
            ecc_codewords = 6;
        }

        /* Copy data into codewords */
        for (i = 0; i < data_codewords; i++) {
            data_blocks[i] = 0;
            if (this.binary.charAt(i * 8) == '1') {
                data_blocks[i] += 0x80;
            }
            if (this.binary.charAt(i * 8 + 1) == '1') {
                data_blocks[i] += 0x40;
            }
            if (this.binary.charAt(i * 8 + 2) == '1') {
                data_blocks[i] += 0x20;
            }
            if (this.binary.charAt(i * 8 + 3) == '1') {
                data_blocks[i] += 0x10;
            }
            if (this.binary.charAt(i * 8 + 4) == '1') {
                data_blocks[i] += 0x08;
            }
            if (this.binary.charAt(i * 8 + 5) == '1') {
                data_blocks[i] += 0x04;
            }
            if (this.binary.charAt(i * 8 + 6) == '1') {
                data_blocks[i] += 0x02;
            }
            if (this.binary.charAt(i * 8 + 7) == '1') {
                data_blocks[i] += 0x01;
            }
        }

        info("Codewords: ");
        for (i = 0; i < data_codewords; i++) {
            infoSpace(data_blocks[i]);
        }
        infoLine();

        /* Calculate Reed-Solomon error codewords */
        rs.init_gf(0x11d);
        rs.init_code(ecc_codewords, 0);
        rs.encode(data_codewords, data_blocks);
        for (i = 0; i < ecc_codewords; i++) {
            ecc_blocks[i] = rs.getResult(i);
        }

        /* Add Reed-Solomon codewords to binary data */
        for (i = 0; i < ecc_codewords; i++) {
            this.binary.append(toBinary(ecc_blocks[ecc_codewords - i - 1], 0x80));
        }
    }

    private void generateM3Symbol(final EccMode ecc_mode) {
        int i, latch;
        int bits_total, bits_left, remainder;
        int data_codewords, ecc_codewords;
        final int[] data_blocks = new int[12];
        final int[] ecc_blocks = new int[12];
        final ReedSolomon rs = new ReedSolomon();

        latch = 0;

        bits_total = 84; // ecc_mode == EccMode.L
        if (ecc_mode == EccMode.M) {
            bits_total = 68;
        }

        /* Manage last (4-bit) block */
        bits_left = bits_total - this.binary.length();
        if (bits_left <= 4) {
            for (i = 0; i < bits_left; i++) {
                this.binary.append("0");
            }
            latch = 1;
        }

        if (latch == 0) {
            /* Complete current byte */
            remainder = 8 - this.binary.length() % 8;
            if (remainder == 8) {
                remainder = 0;
            }
            for (i = 0; i < remainder; i++) {
                this.binary.append("0");
            }

            /* Add padding */
            bits_left = bits_total - this.binary.length();
            if (bits_left > 4) {
                remainder = (bits_left - 4) / 8;
                for (i = 0; i < remainder; i++) {
                    if ((i & 1) != 0) {
                        this.binary.append("00010001");
                    } else {
                        this.binary.append("11101100");
                    }
                }
            }
            this.binary.append("0000");
        }

        data_codewords = 11;
        ecc_codewords = 6; // ecc_mode == EccMode.L
        if (ecc_mode == EccMode.M) {
            data_codewords = 9;
            ecc_codewords = 8;
        }

        /* Copy data into codewords */
        for (i = 0; i < data_codewords - 1; i++) {
            data_blocks[i] = 0;
            if (this.binary.charAt(i * 8) == '1') {
                data_blocks[i] += 0x80;
            }
            if (this.binary.charAt(i * 8 + 1) == '1') {
                data_blocks[i] += 0x40;
            }
            if (this.binary.charAt(i * 8 + 2) == '1') {
                data_blocks[i] += 0x20;
            }
            if (this.binary.charAt(i * 8 + 3) == '1') {
                data_blocks[i] += 0x10;
            }
            if (this.binary.charAt(i * 8 + 4) == '1') {
                data_blocks[i] += 0x08;
            }
            if (this.binary.charAt(i * 8 + 5) == '1') {
                data_blocks[i] += 0x04;
            }
            if (this.binary.charAt(i * 8 + 6) == '1') {
                data_blocks[i] += 0x02;
            }
            if (this.binary.charAt(i * 8 + 7) == '1') {
                data_blocks[i] += 0x01;
            }
        }

        if (ecc_mode == EccMode.L) {
            data_blocks[10] = 0;
            if (this.binary.charAt(80) == '1') {
                data_blocks[10] += 0x08;
            }
            if (this.binary.charAt(81) == '1') {
                data_blocks[10] += 0x04;
            }
            if (this.binary.charAt(82) == '1') {
                data_blocks[10] += 0x02;
            }
            if (this.binary.charAt(83) == '1') {
                data_blocks[10] += 0x01;
            }
        }

        if (ecc_mode == EccMode.M) {
            data_blocks[8] = 0;
            if (this.binary.charAt(64) == '1') {
                data_blocks[8] += 0x08;
            }
            if (this.binary.charAt(65) == '1') {
                data_blocks[8] += 0x04;
            }
            if (this.binary.charAt(66) == '1') {
                data_blocks[8] += 0x02;
            }
            if (this.binary.charAt(67) == '1') {
                data_blocks[8] += 0x01;
            }
        }

        info("Codewords: ");
        for (i = 0; i < data_codewords; i++) {
            infoSpace(data_blocks[i]);
        }
        infoLine();

        /* Calculate Reed-Solomon error codewords */
        rs.init_gf(0x11d);
        rs.init_code(ecc_codewords, 0);
        rs.encode(data_codewords, data_blocks);
        for (i = 0; i < ecc_codewords; i++) {
            ecc_blocks[i] = rs.getResult(i);
        }

        /* Add Reed-Solomon codewords to binary data */
        for (i = 0; i < ecc_codewords; i++) {
            this.binary.append(toBinary(ecc_blocks[ecc_codewords - i - 1], 0x80));
        }
    }

    private void generateM4Symbol(final EccMode ecc_mode) {
        int i;
        int bits_total, bits_left, remainder;
        int data_codewords, ecc_codewords;
        final int[] data_blocks = new int[17];
        final int[] ecc_blocks = new int[15];
        final ReedSolomon rs = new ReedSolomon();

        bits_total = 128; // ecc_mode == EccMode.L
        if (ecc_mode == EccMode.M) {
            bits_total = 112;
        }
        if (ecc_mode == EccMode.Q) {
            bits_total = 80;
        }

        /* Complete current byte */
        remainder = 8 - this.binary.length() % 8;
        if (remainder == 8) {
            remainder = 0;
        }
        for (i = 0; i < remainder; i++) {
            this.binary.append("0");
        }

        /* Add padding */
        bits_left = bits_total - this.binary.length();
        remainder = bits_left / 8;
        for (i = 0; i < remainder; i++) {
            if ((i & 1) != 0) {
                this.binary.append("00010001");
            } else {
                this.binary.append("11101100");
            }
        }

        data_codewords = 16;
        ecc_codewords = 8; // ecc_mode == EccMode.L
        if (ecc_mode == EccMode.M) {
            data_codewords = 14;
            ecc_codewords = 10;
        }
        if (ecc_mode == EccMode.Q) {
            data_codewords = 10;
            ecc_codewords = 14;
        }

        /* Copy data into codewords */
        for (i = 0; i < data_codewords; i++) {
            data_blocks[i] = 0;
            if (this.binary.charAt(i * 8) == '1') {
                data_blocks[i] += 0x80;
            }
            if (this.binary.charAt(i * 8 + 1) == '1') {
                data_blocks[i] += 0x40;
            }
            if (this.binary.charAt(i * 8 + 2) == '1') {
                data_blocks[i] += 0x20;
            }
            if (this.binary.charAt(i * 8 + 3) == '1') {
                data_blocks[i] += 0x10;
            }
            if (this.binary.charAt(i * 8 + 4) == '1') {
                data_blocks[i] += 0x08;
            }
            if (this.binary.charAt(i * 8 + 5) == '1') {
                data_blocks[i] += 0x04;
            }
            if (this.binary.charAt(i * 8 + 6) == '1') {
                data_blocks[i] += 0x02;
            }
            if (this.binary.charAt(i * 8 + 7) == '1') {
                data_blocks[i] += 0x01;
            }
        }

        info("Codewords: ");
        for (i = 0; i < data_codewords; i++) {
            infoSpace(data_blocks[i]);
        }
        infoLine();

        /* Calculate Reed-Solomon error codewords */
        rs.init_gf(0x11d);
        rs.init_code(ecc_codewords, 0);
        rs.encode(data_codewords, data_blocks);
        for (i = 0; i < ecc_codewords; i++) {
            ecc_blocks[i] = rs.getResult(i);
        }

        /* Add Reed-Solomon codewords to binary data */
        for (i = 0; i < ecc_codewords; i++) {
            this.binary.append(toBinary(ecc_blocks[ecc_codewords - i - 1], 0x80));
        }
    }

    private void setupBitGrid(final int size) {
        int i, toggle = 1;

        /* Add timing patterns */
        for (i = 0; i < size; i++) {
            if (toggle == 1) {
                this.grid[i] = 0x21;
                this.grid[i * size] = 0x21;
                toggle = 0;
            } else {
                this.grid[i] = 0x20;
                this.grid[i * size] = 0x20;
                toggle = 1;
            }
        }

        /* Add finder patterns */
        placeFinderPattern(size, 0, 0);

        /* Add separators */
        for (i = 0; i < 7; i++) {
            this.grid[7 * size + i] = 0x10;
            this.grid[i * size + 7] = 0x10;
        }
        this.grid[7 * size + 7] = 0x10;

        /* Reserve space for format information */
        for (i = 0; i < 8; i++) {
            this.grid[8 * size + i] += 0x20;
            this.grid[i * size + 8] += 0x20;
        }
        this.grid[8 * size + 8] += 0x20;
    }

    private void placeFinderPattern(final int size, final int x, final int y) {
        int xp, yp;

        final int[] finder = { 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1 };

        for (xp = 0; xp < 7; xp++) {
            for (yp = 0; yp < 7; yp++) {
                if (finder[xp + 7 * yp] == 1) {
                    this.grid[(yp + y) * size + xp + x] = 0x11;
                } else {
                    this.grid[(yp + y) * size + xp + x] = 0x10;
                }
            }
        }
    }

    private void populateBitGrid(final int size) {
        boolean goingUp = true;
        int row = 0; /* right hand side */

        int i, n, x, y;

        n = this.binary.length();
        y = size - 1;
        i = 0;
        do {
            x = size - 2 - row * 2;

            if ((this.grid[y * size + x + 1] & 0xf0) == 0) {
                if (this.binary.charAt(i) == '1') {
                    this.grid[y * size + x + 1] = 0x01;
                } else {
                    this.grid[y * size + x + 1] = 0x00;
                }
                i++;
            }

            if (i < n) {
                if ((this.grid[y * size + x] & 0xf0) == 0) {
                    if (this.binary.charAt(i) == '1') {
                        this.grid[y * size + x] = 0x01;
                    } else {
                        this.grid[y * size + x] = 0x00;
                    }
                    i++;
                }
            }

            if (goingUp) {
                y--;
            } else {
                y++;
            }
            if (y == 0) {
                /* reached the top */
                row++;
                y = 1;
                goingUp = false;
            }
            if (y == size) {
                /* reached the bottom */
                row++;
                y = size - 1;
                goingUp = true;
            }
        } while (i < n);
    }

    private int applyBitmask(final int size) {
        int x, y;
        int p;
        int local_pattern;
        final int[] value = new int[8];
        int best_val, best_pattern;
        int bit;

        final int[] mask = new int[size * size];
        this.eval = new int[size * size];

        /* Perform data masking */
        for (x = 0; x < size; x++) {
            for (y = 0; y < size; y++) {
                mask[y * size + x] = 0x00;

                if ((this.grid[y * size + x] & 0xf0) == 0) {
                    if ((y & 1) == 0) {
                        mask[y * size + x] += 0x01;
                    }

                    if ((y / 2 + x / 3 & 1) == 0) {
                        mask[y * size + x] += 0x02;
                    }

                    if (((y * x & 1) + y * x % 3 & 1) == 0) {
                        mask[y * size + x] += 0x04;
                    }

                    if (((y + x & 1) + y * x % 3 & 1) == 0) {
                        mask[y * size + x] += 0x08;
                    }
                }
            }
        }

        for (x = 0; x < size; x++) {
            for (y = 0; y < size; y++) {
                if ((this.grid[y * size + x] & 0x01) != 0) {
                    p = 0xff;
                } else {
                    p = 0x00;
                }

                this.eval[y * size + x] = mask[y * size + x] ^ p;
            }
        }

        /* Evaluate result */
        for (local_pattern = 0; local_pattern < 4; local_pattern++) {
            value[local_pattern] = evaluateBitmask(size, local_pattern);
        }

        best_pattern = 0;
        best_val = value[0];
        for (local_pattern = 1; local_pattern < 4; local_pattern++) {
            if (value[local_pattern] > best_val) {
                best_pattern = local_pattern;
                best_val = value[local_pattern];
            }
        }

        /* Apply mask */
        for (x = 0; x < size; x++) {
            for (y = 0; y < size; y++) {
                bit = 0;
                switch (best_pattern) {
                case 0:
                    if ((mask[y * size + x] & 0x01) != 0) {
                        bit = 1;
                    }
                    break;
                case 1:
                    if ((mask[y * size + x] & 0x02) != 0) {
                        bit = 1;
                    }
                    break;
                case 2:
                    if ((mask[y * size + x] & 0x04) != 0) {
                        bit = 1;
                    }
                    break;
                case 3:
                    if ((mask[y * size + x] & 0x08) != 0) {
                        bit = 1;
                    }
                    break;
                }
                if (bit == 1) {
                    if ((this.grid[y * size + x] & 0x01) != 0) {
                        this.grid[y * size + x] = 0x00;
                    } else {
                        this.grid[y * size + x] = 0x01;
                    }
                }
            }
        }

        return best_pattern;
    }

    private int evaluateBitmask(final int size, final int pattern) {
        int sum1, sum2, i, filter = 0, retval;

        switch (pattern) {
        case 0:
            filter = 0x01;
            break;
        case 1:
            filter = 0x02;
            break;
        case 2:
            filter = 0x04;
            break;
        case 3:
            filter = 0x08;
            break;
        }

        sum1 = 0;
        sum2 = 0;
        for (i = 1; i < size; i++) {
            if ((this.eval[i * size + size - 1] & filter) != 0) {
                sum1++;
            }
            if ((this.eval[(size - 1) * size + i] & filter) != 0) {
                sum2++;
            }
        }

        if (sum1 <= sum2) {
            retval = sum1 * 16 + sum2;
        } else {
            retval = sum2 * 16 + sum1;
        }

        return retval;
    }
}