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-2018 Robin Stuart, Daniel Gredler
 *
 * 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.backend.HumanReadableAlignment.CENTER;
import static uk.org.okapibarcode.backend.HumanReadableLocation.BOTTOM;
import static uk.org.okapibarcode.backend.HumanReadableLocation.NONE;
import static uk.org.okapibarcode.backend.HumanReadableLocation.TOP;
import static uk.org.okapibarcode.util.Arrays.containsAt;
import static uk.org.okapibarcode.util.Arrays.positionOf;
import static uk.org.okapibarcode.util.Doubles.roughlyEqual;

import java.awt.Font;
import java.awt.GraphicsEnvironment;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import uk.org.okapibarcode.output.Java2DRenderer;
import uk.org.okapibarcode.util.EciMode;
import uk.org.okapibarcode.util.Gs1;

/**
 * Generic barcode symbology class.
 *
 * TODO: Setting attributes like module width, font size, etc should probably throw an exception if
 * set *after* encoding has already been completed.
 *
 * TODO: GS1 data is encoded slightly differently depending on whether [AI]data content is used, or
 * if FNC1 escape sequences are used. We may want to make sure that they encode to the same output.
 *
 * @author <a href="mailto:rstuart114@gmail.com">Robin Stuart</a>
 */
public abstract class Symbol {

    public static enum DataType {
        ECI, GS1, HIBC
    }

    protected static final int FNC1 = -1;
    protected static final int FNC2 = -2;
    protected static final int FNC3 = -3;
    protected static final int FNC4 = -4;

    protected static final String FNC1_STRING = "\\<FNC1>";
    protected static final String FNC2_STRING = "\\<FNC2>";
    protected static final String FNC3_STRING = "\\<FNC3>";
    protected static final String FNC4_STRING = "\\<FNC4>";

    private static char[] HIBC_CHAR_TABLE = { '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', '-', '.', ' ', '$', '/', '+', '%' };

    // user-specified values and settings

    protected DataType inputDataType = DataType.ECI;
    protected boolean readerInit;
    protected int default_height = 40;
    protected int quietZoneHorizontal = 0;
    protected int quietZoneVertical = 0;
    protected int moduleWidth = 1;
    protected Font font;
    protected String fontName = "Helvetica";
    protected int fontSize = 8;
    protected HumanReadableLocation humanReadableLocation = BOTTOM;
    protected HumanReadableAlignment humanReadableAlignment = CENTER;
    protected boolean emptyContentAllowed = false;

    // internal state calculated when setContent() is called

    protected String content;
    protected int eciMode = -1;
    protected int[] inputData; // usually bytes (values 0-255), but may also contain FNC flags
    protected String readable = "";
    protected String[] pattern;
    protected int row_count = 0;
    protected int[] row_height;
    protected int symbol_height = 0;
    protected int symbol_width = 0;
    protected StringBuilder encodeInfo = new StringBuilder();
    protected List<Rectangle2D.Double> rectangles = new ArrayList<>(); // note positions do not
                                                                       // account for quiet zones
                                                                       // (handled in renderers)
    protected List<TextBox> texts = new ArrayList<>(); // note positions do not account for quiet
                                                       // zones (handled in renderers)
    protected List<Hexagon> hexagons = new ArrayList<>(); // note positions do not account for quiet
                                                          // zones (handled in renderers)
    protected List<Ellipse2D.Double> target = new ArrayList<>(); // note positions do not account
                                                                 // for quiet zones (handled in
                                                                 // renderers)

    /**
     * <p>
     * Sets the type of input data. This setting influences what pre-processing is done on data
     * before encoding in the symbol. For example: for <code>GS1</code> mode the AI data will be
     * used to calculate the position of 'FNC1' characters.
     *
     * <p>
     * Valid values are:
     *
     * <ul>
     * <li><code>ECI</code> Extended Channel Interpretations (default)
     * <li><code>GS1</code> Application Identifier and data pairs in "[AI]DATA" format
     * <li><code>HIBC</code> Health Industry Bar Code number (without check digit)
     * </ul>
     *
     * @param dataType the type of input data
     */
    public void setDataType(final DataType dataType) {
        if (dataType == DataType.GS1 && !gs1Supported()) {
            throw new IllegalArgumentException("This symbology type does not support GS1 data.");
        }
        this.inputDataType = dataType;
    }

