uk.org.okapibarcode.backend.Symbol Maven / Gradle / Ivy
Show all versions of okapibarcode Show documentation
/*
* 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 Robin Stuart
*/
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 = "\\";
protected static final String FNC2_STRING = "\\";
protected static final String FNC3_STRING = "\\";
protected static final String FNC4_STRING = "\\";
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)
/**
* Sets the type of input data. This setting influences what pre-processing is done on
* data before encoding in the symbol. For example: for GS1
mode the AI
* data will be used to calculate the position of 'FNC1' characters.
*
*
Valid values are:
*
*
* ECI
Extended Channel Interpretations (default)
* GS1
Application Identifier and data pairs in "[AI]DATA" format
* HIBC
Health Industry Bar Code number (without check digit)
*
*
* @param dataType the type of input data
*/
public void setDataType(DataType dataType) {
if (dataType == DataType.GS1 && !gs1Supported()) {
throw new IllegalArgumentException("This symbology type does not support GS1 data.");
}
inputDataType = dataType;
}
/**
* Returns the type of input data in this symbol.
*
* @return the type of input data in this symbol
*/
public DataType getDataType() {
return inputDataType;
}
/**
* Returns true
if this type of symbology supports GS1 data.
*
* @return true
if this type of symbology supports GS1 data
*/
protected boolean gs1Supported() {
return false;
}
/**
* If set to true
, the symbol is prefixed with a "Reader Initialization"
* or "Reader Programming" instruction.
*
* @param readerInit whether or not to enable reader initialization
*/
public void setReaderInit(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 readerInit;
}
/**
* Sets the default bar height for this symbol (default value is 40
).
*
* @param barHeight the default bar height for this symbol
*/
public void setBarHeight(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 default_height;
}
/**
* Sets the module width for this symbol (default value is 1
).
*
* @param moduleWidth the module width for this symbol
*/
public void setModuleWidth(int moduleWidth) {
this.moduleWidth = moduleWidth;
}
/**
* Returns the module width for this symbol.
*
* @return the module width for this symbol
*/
public int getModuleWidth() {
return 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(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 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(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 quietZoneVertical;
}
/**
* 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}.
*
*
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(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 font;
}
/**
*
Sets the name of the font to use to render the human-readable text (default value is Helvetica
).
* 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.
*
*
Use this method in combination with {@link #setFontSize(int)}.
*
*
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(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 fontName;
}
/**
*
Sets the size of the font to use to render the human-readable text (default value is 8
).
*
*
Use this method in combination with {@link #setFontName(String)}.
*
*
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(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 fontSize;
}
/**
* Gets the width of the encoded symbol, including the horizontal quiet zone.
*
* @return the width of the encoded symbol
*/
public int getWidth() {
return symbol_width + (2 * 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 symbol_height + getHumanReadableHeight() + (2 * 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 (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(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 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 -1
.
*
* @return the ECI mode used by this symbol
* @see #eciProcess()
*/
public int getEciMode() {
return 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(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 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(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 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 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 texts;
}
/**
* Returns render information about the hexagons in this symbol.
*
* @return render information about the hexagons in this symbol
*/
public List< Hexagon > getHexagons() {
return 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 target;
}
protected static String bin2pat(CharSequence bin) {
int len = 0;
boolean black = true;
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 false
(no empty content allowed).
*
* @param emptyContentAllowed whether or not empty content is allowed
*/
public void setEmptyContentAllowed(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 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 = "";
}
encodeInfo.setLength(0); // clear
switch (inputDataType) {
case GS1:
content = Gs1.verify(data, FNC1_STRING);
readable = data.replace('[', '(').replace(']', ')');
break;
case HIBC:
content = hibcProcess(data);
break;
default:
content = data;
break;
}
if (content.isEmpty() && !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 content;
}
/**
* Returns the human-readable text for this symbol.
*
* @return the human-readable text for this symbol
*/
public String getHumanReadableText() {
return readable;
}
/**
* Chooses the ECI mode most suitable for the content of this symbol.
*/
protected void eciProcess() {
EciMode eci = EciMode.of(content, "ISO8859_1", 3)
.or(content, "ISO8859_2", 4)
.or(content, "ISO8859_3", 5)
.or(content, "ISO8859_4", 6)
.or(content, "ISO8859_5", 7)
.or(content, "ISO8859_6", 8)
.or(content, "ISO8859_7", 9)
.or(content, "ISO8859_8", 10)
.or(content, "ISO8859_9", 11)
.or(content, "ISO8859_10", 12)
.or(content, "ISO8859_11", 13)
.or(content, "ISO8859_13", 15)
.or(content, "ISO8859_14", 16)
.or(content, "ISO8859_15", 17)
.or(content, "ISO8859_16", 18)
.or(content, "Windows_1250", 21)
.or(content, "Windows_1251", 22)
.or(content, "Windows_1252", 23)
.or(content, "Windows_1256", 24)
.or(content, "SJIS", 20)
.or(content, "UTF8", 26);
if (EciMode.NONE.equals(eci)) {
throw new OkapiException("Unable to determine ECI mode.");
}
eciMode = eci.mode;
inputData = toBytes(content, eci.charset);
infoLine("ECI Mode: " + eci.mode);
infoLine("ECI Charset: " + eci.charset.name());
}
protected static int[] toBytes(String s, Charset charset, int... suffix) {
if (!charset.newEncoder().canEncode(s)) {
return null;
}
byte[] fnc1 = FNC1_STRING.getBytes(charset);
byte[] fnc2 = FNC2_STRING.getBytes(charset);
byte[] fnc3 = FNC3_STRING.getBytes(charset);
byte[] fnc4 = FNC4_STRING.getBytes(charset);
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;
rectangles.clear();
texts.clear();
int baseY;
if (humanReadableLocation == TOP) {
baseY = getTheoreticalHumanReadableHeight();
} else {
baseY = 0;
}
h = 0;
y = baseY;
for (yBlock = 0; yBlock < row_count; yBlock++) {
black = true;
x = 0;
for (xBlock = 0; xBlock < pattern[yBlock].length(); xBlock++) {
char c = pattern[yBlock].charAt(xBlock);
w = getModuleWidth(c - '0') * moduleWidth;
if (black) {
if (row_height[yBlock] == -1) {
h = default_height;
} else {
h = row_height[yBlock];
}
if (w != 0 && h != 0) {
Rectangle2D.Double rect = new Rectangle2D.Double(x, y, w, h);
rectangles.add(rect);
}
if (x + w > symbol_width) {
symbol_width = (int) Math.ceil(x + w);
}
}
black = !black;
x += w;
}
if ((y - baseY + h) > symbol_height) {
symbol_height = (int) Math.ceil(y - baseY + h);
}
y += h;
}
if (humanReadableLocation != NONE && !readable.isEmpty()) {
double baseline;
if (humanReadableLocation == TOP) {
baseline = fontSize;
} else {
baseline = symbol_height + fontSize;
}
texts.add(new TextBox(0, baseline, symbol_width, readable, 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(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() {
int before = rectangles.size();
for (int i = rectangles.size() - 1; i >= 0; i--) {
Rectangle2D.Double rect1 = rectangles.get(i);
for (int j = i - 1; j >= 0; j--) {
Rectangle2D.Double rect2 = 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;
rectangles.remove(i);
break;
}
if (rect2.x < rect1.x) {
// we've moved past any rectangles that might be directly above rect1
break;
}
}
}
}
int after = 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 Corresponding Zint code
*/
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;
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(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 (pattern == null || pattern.length == 0) {
return new int[0];
} else {
int count = (int) Math.ceil(pattern[0].length() / (double) size);
int[] codewords = new int[pattern.length * count];
for (int i = 0; i < pattern.length; i++) {
String row = pattern[i];
for (int j = 0; j < count; j++) {
int substringStart = j * size;
int substringEnd = Math.min((j + 1) * size, row.length());
codewords[(i * count) + j] = Integer.parseInt(row.substring(substringStart, substringEnd));
}
}
return codewords;
}
}
protected void info(CharSequence s) {
encodeInfo.append(s);
}
protected void infoLine(String s) {
encodeInfo.append(s).append('\n');
}
protected void infoLine() {
encodeInfo.append('\n');
}
}