    /**
     * Returns the type of input data in this symbol.
     *
     * @return the type of input data in this symbol
     */
    public DataType getDataType() {
        return this.inputDataType;
    }

    /**
     * Returns <code>true</code> if this type of symbology supports GS1 data.
     *
     * @return <code>true</code> if this type of symbology supports GS1 data
     */
    protected boolean gs1Supported() {
        return false;
    }

    /**
     * If set to <code>true</code>, the symbol is prefixed with a "Reader Initialization" or "Reader
     * Programming" instruction.
     *
     * @param readerInit whether or not to enable reader initialization
     */
    public void setReaderInit(final boolean readerInit) {
        this.readerInit = readerInit;
    }

    /**
     * Returns whether or not reader initialization is enabled.
     *
     * @return whether or not reader initialization is enabled
     */
    public boolean getReaderInit() {
        return this.readerInit;
    }

    /**
     * Sets the default bar height for this symbol (default value is <code>40</code>).
     *
     * @param barHeight the default bar height for this symbol
     */
    public void setBarHeight(final int barHeight) {
        this.default_height = barHeight;
    }

    /**
     * Returns the default bar height for this symbol.
     *
     * @return the default bar height for this symbol
     */
    public int getBarHeight() {
        return this.default_height;
    }

    /**
     * Sets the module width for this symbol (default value is <code>1</code>).
     *
     * @param moduleWidth the module width for this symbol
     */
    public void setModuleWidth(final int moduleWidth) {
        this.moduleWidth = moduleWidth;
    }

    /**
     * Returns the module width for this symbol.
     *
     * @return the module width for this symbol
     */
    public int getModuleWidth() {
        return this.moduleWidth;
    }

    /**
     * Sets the horizontal quiet zone (white space) added to the left and to the right of this
     * symbol.
     *
     * @param quietZoneHorizontal the horizontal quiet zone (white space) added to the left and to
     *        the right of this symbol
     */
    public void setQuietZoneHorizontal(final int quietZoneHorizontal) {
        this.quietZoneHorizontal = quietZoneHorizontal;
    }

    /**
     * Returns the horizontal quiet zone (white space) added to the left and to the right of this
     * symbol.
     *
     * @return the horizontal quiet zone (white space) added to the left and to the right of this
     *         symbol
     */
    public int getQuietZoneHorizontal() {
        return this.quietZoneHorizontal;
    }

    /**
     * Sets the vertical quiet zone (white space) added above and below this symbol.
     *
     * @param quietZoneVertical the vertical quiet zone (white space) added above and below this
     *        symbol
     */
    public void setQuietZoneVertical(final int quietZoneVertical) {
        this.quietZoneVertical = quietZoneVertical;
    }

    /**
     * Returns the vertical quiet zone (white space) added above and below this symbol.
     *
     * @return the vertical quiet zone (white space) added above and below this symbol
     */
    public int getQuietZoneVertical() {
        return this.quietZoneVertical;
    }

    /**
     * <p>
     * Sets the font to use to render the human-readable text. This is an alternative to setting the
     * {@link #setFontName(String) font name} and {@link #setFontSize(int) font size} separately.
     * May allow some applications to avoid the use of
     * {@link GraphicsEnvironment#registerFont(Font)} when using the {@link Java2DRenderer}.
     *
     * <p>
     * Do not use this method in combination with {@link #setFontName(String)} or
     * {@link #setFontSize(int)}.
     *
     * @param font the font to use to render the human-readable text
     */
    public void setFont(final Font font) {
        this.font = font;
        this.fontName = font.getFontName();
        this.fontSize = font.getSize();
    }

    /**
     * Returns the font to use to render the human-readable text.
     *
     * @return the font to use to render the human-readable text
     */
    public Font getFont() {
        return this.font;
    }

    /**
     * <p>
     * Sets the name of the font to use to render the human-readable text (default value is
     * <code>Helvetica</code>). The specified font name needs to be registered via
     * {@link GraphicsEnvironment#registerFont(Font)} if you are using the {@link Java2DRenderer}.
     * In order to set the font without registering the font with the graphics environment when
     * using the {@link Java2DRenderer}, you may need to use {@link #setFont(Font)} instead.
     *
     * <p>
     * Use this method in combination with {@link #setFontSize(int)}.
     *
     * <p>
     * Do not use this method in combination with {@link #setFont(Font)}.
     *
     * @param fontName the name of the font to use to render the human-readable text
     */
    public void setFontName(final String fontName) {
        this.fontName = Objects.requireNonNull(fontName, "font name may not be null");
        this.font = null;
    }

    /**
     * Returns the name of the font to use to render the human-readable text.
     *
     * @return the name of the font to use to render the human-readable text
     */
    public String getFontName() {
        return this.fontName;
    }

    /**
     * <p>
     * Sets the size of the font to use to render the human-readable text (default value is
     * <code>8</code>).
     *
     * <p>
     * Use this method in combination with {@link #setFontName(String)}.
     *
     * <p>
     * Do not use this method in combination with {@link #setFont(Font)}.
     *
     * @param fontSize the size of the font to use to render the human-readable text
     */
    public void setFontSize(final int fontSize) {
        this.fontSize = fontSize;
        this.font = null;
    }

    /**
     * Returns the size of the font to use to render the human-readable text.
     *
     * @return the size of the font to use to render the human-readable text
     */
    public int getFontSize() {
        return this.fontSize;
    }

    /**
     * Gets the width of the encoded symbol, including the horizontal quiet zone.
     *
     * @return the width of the encoded symbol
     */
    public int getWidth() {
        return this.symbol_width + 2 * this.quietZoneHorizontal;
    }

    /**
     * Returns the height of the symbol, including the human-readable text, if any, as well as the
     * vertical quiet zone. This height is an approximation, since it is calculated without access
     * to a font engine.
     *
     * @return the height of the symbol, including the human-readable text, if any, as well as the
     *         vertical quiet zone
     */
    public int getHeight() {
        return this.symbol_height + getHumanReadableHeight() + 2 * this.quietZoneVertical;
    }

    /**
     * Returns the height of the human-readable text, including the space between the text and other
     * symbols. This height is an approximation, since it is calculated without access to a font
     * engine.
     *
     * @return the height of the human-readable text
     */
    public int getHumanReadableHeight() {
        if (this.texts.isEmpty()) {
            return 0;
        } else {
            return getTheoreticalHumanReadableHeight();
        }
    }

    /**
     * Returns the height of the human-readable text, assuming this symbol had human-readable text.
     *
     * @return the height of the human-readable text, assuming this symbol had human-readable text
     */
    protected int getTheoreticalHumanReadableHeight() {
        return (int) Math.ceil(this.fontSize * 1.2); // 0.2 space between bars and text
    }

    /**
     * Returns a human readable summary of the decisions made by the encoder when creating a symbol.
     *
     * @return a human readable summary of the decisions made by the encoder when creating a symbol
     */
    public String getEncodeInfo() {
        return this.encodeInfo.toString();
    }

    /**
     * Returns the ECI mode used by this symbol. The ECI mode is chosen automatically during
     * encoding if the symbol data type has been set to {@link DataType#ECI}. If this symbol does
     * not use ECI, this method will return <code>-1</code>.
     *
     * @return the ECI mode used by this symbol
     * @see #eciProcess()
     */
    public int getEciMode() {
        return this.eciMode;
    }

    /**
     * Sets the location of the human-readable text (default value is
     * {@link HumanReadableLocation#BOTTOM}).
     *
     * @param humanReadableLocation the location of the human-readable text
     */
    public void setHumanReadableLocation(final HumanReadableLocation humanReadableLocation) {
        this.humanReadableLocation = humanReadableLocation;
    }

    /**
     * Returns the location of the human-readable text.
     *
     * @return the location of the human-readable text
     */
    public HumanReadableLocation getHumanReadableLocation() {
        return this.humanReadableLocation;
    }

    /**
     * Sets the text alignment of the human-readable text (default value is
     * {@link HumanReadableAlignment#CENTER}).
     *
     * @param humanReadableAlignment the text alignment of the human-readable text
     */
    public void setHumanReadableAlignment(final HumanReadableAlignment humanReadableAlignment) {
        this.humanReadableAlignment = humanReadableAlignment;
    }

    /**
     * Returns the text alignment of the human-readable text.
     *
     * @return the text alignment of the human-readable text
     */
    public HumanReadableAlignment getHumanReadableAlignment() {
        return this.humanReadableAlignment;
    }

    /**
     * Returns render information about the rectangles in this symbol.
     *
     * @return render information about the rectangles in this symbol
     */
    public List<Rectangle2D.Double> getRectangles() {
        return this.rectangles;
    }

    /**
     * Returns render information about the text elements in this symbol.
     *
     * @return render information about the text elements in this symbol
     */
    public List<TextBox> getTexts() {
        return this.texts;
    }

    /**
     * Returns render information about the hexagons in this symbol.
     *
     * @return render information about the hexagons in this symbol
     */
    public List<Hexagon> getHexagons() {
        return this.hexagons;
    }

    /**
     * Returns render information about the target circles in this symbol.
     *
     * @return render information about the target circles in this symbol
     */
    public List<Ellipse2D.Double> getTarget() {
        return this.target;
    }

    protected static String bin2pat(final CharSequence bin) {

        int len = 0;
        boolean black = true;
        final StringBuilder pattern = new StringBuilder(bin.length());

        for (int i = 0; i < bin.length(); i++) {
            if (black) {
                if (bin.charAt(i) == '1') {
                    len++;
                } else {
                    black = false;
                    pattern.append((char) (len + '0'));
                    len = 1;
                }
            } else {
                if (bin.charAt(i) == '0') {
                    len++;
                } else {
                    black = true;
                    pattern.append((char) (len + '0'));
                    len = 1;
                }
            }
        }

        pattern.append((char) (len + '0'));
        return pattern.toString();
    }

    /**
     * Sets whether or not empty content is allowed. Some symbologies may be able to generate empty
     * symbols when no data is present, though this is not usually desired behavior. The default
     * value is <code>false</code> (no empty content allowed).
     *
     * @param emptyContentAllowed whether or not empty content is allowed
     */
    public void setEmptyContentAllowed(final boolean emptyContentAllowed) {
        this.emptyContentAllowed = emptyContentAllowed;
    }

    /**
     * Returns whether or not empty content is allowed.
     *
     * @return whether or not empty content is allowed
     */
    public boolean getEmptyContentAllowed() {
        return this.emptyContentAllowed;
    }

    /**
     * Sets the data to be encoded and triggers encoding. Input data will be assumed to be of the
     * type set by {@link #setDataType(DataType)}.
     *
     * @param data the data to encode
     * @throws OkapiException if no data or data is invalid
     */
    public void setContent(String data) {

        if (data == null) {
            data = "";
        }

        this.encodeInfo.setLength(0); // clear

        switch (this.inputDataType) {
        case GS1:
            this.content = Gs1.verify(data, FNC1_STRING);
            this.readable = data.replace('[', '(').replace(']', ')');
            break;
        case HIBC:
            this.content = hibcProcess(data);
            break;
        default:
            this.content = data;
            break;
        }

        if (this.content.isEmpty() && !this.emptyContentAllowed) {
            throw new OkapiException("No input data");
        }

        encode();
        plotSymbol();
        mergeVerticalBlocks();
    }

    /**
     * Returns the content encoded by this symbol.
     *
     * @return the content encoded by this symbol
     */
    public String getContent() {
        return this.content;
    }

    /**
     * Returns the human-readable text for this symbol.
     *
     * @return the human-readable text for this symbol
     */
    public String getHumanReadableText() {
        return this.readable;
    }

    /**
     * Chooses the ECI mode most suitable for the content of this symbol.
     */
    protected void eciProcess() {

        final EciMode eci = EciMode.of(this.content, "ISO8859_1", 3).or(this.content, "ISO8859_2", 4).or(this.content, "ISO8859_3", 5).or(this.content, "ISO8859_4", 6).or(this.content, "ISO8859_5", 7)
                .or(this.content, "ISO8859_6", 8).or(this.content, "ISO8859_7", 9).or(this.content, "ISO8859_8", 10).or(this.content, "ISO8859_9", 11).or(this.content, "ISO8859_10", 12)
                .or(this.content, "ISO8859_11", 13).or(this.content, "ISO8859_13", 15).or(this.content, "ISO8859_14", 16).or(this.content, "ISO8859_15", 17).or(this.content, "ISO8859_16", 18)
                .or(this.content, "Windows_1250", 21).or(this.content, "Windows_1251", 22).or(this.content, "Windows_1252", 23).or(this.content, "Windows_1256", 24).or(this.content, "SJIS", 20)
                .or(this.content, "UTF8", 26);

        if (EciMode.NONE.equals(eci)) {
            throw new OkapiException("Unable to determine ECI mode.");
        }

        this.eciMode = eci.mode;
        this.inputData = toBytes(this.content, eci.charset);

        infoLine("ECI Mode: " + eci.mode);
        infoLine("ECI Charset: " + eci.charset.name());
    }

    protected static int[] toBytes(final String s, final Charset charset, final int... suffix) {

        if (!charset.newEncoder().canEncode(s)) {
            return null;
        }

        final byte[] fnc1 = FNC1_STRING.getBytes(charset);
        final byte[] fnc2 = FNC2_STRING.getBytes(charset);
        final byte[] fnc3 = FNC3_STRING.getBytes(charset);
        final byte[] fnc4 = FNC4_STRING.getBytes(charset);

        final byte[] bytes = s.getBytes(charset);
        int[] data = new int[bytes.length + suffix.length];

        int i = 0, j = 0;
        for (; i < bytes.length; i++, j++) {
            if (containsAt(bytes, fnc1, i)) {
                data[j] = FNC1;
                i += fnc1.length - 1;
            } else if (containsAt(bytes, fnc2, i)) {
                data[j] = FNC2;
                i += fnc1.length - 1;
            } else if (containsAt(bytes, fnc3, i)) {
                data[j] = FNC3;
                i += fnc1.length - 1;
            } else if (containsAt(bytes, fnc4, i)) {
                data[j] = FNC4;
                i += fnc1.length - 1;
            } else {
                data[j] = bytes[i] & 0xff;
            }
        }

        int k = 0;
        for (; k < suffix.length; k++) {
            data[j + k] = suffix[k];
        }

        if (j + k < i) {
            data = Arrays.copyOf(data, j + k);
        }

        return data;
    }

    protected abstract void encode();

    protected void plotSymbol() {
        int xBlock, yBlock;
        double x, y, w, h;
        boolean black;

        this.rectangles.clear();
        this.texts.clear();

        int baseY;
        if (this.humanReadableLocation == TOP) {
            baseY = getTheoreticalHumanReadableHeight();
        } else {
            baseY = 0;
        }

        h = 0;
        y = baseY;

        for (yBlock = 0; yBlock < this.row_count; yBlock++) {
            black = true;
            x = 0;
            for (xBlock = 0; xBlock < this.pattern[yBlock].length(); xBlock++) {
                final char c = this.pattern[yBlock].charAt(xBlock);
                w = getModuleWidth(c - '0') * this.moduleWidth;
                if (black) {
                    if (this.row_height[yBlock] == -1) {
                        h = this.default_height;
                    } else {
                        h = this.row_height[yBlock];
                    }
                    if (w != 0 && h != 0) {
                        final Rectangle2D.Double rect = new Rectangle2D.Double(x, y, w, h);
                        this.rectangles.add(rect);
                    }
                    if (x + w > this.symbol_width) {
                        this.symbol_width = (int) Math.ceil(x + w);
                    }
                }
                black = !black;
                x += w;
            }
            if (y - baseY + h > this.symbol_height) {
                this.symbol_height = (int) Math.ceil(y - baseY + h);
            }
            y += h;
        }

        if (this.humanReadableLocation != NONE && !this.readable.isEmpty()) {
            double baseline;
            if (this.humanReadableLocation == TOP) {
                baseline = this.fontSize;
            } else {
                baseline = this.symbol_height + this.fontSize;
            }
            this.texts.add(new TextBox(0, baseline, this.symbol_width, this.readable, this.humanReadableAlignment));
        }
    }

    /**
     * Returns the module width to use for the specified original module width, taking into account
     * any module width ratio customizations. Intended to be overridden by subclasses that support
     * such module width ratio customization.
     *
     * @param originalWidth the original module width
     * @return the module width to use for the specified original module width
     */
    protected double getModuleWidth(final int originalWidth) {
        return originalWidth;
    }

    /**
     * Search for rectangles which have the same width and x position, and which join together
     * vertically and merge them together to reduce the number of rectangles needed to describe a
     * symbol. This can actually take a non-trivial amount of time for symbols with a large number
     * of rectangles (like large PDF417 symbols) so we exploit the fact that the rectangles are
     * ordered by rows (and within the rows that they are ordered by x position).
     */
    protected void mergeVerticalBlocks() {

        final int before = this.rectangles.size();

        for (int i = this.rectangles.size() - 1; i >= 0; i--) {
            final Rectangle2D.Double rect1 = this.rectangles.get(i);
            for (int j = i - 1; j >= 0; j--) {
                final Rectangle2D.Double rect2 = this.rectangles.get(j);
                if (roughlyEqual(rect1.y, rect2.y + rect2.height)) {
                    // rect2 is in the segment of rectangles for the row directly above rect1
                    if (roughlyEqual(rect1.x, rect2.x) && roughlyEqual(rect1.width, rect2.width)) {
                        // we've found a match; merge the rectangles
                        rect2.height += rect1.height;
                        this.rectangles.remove(i);
                        break;
                    }
                    if (rect2.x < rect1.x) {
                        // we've moved past any rectangles that might be directly above rect1
                        break;
                    }
                }
            }
        }

        final int after = this.rectangles.size();
        if (before != after) {
            infoLine("Blocks Merged: " + before + " -> " + after);
        }
    }

    /**
     * Adds the HIBC prefix and check digit to the specified data, returning the resultant data
     * string.
     *
     * @see <a href=
     *      "https://sourceforge.net/p/zint/code/ci/master/tree/backend/library.c">Corresponding
     *      Zint code</a>
     */
    private String hibcProcess(String source) {

        // HIBC 2.6 allows up to 110 characters, not including the "+" prefix or the check digit
        if (source.length() > 110) {
            throw new OkapiException("Data too long for HIBC LIC");
        }

        source = source.toUpperCase();
        if (!source.matches("[A-Z0-9-\\. \\$/+\\%]+?")) {
            throw new OkapiException("Invalid characters in input");
        }

        int counter = 41;
        for (int i = 0; i < source.length(); i++) {
            counter += positionOf(source.charAt(i), HIBC_CHAR_TABLE);
        }
        counter = counter % 43;

        final char checkDigit = HIBC_CHAR_TABLE[counter];

        infoLine("HIBC Check Digit Counter: " + counter);
        infoLine("HIBC Check Digit: " + checkDigit);

        return "+" + source + checkDigit;
    }

    /**
     * Returns the intermediate coding of this bar code. Symbol types that use the test
     * infrastructure should override this method.
     *
     * @return the intermediate coding of this bar code
     */
    protected int[] getCodewords() {
        throw new UnsupportedOperationException();
    }

    /**
     * Returns this bar code's pattern, converted into a set of corresponding codewords. Useful for
     * bar codes that encode their content as a pattern.
     *
     * @param size the number of digits in each codeword
     * @return this bar code's pattern, converted into a set of corresponding codewords
     */
    protected int[] getPatternAsCodewords(final int size) {
        if (size >= 10) {
            throw new IllegalArgumentException("Pattern groups of 10 or more digits are likely to be too large to parse as integers.");
        }
        if (this.pattern == null || this.pattern.length == 0) {
            return new int[0];
        } else {
            final int count = (int) Math.ceil(this.pattern[0].length() / (double) size);
            final int[] codewords = new int[this.pattern.length * count];
            for (int i = 0; i < this.pattern.length; i++) {
                final String row = this.pattern[i];
                for (int j = 0; j < count; j++) {
                    final int substringStart = j * size;
                    final int substringEnd = Math.min((j + 1) * size, row.length());
                    codewords[i * count + j] = Integer.parseInt(row.substring(substringStart, substringEnd));
                }
            }
            return codewords;
        }
    }

    protected void info(final CharSequence s) {
        this.encodeInfo.append(s);
    }

    protected void infoSpace(final int i) {
        this.encodeInfo.append(i).append(' ');
    }

    protected void infoSpace(final char c) {
        this.encodeInfo.append(c).append(' ');
    }

    protected void infoLine(final CharSequence s) {
        this.encodeInfo.append(s).append('\n');
    }

    protected void infoLine() {
        this.encodeInfo.append('\n');
    }
}